Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Leveling Up Developer Tooling for the Modern Ra...

Marco Roth
November 20, 2024

Leveling Up Developer Tooling for the Modern Rails & Hotwire Era @ Ruby Türkiye, November 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.

Marco Roth

November 20, 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. { id: 1, method: "textDocument/completion", params: { "textDocument": { "uri":

    "file:///.../lsp-example/.../test.txt" }, "position": { "line": 1, "character": 7 }, "context": { "triggerKind": 1 } } }
  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, result: [ { "label": "TypeScript", "kind": 1,

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

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

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

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

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

    "output"] connect() { ... } greet() { ... } disconnect() { ... } }
  14. 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) })
  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. 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) } }, }) }
  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. export default class extends Controller { static targets = ["name",

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

    return [ { name: "data-controller" }, { name: "data-action" }, ...targets, ] }
  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. 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 }
  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. import { application } from "controllers/application" import { eagerLoadControllersFrom }

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

    files inspected, 0 offenses detected, 0 offenses autocorrectable
  29. <%= 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
  30. So it ’ s only really useful if we can

    provide intelligence for ERB
  31. a

  32. <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" } ) %>
  33. == <%= tag.div( data: { controller: "hello", action: "click->hello#greet" }

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

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

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

    <!-- _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. Turbo Streams Debug Bar Pause Streams Processing Step through Streams

    Revert Turbo Stream Actions Show Diff of what an action changed and more…
  39. 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
  40. I believe that tools like these are part of the

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

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

    future of Rails applications.
  43. 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 1600+ subscribers and growing WEEKLY