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

Developer Tooling For The Modern Rails & Hotwir...

Marco Roth
August 02, 2024
180

Developer Tooling For The Modern Rails & Hotwire Era @ Madison Ruby 2024

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

This talk aims to showcase 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.

The session will extend beyond LSPs, to explore the potential of browser extensions and development tools tailored specifically for Turbo, Stimulus, and Hotwire.

Marco Roth

August 02, 2024
Tweet

More Decks by Marco Roth

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. 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. 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) } }, }) }
  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. provideAttributes(_tag: string) { const targets = controllers.map(controller => `data-${controller.identifier}-target` )

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

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

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

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

    files inspected, 0 offenses detected, 0 offenses autocorrectable
  27. <%= 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
  28. <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" } ) %>
  29. In which the ERB view helpers also have an HTML

    tag node for Rails View Helpers
  30. == <%= tag.div( data: { controller: "hello", action: "click->hello#greet" }

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

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

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

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

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

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

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

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

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