From ff3dd7612882ba19236daf281239ff7aa213731d Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Mon, 4 May 2026 01:10:27 -0700 Subject: [PATCH] fix: prod usage now works --- Island.cfm | 15 ++++++++++----- coldspa/renderers/React.cfm | 8 ++------ coldspa/renderers/Vue.cfm | 14 ++------------ coldspa/renderers/react-client.js | 16 +++++++++++++--- coldspa/renderers/vue-client.js | 20 ++++++++++++++++---- vite.config.js | 16 ++++++++++++---- 6 files changed, 55 insertions(+), 34 deletions(-) diff --git a/Island.cfm b/Island.cfm index 22573e8..93c3216 100644 --- a/Island.cfm +++ b/Island.cfm @@ -74,17 +74,22 @@ function resolveAsset(required string path) { uid = lcase(replace(createUUID(), "-", "", "all")); mountId = "island-" & uid; // DOM id (hyphens fine) jsId = "island_" & uid; // JS-identifier-safe (no hyphens) -resolvedPath = resolveAsset(attributes.path); -propsJson = serializeJSON(attributes.props); -// Resolve the renderer's client-entry shim through the same Vite-aware path resolver. -// Renderers SHOULD declare `clientEntry`; tolerate older renderers that don't. +// Component is loaded dynamically by the framework's client entry via +// import.meta.glob, so we don't resolve it through Vite ourselves -- we just +// normalize the path to a glob key (e.g. "./src/App.vue" -> "/src/App.vue"). +componentGlobKey = reReplace(attributes.path, "^\./", "/"); + +propsJson = serializeJSON(attributes.props); + +// The client entry (the JS shim with the bare `vue` import) IS resolved through +// Vite, since it's a real JS file Vite serves/bundles. resolvedClientEntry = ""; if (structKeyExists(attributes.framework, "clientEntry")) { resolvedClientEntry = resolveAsset(attributes.framework.clientEntry); } -rendered = attributes.framework.render(mountId, resolvedPath, propsJson, resolvedClientEntry); +rendered = attributes.framework.render(mountId, componentGlobKey, 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 45963fe..705ef50 100644 --- a/coldspa/renderers/React.cfm +++ b/coldspa/renderers/React.cfm @@ -1,24 +1,20 @@ React = { "name": "React", "clientEntry": "./coldspa/renderers/react-client.js", - "render": function(mountId, resolvedPath, propsJson, resolvedClientEntry) { + "render": function(mountId, componentGlobKey, propsJson, resolvedClientEntry) { var jsId = arguments.mountId.replace('-', '_', 'all'); return { "imports": " import { mount as __coldspa_mount_#jsId# } from '#arguments.resolvedClientEntry#'; - import __coldspa_Component_#jsId# from '#arguments.resolvedPath#'; ", "body": " __coldspa_mount_#jsId#( - __coldspa_Component_#jsId#, + '#arguments.componentGlobKey#', document.getElementById('#arguments.mountId#'), #arguments.propsJson# ); diff --git a/coldspa/renderers/Vue.cfm b/coldspa/renderers/Vue.cfm index f1aa714..79904df 100644 --- a/coldspa/renderers/Vue.cfm +++ b/coldspa/renderers/Vue.cfm @@ -1,30 +1,20 @@ Vue = { "name": "Vue", "clientEntry": "./coldspa/renderers/vue-client.js", - "render": function(mountId, resolvedPath, propsJson, resolvedClientEntry) { + "render": function(mountId, componentGlobKey, propsJson, resolvedClientEntry) { var jsId = arguments.mountId.replace('-', '_', 'all'); return { "imports": " import { mount as __coldspa_mount_#jsId# } from '#arguments.resolvedClientEntry#'; - import __coldspa_Component_#jsId# from '#arguments.resolvedPath#'; ", "body": " __coldspa_mount_#jsId#( - __coldspa_Component_#jsId#, + '#arguments.componentGlobKey#', document.getElementById('#arguments.mountId#'), #arguments.propsJson# ); diff --git a/coldspa/renderers/react-client.js b/coldspa/renderers/react-client.js index 739ded3..1832bd0 100644 --- a/coldspa/renderers/react-client.js +++ b/coldspa/renderers/react-client.js @@ -1,9 +1,19 @@ // 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) { +const components = import.meta.glob('/src/**/*.{jsx,tsx}'); + +export async function mount(componentPath, el, props) { if (!el) return; - createRoot(el).render(React.createElement(Component, props)); + const loader = components[componentPath]; + if (!loader) { + console.error( + `[Coldspa] No React component registered for path "${componentPath}". ` + + `Make sure it matches the glob in coldspa/renderers/react-client.js.` + ); + return; + } + const mod = await loader(); + createRoot(el).render(React.createElement(mod.default, props)); } diff --git a/coldspa/renderers/vue-client.js b/coldspa/renderers/vue-client.js index 66ba8e6..557714d 100644 --- a/coldspa/renderers/vue-client.js +++ b/coldspa/renderers/vue-client.js @@ -1,9 +1,21 @@ // 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. +// Uses import.meta.glob so Vite knows about all island components at build time +// and bundles each as its own dynamic chunk. In dev, Vite serves them directly. import { createApp } from 'vue'; -export function mount(Component, el, props) { +// Match any .vue under src/. Adjust the glob if you keep components elsewhere. +const components = import.meta.glob('/src/**/*.vue'); + +export async function mount(componentPath, el, props) { if (!el) return; - createApp(Component, props).mount(el); + const loader = components[componentPath]; + if (!loader) { + console.error( + `[Coldspa] No Vue component registered for path "${componentPath}". ` + + `Make sure it matches the glob in coldspa/renderers/vue-client.js.` + ); + return; + } + const mod = await loader(); + createApp(mod.default, props).mount(el); } diff --git a/vite.config.js b/vite.config.js index cbc6b87..2cd489f 100644 --- a/vite.config.js +++ b/vite.config.js @@ -2,8 +2,12 @@ import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import { resolve } from 'path'; -export default defineConfig({ +export default defineConfig(({ command }) => ({ plugins: [vue()], + // In production builds, assets live under /dist/ on the CF server, so all + // built URLs (dynamic imports, CSS preloads, etc.) need to be prefixed. + // In dev, the Vite server serves from root. + base: command === 'build' ? '/dist/' : '/', server: { port: 5173, cors: true, @@ -13,11 +17,15 @@ export default defineConfig({ manifest: true, outDir: 'dist', rollupOptions: { + // Force entries to keep their export signatures even if they look + // tree-shakeable. Without this, Rollup may emit empty entry chunks. + preserveEntrySignatures: 'strict', + // Only the client entries are explicit inputs. User components + // are picked up automatically via import.meta.glob inside them. 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') + 'react-client': resolve(__dirname, 'coldspa/renderers/react-client.js') } } } -}); +})); -- 2.51.2