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 @ RailsHöck Switzerland, August 2024

Ruby on Rails Switzerland - Meetup August 2024

Organized by Renuo.ch - Hosted by Puzzle.ch

https://www.meetup.com/rubyonrails-ch/events/298503717

Marco Roth

August 21, 2024
Tweet

More Decks by Marco Roth

Other Decks in Programming

Transcript

  1. Developer Tooling for the Modern Rails & Hotwire Era Marco

    Roth Pragmatic Rails - rails_api_logger Alessandro Rodi (Renuo) nochmal Matthias Viehweger (Puzzle ITC) Programatic Rails again - validation_errors Alessandro Rodi (Renuo) News Crawler via Langchain.RB and Gemini APIs Riccardo Carlesso (Google Could)
  2. 👋 Marco Roth t @marcoroth_ M @[email protected] g marcoroth.dev g

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

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

  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. import { IHTMLDataProvider } from "vscode-html-languageservice" export class StimulusHTMLDataProvider implements

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

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

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

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

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

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

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

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

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

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

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

    </div> <!-- _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. <!-- main.html.erb --> <div data-controller="hello"></div> <%= render partial: "input" %>

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

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

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

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

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