turbo-mount 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2a99bcba9d009ca4dc95d68bd54a8f98e724f487c8d25eff234f406feb01c56e
4
- data.tar.gz: 186eb9a15926d54e14999febfe36c809dbf988d30857cf3d0ba29fd58b73eafc
3
+ metadata.gz: a1e390ef4fc7a3d329693a294437fa8f5d27c9157654b3a9b21621b26dd6c7c5
4
+ data.tar.gz: 591ea18f8b02da05da9a9d6dba5249eccdf01ec2b4d00477341df76166c0bd7d
5
5
  SHA512:
6
- metadata.gz: 5ec1ca4c48650c82e92c1ff0419df6ef9d3c5f1ebaa9c8ac31a41c0ac043fe22c78169d60ce9589ee3067e5d7643b851fcd3b93118ee3e8e64e2caad1dda552a
7
- data.tar.gz: c5c7639b39dcaa2443d29188e213c3592473eb3c9d83ca2d7278f37440e40f4daa7cdee4f357449a8ef9e08f9c4b0b0caa1f9f5837e0a810d09235cccc36f528
6
+ metadata.gz: fd306af35603685bebc4c2a7ef0fe31909fa56b521f0808562aeee3f8c90f2d5098aa846e6633356b716ced5aa51b8c5e6131252b97c5b91ccf14242aa90baff
7
+ data.tar.gz: 8a7221dad2c96c5c8c873e841a4007f1b45bcf6adada668af008692b035589590a230c411756f1cac86918d74521d0f6cec47d8c2125feddb1b5bfca91187ec3
data/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning].
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2024-11-03
11
+
12
+ ### Added
13
+
14
+ - [BREAKING] Support Svelte 5. ([@skryukov])
15
+ `turbo-mount/svelte` is now the Svelte 5 plugin. The Svelte 4 plugin is now `turbo-mount/svelte4`.
16
+
17
+ ## [0.3.3] - 2024-09-24
18
+
19
+ ### Added
20
+
21
+ - Support components in nested directories. ([@skryukov])
22
+ - Support pnpm build tool. ([@jkogara])
23
+
10
24
  ## [0.3.2] - 2024-06-24
11
25
 
12
26
  ### Fixed
@@ -65,10 +79,13 @@ and this project adheres to [Semantic Versioning].
65
79
 
66
80
  - Initial implementation. ([@skryukov])
67
81
 
82
+ [@jkogara]: https://github.com/jkogara
68
83
  [@skryukov]: https://github.com/skryukov
69
84
 
70
- [Unreleased]: https://github.com/skryukov/turbo-mount/compare/v0.3.2...HEAD
71
- [0.3.1]: https://github.com/skryukov/turbo-mount/compare/v0.3.1...v0.3.2
85
+ [Unreleased]: https://github.com/skryukov/turbo-mount/compare/v0.4.0...HEAD
86
+ [0.4.0]: https://github.com/skryukov/turbo-mount/compare/v0.3.3...v0.4.0
87
+ [0.3.3]: https://github.com/skryukov/turbo-mount/compare/v0.3.2...v0.3.3
88
+ [0.3.2]: https://github.com/skryukov/turbo-mount/compare/v0.3.1...v0.3.2
72
89
  [0.3.1]: https://github.com/skryukov/turbo-mount/compare/v0.3.0...v0.3.1
73
90
  [0.3.0]: https://github.com/skryukov/turbo-mount/compare/v0.2.3...v0.3.0
74
91
  [0.2.3]: https://github.com/skryukov/turbo-mount/compare/v0.2.2...v0.2.3
data/README.md CHANGED
@@ -1,8 +1,12 @@
1
- # Turbo Mount
1
+ <p align="center">
2
+ <img src="./assets/logo.svg" title="Turbo Mount logo" width="220" height="45">
3
+ </p>
4
+
5
+ <h1 align="center">Turbo Mount</h1>
2
6
 
