diff --git a/coldspa/vite/clients/react-client.js b/coldspa/vite/clients/react-client.js index 5b10e26..53f4006 100644 --- a/coldspa/vite/clients/react-client.js +++ b/coldspa/vite/clients/react-client.js @@ -1,8 +1,9 @@ // Coldspa React client entry. Bundled by Vite via the coldspa plugin. +// __COLDSPA_GLOB__ is replaced at build time by the plugin's transform hook. import React from 'react'; import { createRoot } from 'react-dom/client'; -const components = import.meta.glob('/src/**/*.{jsx,tsx}'); +const components = import.meta.glob('__COLDSPA_GLOB__'); export async function mount(componentPath, el, props) { if (!el) return; diff --git a/coldspa/vite/clients/vue-client.js b/coldspa/vite/clients/vue-client.js index 77b8c9e..6416877 100644 --- a/coldspa/vite/clients/vue-client.js +++ b/coldspa/vite/clients/vue-client.js @@ -1,7 +1,8 @@ // Coldspa Vue client entry. Bundled by Vite via the coldspa plugin. +// __COLDSPA_GLOB__ is replaced at build time by the plugin's transform hook. import { createApp } from 'vue'; -const components = import.meta.glob('/src/**/*.vue'); +const components = import.meta.glob('__COLDSPA_GLOB__'); export async function mount(componentPath, el, props) { if (!el) return; diff --git a/coldspa/vite/plugin.js b/coldspa/vite/plugin.js index 52de37a..9599561 100644 --- a/coldspa/vite/plugin.js +++ b/coldspa/vite/plugin.js @@ -5,33 +5,48 @@ // - 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 +// - Substitutes the user's component glob into the client entries at transform time // -// Usage: -// import { defineConfig } from 'vite'; -// import coldspa from './coldspa/vite/plugin.js'; +// Usage (simple): +// coldspa({ frameworks: ['vue'] }) // -// export default defineConfig({ -// plugins: [coldspa({ frameworks: ['vue'] })] -// }); +// Usage (custom component locations): +// coldspa({ +// frameworks: ['vue', 'react'], +// globs: { +// vue: '/app/**/*.vue', +// react: ['/app/**/*.jsx', '/app/**/*.tsx'] +// } +// }) // // 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) +// frameworks string[] default ['vue'] +// globs Record per-framework component glob(s) +// vitePort number default 5173 +// outDir string default 'dist' +// base string default '/dist/' (build only) import { fileURLToPath } from 'url'; import { dirname, resolve } from 'path'; const __dirname = dirname(fileURLToPath(import.meta.url)); +// Vite normalizes module ids to forward slashes on all platforms; do the same +// here so our id-based lookups work on Windows. +const toViteId = (p) => resolve(p).split('\\').join('/'); + const FRAMEWORK_CLIENTS = { - vue: resolve(__dirname, 'clients/vue-client.js'), - react: resolve(__dirname, 'clients/react-client.js') + vue: toViteId(resolve(__dirname, 'clients/vue-client.js')), + react: toViteId(resolve(__dirname, 'clients/react-client.js')) +}; + +const DEFAULT_GLOBS = { + vue: '/src/**/*.vue', + react: '/src/**/*.{jsx,tsx}' }; -// Framework sub-plugins are imported lazily so consumers only need to install -// the @vitejs/plugin-* packages for the frameworks they actually use. +const GLOB_PLACEHOLDER = '__COLDSPA_GLOB__'; + async function loadFrameworkPlugin(name) { switch (name) { case 'vue': { @@ -47,11 +62,22 @@ async function loadFrameworkPlugin(name) { } } +// Renders a glob option as the literal source text of an import.meta.glob argument: +// "/src/**/*.vue" -> '/src/**/*.vue' +// ["/a/**", "/b/**"] -> ["/a/**","/b/**"] +function renderGlobLiteral(glob) { + if (Array.isArray(glob)) { + return JSON.stringify(glob); + } + return JSON.stringify(glob); +} + 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/'; + const userGlobs = options.globs ?? {}; for (const fw of frameworks) { if (!(fw in FRAMEWORK_CLIENTS)) { @@ -59,22 +85,23 @@ export default function coldspa(options = {}) { } } - // Build the rollup input map from selected frameworks. + // Resolve final glob per framework, with absolute client-entry paths as keys + // for fast lookup in the transform hook. + const globsByClientPath = {}; const input = {}; for (const fw of frameworks) { - input[`${fw}-client`] = FRAMEWORK_CLIENTS[fw]; + const clientPath = FRAMEWORK_CLIENTS[fw]; + input[`${fw}-client`] = clientPath; + globsByClientPath[clientPath] = userGlobs[fw] ?? DEFAULT_GLOBS[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 : '/', @@ -92,7 +119,30 @@ export default function coldspa(options = {}) { } } }; + }, + + // Replace the glob placeholder in our client entries with the + // user-configured glob. Must run BEFORE vite:import-glob (which is + // what actually transforms import.meta.glob() into the dynamic + // import map). + enforce: 'pre', + transform(code, id) { + // id may include a query string (?import, ?used, etc); strip it + const cleanId = id.split('?')[0]; + const glob = globsByClientPath[cleanId]; + if (!glob) return null; + if (!code.includes(GLOB_PLACEHOLDER)) return null; + + // Replace the literal "'__COLDSPA_GLOB__'" (with quotes) in the + // source with the rendered glob literal. This keeps the result + // a valid call to import.meta.glob(...). + const replaced = code.replace( + `'${GLOB_PLACEHOLDER}'`, + renderGlobLiteral(glob) + ); + return { code: replaced, map: null }; } } ]; } +