turbo-mount 0.3.3 → 0.4.1

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: 6385a766c9e0173e886eb33c27946e222dba5ee810744af4c6d317a93d80b315
4
- data.tar.gz: 0c4402e72e9a5b6a98b8219e0ad8749ee3b2716f99412ebe3fcd35783798be7a
3
+ metadata.gz: 0b29826ddb780be9b604f24aa2a2a4a8635b1dc95106315baeef4323ace52172
4
+ data.tar.gz: 8f70abb8e1a8a44d26c294f30173b366502e4fe7f6a6e55c4dfe87778f59d883
5
5
  SHA512:
6
- metadata.gz: bf22bb88a3042c20b96774167c25b500146f1ba002f2224609e4444afd968e68527c664970ca823c1b73f09e74c685cd7d51f7354fd838bceb8d1b00a5f13d14
7
- data.tar.gz: e4c5cb4ee14f437e35d49d4d36ec5e423649f8e5685319dec43c1c6f35f1fca8f7231eb1bf016ce9b0fb274eab184145049446d6ccfa579657fce151bffc3fcf
6
+ metadata.gz: aaf7c5d7d5813d7a34137103cee1701ba05400bb3b60e1a5c72da1a261cf4930fa2566c7cba4184740cb4dc0ae91ff28c7ce724d8aca3015b46b807dbe480afa
7
+ data.tar.gz: 72dd49789b076a28aa63df75a607438998748dd69793dbd2db82d300c4022007e567b88342755aa6212d5402b4be86579c9d50549f2941e436bbdf76600441d1
data/CHANGELOG.md CHANGED
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning].
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.1] - 2024-11-26
11
+
12
+ ### Added
13
+
14
+ - Support Shakapacker in the installation script. ([@skryukov])
15
+
16
+ ### Fixed
17
+
18
+ - Fix installation script interactive mode. ([@skryukov])
19
+ - Fix installation script dependencies installation. ([@skryukov])
20
+
21
+ ## [0.4.0] - 2024-11-03
22
+
23
+ ### Added
24
+
25
+ - [BREAKING] Support Svelte 5. ([@skryukov])
26
+ `turbo-mount/svelte` is now the Svelte 5 plugin. The Svelte 4 plugin is now `turbo-mount/svelte4`.
27
+
10
28
  ## [0.3.3] - 2024-09-24
11
29
 
12
30
  ### Added
@@ -75,7 +93,9 @@ and this project adheres to [Semantic Versioning].
75
93
  [@jkogara]: https://github.com/jkogara
76
94
  [@skryukov]: https://github.com/skryukov
77
95
 
78
- [Unreleased]: https://github.com/skryukov/turbo-mount/compare/v0.3.3...HEAD
96
+ [Unreleased]: https://github.com/skryukov/turbo-mount/compare/v0.4.1...HEAD
97
+ [0.4.1]: https://github.com/skryukov/turbo-mount/compare/v0.4.0...v0.4.1
98
+ [0.4.0]: https://github.com/skryukov/turbo-mount/compare/v0.3.3...v0.4.0
79
99
  [0.3.3]: https://github.com/skryukov/turbo-mount/compare/v0.3.2...v0.3.3
80
100
  [0.3.2]: https://github.com/skryukov/turbo-mount/compare/v0.3.1...v0.3.2
81
101
  [0.3.1]: https://github.com/skryukov/turbo-mount/compare/v0.3.0...v0.3.1
data/README.md CHANGED
@@ -1,4 +1,11 @@
1
- # Turbo Mount
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="assets/logo-dark.svg">
4
+ <img src="assets/logo.svg" title="Turbo Mount logo" width="220" height="45">
5
+ </picture>
6
+ </p>
7
+
8
+ <h1 align="center">Turbo Mount</h1>
2
9
 
