turbo-mount 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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":"iEAGM,MAAOA,UAA6BC,EAA1C,WAAAC,uBAOUC,KAAuBC,yBAAG,CAsEnC,CA7DC,OAAAC,GACEF,KAAKG,2BAALH,KAAKG,yBAA6BH,KAAKI,eACrCJ,KAAKK,aACLL,KAAKM,kBACLN,KAAKO,gBAER,CAED,UAAAC,GACER,KAAKS,iBACN,CAED,iBAAAC,GAEMV,KAAKC,wBACPD,KAAKC,yBAA0B,GAIjCD,KAAKS,kBACLT,KAAKG,2BAALH,KAAKG,yBAA6BH,KAAKI,eACrCJ,KAAKK,aACLL,KAAKM,kBACLN,KAAKO,iBAER,CAED,kBAAIA,GACF,OAAOP,KAAKW,UACb,CAED,gBAAIN,GACF,OAAOL,KAAKY,eAAiBZ,KAAKa,YAAcb,KAAKc,OACtD,CAED,qBAAIR,GACF,OAAON,KAAKe,eAAef,KAAKgB,gBAAgBC,SACjD,CAED,kBAAIC,GACF,OAAOlB,KAAKe,eAAef,KAAKgB,gBAAgBG,MACjD,CAED,eAAAV,GACET,KAAKG,0BAA4BH,KAAKG,2BACtCH,KAAKG,8BAA2BiB,CACjC,CAED,cAAAhB,CAAeiB,EAAaC,EAAoBC,GAC9C,OAAOvB,KAAKkB,eAAed,eAAe,CAAEiB,KAAIC,YAAWC,SAC5D,CAED,cAAAR,CAAeE,GAEb,OADYjB,KAAKwB,YACNC,WAAWC,QAAQT,EAC/B,CAED,iBAAAU,CAAkBJ,GAChBvB,KAAKC,yBAA0B,EAC/BD,KAAKW,WAAaY,CACnB,EA3EM1B,EAAA+B,OAAS,CACdL,MAAOM,OACPZ,UAAWa,QAENjC,EAAAkC,QAAU,CAAC,SCRb,MAAMC,EAAoBC,GACxBA,EAAIC,QAAQ,kBAAmB,SAASC,oBCqCpCC,EAKX,WAAArC,CAAYwB,EAAyB,IACnCvB,KAAKqC,WAAa,IAAIC,IACtBtC,KAAKwB,YAAcxB,KAAKuC,uBAAuBhB,EAAMC,aACrDxB,KAAKwB,YAAYC,WAAazB,KAC9BA,KAAKwB,YAAYgB,SAAS,cAAe3C,GAEzC4C,SAASC,iBAAiB,8BAA+BC,UACvD,MAAMC,EAAkBD,GAClBE,OAAEA,EAAMC,OAAEA,GAAWF,GAEe,QAAtCG,EAAAF,EAAOG,aAAa,0BAAkB,IAAAD,OAAA,EAAAA,EAAEE,SAAS,kBACnDJ,EAAOK,aACL,+BACAJ,EAAOK,WAAWH,aAAa,iCAC7B,MAEJL,EAAMS,iBACP,GAEJ,CAED,QAAAZ,CACErB,EACAkC,EACApC,EACAqC,GAGA,GADAA,IAAAA,EAAezD,GACXG,KAAKqC,WAAWkB,IAAIF,GACtB,MAAM,IAAIG,MAAM,cAAcH,6BAIhC,GAFArD,KAAKqC,WAAWoB,IAAIJ,EAAM,CAAEpC,YAAWE,WAEnCmC,EAAY,CACd,MAAMI,EAAiB,eAAe1B,EAAiBqB,KACvDrD,KAAKwB,YAAYgB,SAASkB,EAAgBJ,EAC3C,CACF,CAED,OAAA5B,CAAQ2B,GACN,MAAMpC,EAAYjB,KAAKqC,WAAWsB,IAAIN,GACtC,IAAKpC,EACH,MAAM,IAAIuC,MAAM,sBAAsBH,KAExC,OAAOpC,CACR,CAEO,sBAAAsB,CAAuBqB,GAC7B,IAAIpC,EAAcoC,GAAeC,OAAOC,SAMxC,OAJKtC,IACHA,EAAcuC,EAAYC,QAC1BH,OAAOC,SAAWtC,GAEbA,CACR,EAGG,SAAUyC,EAAyB9C,GACvC,MAAO,CACLM,EACA4B,EACApC,EACAqC,KAEA7B,EAAWe,SAASrB,EAAQkC,EAAMpC,EAAWqC,EAAW,CAE5D,CC3FA,MAAMY,EAAmBb,IACvB,MAAMK,EAAiB1B,EAAiBqB,GAExC,MAAO,CAAC,gBAAgBK,IAAkB,eAAeA,IAAiB,EAG/DS,EAAyB,EACpChD,SACAM,aACAY,aACA+B,wBAEA,MAAMC,EAAoBD,QAAAA,EAAe,GAEzC,IAAK,MAAME,OAAEA,EAAMC,SAAEA,KAAclC,EAAY,CAC7C,MAAMgB,EAAOkB,EACVrC,QAAQ,SAAU,IAClBA,QAAQ,qBAAsB,IAE3BsC,EAAcN,EAAgBb,GAE9BC,EAAae,EAAkBI,MAAK,EAAGC,gBAC3CF,EAAYvB,SAASyB,KAEjBzD,EAA0B,QAAd8B,EAAAuB,EAAOK,eAAO,IAAA5B,EAAAA,EAAIuB,EAEhChB,EACF7B,EAAWe,SACTrB,EACAkC,EACApC,EACAqC,EAAWsB,uBAGbnD,EAAWe,SAASrB,EAAQkC,EAAMpC,EAErC"}
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.