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 +4 -4
- data/CHANGELOG.md +35 -0
- data/lib/commands/senren/add/add_command.rb +4 -1
- data/lib/commands/senren/locales/locales_command.rb +38 -0
- data/lib/generators/senren/install/install_generator.rb +19 -0
- data/lib/generators/senren/install/templates/installed_components.yml.tt +1 -1
- data/lib/senren/rails/asset_path_guard.rb +36 -33
- data/lib/senren/rails/doctor.rb +5 -1
- data/lib/senren/rails/version.rb +1 -1
- metadata +18 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6a7c6c06cf2fbc28902f954562926b0b5a75a5b22591074c2bd7bc94a398ebf0
|
|
4
|
+
data.tar.gz: 1c272c73f0008bca421d43a85ca55d217c10d563aa8ec0880083fad0c412dab0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
|
|
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
|
-
|
|
75
|
-
return false unless overlap?(asset, components)
|
|
74
|
+
return false unless asset.directory?
|
|
76
75
|
|
|
77
|
-
|
|
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
|
|
87
|
-
|
|
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(', ')}).
|
data/lib/senren/rails/doctor.rb
CHANGED
|
@@ -50,7 +50,11 @@ module Senren
|
|
|
50
50
|
|
|
51
51
|
def runtime_checks
|
|
52
52
|
[
|
|
53
|
-
|
|
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') }
|
data/lib/senren/rails/version.rb
CHANGED
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.
|
|
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.
|
|
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.
|
|
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
|