3
10
  [![Gem Version](https://badge.fury.io/rb/turbo-mount.svg)](https://rubygems.org/gems/turbo-mount)
4
11
 
@@ -143,7 +150,8 @@ This will generate the following HTML:
143
150
 
144
151
  - React: `"turbo-mount/react"`
145
152
  - Vue: `"turbo-mount/vue"`
146
- - Svelte: `"turbo-mount/svelte"`
153
+ - Svelte 4: `"turbo-mount/svelte4"`
154
+ - Svelte 5: `"turbo-mount/svelte"`
147
155
 
148
156
  To add support for other frameworks, create a custom plugin. See included plugins for examples.
149
157
 
@@ -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();
@@ -112,15 +111,14 @@ const identifierNames = (name) => {
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).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})=>{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).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","_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":"iEAGM,MAAOA,UAA6BC,EAA1C,WAAAC,uBAOUC,KAAuBC,yBAAG,EASlC,OAAAC,GACEF,KAAKG,2BAALH,KAAKG,yBAA6BH,KAAKI,eACrCJ,KAAKK,aACLL,KAAKM,kBACLN,KAAKO,iBAIT,UAAAC,GACER,KAAKS,kBAGP,iBAAAC,GAEMV,KAAKC,wBACPD,KAAKC,yBAA0B,GAIjCD,KAAKS,kBACLT,KAAKG,2BAALH,KAAKG,yBAA6BH,KAAKI,eACrCJ,KAAKK,aACLL,KAAKM,kBACLN,KAAKO,kBAIT,kBAAIA,GACF,OAAOP,KAAKW,WAGd,gBAAIN,GACF,OAAOL,KAAKY,eAAiBZ,KAAKa,YAAcb,KAAKc,QAGvD,qBAAIR,GACF,OAAON,KAAKe,eAAef,KAAKgB,gBAAgBC,UAGlD,kBAAIC,GACF,OAAOlB,KAAKe,eAAef,KAAKgB,gBAAgBG,OAGlD,eAAAV,GACET,KAAKG,0BAA4BH,KAAKG,2BACtCH,KAAKG,8BAA2BiB,EAGlC,cAAAhB,CAAeiB,EAAaC,EAAoBC,GAC9C,OAAOvB,KAAKkB,eAAed,eAAe,CAAEiB,KAAIC,YAAWC,UAG7D,cAAAR,CAAeE,GAEb,OADYjB,KAAKwB,YACNC,WAAWC,QAAQT,GAGhC,iBAAAU,CAAkBJ,GAChBvB,KAAKC,yBAA0B,EAC/BD,KAAKW,WAAaY,GA1Eb1B,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,IACvD,MAAMC,EAAkBD,GAClBE,OAAEA,EAAMC,OAAEA,GAAWF,EAEvBC,EAAOE,aAAa,oBAAoBC,SAAS,iBACnDH,EAAOI,aACL,+BACAH,EAAOI,WAAWH,aAAa,iCAC7B,MAEJJ,EAAMQ,qBAKZ,QAAAX,CACErB,EACAiC,EACAnC,EACAoC,GAGA,GADAA,IAAAA,EAAexD,GACXG,KAAKqC,WAAWiB,IAAIF,GACtB,MAAM,IAAIG,MAAM,cAAcH,6BAIhC,GAFApD,KAAKqC,WAAWmB,IAAIJ,EAAM,CAAEnC,YAAWE,WAEnCkC,EAAY,CACd,MAAMI,EAAiB,eAAezB,EAAiBoB,GAAMlB,QAAQ,IAAK,QAC1ElC,KAAKwB,YAAYgB,SAASiB,EAAgBJ,IAI9C,OAAA3B,CAAQ0B,GACN,MAAMnC,EAAYjB,KAAKqC,WAAWqB,IAAIN,GACtC,IAAKnC,EACH,MAAM,IAAIsC,MAAM,sBAAsBH,KAExC,OAAOnC,EAGD,sBAAAsB,CAAuBoB,GAC7B,IAAInC,EAAcmC,GAAeC,OAAOC,SAMxC,OAJKrC,IACHA,EAAcsC,EAAYC,QAC1BH,OAAOC,SAAWrC,GAEbA,GAIL,SAAUwC,EAAyB7C,GACvC,MAAO,CACLM,EACA2B,EACAnC,EACAoC,KAEA5B,EAAWe,SAASrB,EAAQiC,EAAMnC,EAAWoC,EAAW,CAE5D,CC3FA,MAAMY,EAAmBb,IACvB,MAAMK,EAAiBzB,EAAiBoB,GAAMlB,QAAQ,IAAK,MAE3D,MAAO,CAAC,gBAAgBuB,IAAkB,eAAeA,IAAiB,EAG/DS,EAAyB,EACpC/C,SACAM,aACAY,aACA8B,kBAEA,MAAMC,EAAoBD,GAAe,GAEzC,IAAK,MAAME,OAAEA,EAAMC,SAAEA,KAAcjC,EAAY,CAC7C,MAAMe,EAAOkB,EACVpC,QAAQ,SAAU,IAClBA,QAAQ,qBAAsB,IAE3BqC,EAAcN,EAAgBb,GAE9BC,EAAae,EAAkBI,MAAK,EAAGC,gBAC3CF,EAAYvB,SAASyB,KAEjBxD,EAAYoD,EAAOK,SAAWL,EAEhChB,EACF5B,EAAWe,SACTrB,EACAiC,EACAnC,EACAoC,EAAWsB,uBAGblD,EAAWe,SAASrB,EAAQiC,EAAMnC"}
@@ -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,67 @@
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
+ elsif shakapacker?
24
+ js_file_path "packs/application.js"
25
+ else
26
+ js_file_path "application.js"
27
+ end
28
+ end
29
+
30
+ def js_file_path(*relative_path)
31
+ File.join(js_destination_root, *relative_path)
32
+ end
33
+
34
+ def file?(*relative_path)
35
+ File.file?(file_path(*relative_path))
36
+ end
37
+
38
+ def file_path(*relative_path)
39
+ File.join(destination_root, *relative_path)
40
+ end
41
+
42
+ def vite?
43
+ file?("config/vite.json") && Dir.glob(file_path("vite.config.*")).any?
44
+ end
45
+
46
+ def shakapacker?
47
+ file?("config/shakapacker.yml") || file?("config/webpacker.yml")
48
+ end
49
+
50
+ # Interactivity Helpers
51
+ def ask(*)
52
+ unless options[:interactive]
53
+ say_error "Specify all options when running the generator non-interactively.", :red
54
+ exit(1)
55
+ end
56
+
57
+ super
58
+ end
59
+
60
+ def yes?(*)
61
+ return false unless options[:interactive]
62
+
63
+ super
64
+ end
65
+ end
66
+ end
67
+ 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,43 @@
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 :interactive, type: :boolean, default: true,
29
+ desc: "Whether to prompt for optional installations"
30
+
31
+ class_option :verbose, type: :boolean, default: false,
32
+ desc: "Run the generator in verbose mode"
33
+
26
34
  def install
