senren-ui 0.4.0 → 0.4.2

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: 99b7430e710e44023cbc886c862b4d237d06bb489a4a00f5e30685b59768f6dc
4
- data.tar.gz: 6ead04eac8e4e518cbf7129c65dfb63ad683db3dfb44b13a794366eee8be37dd
3
+ metadata.gz: 6a7c6c06cf2fbc28902f954562926b0b5a75a5b22591074c2bd7bc94a398ebf0
4
+ data.tar.gz: 1c272c73f0008bca421d43a85ca55d217c10d563aa8ec0880083fad0c412dab0
5
5
  SHA512:
6
- metadata.gz: 7cf42a8f5992b7de9bc4135c7d6bea7c44987ca3844de06b575876957877efd773711cdfcb9cd54d2ddc1ca878f38842d4df082b0fb7451b5b63613c9bb5f056
7
- data.tar.gz: e3c329bc7eb8ef0c7b90af18971c92f24dfdea4822e17a9e43faff3b1e94a6815ed4b6c18cff30a2e65932cd8bf96d88b2e64153671d72e2a52c8ffe2d759c76
6
+ metadata.gz: b200e044d76d070cfa3657f967603adff7236a1b224a20ee08ed3e5a9e9ff35492d57a28782257b470df1c2901112ec6a60874556e292f5fc075a7ee5f29abb9
7
+ data.tar.gz: edee24c78567f1199468eebe25c676f23044fb622122a0bdb1658cbb4398fd3e8ac9d664812d7ccd3b13a52c8cfbe9039e61aa5e49ecbeeb124f01da13df24f6
data/CHANGELOG.md CHANGED
@@ -7,6 +7,41 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
  v0.x is a pre-stable line: minor bumps may break things; patch bumps are
8
8
  bug fixes only.
9
9
 
10
+ ## [0.4.2] — 2026-10-26
11
+
12
+ ### Security
13
+
14
+ - Refuse `senren:install` destinations that resolve outside the host app,
15
+ including symlinked files, parent directories, and dangling links, before
16
+ creating or changing files.
17
+ - Detect component source exposed through symlinked asset paths before a
18
+ deployed app boots or precompiles assets. Dedicated sidecar asset directories
19
+ remain supported.
20
+ - Require Crass 1.0.8 and ViewComponent 4.14.0 or newer so consumer bundles
21
+ enforce the CSS sanitizer and render-state security fixes.
22
+ - Update development tooling's `source-map-js` to 1.2.2 for CVE-2026-93749
23
+ and add JavaScript dependency audits to local and GitHub CI.
24
+
25
+ ## [0.4.1] — 2026-10-08
26
+
27
+ ### Fixed
28
+
29
+ - `bin/rails senren:locales vi`, as the README, `docs/i18n.md` and the install
30
+ generator write it, failed with `Unrecognized command "vi"`: the task was a
31
+ rake task only, so Rails passed `vi` to rake as a second task name, and the
32
+ one spelling that worked was `bin/rails 'senren:locales[vi]'`. It is now a
33
+ Rails command, like `senren:add`, so `bin/rails senren:locales vi`,
34
+ `senren:locales en vi` and `senren:locales vi --force` work as documented.
35
+ - `bin/rails senren:doctor` reported `✗ ViewComponent gem available` and exited
36
+ 1 on a healthy app. It checked only whether the `ViewComponent` constant was
37
+ loaded, and a host's Gemfile lists `senren-ui`, not `view_component`, so
38
+ nothing loads it until the first component renders. The check now also asks
39
+ whether the gem is installed, as the Turbo check already did.
40
+ - `bin/rails senren:add` and `senren:locales` given a name they do not know (or
41
+ `senren:add` given none) printed a Thor backtrace with the useful line at the
42
+ top of it. They now print the message alone and exit 1, as the rake tasks
43
+ always did.
44
+
10
45
  ## [0.4.0] — 2026-10-08
11
46
 
12
47
  ### Added
@@ -22,7 +22,10 @@ module Senren
22
22
  force: options[:force]
23
23
  )
24
24
  rescue ArgumentError => e
25
- raise ::Rails::Command::Base::Error, e.message
25
+ # The message alone, as the rake task prints it. Rails does not rescue
26
+ # an error raised from a command, so raising printed a Thor backtrace
27
+ # with the one line that says what to type buried at the top of it.
28
+ abort e.message
26
29
  end
27
30
 
