Skip to content

[Improvement] Docs: symlink Stimulus controllers to gem source instead of copying - #493

Open
djalmaaraujo wants to merge 1 commit into
mainfrom
refactor/docs-stimulus-symlinks
Open

[Improvement] Docs: symlink Stimulus controllers to gem source instead of copying#493
djalmaaraujo wants to merge 1 commit into
mainfrom
refactor/docs-stimulus-symlinks

Conversation

@djalmaaraujo

@djalmaaraujo djalmaaraujo commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Problem

`docs/app/javascript/controllers/ruby_ui/_controller.js` was a hand-maintained copy of `gem/lib/ruby_ui//_controller.js`, kept in sync by manually copying files. This drifted silently: the accordion Stimulus fix landed in the gem (commit 0897b2a) but the docs copy was never updated, so the live accordion preview at rubyui.com/docs/accordion broke (content never revealed on click). That fix is currently sitting in still-open PR #490.

The Ruby side of this exact problem is already solved — `docs/config/initializers/ruby_ui.rb` autoloads Phlex components straight from `gem/lib/ruby_ui` via Zeitwerk, no copy at all. Only the JS side still duplicated.

Fix

Replace all 37 docs controller files with relative symlinks into the corresponding gem source file, e.g.:

```
docs/app/javascript/controllers/ruby_ui/accordion_controller.js
-> ../../../../../gem/lib/ruby_ui/accordion/accordion_controller.js
```

The gem subdirectory doesn't always match the controller basename (e.g. `checkbox_group_controller.js` lives under `gem/lib/ruby_ui/checkbox/`, `select_item_controller.js` under `gem/lib/ruby_ui/select/`) — the mapping was built from a real `find` over both trees, not guessed.