27
35
  say "Installing Turbo Mount"
28
36
 
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
37
+ package_manager.validate!
33
38
 
34
- if importmap?
39
+ create_initializer
40
+ if package_manager.importmap?
35
41
  install_importmap
36
42
  else
37
43
  install_nodejs
@@ -42,86 +48,51 @@ module TurboMount
42
48
 
43
49
  private
44
50
 
45
- def install_nodejs
46
- case build_tool
47
- when "npm"
48
- run "npm install turbo-mount #{FRAMEWORKS[framework][:npm_packages]}"
49
- when "pnpm"
50
- run "pnpm install turbo-mount #{FRAMEWORKS[framework][:npm_packages]}"
51
- when "yarn"
52
- run "yarn add turbo-mount #{FRAMEWORKS[framework][:npm_packages]}"
53
- when "bun"
54
- run "bun add turbo-mount #{FRAMEWORKS[framework][:npm_packages]}"
55
- end
56
-
51
+ def create_initializer
57
52
  say "Creating Turbo Mount initializer"
58
- template "turbo-mount.js", File.join("app/javascript/turbo-mount.js")
53
+
54
+ needs_alias = shakapacker? || package_manager.importmap?
55
+ initializer_path = needs_alias ? "turbo-mount-initializer.js" : "turbo-mount.js"
56
+ initializer_import = needs_alias ? %(import "turbo-mount-initializer"\n) : %(import "./turbo-mount"\n)
57
+
58
+ template "turbo-mount.js", js_file_path(initializer_path)
59
59
  begin