3
7
  [![Gem Version](https://badge.fury.io/rb/turbo-mount.svg)](https://rubygems.org/gems/turbo-mount)
4
8
 
5
- `TurboMount` is a simple library that allows you to add highly interactive components from React, Vue, Svelte, and other gframeworks to your Hotwire application.
9
+ `TurboMount` is a simple library that allows you to add highly interactive components from React, Vue, Svelte, and other frameworks to your Hotwire application.
6
10
 
7
11
  ### Learn more
8
12
 
@@ -21,6 +25,7 @@
21
25
  - [Supported Frameworks](#supported-frameworks)
22
26
  - [Custom Controllers](#custom-controllers)
23
27
  - [Auto-Loading Components](#auto-loading-components)
28
+ - [Components in Nested Directories](#components-in-nested-directories)
24
29
  - [Vite Integration](#vite-integration)
25
30
  - [ESBuild Integration](#esbuild-integration)
26
31
  - [Mount Target](#mount-target)
@@ -142,7 +147,8 @@ This will generate the following HTML:
142
147
 
143
148
  - React: `"turbo-mount/react"`
144
149
  - Vue: `"turbo-mount/vue"`
145
- - Svelte: `"turbo-mount/svelte"`
150
+ - Svelte 4: `"turbo-mount/svelte4"`
151
+ - Svelte 5: `"turbo-mount/svelte"`
146
152
 
147
153
  To add support for other frameworks, create a custom plugin. See included plugins for examples.
148
154
 
@@ -185,6 +191,28 @@ The `registerComponents` helpers search for controllers in the following paths:
185
191
  - `controllers/turbo-mount/${controllerName}`
186
192
  - `controllers/turbo-mount-${controllerName}`
187
193
 
194
+ #### Components in Nested Directories
195
+
196
+ Turbo Mount supports components located in nested directories. For example, if you have a component structure like:
197
+
198
+ ```
199
+ components/
200
+ ├── Dashboard/
201
+ │ └── WeatherWidget.tsx
202
+ └── ...
203
+ ```
204
+
205
+ You can use the following helper to mount the component:
206
+
207
+ ```erb
208
+ <%= turbo_mount("Dashboard/WeatherWidget") %>
209
+ ```
210
+
211
+ For nested components, controllers are searched in these paths:
212
+
213
+ - `controllers/turbo_mount/dashboard/weather_widget_controller.js`
214
+ - `controllers/turbo_mount_dashboard__weather_widget_controller.js`
215
+
188
216
  #### Vite Integration
189
217
 
190
218
  Vite helper requires the `stimulus-vite-helpers` package to load components and controllers. Here's how to set it up:
@@ -1,12 +1,13 @@
1
1
  import { buildRegisterFunction } from 'turbo-mount';
2
2
  export { TurboMount } from 'turbo-mount';
3
+ import { mount, unmount } from 'svelte';
3
4
 
4
5
  const plugin = {
5
6
  mountComponent: (mountProps) => {
6
7
  const { el, Component, props } = mountProps;
7
- const component = new Component({ target: el, props });
8
+ const component = mount(Component, { target: el, props });
8
9
  return () => {
9
- component.$destroy();
10
+ unmount(component);
10
11
  };
11
12
  },
12
13
  };
@@ -1,2 +1,2 @@
1
- import{buildRegisterFunction as o}from"turbo-mount";export{TurboMount}from"turbo-mount";const t={mountComponent:o=>{const{el:t,Component:r,props:n}=o,e=new r({target:t,props:n});return()=>{e.$destroy()}}},r=o(t);export{t as default,r as registerComponent};
1
+ import{buildRegisterFunction as o}from"turbo-mount";export{TurboMount}from"turbo-mount";import{mount as t,unmount as r}from"svelte";const n={mountComponent:o=>{const{el:n,Component:e,props:m}=o,p=t(e,{target:n,props:m});return()=>{r(p)}}},e=o(n);export{n as default,e as registerComponent};
2
2
  //# sourceMappingURL=svelte.min.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"svelte.min.js","sources":["../../src/plugins/svelte/index.ts"],"sourcesContent":["import { buildRegisterFunction, Plugin, TurboMount } from \"turbo-mount\";\nimport { ComponentType } from \"svelte\";\n\nconst plugin: Plugin<ComponentType> = {\n mountComponent: (mountProps) => {\n const { el, Component, props } = mountProps;\n const component = new Component({ target: el, props });\n\n return () => {\n component.$destroy();\n };\n },\n};\n\nconst registerComponent = buildRegisterFunction(plugin);\n\nexport { TurboMount, registerComponent };\n\nexport default plugin;\n"],"names":["plugin","mountComponent","mountProps","el","Component","props","component","target","$destroy","registerComponent","buildRegisterFunction"],"mappings":"wFAGA,MAAMA,EAAgC,CACpCC,eAAiBC,IACf,MAAMC,GAAEA,EAAEC,UAAEA,EAASC,MAAEA,GAAUH,EAC3BI,EAAY,IAAIF,EAAU,CAAEG,OAAQJ,EAAIE,UAE9C,MAAO,KACLC,EAAUE,UAAU,CACrB,GAICC,EAAoBC,EAAsBV"}
1
+ {"version":3,"file":"svelte.min.js","sources":["../../src/plugins/svelte/index.ts"],"sourcesContent":["import { buildRegisterFunction, Plugin, TurboMount } from \"turbo-mount\";\nimport { Component, mount, unmount } from \"svelte\";\n\nconst plugin: Plugin<Component> = {\n mountComponent: (mountProps) => {\n const { el, Component, props } = mountProps;\n const component = mount(Component, { target: el, props });\n return () => {\n unmount(component);\n };\n },\n};\n\nconst registerComponent = buildRegisterFunction(plugin);\n\nexport { TurboMount, registerComponent };\n\nexport default plugin;\n"],"names":["plugin","mountComponent","mountProps","el","Component","props","component","mount","target","unmount","registerComponent","buildRegisterFunction"],"mappings":"oIAGA,MAAMA,EAA4B,CAChCC,eAAiBC,IACf,MAAMC,GAAEA,EAAEC,UAAEA,EAASC,MAAEA,GAAUH,EAC3BI,EAAYC,EAAMH,EAAW,CAAEI,OAAQL,EAAIE,UACjD,MAAO,KACLI,EAAQH,EAAU,CACnB,GAICI,EAAoBC,EAAsBX"}
@@ -0,0 +1,15 @@
1
+ import { buildRegisterFunction } from 'turbo-mount';
2
+ export { TurboMount } from 'turbo-mount';
3
+
4
+ const plugin = {
5
+ mountComponent: (mountProps) => {
6
+ const { el, Component, props } = mountProps;
7
+ const component = new Component({ target: el, props });
8
+ return () => {
9
+ component.$destroy();
10
+ };
11
+ },
12
+ };
13
+ const registerComponent = buildRegisterFunction(plugin);
14
+
15
+ export { plugin as default, registerComponent };
@@ -0,0 +1,2 @@
1
+ import{buildRegisterFunction as o}from"turbo-mount";export{TurboMount}from"turbo-mount";const t={mountComponent:o=>{const{el:t,Component:r,props:n}=o,e=new r({target:t,props:n});return()=>{e.$destroy()}}},r=o(t);export{t as default,r as registerComponent};
2
+ //# sourceMappingURL=svelte4.min.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"svelte4.min.js","sources":["../../src/plugins/svelte4/index.ts"],"sourcesContent":["import { buildRegisterFunction, Plugin, TurboMount } from \"turbo-mount\";\nimport { ComponentType } from \"svelte\";\n\nconst plugin: Plugin<ComponentType> = {\n mountComponent: (mountProps) => {\n const { el, Component, props } = mountProps;\n\n const component = new Component({ target: el, props });\n\n return () => {\n component.$destroy();\n };\n },\n};\n\nconst registerComponent = buildRegisterFunction(plugin);\n\nexport { TurboMount, registerComponent };\n\nexport default plugin;\n"],"names":["plugin","mountComponent","mountProps","el","Component","props","component","target","$destroy","registerComponent","buildRegisterFunction"],"mappings":"wFAGA,MAAMA,EAAgC,CACpCC,eAAiBC,IACf,MAAMC,GAAEA,EAAEC,UAAEA,EAASC,MAAEA,GAAUH,EAE3BI,EAAY,IAAIF,EAAU,CAAEG,OAAQJ,EAAIE,UAE9C,MAAO,KACLC,EAAUE,UAAU,CACrB,GAICC,EAAoBC,EAAsBV"}
@@ -64,10 +64,9 @@ class TurboMount {
64
64
  this.application.turboMount = this;
65
65
  this.application.register("turbo-mount", TurboMountController);
66
66
  document.addEventListener("turbo:before-morph-element", (event) => {
67
- var _a;
68
67
  const turboMorphEvent = event;
69
68
  const { target, detail } = turboMorphEvent;
70
- if ((_a = target.getAttribute("data-controller")) === null || _a === void 0 ? void 0 : _a.includes("turbo-mount")) {
69
+ if (target.getAttribute("data-controller")?.includes("turbo-mount")) {
71
70
  target.setAttribute("data-turbo-mount-props-value", detail.newElement.getAttribute("data-turbo-mount-props-value") ||
72
71
  "{}");
73
72
  event.preventDefault();
@@ -81,7 +80,7 @@ class TurboMount {
81
80
  }
82
81
  this.components.set(name, { component, plugin });
83
82
  if (controller) {
84
- const controllerName = `turbo-mount-${camelToKebabCase(name)}`;
83
+ const controllerName = `turbo-mount-${camelToKebabCase(name).replace("/", "--")}`;
85
84
  this.application.register(controllerName, controller);
86
85
  }
87
86
  }
@@ -108,19 +107,18 @@ function buildRegisterFunction(plugin) {
108
107
  }
109
108
 
110
109
  const identifierNames = (name) => {
111
- const controllerName = camelToKebabCase(name);
110
+ const controllerName = camelToKebabCase(name).replace("/", "--");
112
111
  return [`turbo-mount--${controllerName}`, `turbo-mount-${controllerName}`];
113
112
  };
114
113
  const registerComponentsBase = ({ plugin, turboMount, components, controllers, }) => {
115
- var _a;
116
- const controllerModules = controllers !== null && controllers !== void 0 ? controllers : [];
114
+ const controllerModules = controllers ?? [];
117
115
  for (const { module, filename } of components) {
118
116
  const name = filename
119
117
  .replace(/\.\w*$/, "")
120
118
  .replace(/^[./]*components\//, "");
121
119
  const identifiers = identifierNames(name);
122
120
  const controller = controllerModules.find(({ identifier }) => identifiers.includes(identifier));
123
- const component = (_a = module.default) !== null && _a !== void 0 ? _a : module;
121
+ const component = module.default ?? module;
124
122
  if (controller) {
125
123
  turboMount.register(plugin, name, component, controller.controllerConstructor);
126
124
  }
@@ -1,2 +1,2 @@
1
- import{Controller as t,Application as o}from"@hotwired/stimulus";class n extends t{constructor(){super(...arguments),this.skipPropsChangeCallback=!1}connect(){this._umountComponentCallback||(this._umountComponentCallback=this.mountComponent(this.mountElement,this.resolvedComponent,this.componentProps))}disconnect(){this.umountComponent()}propsValueChanged(){this.skipPropsChangeCallback?this.skipPropsChangeCallback=!1:(this.umountComponent(),this._umountComponentCallback||(this._umountComponentCallback=this.mountComponent(this.mountElement,this.resolvedComponent,this.componentProps)))}get componentProps(){return this.propsValue}get mountElement(){return this.hasMountTarget?this.mountTarget:this.element}get resolvedComponent(){return this.resolveMounted(this.componentValue).component}get resolvedPlugin(){return this.resolveMounted(this.componentValue).plugin}umountComponent(){this._umountComponentCallback&&this._umountComponentCallback(),this._umountComponentCallback=void 0}mountComponent(t,o,n){return this.resolvedPlugin.mountComponent({el:t,Component:o,props:n})}resolveMounted(t){return this.application.turboMount.resolve(t)}setComponentProps(t){this.skipPropsChangeCallback=!0,this.propsValue=t}}n.values={props:Object,component:String},n.targets=["mount"];const e=t=>t.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase();class r{constructor(t={}){this.components=new Map,this.application=this.findOrStartApplication(t.application),this.application.turboMount=this,this.application.register("turbo-mount",n),document.addEventListener("turbo:before-morph-element",(t=>{var o;const n=t,{target:e,detail:r}=n;(null===(o=e.getAttribute("data-controller"))||void 0===o?void 0:o.includes("turbo-mount"))&&(e.setAttribute("data-turbo-mount-props-value",r.newElement.getAttribute("data-turbo-mount-props-value")||"{}"),t.preventDefault())}))}register(t,o,r,s){if(s||(s=n),this.components.has(o))throw new Error(`Component '${o}' is already registered.`);if(this.components.set(o,{component:r,plugin:t}),s){const t=`turbo-mount-${e(o)}`;this.application.register(t,s)}}resolve(t){const o=this.components.get(t);if(!o)throw new Error(`Unknown component: ${t}`);return o}findOrStartApplication(t){let n=t||window.Stimulus;return n||(n=o.start(),window.Stimulus=n),n}}function s(t){return(o,n,e,r)=>{o.register(t,n,e,r)}}const i=t=>{const o=e(t);return[`turbo-mount--${o}`,`turbo-mount-${o}`]},u=({plugin:t,turboMount:o,components:n,controllers:e})=>{var r;const s=null!=e?e:[];for(const{module:e,filename:u}of n){const n=u.replace(/\.\w*$/,"").replace(/^[./]*components\//,""),l=i(n),p=s.find((({identifier:t})=>l.includes(t))),a=null!==(r=e.default)&&void 0!==r?r:e;p?o.register(t,n,a,p.controllerConstructor):o.register(t,n,a)}};export{r as TurboMount,n as TurboMountController,s as buildRegisterFunction,u as registerComponentsBase};
1
+ import{Controller as t,Application as o}from"@hotwired/stimulus";class n extends t{constructor(){super(...arguments),this.skipPropsChangeCallback=!1}connect(){this._umountComponentCallback||(this._umountComponentCallback=this.mountComponent(this.mountElement,this.resolvedComponent,this.componentProps))}disconnect(){this.umountComponent()}propsValueChanged(){this.skipPropsChangeCallback?this.skipPropsChangeCallback=!1:(this.umountComponent(),this._umountComponentCallback||(this._umountComponentCallback=this.mountComponent(this.mountElement,this.resolvedComponent,this.componentProps)))}get componentProps(){return this.propsValue}get mountElement(){return this.hasMountTarget?this.mountTarget:this.element}get resolvedComponent(){return this.resolveMounted(this.componentValue).component}get resolvedPlugin(){return this.resolveMounted(this.componentValue).plugin}umountComponent(){this._umountComponentCallback&&this._umountComponentCallback(),this._umountComponentCallback=void 0}mountComponent(t,o,n){return this.resolvedPlugin.mountComponent({el:t,Component:o,props:n})}resolveMounted(t){return this.application.turboMount.resolve(t)}setComponentProps(t){this.skipPropsChangeCallback=!0,this.propsValue=t}}n.values={props:Object,component:String},n.targets=["mount"];const e=t=>t.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase();class r{constructor(t={}){this.components=new Map,this.application=this.findOrStartApplication(t.application),this.application.turboMount=this,this.application.register("turbo-mount",n),document.addEventListener("turbo:before-morph-element",(t=>{const o=t,{target:n,detail:e}=o;n.getAttribute("data-controller")?.includes("turbo-mount")&&(n.setAttribute("data-turbo-mount-props-value",e.newElement.getAttribute("data-turbo-mount-props-value")||"{}"),t.preventDefault())}))}register(t,o,r,s){if(s||(s=n),this.components.has(o))throw new Error(`Component '${o}' is already registered.`);if(this.components.set(o,{component:r,plugin:t}),s){const t=`turbo-mount-${e(o).replace("/","--")}`;this.application.register(t,s)}}resolve(t){const o=this.components.get(t);if(!o)throw new Error(`Unknown component: ${t}`);return o}findOrStartApplication(t){let n=t||window.Stimulus;return n||(n=o.start(),window.Stimulus=n),n}}function s(t){return(o,n,e,r)=>{o.register(t,n,e,r)}}const i=t=>{const o=e(t).replace("/","--");return[`turbo-mount--${o}`,`turbo-mount-${o}`]},u=({plugin:t,turboMount:o,components:n,controllers:e})=>{const r=e??[];for(const{module:e,filename:s}of n){const n=s.replace(/\.\w*$/,"").replace(/^[./]*components\//,""),u=i(n),p=r.find((({identifier:t})=>u.includes(t))),l=e.default??e;p?o.register(t,n,l,p.controllerConstructor):o.register(t,n,l)}};export{r as TurboMount,n as TurboMountController,s as buildRegisterFunction,u as registerComponentsBase};
2
2
  //# sourceMappingURL=turbo-mount.min.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"turbo-mount.min.js","sources":["../src/turbo-mount-controller.ts","../src/helpers.ts","../src/turbo-mount.ts","../src/registerComponentsBase.ts"],"sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\nimport { ApplicationWithTurboMount } from \"./turbo-mount\";\n\nexport class TurboMountController extends Controller {\n static values = {\n props: Object,\n component: String,\n };\n static targets = [\"mount\"];\n\n private skipPropsChangeCallback = false;\n\n declare propsValue: object;\n declare componentValue: string;\n declare readonly hasMountTarget: boolean;\n declare readonly mountTarget: Element;\n\n _umountComponentCallback?: () => void;\n\n connect() {\n this._umountComponentCallback ||= this.mountComponent(\n this.mountElement,\n this.resolvedComponent,\n this.componentProps,\n );\n }\n\n disconnect() {\n this.umountComponent();\n }\n\n propsValueChanged() {\n // Prevent re-mounting the component if the props are being set by the component itself\n if (this.skipPropsChangeCallback) {\n this.skipPropsChangeCallback = false;\n return;\n }\n\n this.umountComponent();\n this._umountComponentCallback ||= this.mountComponent(\n this.mountElement,\n this.resolvedComponent,\n this.componentProps,\n );\n }\n\n get componentProps() {\n return this.propsValue;\n }\n\n get mountElement() {\n return this.hasMountTarget ? this.mountTarget : this.element;\n }\n\n get resolvedComponent() {\n return this.resolveMounted(this.componentValue).component;\n }\n\n get resolvedPlugin() {\n return this.resolveMounted(this.componentValue).plugin;\n }\n\n umountComponent() {\n this._umountComponentCallback && this._umountComponentCallback();\n this._umountComponentCallback = undefined;\n }\n\n mountComponent(el: Element, Component: unknown, props: object) {\n return this.resolvedPlugin.mountComponent({ el, Component, props });\n }\n\n resolveMounted(component: string) {\n const app = this.application as ApplicationWithTurboMount;\n return app.turboMount.resolve(component);\n }\n\n setComponentProps(props: object) {\n this.skipPropsChangeCallback = true;\n this.propsValue = props;\n }\n}\n","export const camelToKebabCase = (str: string) => {\n return str.replace(/([a-z])([A-Z])/g, \"$1-$2\").toLowerCase();\n};\n","import { Application, ControllerConstructor } from \"@hotwired/stimulus\";\n\nimport { camelToKebabCase } from \"./helpers\";\nimport { TurboMountController } from \"./turbo-mount-controller\";\n\ndeclare global {\n interface Window {\n Stimulus?: Application;\n }\n}\n\nexport interface ApplicationWithTurboMount extends Application {\n turboMount: TurboMount;\n}\n\nexport type MountComponentProps<T> = {\n el: Element;\n Component: T;\n props: object;\n};\n\nexport type Plugin<T> = {\n mountComponent: (props: MountComponentProps<T>) => () => void;\n};\n\nexport type TurboMountProps = {\n application?: Application;\n};\n\ntype TurboMountComponents<T> = Map<string, { component: T; plugin: Plugin<T> }>;\n\ninterface TurboMorphEvent extends CustomEvent {\n target: Element;\n detail: {\n newElement: Element;\n };\n}\n\nexport class TurboMount {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n components: TurboMountComponents<any>;\n application: ApplicationWithTurboMount;\n\n constructor(props: TurboMountProps = {}) {\n this.components = new Map();\n this.application = this.findOrStartApplication(props.application);\n this.application.turboMount = this;\n this.application.register(\"turbo-mount\", TurboMountController);\n\n document.addEventListener(\"turbo:before-morph-element\", (event) => {\n const turboMorphEvent = event as unknown as TurboMorphEvent;\n const { target, detail } = turboMorphEvent;\n\n if (target.getAttribute(\"data-controller\")?.includes(\"turbo-mount\")) {\n target.setAttribute(\n \"data-turbo-mount-props-value\",\n detail.newElement.getAttribute(\"data-turbo-mount-props-value\") ||\n \"{}\",\n );\n event.preventDefault();\n }\n });\n }\n\n register<T>(\n plugin: Plugin<T>,\n name: string,\n component: T,\n controller?: ControllerConstructor,\n ) {\n controller ||= TurboMountController;\n if (this.components.has(name)) {\n throw new Error(`Component '${name}' is already registered.`);\n }\n this.components.set(name, { component, plugin });\n\n if (controller) {\n const controllerName = `turbo-mount-${camelToKebabCase(name)}`;\n this.application.register(controllerName, controller);\n }\n }\n\n resolve(name: string) {\n const component = this.components.get(name);\n if (!component) {\n throw new Error(`Unknown component: ${name}`);\n }\n return component;\n }\n\n private findOrStartApplication(hydratedApp?: Application) {\n let application = hydratedApp || window.Stimulus;\n\n if (!application) {\n application = Application.start();\n window.Stimulus = application;\n }\n return application as ApplicationWithTurboMount;\n }\n}\n\nexport function buildRegisterFunction<T>(plugin: Plugin<T>) {\n return (\n turboMount: TurboMount,\n name: string,\n component: T,\n controller?: ControllerConstructor,\n ) => {\n turboMount.register(plugin, name, component, controller);\n };\n}\n","import { Definition } from \"@hotwired/stimulus\";\n\nimport { TurboMount, Plugin } from \"./turbo-mount\";\nimport { camelToKebabCase } from \"./helpers\";\n\nexport type ComponentModule = { default: never } | never;\n\nexport type ComponentDefinition = {\n filename: string;\n module: ComponentModule;\n};\n\ntype RegisterComponentsProps<T> = {\n plugin: Plugin<T>;\n turboMount: TurboMount;\n components: ComponentDefinition[];\n controllers?: Definition[];\n};\n\nconst identifierNames = (name: string) => {\n const controllerName = camelToKebabCase(name);\n\n return [`turbo-mount--${controllerName}`, `turbo-mount-${controllerName}`];\n};\n\nexport const registerComponentsBase = <T>({\n plugin,\n turboMount,\n components,\n controllers,\n}: RegisterComponentsProps<T>) => {\n const controllerModules = controllers ?? [];\n\n for (const { module, filename } of components) {\n const name = filename\n .replace(/\\.\\w*$/, \"\")\n .replace(/^[./]*components\\//, \"\");\n\n const identifiers = identifierNames(name);\n\n const controller = controllerModules.find(({ identifier }) =>\n identifiers.includes(identifier),\n );\n const component = module.default ?? module;\n\n if (controller) {\n turboMount.register(\n plugin,\n name,\n component,\n controller.controllerConstructor,\n );\n } else {\n turboMount.register(plugin, name, component);\n }\n }\n};\n"],"names":["TurboMountController","Controller","constructor","this","skipPropsChangeCallback","connect","_umountComponentCallback","mountComponent","mountElement","resolvedComponent","componentProps","disconnect","umountComponent","propsValueChanged","propsValue","hasMountTarget","mountTarget","element","resolveMounted","componentValue","component","resolvedPlugin","plugin","undefined","el","Component","props","application","turboMount","resolve","setComponentProps","values","Object","String","targets","camelToKebabCase","str","replace","toLowerCase","TurboMount","components","Map","findOrStartApplication","register","document","addEventListener","event","turboMorphEvent","target","detail","_a","getAttribute","includes","setAttribute","newElement","preventDefault","name","controller","has","Error","set","controllerName","get","hydratedApp","window","Stimulus","Application","start","buildRegisterFunction","identifierNames","registerComponentsBase","controllers","controllerModules","module","filename","identifiers","find","identifier","default","controllerConstructor"],"mappings":"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"}
1
+ {"version":3,"file":"turbo-mount.min.js","sources":["../src/turbo-mount-controller.ts","../src/helpers.ts","../src/turbo-mount.ts","../src/registerComponentsBase.ts"],"sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\nimport { ApplicationWithTurboMount } from \"./turbo-mount\";\n\nexport class TurboMountController extends Controller {\n static values = {\n props: Object,\n component: String,\n };\n static targets = [\"mount\"];\n\n private skipPropsChangeCallback = false;\n\n declare propsValue: object;\n declare componentValue: string;\n declare readonly hasMountTarget: boolean;\n declare readonly mountTarget: Element;\n\n _umountComponentCallback?: () => void;\n\n connect() {\n this._umountComponentCallback ||= this.mountComponent(\n this.mountElement,\n this.resolvedComponent,\n this.componentProps,\n );\n }\n\n disconnect() {\n this.umountComponent();\n }\n\n propsValueChanged() {\n // Prevent re-mounting the component if the props are being set by the component itself\n if (this.skipPropsChangeCallback) {\n this.skipPropsChangeCallback = false;\n return;\n }\n\n this.umountComponent();\n this._umountComponentCallback ||= this.mountComponent(\n this.mountElement,\n this.resolvedComponent,\n this.componentProps,\n );\n }\n\n get componentProps() {\n return this.propsValue;\n }\n\n get mountElement() {\n return this.hasMountTarget ? this.mountTarget : this.element;\n }\n\n get resolvedComponent() {\n return this.resolveMounted(this.componentValue).component;\n }\n\n get resolvedPlugin() {\n return this.resolveMounted(this.componentValue).plugin;\n }\n\n umountComponent() {\n this._umountComponentCallback && this._umountComponentCallback();\n this._umountComponentCallback = undefined;\n }\n\n mountComponent(el: Element, Component: unknown, props: object) {\n return this.resolvedPlugin.mountComponent({ el, Component, props });\n }\n\n resolveMounted(component: string) {\n const app = this.application as ApplicationWithTurboMount;\n return app.turboMount.resolve(component);\n }\n\n setComponentProps(props: object) {\n this.skipPropsChangeCallback = true;\n this.propsValue = props;\n }\n}\n","export const camelToKebabCase = (str: string) => {\n return str.replace(/([a-z])([A-Z])/g, \"$1-$2\").toLowerCase();\n};\n","import { Application, ControllerConstructor } from \"@hotwired/stimulus\";\n\nimport { camelToKebabCase } from \"./helpers\";\nimport { TurboMountController } from \"./turbo-mount-controller\";\n\ndeclare global {\n interface Window {\n Stimulus?: Application;\n }\n}\n\nexport interface ApplicationWithTurboMount extends Application {\n turboMount: TurboMount;\n}\n\nexport type MountComponentProps<T> = {\n el: Element;\n Component: T;\n props: object;\n};\n\nexport type Plugin<T> = {\n mountComponent: (props: MountComponentProps<T>) => () => void;\n};\n\nexport type TurboMountProps = {\n application?: Application;\n};\n\ntype TurboMountComponents<T> = Map<string, { component: T; plugin: Plugin<T> }>;\n\ninterface TurboMorphEvent extends CustomEvent {\n target: Element;\n detail: {\n newElement: Element;\n };\n}\n\nexport class TurboMount {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n components: TurboMountComponents<any>;\n application: ApplicationWithTurboMount;\n\n constructor(props: TurboMountProps = {}) {\n this.components = new Map();\n this.application = this.findOrStartApplication(props.application);\n this.application.turboMount = this;\n this.application.register(\"turbo-mount\", TurboMountController);\n\n document.addEventListener(\"turbo:before-morph-element\", (event) => {\n const turboMorphEvent = event as unknown as TurboMorphEvent;\n const { target, detail } = turboMorphEvent;\n\n if (target.getAttribute(\"data-controller\")?.includes(\"turbo-mount\")) {\n target.setAttribute(\n \"data-turbo-mount-props-value\",\n detail.newElement.getAttribute(\"data-turbo-mount-props-value\") ||\n \"{}\",\n );\n event.preventDefault();\n }\n });\n }\n\n register<T>(\n plugin: Plugin<T>,\n name: string,\n component: T,\n controller?: ControllerConstructor,\n ) {\n controller ||= TurboMountController;\n if (this.components.has(name)) {\n throw new Error(`Component '${name}' is already registered.`);\n }\n this.components.set(name, { component, plugin });\n\n if (controller) {\n const controllerName = `turbo-mount-${camelToKebabCase(name).replace(\"/\", \"--\")}`;\n this.application.register(controllerName, controller);\n }\n }\n\n resolve(name: string) {\n const component = this.components.get(name);\n if (!component) {\n throw new Error(`Unknown component: ${name}`);\n }\n return component;\n }\n\n private findOrStartApplication(hydratedApp?: Application) {\n let application = hydratedApp || window.Stimulus;\n\n if (!application) {\n application = Application.start();\n window.Stimulus = application;\n }\n return application as ApplicationWithTurboMount;\n }\n}\n\nexport function buildRegisterFunction<T>(plugin: Plugin<T>) {\n return (\n turboMount: TurboMount,\n name: string,\n component: T,\n controller?: ControllerConstructor,\n ) => {\n turboMount.register(plugin, name, component, controller);\n };\n}\n","import { Definition } from \"@hotwired/stimulus\";\n\nimport { TurboMount, Plugin } from \"./turbo-mount\";\nimport { camelToKebabCase } from \"./helpers\";\n\nexport type ComponentModule = { default: never } | never;\n\nexport type ComponentDefinition = {\n filename: string;\n module: ComponentModule;\n};\n\ntype RegisterComponentsProps<T> = {\n plugin: Plugin<T>;\n turboMount: TurboMount;\n components: ComponentDefinition[];\n controllers?: Definition[];\n};\n\nconst identifierNames = (name: string) => {\n const controllerName = camelToKebabCase(name).replace(\"/\", \"--\");\n\n return [`turbo-mount--${controllerName}`, `turbo-mount-${controllerName}`];\n};\n\nexport const registerComponentsBase = <T>({\n plugin,\n turboMount,\n components,\n controllers,\n}: RegisterComponentsProps<T>) => {\n const controllerModules = controllers ?? [];\n\n for (const { module, filename } of components) {\n const name = filename\n .replace(/\\.\\w*$/, \"\")\n .replace(/^[./]*components\\//, \"\");\n\n const identifiers = identifierNames(name);\n\n const controller = controllerModules.find(({ identifier }) =>\n identifiers.includes(identifier),\n );\n const component = module.default ?? module;\n\n if (controller) {\n turboMount.register(\n plugin,\n name,\n component,\n controller.controllerConstructor,\n );\n } else {\n turboMount.register(plugin, name, component);\n }\n }\n};\n"],"names":["TurboMountController","Controller","constructor","this","skipPropsChangeCallback","connect","_umountComponentCallback","mountComponent","mountElement","resolvedComponent","componentProps","disconnect","umountComponent","propsValueChanged","propsValue","hasMountTarget","mountTarget","element","resolveMounted","componentValue","component","resolvedPlugin","plugin","undefined","el","Component","props","application","turboMount","resolve","setComponentProps","values","Object","String","targets","camelToKebabCase","str","replace","toLowerCase","TurboMount","components","Map","findOrStartApplication","register","document","addEventListener","event","turboMorphEvent","target","detail","getAttribute","includes","setAttribute","newElement","preventDefault","name","controller","has","Error","set","controllerName","get","hydratedApp","window","Stimulus","Application","start","buildRegisterFunction","identifierNames","registerComponentsBase","controllers","controllerModules","module","filename","identifiers","find","identifier","default","controllerConstructor"],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ react:
2
+ pins: "react react-dom react-dom/client"
3
+ npm_packages: "react react-dom"
4
+ vite_plugin: "@vitejs/plugin-react"
5
+ extension: "jsx"
6
+
7
+ vue:
8
+ pins: "vue"
9
+ npm_packages: "vue"
10
+ vite_plugin: "@vitejs/plugin-vue"
11
+ extension: "vue"
12
+
13
+ svelte4:
14
+ pins: "svelte"
15
+ npm_packages: "svelte@4"
16
+ vite_plugin: "@sveltejs/vite-plugin-svelte"
17
+ extension: "svelte"
18
+
19
+ svelte:
20
+ pins: "svelte"
21
+ npm_packages: "svelte@5"
22
+ vite_plugin: "@sveltejs/vite-plugin-svelte"
23
+ extension: "svelte"
@@ -0,0 +1,61 @@
1
+ module TurboMount
2
+ module Generators
3
+ module Helpers
4
+ ### FS Helpers
5
+ def js_destination_path
6
+ return ViteRuby.config.source_code_dir if defined?(ViteRuby)
7
+
8
+ if file?("config/vite.json")
9
+ source_code_dir = JSON.parse(File.read(file_path("config/vite.json"))).dig("all", "sourceCodeDir")
10
+ return source_code_dir || "app/frontend"
11
+ end
12
+
13
+ "app/javascript"
14
+ end
15
+
16
+ def js_destination_root
17
+ file_path(js_destination_path)
18
+ end
19
+
20
+ def js_entrypoint
21
+ if vite?
22
+ js_file_path "entrypoints/application.js"
23
+ else
24
+ js_file_path "application.js"
25
+ end
26
+ end
27
+
28
+ def js_file_path(*relative_path)
29
+ File.join(js_destination_root, *relative_path)
30
+ end
31
+
32
+ def file?(*relative_path)
33
+ File.file?(file_path(*relative_path))
34
+ end
35
+
36
+ def file_path(*relative_path)
37
+ File.join(destination_root, *relative_path)
38
+ end
39
+
40
+ def vite?
41
+ file?("config/vite.json") && Dir.glob(file_path("vite.config.*")).any?
42
+ end
43
+
44
+ # Interactivity Helpers
45
+ def ask(*)
46
+ unless options[:interactive]
47
+ say_error "Specify all options when running the generator non-interactively.", :red
48
+ exit(1)
49
+ end
50
+
51
+ super
52
+ end
53
+
54
+ def yes?(*)
55
+ return false unless options[:interactive]
56
+
57
+ super
58
+ end
59
+ end
60
+ end
61
+ end
@@ -13,6 +13,6 @@ const turboMount = new TurboMount();
13
13
  // If you want to automatically register components use:
14
14
  // import { registerComponents } from "turbo-mount/registerComponents/<%= framework %>";
15
15
  // const controllers = import.meta.glob("/controllers/**/*_controller.js", { eager: true });
16
- // const components = import.meta.glob("/components/**/*.jsx", { eager: true });
16
+ // const components = import.meta.glob("/components/**/*.<%= extension %>", { eager: true });
17
17
  // registerComponents({ turboMount, components, controllers });
18
18
  <%- end -%>
@@ -1,37 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "yaml"
4
+ require "rails/generators"
5
+ require "rails/generators/base"
6
+
7
+ require_relative "helpers"
8
+ require_relative "js_package_manager"
9
+
3
10
  module TurboMount
4
11
  module Generators
5
12
  class InstallGenerator < Rails::Generators::Base
6
- FRAMEWORKS = {
7
- "react" => {
8
- pins: "react react-dom react-dom/client",
9
- npm_packages: "react react-dom",
10
- vite_plugin: "@vitejs/plugin-react"
11
- },
12
- "vue" => {
13
- pins: "vue",
14
- npm_packages: "vue",
15
- vite_plugin: "@vitejs/plugin-vue"
16
- },
17
- "svelte" => {
18
- pins: "svelte",
19
- npm_packages: "svelte",
20
- vite_plugin: "@sveltejs/vite-plugin-svelte"
21
- }
22
- }.freeze
13
+ include Helpers
14
+
15
+ FRAMEWORKS = YAML.load_file(File.expand_path("./frameworks.yml", __dir__))
23
16
 
24
17
  source_root File.expand_path("install", __dir__)
25
18
 
19
+ class_option :framework, type: :string,
20
+ desc: "The framework you want to use with Turbo Mount",
21
+ enum: FRAMEWORKS.keys,
22
+ default: nil
23
+
24
+ class_option :package_manager, type: :string, default: nil,
25
+ enum: JSPackageManager.package_managers,
26
+ desc: "The package manager you want to use to install Turbo Mount"
27
+
28
+ class_option :verbose, type: :boolean, default: false,
29
+ desc: "Run the generator in verbose mode"
30
+
26
31
  def install
27
32
  say "Installing Turbo Mount"
28
33
 
29
- if build_tool.nil?
30
- say "Could not find a package.json or config/importmap.rb file to add the turbo-mount dependency to, please add it manually.", :red
31
- exit!
32
- end
34
+ package_manager.validate!
33
35
 
34
- if importmap?
36
+ if package_manager.importmap?
35
37
  install_importmap
36
38
  else
37
39
  install_nodejs
@@ -43,17 +45,10 @@ module TurboMount
43
45
  private
44
46
 
45
47
  def install_nodejs
46
- case build_tool
47
- when "npm"
48
- run "npm install turbo-mount #{FRAMEWORKS[framework][:npm_packages]}"
49
- when "yarn"
50
- run "yarn add turbo-mount #{FRAMEWORKS[framework][:npm_packages]}"
51
- when "bun"
52
- run "bun add turbo-mount #{FRAMEWORKS[framework][:npm_packages]}"
53
- end
48
+ package_manager.add_dependencies("turbo-mount", FRAMEWORKS[framework][:npm_packages])
54
49
 
55
50
  say "Creating Turbo Mount initializer"
56
- template "turbo-mount.js", File.join("app/javascript/turbo-mount.js")
51
+ template "turbo-mount.js", js_file_path("turbo-mount.js")
57
52
  begin
58
53
  append_to_file js_entrypoint, %(import "./turbo-mount"\n)
59
54
  rescue
@@ -64,7 +59,7 @@ module TurboMount
64
59
 
65
60
  def install_importmap
66
61
  say "Creating Turbo Mount initializer"
67
- template "turbo-mount.js", File.join("app/javascript/turbo-mount-initializer.js")
62
+ template "turbo-mount.js", js_file_path("turbo-mount-initializer.js")
68
63
  append_to_file "app/javascript/application.js", %(import "turbo-mount-initializer"\n)
69
64
 
70
65
  say "Pinning Turbo Mount to the importmap"
@@ -73,51 +68,23 @@ module TurboMount
73
68
  append_to_file "config/importmap.rb", %(pin "turbo-mount-initializer"\n)
74
69
 
75
70
  say "Pinning framework dependencies to the importmap"
76
- run "bin/importmap pin #{FRAMEWORKS[framework][:pins]}"
77
- end
78
-
79
- def js_entrypoint
80
- if vite?
81
- "app/javascript/entrypoints/application.js"
82
- else
83
- "app/javascript/application.js"
84
- end
85
- end
86
-
87
- def vite?
88
- Dir.glob(Rails.root.join("vite.config.*")).any?
89
- end
90
-
91
- def importmap?
92
- build_tool == "importmap"
71
+ package_manager.add_dependencies(FRAMEWORKS[framework][:pins])
93
72
  end
94
73
 
95
74
  def warn_about_vite_plugin
96
75
  say "Make sure to install and add #{FRAMEWORKS[framework][:vite_plugin]} to your Vite config", :yellow
97
76
  end
98
77
 
99
- def build_tool
100
- return @build_tool if defined?(@build_tool)
101
-
102
- @build_tool = detect_build_tool
78
+ def package_manager
79
+ @package_manager ||= JSPackageManager.new(self)
103
80
  end
104
81
 
105
- def detect_build_tool
106
- if Rails.root.join("package.json").exist?
107
- if Rails.root.join("package-lock.json").exist?
108
- "npm"
109
- elsif Rails.root.join("bun.config.js").exist?
110
- "bun"
111
- else
112
- "yarn"
113
- end
114
- elsif Rails.root.join("config/importmap.rb").exist?
115
- "importmap"
116
- end
82
+ def extension
83
+ FRAMEWORKS[framework][:extension]
117
84
  end
118
85
 
119
86
  def framework
120
- @framework ||= ask("What framework do you want to use with Turbo Mount?", limited_to: FRAMEWORKS.keys, default: "react")
87
+ @framework ||= options[:framework] || ask("What framework do you want to use with Turbo Mount?", :green, limited_to: FRAMEWORKS.keys, default: "react")
121
88
  end
122
89
  end
123
90
  end
@@ -0,0 +1,57 @@
1
+ module TurboMount
2
+ module Generators
3
+ class JSPackageManager
4
+ def self.package_managers
5
+ %w[npm yarn bun pnpm importmap]
6
+ end
7
+
8
+ def initialize(generator)
9
+ @generator = generator
10
+ @package_manager = generator.options[:package_manager] || detect_package_manager
11
+ end
12
+
13
+ def validate!
14
+ return if @package_manager.present?
15
+
16
+ @generator.say "Could not find a package.json or config/importmap.rb file to add the turbo-mount dependency to, please add it manually.", :red
17
+ exit(1)
18
+ end
19
+
20
+ def importmap?
21
+ @package_manager == "importmap"
22
+ end
23
+
24
+ def add_dependencies(*dependencies)
25
+ cmd =
26
+ if importmap?
27
+ "bin/importmap pin #{dependencies.join(" ")}"
28
+ else
29
+ "#{@package_manager} add #{dependencies.join(" ")}#{@generator.options[:verbose] ? "" : " --silent"}"
30
+ end
31
+ @generator.in_root { @generator.run cmd }
32
+ end
33
+
34
+ private
35
+
36
+ def detect_package_manager
37
+ if file?("package.json")
38
+ if file?("package-lock.json")
39
+ "npm"
40
+ elsif file?("pnpm-lock.yaml")
41
+ "pnpm"
42
+ elsif file?("bun.lockb")
43
+ "bun"
44
+ else
45
+ "yarn"
46
+ end
47
+ elsif file?("config/importmap.rb")
48
+ "importmap"
49
+ end
50
+ end
51
+
52
+ def file?(*relative_path)
53
+ @generator.file?(*relative_path)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -6,7 +6,7 @@ module Turbo
6
6
  def turbo_mount(component_name, props: {}, tag: "div", **attrs, &block)
7
7
  raise TypeError, "Component name expected" unless component_name.is_a? String
8
8
 
9
- controller_name = "turbo-mount-#{component_name.underscore.dasherize}"
9
+ controller_name = "turbo-mount-#{component_name.underscore.dasherize.gsub("/", "--")}"
10
10
  attrs["data-controller"] = controller_name
11
11
  prefix = "data-#{controller_name}"
12
12
  attrs["#{prefix}-component-value"] = component_name
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Turbo
4
4
  module Mount
5
- VERSION = "0.3.2"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: turbo-mount
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Svyatoslav Kryukov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-06-24 00:00:00.000000000 Z
11
+ date: 2024-11-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -44,11 +44,17 @@ files:
44
44
  - app/assets/javascripts/turbo-mount/svelte.js
45
45
  - app/assets/javascripts/turbo-mount/svelte.min.js
46
46
  - app/assets/javascripts/turbo-mount/svelte.min.js.map
47
+ - app/assets/javascripts/turbo-mount/svelte4.js
48
+ - app/assets/javascripts/turbo-mount/svelte4.min.js
49
+ - app/assets/javascripts/turbo-mount/svelte4.min.js.map
47
50
  - app/assets/javascripts/turbo-mount/vue.js
48
51
  - app/assets/javascripts/turbo-mount/vue.min.js
49
52
  - app/assets/javascripts/turbo-mount/vue.min.js.map
53
+ - lib/generators/turbo_mount/frameworks.yml
54
+ - lib/generators/turbo_mount/helpers.rb
50
55
  - lib/generators/turbo_mount/install/turbo-mount.js.tt
51
56
  - lib/generators/turbo_mount/install_generator.rb
57
+ - lib/generators/turbo_mount/js_package_manager.rb
52
58
  - lib/turbo/mount.rb
53
59
  - lib/turbo/mount/engine.rb
54
60
  - lib/turbo/mount/helpers.rb
@@ -78,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
78
84
  - !ruby/object:Gem::Version
79
85
  version: '0'
80
86
  requirements: []
81
- rubygems_version: 3.5.7
87
+ rubygems_version: 3.5.17
82
88
  signing_key:
83
89
  specification_version: 4
84
90
  summary: Use React, Vue, Svelte, and other components with Hotwire.