Files with genuinely stale content pre-migration (diffed docs copy vs. gem source before symlinking):

  • `accordion_controller.js` — missing the hidden-attribute handling (the bug fixed in PR [Bug Fix] Accordion: sync docs Stimulus controller with gem #490)
  • `avatar_controller.js` — missing a couple of defensive checks/comments
  • `combobox_controller.js` — missing minPopoverWidth/placement Stimulus values, hardcoded 'bottom-start' placement
  • `sheet_controller.js` — missing an open Stimulus value + connect() auto-open behavior

The other 33 were already byte-identical.

This PR supersedes #490 for `accordion_controller.js` specifically — the symlink now points at the gem's current (correct) source regardless of whether #490 merges. Not touching or closing #490; leaving its fate to a human.

New dev workflow for adding a component

`rake ruby_ui:sync_controller_symlinks` (new task, `docs/lib/tasks/ruby_ui.rake`) scans `gem/lib/ruby_ui/**/*_controller.js` and creates/repairs any missing symlink. Idempotent — safe to re-run. It replaces the old "copy the file by hand" step. It intentionally does not touch the Stimulus manifest — `bin/rails stimulus:manifest:update` is still a required separate step to register a brand-new controller in `controllers/index.js`.

esbuild wrinkle found along the way

esbuild's default symlink handling resolves a symlinked module's imports relative to its real path. Since `gem/` and `docs/` are sibling directories with no shared ancestor `node_modules`, this broke resolution of `@hotwired/stimulus`, `@floating-ui/dom`, and `motion` for every symlinked controller with an external import. Fixed by adding `--preserve-symlinks` to the `build` script in `docs/package.json`, which makes esbuild resolve relative to the symlink's own location instead.

Docs updated in this PR

  • Root `CLAUDE.md` — routing note on the symlink relationship + new rake task.
  • `gem/AGENTS.md` — JavaScript section notes docs symlinks straight to these files now.
  • `docs/CLAUDE.md` — new short section on the symlinked directory + workflow.
  • `.claude/skills/ruby-ui-stimulus/SKILL.md` — "Register & declare deps" step now mentions the symlink + sync task.

Test plan

Verified in the docs devcontainer (a fresh instance mounted against this branch, ports 3002/host to avoid clashing with any other running instance):

  • All 37 symlinks created; each verified individually with readlink -f resolving to the correct existing gem file (not just trusting the naming pattern).
  • bin/rails stimulus:manifest:update regenerates app/javascript/controllers/index.js byte-identical to before the migration (import paths are relative to app/javascript/controllers, unaffected by symlinks).
  • New rake ruby_ui:sync_controller_symlinks is idempotent on a fully-synced tree, and correctly recreates a symlink I deleted as a test (verified the recreated link's target).
  • pnpm build — failed initially on unresolved @hotwired/stimulus / @floating-ui/dom / motion imports (see esbuild wrinkle above); passes after adding --preserve-symlinks.
  • pnpm build:css — passes.
  • Grepped the built app/assets/builds/application.js for controller-specific code (not just import statements): found content.removeAttribute("hidden") / content.setAttribute("hidden", "") from the gem's fixed accordion controller, and computePosition from the real @floating-ui/dom package — confirms the bundle contains real gem-sourced content, not broken/empty imports.
  • Booted bin/rails server in the container and curl'd it directly: /docs/accordion, /docs/tooltip, /docs/popover, /docs/checkbox, /docs/select all return 200; fetched the served, fingerprinted application-*.js asset over HTTP and re-confirmed the same accordion/floating-ui strings are present in what the server actually serves.
  • bin/rails test (docs) — 75 runs, 107 assertions, 0 failures, 0 errors.
  • bundle exec standardrb (docs) — clean, exit 0.

Not verified: no real browser was available in this environment, so I could not click through the accordion/combobox/tooltip UI interactively. Confidence here rests on the four checks above (manifest diff, bundle content grep, HTTP 200 + asset content over curl, and the existing Rails test suite) rather than a driven browser session — flagging this explicitly rather than overclaiming.


Summary by cubic

Symlinked all docs Stimulus controllers to the gem source to remove drift and ensure previews use the latest code. Fixes the broken accordion demo and updates the build to support symlinks.

  • Refactors

    • Replaced 37 files in docs/app/javascript/controllers/ruby_ui/ with symlinks to gem/lib/ruby_ui/<component>/….
    • Updated esbuild build script with --preserve-symlinks so imports from @hotwired/stimulus, @floating-ui/dom, and motion resolve correctly.
  • Migration

    • For new components: run bin/rails ruby_ui:sync_controller_symlinks, then bin/rails stimulus:manifest:update.
    • No changes needed for existing components; docs now serve the gem controllers directly.

Written for commit 4ca0534. Summary will update on new commits.

Review in cubic

…d of copying

docs/app/javascript/controllers/ruby_ui/*_controller.js was a hand-maintained
copy of gem/lib/ruby_ui/<component>/*_controller.js. The two silently drifted:
the gem's accordion fix (0897b2a) never landed in the docs copy, breaking the
live accordion preview. Replace all 37 copies with relative symlinks into the
gem source, matching how docs already autoloads Phlex components straight
from gem/ (config/initializers/ruby_ui.rb) with no copy step.

- accordion_controller.js, avatar_controller.js, combobox_controller.js, and
  sheet_controller.js had genuinely stale docs content pre-migration (beyond
  just accordion) - the symlink now serves the gem's current, correct version.
- esbuild's default symlink handling resolves module imports relative to the
  symlink's real (gem/) path, which breaks node_modules resolution since gem/
  and docs/ are siblings with no shared ancestor node_modules. Build with
  --preserve-symlinks so imports resolve against the symlink location instead.
- Add `rake ruby_ui:sync_controller_symlinks` (docs/lib/tasks/ruby_ui.rake):
  scans gem/lib/ruby_ui for *_controller.js and creates/repairs the matching
  symlink. Idempotent. Replaces the old "copy the file by hand" step for new
  components; stimulus:manifest:update is still required separately to
  register a brand-new controller in the manifest.
- Update CLAUDE.md, docs/CLAUDE.md, gem/AGENTS.md, and the ruby-ui-stimulus
  skill to describe the symlink relationship and new workflow.

Supersedes still-open PR #490 for accordion_controller.js specifically: that
PR's docs-side fix is now moot since the file is a symlink to the (already
correct) gem source. Leaving #490's fate to a human reviewer.
@djalmaaraujo
djalmaaraujo requested a review from cirdes as a code owner July 27, 2026 20:27

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 80 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name=".claude/skills/ruby-ui-stimulus/SKILL.md">

<violation number="1" location=".claude/skills/ruby-ui-stimulus/SKILL.md:47">
P3: The command `stimulus:manifest:update` is missing its `rake` or `bin/rails` prefix, so a developer following these steps verbatim would get a shell error. The earlier bullet in the same section uses `rake stimulus:manifest:update` — the new instruction should follow that format for consistency.</violation>
</file>

<file name="docs/lib/tasks/ruby_ui.rake">

<violation number="1" location="docs/lib/tasks/ruby_ui.rake:18">
P2: Silent symlink collision when two gem controllers share the same basename. If a future gem addition introduces a controller whose filename already exists from a different subdirectory, this task overwrites the first symlink without warning, losing the reference to the other controller. Consider adding a collision check that warns or aborts when `link_path` already exists with a different target before overwriting.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

abort "No *_controller.js files found under #{gem_root} - is the gem/ checkout present?"
end

controller_files.each do |gem_path|

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Silent symlink collision when two gem controllers share the same basename. If a future gem addition introduces a controller whose filename already exists from a different subdirectory, this task overwrites the first symlink without warning, losing the reference to the other controller. Consider adding a collision check that warns or aborts when link_path already exists with a different target before overwriting.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/lib/tasks/ruby_ui.rake, line 18:

<comment>Silent symlink collision when two gem controllers share the same basename. If a future gem addition introduces a controller whose filename already exists from a different subdirectory, this task overwrites the first symlink without warning, losing the reference to the other controller. Consider adding a collision check that warns or aborts when `link_path` already exists with a different target before overwriting.</comment>

<file context>
@@ -0,0 +1,41 @@
+      abort "No *_controller.js files found under #{gem_root} - is the gem/ checkout present?"
+    end
+
+    controller_files.each do |gem_path|
+      gem_path = Pathname.new(gem_path)
+      link_path = controllers_dir.join(gem_path.basename)
</file context>

- `docs/app/javascript/controllers/ruby_ui/<component>_controller.js` is a
symlink to the gem file above, not a copy. For a brand-new controller, run
`bin/rails ruby_ui:sync_controller_symlinks` in `docs/` first to create the
symlink, then `stimulus:manifest:update` to register it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The command stimulus:manifest:update is missing its rake or bin/rails prefix, so a developer following these steps verbatim would get a shell error. The earlier bullet in the same section uses rake stimulus:manifest:update — the new instruction should follow that format for consistency.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .claude/skills/ruby-ui-stimulus/SKILL.md, line 47:

<comment>The command `stimulus:manifest:update` is missing its `rake` or `bin/rails` prefix, so a developer following these steps verbatim would get a shell error. The earlier bullet in the same section uses `rake stimulus:manifest:update` — the new instruction should follow that format for consistency.</comment>

<file context>
@@ -41,6 +41,10 @@ Building or changing a Stimulus-backed component:
+   - `docs/app/javascript/controllers/ruby_ui/<component>_controller.js` is a
+     symlink to the gem file above, not a copy. For a brand-new controller, run
+     `bin/rails ruby_ui:sync_controller_symlinks` in `docs/` first to create the
+     symlink, then `stimulus:manifest:update` to register it.
    - New JS packages go in `gem/package.json` **and** per-component in
      `gem/lib/generators/ruby_ui/dependencies.yml`.
</file context>
Suggested change
symlink, then `stimulus:manifest:update` to register it.
symlink, then `rake stimulus:manifest:update` to register it.

cirdes commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

@djalmaaraujo, please take a look on cubic issues!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants