Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Developer Tooling For The Modern Rails & Hotwir...

Developer Tooling For The Modern Rails & Hotwire Era @ Geneva.rb Meetup, October 2024

The evolution of developer experience (DX) tooling has been a game-changer in how we build, debug, and enhance web applications.

This talk aims to illuminate the path toward enriching the Ruby on Rails ecosystem with advanced DX tools, focusing on the implementation of Language Server Protocols (LSP) for Stimulus and Turbo.

Drawing inspiration from the rapid advancements in JavaScript tooling, we explore the horizon of possibilities for Rails developers, including how advanced browser extensions and tools specifically designed for the Hotwire ecosystem can level up your developer experience.

https://marcoroth.dev/talks/geneva-rb-meetup-october-2024
https://www.meetup.com/geneva-rb/events/303018643

Marco Roth

October 16, 2024
Tweet

More Decks by Marco Roth

Other Decks in Programming

Transcript

  1. Marco Roth 👋 t @marcoroth_ M @[email protected] g marcoroth.dev g

    @marcoroth Full-Stack Developer & Open Source Contributor
  2. Developer Tooling For The Modern Hotwire & Rails Era Marco

    Roth Full-Stack Developer & Open Source Contributor
  3. X

  4. HTML Language Client Ruby Language Client Ruby Language Server HTML

    Language Server Editor Workspace Source: https://code.visualstudio.com/api/language-extensions/language-server-extension-guide
  5. { id: 1, method: "textDocument/completion", params: { "textDocument": { "uri":

    "file:///.../lsp-example/.../test.txt" }, "position": { "line": 1, "character": 7 }, "context": { "triggerKind": 1 } } }
  6. { id: 1, method: "textDocument/completion", params: { "textDocument": { "uri":

    "file:///.../lsp-example/.../test.txt" }, "position": { "line": 1, "character": 7 }, "context": { "triggerKind": 1 } } }
  7. { id: 1, method: "textDocument/completion", params: { "textDocument": { "uri":

    "file:///.../lsp-example/.../test.txt" }, "position": { "line": 1, "character": 7 }, "context": { "triggerKind": 1 } } }
  8. { id: 1, method: "textDocument/completion", params: { "textDocument": { "uri":

    "file:///.../lsp-example/.../test.txt" }, "position": { "line": 1, "character": 7 }, "context": { "triggerKind": 1 } } }
  9. { id: 1, result: [ { "label": "TypeScript", "kind": 1,

    "data": 1 }, { "label": "JavaScript", "kind": 1, "data": 2 } ] }
  10. import { IHTMLDataProvider } from "vscode-html-languageservice" export class StimulusHTMLDataProvider implements

    IHTMLDataProvider { provideTags() { ... } provideAttributes(tag: string) { ... } provideValues(tag: string, attribute: string) { ... } }
  11. class ControllerDefinition { readonly path: string methods: Array<string> = []

    targets: Array<string> = [] classes: Array<string> = [] values: { [key: string]: Value } = {} }
  12. import { Parser as AcornParser } from "acorn" import {

    simple as walk } from "acorn-walk" class Parser { parseController(code: string, filename: string) { ... } }
  13. export default class extends Controller { static targets = ["name",

    "output"] connect() { ... } greet() { ... } disconnect() { ... } }
  14. export default class extends Controller { static targets = ["name",

    "output"] connect() { ... } greet() { ... } disconnect() { ... } }
  15. const pattern = "app/javascript/controllers/**/*_controller.js" const controllerFiles = await glob(pattern) controllerFiles.forEach(async

    path => { const code = await fs.readFile(path, "utf8") parser.parseController(code, path) })
  16. const pattern = "app/javascript/controllers/**/*_controller.js" const controllerFiles = await glob(pattern) controllerFiles.forEach(async

    path => { const code = await fs.readFile(path, "utf8") parser.parseController(code, path) })
  17. const pattern = "app/javascript/controllers/**/*_controller.js" const controllerFiles = await glob(pattern) controllerFiles.forEach(async

    path => { const code = await fs.readFile(path, "utf8") parser.parseController(code, path) })
  18. parseController(code: string, filename: string) { const ast = this.parse(code) const

    controller = new ControllerDefinition(filename) walk(ast, { MethodDefinition(node) { if (node.kind === "method") { controller.methods.push(node.key.name) } }, }) }
  19. parseController(code: string, filename: string) { const ast = this.parse(code) const

    controller = new ControllerDefinition(filename) walk(ast, { MethodDefinition(node) { if (node.kind === "method") { controller.methods.push(node.key.name) } }, }) }
  20. parseController(code: string, filename: string) { const ast = this.parse(code) const

    controller = new ControllerDefinition(filename) walk(ast, { MethodDefinition(node) { if (node.kind === "method") { controller.methods.push(node.key.name) } }, }) }
  21. parseController(code: string, filename: string) { const ast = this.parse(code) const

    controller = new ControllerDefinition(filename) walk(ast, { MethodDefinition(node) { if (node.kind === "method") { controller.methods.push(node.key.name) } }, }) }
  22. export default class extends Controller { static targets = ["name",

    "output"] connect() { ... } greet() { ... } disconnect() { ... } }
  23. provideAttributes(_tag: string) { const targets = controllers.map(controller => `data-${controller.identifier}-target` )

    return [ { name: "data-controller" }, { name: "data-action" }, ...targets, ] }
  24. provideAttributes(_tag: string) { const targets = controllers.map(controller => `data-${controller.identifier}-target` )

    return [ { name: "data-controller" }, { name: "data-action" }, ...targets, ] }
  25. provideAttributes(_tag: string) { const targets = controllers.map(controller => `data-${controller.identifier}-target` )

    return [ { name: "data-controller" }, { name: "data-action" }, ...targets, ] }
  26. provideValues(_tag: string, attribute: string) { if (attribute === "data-controller") {

    return this.controllers.map(controller => controller.identifier ) } const match = attribute.match(/data-(.+)-target/) const controller = this.controllers.find(controller => controller.identifier == match[1] ) return controller.targets }
  27. provideValues(_tag: string, attribute: string) { if (attribute === "data-controller") {

    return this.controllers.map(controller => controller.identifier ) } const match = attribute.match(/data-(.+)-target/) const controller = this.controllers.find(controller => controller.identifier == match[1] ) return controller.targets }
  28. import { application } from "controllers/application" import { eagerLoadControllersFrom }

    from "@hotwired/stimulus-loading" eagerLoadControllersFrom("controllers", application)
  29. $ stimulus-lint Stimulus Lint is inspecting 25 files ........................ 25

    files inspected, 0 offenses detected, 0 offenses autocorrectable
  30. <%= tag.div( data: { controller: "filter", filter_open_class: "border-white", filter_close_class: "hover:bg-gray-100

    border-gray-300" } ) %> Full support for HTML+ERB and Rails-specific helpers
  31. So it ’ s only really useful if we can

    provide intelligence for ERB
  32. a

  33. <article id="<%= dom_id(article) %>"></article> <input <% if true %> type="text"

    <% end %> /> <% @posts.each do |post| %> <h1><%= post.title %></h1> <% end %> <%= content_tag(:p, "Hello world!") %> <%= tag.div tag.p("Hello world!") %> <%= tag.p do %> Hello world! <% end %> <%= tag.div( data: { controller: "hello", action: "click->hello#greet" } ) %>
  34. == <%= tag.div( data: { controller: "hello", action: "click->hello#greet" }

    ) %> <div data-controller="hello" data-action="click->hello#greet" ></div>
  35. This might also open to door to support Haml, Slim,

    Liquid, Phlex, Blade and other template languages
  36. <!-- main.html.erb --> <div data-controller="hello"> <%= render partial: "input" %>

    </div> <!-- _input.html.erb --> <input data-hello-target="name" type="text">
  37. <!-- main.html.erb --> <div data-controller="hello"></div> <%= render partial: "input" %>

    <!-- _input.html.erb --> <input data-hello-target="name" type="text">
  38. <!-- main.html.erb --> <div data-controller="hello"></div> <%= render partial: "input" %>

    <!-- _input.html.erb --> <input data-hello-target="name" type="text">
  39. Turbo Streams Debug Bar Pause Streams Processing Step through Streams

    Revert Turbo Stream Actions Show Diff of what an action changed and more…
  40. Turbo Morphing Debug Turbo Morphing Visual Diffs Turbo Form Submission

    Debug Turbo Streams Header Debug ActionCable / Turbo Rails Cable Debug Turbo Frame Lazy Loading Debug Stimulus Controller Tree-View Panel … Ideas & Roadmap
  41. I believe that tools like these are part of the

    reason why a language/ framework keeps it ’ s relevancy
  42. X

  43. I want to help build the tools needed for the

    future of Rails applications.
  44. Hotwire Weekly • A new Hotwire-focused newsletter • Delivered Weekly

    • Explore what ’ s happening in the world of Hotwire • Progress and updates while we are building out hotwire.io • Been running since Oct 2023 • Over 1550+ subscribers and growing WEEKLY