28
31
  private
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/command'
4
+ require 'senren/rails'
5
+ require 'senren/rails/locale_installer'
6
+
7
+ module Senren
8
+ module Command
9
+ # `bin/rails senren:locales vi` as the docs write it. As a rake task only,
10
+ # Rails handed `vi` to rake as a second task name and failed with
11
+ # `Unrecognized command "vi"`; the only spelling that worked was
12
+ # `bin/rails 'senren:locales[vi]'`. A Rails command receives the words after
13
+ # it as arguments, the same way senren:add does.
14
+ class LocalesCommand < ::Rails::Command::Base
15
+ class_option :force, type: :boolean, default: false,
16
+ desc: 'Replace a locale file that already exists with the shipped one.'
17
+
18
+ desc 'locales LOCALE [LOCALE...]',
19
+ 'Copy shipped Senren translations into config/locales.'
20
+ def perform(*names)
21
+ require_application!
22
+
23
+ locale_installer.install(locales: names.flat_map { |name| name.split(',') }, force: options[:force])
24
+ rescue ArgumentError => e
25
+ # The message alone, as the rake task prints it. Rails does not rescue
26
+ # an error raised from a command, so raising printed a Thor backtrace
27
+ # with the one line that says what to type buried at the top of it.
28
+ abort e.message
29
+ end
30
+
31
+ private
32
+
33
+ def locale_installer
34
+ Senren::Rails::LocaleInstaller.new
35
+ end
36
+ end
37
+ end
38
+ end
@@ -29,6 +29,25 @@ module Senren
29
29
  class_option :force_locales, type: :boolean, default: false,
30
30
  desc: 'Replace locale files that already exist with the shipped ones.'
31
31
 
32
+ # Thor's template/copy/gsub actions follow destination symlinks. Check
33
+ # every destination before the first directory is created, including
34
+ # files read before rewriting and links whose targets do not exist yet.
35
+ def validate_install_destinations
36
+ destinations = [
37
+ host_paths.senren_dir, host_paths.components_dir, host_paths.stimulus_dir,
38
+ host_paths.stylesheet_path, host_paths.stylesheet_path.dirname.join('senren_themes.css'),
39
+ host_paths.base_component_path, host_paths.conventions_file, host_paths.installed_components,
40
+ host_paths.registry_mirror, host_paths.skill_file, host_paths.agent_rules_file,
41
+ host_paths.codex_agents_md, host_paths.claude_md, host_paths.copilot_instructions,
42
+ host_paths.cursor_rule_file,
43
+ host_paths.root.join('app/javascript/controllers/index.js'), host_paths.root.join('config/importmap.rb')
44
+ ]
45
+ destinations.concat(options[:locales].map { |locale| host_paths.locale_file(locale) })
46
+ destinations.each do |path|
47
+ Senren::Rails::SafeWrite.assert_inside!(path, host_paths.root, 'senren:install')
48
+ end
49
+ end
50
+
32
51
  def create_senren_dir
33
52
  empty_directory '.senren'
34
53
  end
@@ -1,4 +1,4 @@
1
1
  # This file is the ledger of Senren components installed in this app.
2
- # It is updated by `bin/rails senren:add` and `bin/rails senren:install`.
2
+ # It is updated by `bin/rails senren:add` and `bin/rails generate senren:install`.
3
3
  # Do not edit by hand unless you know what you are doing.
4
4
  installed: []
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'pathname'
4
+
3
5
  module Senren
4
6
  module Rails
5
7
  # Refuses to let component source be served as a static asset.
@@ -58,23 +60,20 @@ module Senren
58
60
  components = components_dir(app)
59
61
  return [] unless components&.directory?
60
62
 
61
- asset_paths(app).select { |path| publishes_source?(path, components) }
63
+ sources = component_sources(components)
64
+ return [] if sources.empty?
65
+
66
+ asset_paths(app).select { |path| publishes_source?(path, sources) }
62
67
  end
63
68
 
64
- # Whether serving this asset path would serve component source.
65
- #
66
- # `source_files?` used to be asked once about app/components as a whole,
67
- # which is the wrong question for a descendant path: with that shape,
68
- # app/components/assets holding nothing but CSS looked identical to
69
- # app/components/senren holding every component. Only the overlapping
70
- # subtree can publish anything, and that subtree is the deeper of the two
71
- # paths.
72
- def publishes_source?(asset_path, components)
69
+ # Propshaft follows directory and file symlinks when collecting assets.
70
+ # Compare their real targets, including links nested below an asset path,
71
+ # so an alias cannot publish source past the production boot guard.
72
+ def publishes_source?(asset_path, sources)
73
73
  asset = Pathname.new(asset_path.to_s).expand_path
74
- components = components.expand_path
75
- return false unless overlap?(asset, components)
74
+ return false unless asset.directory?
76
75
 
77
- source_files?(asset.to_s.length >= components.to_s.length ? asset : components)
76
+ each_file(asset).any? { |_, target| sources.key?(target) }
78
77
  end
79
78
 
