From d8b745576f2543d1fca83a67ee7e4a7ecc025a59 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Mon, 4 May 2026 01:02:19 -0700 Subject: [PATCH] chore: transform mount methods using Vite --- Island.cfm | 10 +++++++++- coldspa/renderers/React.cfm | 18 ++++++++---------- coldspa/renderers/Vue.cfm | 31 +++++++++++++++---------------- coldspa/renderers/react-client.js | 9 +++++++++ coldspa/renderers/vue-client.js | 9 +++++++++ package-lock.json | 30 ++++++++++++++++++++++++++++++ package.json | 2 ++ src/App.vue | 8 ++++++++ vite.config.js | 2 ++ 9 files changed, 92 insertions(+), 27 deletions(-) create mode 100644 coldspa/renderers/react-client.js create mode 100644 coldspa/renderers/vue-client.js diff --git a/Island.cfm b/Island.cfm index 1735ac1..22573e8 100644 --- a/Island.cfm +++ b/Island.cfm @@ -76,7 +76,15 @@ mountId = "island-" & uid; // DOM id (hyphens fine) jsId = "island_" & uid; // JS-identifier-safe (no hyphens) resolvedPath = resolveAsset(attributes.path); propsJson = serializeJSON(attributes.props); -rendered = attributes.framework.render(mountId, resolvedPath, propsJson); + +// Resolve the renderer's client-entry shim through the same Vite-aware path resolver. +// Renderers SHOULD declare `clientEntry`; tolerate older renderers that don't. +resolvedClientEntry = ""; +if (structKeyExists(attributes.framework, "clientEntry")) { + resolvedClientEntry = resolveAsset(attributes.framework.clientEntry); +} + +rendered = attributes.framework.render(mountId, resolvedPath, propsJson, resolvedClientEntry); // Backwards-compat: allow renderers that still return a plain string (treated as body, no imports) if (isSimpleValue(rendered)) { diff --git a/coldspa/renderers/React.cfm b/coldspa/renderers/React.cfm index 6cb46a7..45963fe 100644 --- a/coldspa/renderers/React.cfm +++ b/coldspa/renderers/React.cfm @@ -8,22 +8,20 @@ React = { "name": "React", - "render": function(mountId, resolvedPath, propsJson) { + "clientEntry": "./coldspa/renderers/react-client.js", + "render": function(mountId, resolvedPath, propsJson, resolvedClientEntry) { var jsId = arguments.mountId.replace('-', '_', 'all'); return { "imports": " - import __coldspa_React_#jsId# from 'react'; - import { createRoot as __coldspa_createRoot_#jsId# } from 'react-dom/client'; + import { mount as __coldspa_mount_#jsId# } from '#arguments.resolvedClientEntry#'; import __coldspa_Component_#jsId# from '#arguments.resolvedPath#'; ", "body": " - const el = document.getElementById('#arguments.mountId#'); - if (el) { - const props = #arguments.propsJson#; - __coldspa_createRoot_#jsId#(el).render( - __coldspa_React_#jsId#.createElement(__coldspa_Component_#jsId#, props) - ); - } + __coldspa_mount_#jsId#( + __coldspa_Component_#jsId#, + document.getElementById('#arguments.mountId#'), + #arguments.propsJson# + ); " }; } diff --git a/coldspa/renderers/Vue.cfm b/coldspa/renderers/Vue.cfm index 0569860..f1aa714 100644 --- a/coldspa/renderers/Vue.cfm +++ b/coldspa/renderers/Vue.cfm @@ -5,30 +5,29 @@ - Sets a `Vue` struct in the including scope with: - - name: "Vue" - - render: function(mountId, resolvedPath, propsJson) -> { imports, body } - imports: ES module imports to hoist to module top level - body: statements to run inside the (possibly deferred) boot function + Sets a `Vue` struct in the including scope: + - name: "Vue" + - clientEntry: path to the client entry shim (resolved by Island.cfm via Vite) + - render: function(mountId, resolvedComponentPath, propsJson, resolvedClientEntry) + -> { imports, body } ---> Vue = { "name": "Vue", - "render": function(mountId, resolvedPath, propsJson) { + "clientEntry": "./coldspa/renderers/vue-client.js", + "render": function(mountId, resolvedPath, propsJson, resolvedClientEntry) { + var jsId = arguments.mountId.replace('-', '_', 'all'); return { "imports": " - import { createApp as __coldspa_createApp_#arguments.mountId.replace('-', '_', 'all')# } from 'vue'; - import __coldspa_Component_#arguments.mountId.replace('-', '_', 'all')# from '#arguments.resolvedPath#'; + import { mount as __coldspa_mount_#jsId# } from '#arguments.resolvedClientEntry#'; + import __coldspa_Component_#jsId# from '#arguments.resolvedPath#'; ", "body": " - const el = document.getElementById('#arguments.mountId#'); - if (el) { - const props = #arguments.propsJson#; - __coldspa_createApp_#arguments.mountId.replace('-', '_', 'all')#( - __coldspa_Component_#arguments.mountId.replace('-', '_', 'all')#, - props - ).mount(el); - } + __coldspa_mount_#jsId#( + __coldspa_Component_#jsId#, + document.getElementById('#arguments.mountId#'), + #arguments.propsJson# + ); " }; } diff --git a/coldspa/renderers/react-client.js b/coldspa/renderers/react-client.js new file mode 100644 index 0000000..739ded3 --- /dev/null +++ b/coldspa/renderers/react-client.js @@ -0,0 +1,9 @@ +// Coldspa React client entry. +// Served/bundled by Vite so bare `react` and `react-dom/client` specifiers resolve. +import React from 'react'; +import { createRoot } from 'react-dom/client'; + +export function mount(Component, el, props) { + if (!el) return; + createRoot(el).render(React.createElement(Component, props)); +} diff --git a/coldspa/renderers/vue-client.js b/coldspa/renderers/vue-client.js new file mode 100644 index 0000000..66ba8e6 --- /dev/null +++ b/coldspa/renderers/vue-client.js @@ -0,0 +1,9 @@ +// Coldspa Vue client entry. +// This file is served by Vite (dev) or bundled by Vite (prod), so the +// bare `vue` specifier gets resolved by Vite, not the browser. +import { createApp } from 'vue'; + +export function mount(Component, el, props) { + if (!el) return; + createApp(Component, props).mount(el); +} diff --git a/package-lock.json b/package-lock.json index 8525068..39d9028 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "coldspa-demo", "version": "0.0.1", "dependencies": { + "react": "^19.2.5", + "react-dom": "^19.2.5", "vue": "^3.4.0" }, "devDependencies": { @@ -1068,6 +1070,28 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, "node_modules/rollup": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", @@ -1113,6 +1137,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/package.json b/package.json index 2c12f68..e85eeeb 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "build": "vite build" }, "dependencies": { + "react": "^19.2.5", + "react-dom": "^19.2.5", "vue": "^3.4.0" }, "devDependencies": { diff --git a/src/App.vue b/src/App.vue index d6e0beb..b4f6b0f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,8 +1,15 @@ diff --git a/vite.config.js b/vite.config.js index 11ab82b..cbc6b87 100644 --- a/vite.config.js +++ b/vite.config.js @@ -14,6 +14,8 @@ export default defineConfig({ outDir: 'dist', rollupOptions: { input: { + 'vue-client': resolve(__dirname, 'coldspa/renderers/vue-client.js'), + 'react-client': resolve(__dirname, 'coldspa/renderers/react-client.js'), 'src/App.vue': resolve(__dirname, 'src/App.vue') } } -- 2.51.2