From 76beafd56befc9e0dae9e51e0d2bb51bbae9813e Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Mon, 4 May 2026 01:14:23 -0700 Subject: [PATCH] feat: initial vite plugin --- coldspa/renderers/React.cfm | 2 +- coldspa/renderers/Vue.cfm | 2 +- .../clients}/react-client.js | 4 +- .../{renderers => vite/clients}/vue-client.js | 7 +- coldspa/vite/plugin.js | 98 +++++++++++++++++++ package-lock.json | 30 ------ package.json | 2 - vite.config.js | 36 ++----- 8 files changed, 111 insertions(+), 70 deletions(-) rename coldspa/{renderers => vite/clients}/react-client.js (77%) rename coldspa/{renderers => vite/clients}/vue-client.js (56%) create mode 100644 coldspa/vite/plugin.js diff --git a/coldspa/renderers/React.cfm b/coldspa/renderers/React.cfm index 705ef50..48bd883 100644 --- a/coldspa/renderers/React.cfm +++ b/coldspa/renderers/React.cfm @@ -5,7 +5,7 @@ React = { "name": "React", - "clientEntry": "./coldspa/renderers/react-client.js", + "clientEntry": "./coldspa/vite/clients/react-client.js", "render": function(mountId, componentGlobKey, propsJson, resolvedClientEntry) { var jsId = arguments.mountId.replace('-', '_', 'all'); return { diff --git a/coldspa/renderers/Vue.cfm b/coldspa/renderers/Vue.cfm index 79904df..c76f73d 100644 --- a/coldspa/renderers/Vue.cfm +++ b/coldspa/renderers/Vue.cfm @@ -5,7 +5,7 @@ Vue = { "name": "Vue", - "clientEntry": "./coldspa/renderers/vue-client.js", + "clientEntry": "./coldspa/vite/clients/vue-client.js", "render": function(mountId, componentGlobKey, propsJson, resolvedClientEntry) { var jsId = arguments.mountId.replace('-', '_', 'all'); return { diff --git a/coldspa/renderers/react-client.js b/coldspa/vite/clients/react-client.js similarity index 77% rename from coldspa/renderers/react-client.js rename to coldspa/vite/clients/react-client.js index 1832bd0..5b10e26 100644 --- a/coldspa/renderers/react-client.js +++ b/coldspa/vite/clients/react-client.js @@ -1,4 +1,4 @@ -// Coldspa React client entry. +// Coldspa React client entry. Bundled by Vite via the coldspa plugin. import React from 'react'; import { createRoot } from 'react-dom/client'; @@ -10,7 +10,7 @@ export async function mount(componentPath, el, props) { if (!loader) { console.error( `[Coldspa] No React component registered for path "${componentPath}". ` + - `Make sure it matches the glob in coldspa/renderers/react-client.js.` + `Make sure it matches the glob configured in the coldspa Vite plugin.` ); return; } diff --git a/coldspa/renderers/vue-client.js b/coldspa/vite/clients/vue-client.js similarity index 56% rename from coldspa/renderers/vue-client.js rename to coldspa/vite/clients/vue-client.js index 557714d..77b8c9e 100644 --- a/coldspa/renderers/vue-client.js +++ b/coldspa/vite/clients/vue-client.js @@ -1,9 +1,6 @@ -// Coldspa Vue client entry. -// 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. +// Coldspa Vue client entry. Bundled by Vite via the coldspa plugin. import { createApp } from 'vue'; -// 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) { @@ -12,7 +9,7 @@ export async function mount(componentPath, el, props) { if (!loader) { console.error( `[Coldspa] No Vue component registered for path "${componentPath}". ` + - `Make sure it matches the glob in coldspa/renderers/vue-client.js.` + `Make sure it matches the glob configured in the coldspa Vite plugin.` ); return; } diff --git a/coldspa/vite/plugin.js b/coldspa/vite/plugin.js new file mode 100644 index 0000000..52de37a --- /dev/null +++ b/coldspa/vite/plugin.js @@ -0,0 +1,98 @@ +// Coldspa Vite plugin. +// +// Encapsulates everything Vite needs to support cf_Island: +// - Loads framework sub-plugins (@vitejs/plugin-vue, @vitejs/plugin-react) +// - Registers the matching client-entry shims as build inputs +// - Sets manifest output, base path, dev server port/CORS for the CF integration +// - Forces preserveEntrySignatures so the entry chunks aren't tree-shaken away +// +// Usage: +// import { defineConfig } from 'vite'; +// import coldspa from './coldspa/vite/plugin.js'; +// +// export default defineConfig({ +// plugins: [coldspa({ frameworks: ['vue'] })] +// }); +// +// Options: +// frameworks string[] default ['vue'] -- which renderers to wire up +// vitePort number default 5173 -- dev server port (must match IslandConfig) +// outDir string default 'dist' -- production output dir +// base string default '/dist/' -- public path for built assets (build only) + +import { fileURLToPath } from 'url'; +import { dirname, resolve } from 'path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const FRAMEWORK_CLIENTS = { + vue: resolve(__dirname, 'clients/vue-client.js'), + react: resolve(__dirname, 'clients/react-client.js') +}; + +// Framework sub-plugins are imported lazily so consumers only need to install +// the @vitejs/plugin-* packages for the frameworks they actually use. +async function loadFrameworkPlugin(name) { + switch (name) { + case 'vue': { + const mod = await import('@vitejs/plugin-vue'); + return mod.default(); + } + case 'react': { + const mod = await import('@vitejs/plugin-react'); + return mod.default(); + } + default: + throw new Error(`[coldspa] Unknown framework "${name}". Supported: vue, react.`); + } +} + +export default function coldspa(options = {}) { + const frameworks = options.frameworks ?? ['vue']; + const vitePort = options.vitePort ?? 5173; + const outDir = options.outDir ?? 'dist'; + const baseProd = options.base ?? '/dist/'; + + for (const fw of frameworks) { + if (!(fw in FRAMEWORK_CLIENTS)) { + throw new Error(`[coldspa] Unknown framework "${fw}". Supported: ${Object.keys(FRAMEWORK_CLIENTS).join(', ')}.`); + } + } + + // Build the rollup input map from selected frameworks. + const input = {}; + for (const fw of frameworks) { + input[`${fw}-client`] = FRAMEWORK_CLIENTS[fw]; + } + + // Load sub-plugins eagerly (Vite expects a flat plugin array synchronously, + // but plugins themselves can be Promises — we resolve them via a wrapper). + const subPluginsPromise = Promise.all(frameworks.map(loadFrameworkPlugin)); + + return [ + // Spread the resolved sub-plugins into the array. Vite supports promises + // at the top level of the plugins array. + subPluginsPromise.then(plugins => plugins), + { + name: 'coldspa', + config(_userConfig, { command }) { + return { + base: command === 'build' ? baseProd : '/', + server: { + port: vitePort, + cors: true, + origin: `http://localhost:${vitePort}` + }, + build: { + manifest: true, + outDir, + rollupOptions: { + preserveEntrySignatures: 'strict', + input + } + } + }; + } + } + ]; +} diff --git a/package-lock.json b/package-lock.json index 39d9028..8525068 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,6 @@ "name": "coldspa-demo", "version": "0.0.1", "dependencies": { - "react": "^19.2.5", - "react-dom": "^19.2.5", "vue": "^3.4.0" }, "devDependencies": { @@ -1070,28 +1068,6 @@ "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", @@ -1137,12 +1113,6 @@ "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 e85eeeb..2c12f68 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,6 @@ "build": "vite build" }, "dependencies": { - "react": "^19.2.5", - "react-dom": "^19.2.5", "vue": "^3.4.0" }, "devDependencies": { diff --git a/vite.config.js b/vite.config.js index 2cd489f..02db26f 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,31 +1,9 @@ import { defineConfig } from 'vite'; -import vue from '@vitejs/plugin-vue'; -import { resolve } from 'path'; +import coldspa from './coldspa/vite/plugin.js'; + +export default defineConfig({ + plugins: [ + coldspa({ frameworks: ['vue'] }) + ] +}); -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, - origin: 'http://localhost:5173' - }, - build: { - 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') - } - } - } -})); -- 2.51.2