80
79
  def components_dir(app)
@@ -83,8 +82,30 @@ module Senren
83
82
  nil
84
83
  end
85
84
 
86
- def source_files?(dir)
87
- SOURCE_EXTENSIONS.any? { |ext| Dir.glob(dir.join("**/*#{ext}")).any? }
85
+ def component_sources(components)
86
+ each_file(components).with_object({}) do |(path, target), sources|
87
+ sources[target] = true if [path.extname, target.extname].any? { |ext| SOURCE_EXTENSIONS.include?(ext) }
88
+ end
89
+ end
90
+
91
+ # Track real directory identities so aliases and cycles are visited once.
92
+ # Keep the original filename as well: widget.rb may point to a differently
93
+ # named file outside app/components, which is still component source.
94
+ def each_file(path, visited = {}, &block)
95
+ return enum_for(__method__, path, visited) unless block
96
+
97
+ target = path.realpath
98
+ if target.directory?
99
+ return if visited[target]
100
+
101
+ visited[target] = true
102
+ target.children.each { |child| each_file(child, visited, &block) }
103
+ else
104
+ block.call(path, target)
105
+ end
106
+ rescue Errno::ENOENT, Errno::ENOTDIR, Errno::ELOOP
107
+ # Broken links cannot publish content; visited directories stop cycles.
108
+ nil
88
109
  end
89
110
 
90
111
  def asset_paths(app)
@@ -93,24 +114,6 @@ module Senren
93
114
  []
94
115
  end
95
116
 
96
- # Two paths overlap when one contains the other, in EITHER direction, and
97
- # the test has to be separator-aware:
98
- #
99
- # app/components is an ancestor -> publishes everything
100
- # app/components/senren is a descendant -> publishes the components
101
- # app/comp shares a prefix -> publishes nothing
102
- #
103
- # The first version tested only the ancestor direction with a bare
104
- # start_with?, so it missed the descendant case — which is what a
105
- # developer writes after reading this module's own remediation text — and
106
- # it blocked production boots over an unrelated app/comp directory.
107
- def overlap?(one, other)
108
- a = "#{one}#{File::SEPARATOR}"
109
- b = "#{other}#{File::SEPARATOR}"
110
-
111
- a.start_with?(b) || b.start_with?(a)
112
- end
113
-
114
117
  def message_for(offenders)
115
118
  <<~MESSAGE.strip
116
119
  app/components is on the asset load path (#{offenders.join(', ')}).
@@ -50,7 +50,11 @@ module Senren
50
50
 
51
51
  def runtime_checks
52
52
  [
53
- check('ViewComponent gem available') { defined?(::ViewComponent) },
53
+ # Not just defined?(::ViewComponent): a host's Gemfile lists senren-ui,
54
+ # not view_component, so Bundler.require never loads it, and nothing
55
+ # does until BaseComponent is autoloaded on the first render. A healthy
56
+ # app booted for this task failed the check.
57
+ check('ViewComponent gem available') { defined?(::ViewComponent) || gem_loadable?('view_component') },
54
58
  check('TailwindCSS stylesheet present') { paths.stylesheet_path.exist? },
55
59
  check('Stimulus directory present') { paths.stimulus_dir.directory? },
56
60
  check('Turbo gem available') { defined?(::Turbo) || gem_loadable?('turbo-rails') }
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Senren
4
4
  module Rails
5
- VERSION = '0.4.0'
5
+ VERSION = '0.4.2'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: senren-ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - vutt
@@ -9,6 +9,20 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: crass
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 1.0.8
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 1.0.8
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: rails
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -29,14 +43,14 @@ dependencies:
29
43
  requirements:
30
44
  - - ">="
31
45
  - !ruby/object:Gem::Version
32
- version: 4.9.0
46
+ version: 4.14.0
33
47
  type: :runtime
34
48
  prerelease: false
35
49
  version_requirements: !ruby/object:Gem::Requirement
36
50
  requirements:
37
51
  - - ">="
38
52
  - !ruby/object:Gem::Version
39
- version: 4.9.0
53
+ version: 4.14.0
40
54
  description: |
41
55
  Senren UI is a Rails-native UI component library inspired by the developer
42
56
  experience of shadcn/ui. It ships generators, a registry, and a centralized
@@ -59,6 +73,7 @@ files:
59
73
  - docs/performance_testing.md
60
74
  - docs/visual_style.md
61
75
  - lib/commands/senren/add/add_command.rb
76
+ - lib/commands/senren/locales/locales_command.rb
62
77
  - lib/generators/senren/component/component_generator.rb
63
78
  - lib/generators/senren/component/templates/component.html.erb.tt
64
79
  - lib/generators/senren/component/templates/component.rb.tt