60
- append_to_file js_entrypoint, %(import "./turbo-mount"\n)
60
+ append_to_file js_entrypoint, initializer_import
61
61
  rescue
62
- say 'Could not find the application entrypoint, please add `import "./turbo-mount"` manually.', :yellow
62
+ say "Could not find the application entrypoint, please add `#{initializer_import.strip}` manually.", :yellow
63
63
  end
64
+ end
65
+
66
+ def install_nodejs
67
+ package_manager.add_dependencies("turbo-mount", FRAMEWORKS[framework]["npm_packages"])
68
+
64
69
  warn_about_vite_plugin if vite?
65
70
  end
66
71
 
67
72
  def install_importmap
68
- say "Creating Turbo Mount initializer"
69
- template "turbo-mount.js", File.join("app/javascript/turbo-mount-initializer.js")
70
- append_to_file "app/javascript/application.js", %(import "turbo-mount-initializer"\n)
71
-
72
73
  say "Pinning Turbo Mount to the importmap"
73
74
  append_to_file "config/importmap.rb", %(pin "turbo-mount", to: "turbo-mount.min.js"\n)
74
75
  append_to_file "config/importmap.rb", %(pin "turbo-mount/#{framework}", to: "turbo-mount/#{framework}.min.js"\n)
75
76
  append_to_file "config/importmap.rb", %(pin "turbo-mount-initializer"\n)
76
77
 
77
78
  say "Pinning framework dependencies to the importmap"
78
- run "bin/importmap pin #{FRAMEWORKS[framework][:pins]}"
79
- end
80
-
81
- def js_entrypoint
82
- if vite?
83
- "app/javascript/entrypoints/application.js"
84
- else
85
- "app/javascript/application.js"
86
- end
87
- end
88
-
89
- def vite?
90
- Dir.glob(Rails.root.join("vite.config.*")).any?
91
- end
92
-
93
- def importmap?
94
- build_tool == "importmap"
79
+ package_manager.add_dependencies(FRAMEWORKS[framework]["pins"])
95
80
  end
96
81
 
97
82
  def warn_about_vite_plugin
98
- say "Make sure to install and add #{FRAMEWORKS[framework][:vite_plugin]} to your Vite config", :yellow
83
+ say "Make sure to install and add #{FRAMEWORKS[framework]["vite_plugin"]} to your Vite config", :yellow
99
84
  end
100
85
 
101
- def build_tool
102
- return @build_tool if defined?(@build_tool)
103
-
104
- @build_tool = detect_build_tool
86
+ def package_manager
87
+ @package_manager ||= JSPackageManager.new(self)
105
88
  end
106
89
 
107
- def detect_build_tool
108
- if Rails.root.join("package.json").exist?
109
- if Rails.root.join("package-lock.json").exist?
110
- "npm"
111
- elsif Rails.root.join("pnpm-lock.yaml").exist?
112
- "pnpm"
113
- elsif Rails.root.join("bun.config.js").exist?
114
- "bun"
115
- else
116
- "yarn"
117
- end
118
- elsif Rails.root.join("config/importmap.rb").exist?
119
- "importmap"
120
- end
90
+ def extension
91
+ FRAMEWORKS[framework]["extension"]
121
92
  end
122
93
 
123
94
  def framework
124
- @framework ||= ask("What framework do you want to use with Turbo Mount?", limited_to: FRAMEWORKS.keys, default: "react")
95
+ @framework ||= options[:framework] || ask("What framework do you want to use with Turbo Mount?", :green, limited_to: FRAMEWORKS.keys, default: "react")
125
96
  end
126
97
  end
127
98
  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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Turbo
4
4
  module Mount
5
- VERSION = "0.3.3"
5
+ VERSION = "0.4.1"
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.3
4
+ version: 0.4.1
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-09-24 00:00:00.000000000 Z
11
+ date: 2024-11-26 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.17
87
+ rubygems_version: 3.5.23
82
88
  signing_key:
83
89
  specification_version: 4
84
90
  summary: Use React, Vue, Svelte, and other components with Hotwire.