diff --git a/examples/spa/.gitignore b/examples/spa/.gitignore new file mode 100644 index 00000000..e985853e --- /dev/null +++ b/examples/spa/.gitignore @@ -0,0 +1 @@ +.vercel diff --git a/examples/spa/app.js b/examples/spa/app.js new file mode 100644 index 00000000..b1eb0cdb --- /dev/null +++ b/examples/spa/app.js @@ -0,0 +1,27 @@ +import reactRefresh from "@vitejs/plugin-react"; +import { createApp } from "vinxi"; + +export default createApp({ + routers: [ + { + name: "public", + mode: "static", + build: { + outDir: "./.build/client", + }, + dir: "./public", + base: "/", + }, + { + name: "client", + mode: "spa", + handler: "./index.html", + build: { + target: "browser", + outDir: "./.build/api", + plugins: () => [reactRefresh()], + }, + base: "/", + }, + ], +}); diff --git a/examples/spa/app/client.tsx b/examples/spa/app/client.tsx new file mode 100644 index 00000000..97c33a34 --- /dev/null +++ b/examples/spa/app/client.tsx @@ -0,0 +1,4 @@ +/// +import "./style.css"; + +alert("Hello world!"); diff --git a/examples/spa/app/style.css b/examples/spa/app/style.css new file mode 100644 index 00000000..b1284a35 --- /dev/null +++ b/examples/spa/app/style.css @@ -0,0 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +* { + color: red; +} \ No newline at end of file diff --git a/examples/spa/index.html b/examples/spa/index.html new file mode 100644 index 00000000..3afeac58 --- /dev/null +++ b/examples/spa/index.html @@ -0,0 +1,13 @@ + + + + + + Document + + + + Hello world + + + diff --git a/examples/spa/package.json b/examples/spa/package.json new file mode 100644 index 00000000..1b18f24c --- /dev/null +++ b/examples/spa/package.json @@ -0,0 +1,22 @@ +{ + "type": "module", + "scripts": { + "dev": "vinxi dev", + "build": "vinxi build", + "start": "vinxi start" + }, + "dependencies": { + "@picocss/pico": "^1.5.7", + "@vinxi/react": "workspace:^", + "@vitejs/plugin-react": "^4.0.1", + "autoprefixer": "^10.4.14", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "tailwindcss": "^3.3.2", + "vinxi": "workspace:^" + }, + "devDependencies": { + "@types/react": "^18.2.14", + "@types/react-dom": "^18.2.6" + } +} diff --git a/examples/spa/postcss.config.cjs b/examples/spa/postcss.config.cjs new file mode 100644 index 00000000..33ad091d --- /dev/null +++ b/examples/spa/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/examples/spa/public/favicon.ico b/examples/spa/public/favicon.ico new file mode 100644 index 00000000..129ee136 Binary files /dev/null and b/examples/spa/public/favicon.ico differ diff --git a/examples/spa/tailwind.config.cjs b/examples/spa/tailwind.config.cjs new file mode 100644 index 00000000..e1e70f61 --- /dev/null +++ b/examples/spa/tailwind.config.cjs @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./app/**/*.tsx", "./app/**/*.ts", "./app/**/*.js"], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/packages/vinxi/lib/build.js b/packages/vinxi/lib/build.js index 939ba6b6..7a9a8177 100644 --- a/packages/vinxi/lib/build.js +++ b/packages/vinxi/lib/build.js @@ -33,9 +33,8 @@ export async function createBuild(app, buildConfig) { fileURLToPath(new URL("./prod-manifest.js", import.meta.url)), ], handlers: [ - ...app.config.routers - .filter((router) => router.mode === "handler") - .map((router) => { + ...app.config.routers.map((router) => { + if (router.mode === "handler") { const bundlerManifest = JSON.parse( readFileSync( join(router.build.outDir, router.base, "manifest.json"), @@ -51,8 +50,14 @@ export async function createBuild(app, buildConfig) { bundlerManifest[relative(app.config.root, router.handler)].file, ), }; - }), - ], + } else if (router.mode === "spa") { + return { + route: router.base.length === 1 ? "/**" : `${router.base}/**`, + handler: "#vinxi/spa", + }; + } + }), + ].filter(Boolean), rollupConfig: { plugins: [visualizer()], }, @@ -64,7 +69,20 @@ export async function createBuild(app, buildConfig) { baseURL: router.base, passthrough: true, })), - { dir: ".build/api/_build", baseURL: "/_build", fallthrough: true }, + ...app.config.routers + .filter((router) => router.mode === "build") + .map((router) => ({ + dir: join(router.build.outDir, router.base), + baseURL: router.base, + passthrough: true, + })), + ...app.config.routers + .filter((router) => router.mode === "spa") + .map((router) => ({ + dir: join(router.build.outDir, router.base), + baseURL: router.base, + passthrough: true, + })), ], scanDirs: [], appConfigFiles: [], @@ -99,6 +117,22 @@ export async function createBuild(app, buildConfig) { globalThis.app = prodApp } `, + "#vinxi/spa": () => { + const router = app.config.routers.find( + (router) => router.mode === "spa", + ); + const indexHtml = readFileSync( + join(router.build.outDir, router.base, "index.html"), + "utf-8", + ); + return ` + import { eventHandler } from 'h3' + const html = ${JSON.stringify(indexHtml)} + export default eventHandler(event => { + return html + }) + `; + }, }, }); diff --git a/packages/vinxi/lib/dev-server.js b/packages/vinxi/lib/dev-server.js index 3c343d9d..11eb5550 100644 --- a/packages/vinxi/lib/dev-server.js +++ b/packages/vinxi/lib/dev-server.js @@ -3,6 +3,7 @@ import { createNitro } from "nitropack"; import { createDevManifest } from "./manifest/dev-server-manifest.js"; import { createDevServer as createDevNitroServer } from "./nitro-dev.js"; +import { config } from "./plugins/config.js"; import { css } from "./plugins/css.js"; import { manifest } from "./plugins/manifest.js"; import { routes } from "./plugins/routes.js"; @@ -43,20 +44,25 @@ async function createViteServer(config) { return vite.createServer(config); } -const devPlugin = { +const targetDevPlugin = { browser: () => [css()], node: () => [], }; +const routerModeDevPlugin = { + spa: () => [config({ appType: "spa" })], + handler: () => [config({ appType: "custom" })], +}; + async function createViteSSREventHandler(router, serveConfig) { const viteDevServer = await createViteServer({ base: router.base, - appType: "custom", plugins: [ routes(), devEntries(), manifest(), - devPlugin[router.build.target]?.(), + ...(targetDevPlugin[router.build.target]?.() ?? []), + ...(routerModeDevPlugin[router.mode]?.() ?? []), ...(router.build?.plugins?.() || []), ], router, @@ -78,6 +84,8 @@ async function createViteSSREventHandler(router, serveConfig) { ); return handler(event); }); + } else if (router.mode === "spa") { + return defineEventHandler(fromNodeMiddleware(viteDevServer.middlewares)); } else { return defineEventHandler(fromNodeMiddleware(viteDevServer.middlewares)); } diff --git a/packages/vinxi/lib/plugins/config.js b/packages/vinxi/lib/plugins/config.js new file mode 100644 index 00000000..4543d176 --- /dev/null +++ b/packages/vinxi/lib/plugins/config.js @@ -0,0 +1,8 @@ +export function config(conf) { + return { + name: "vinxi:config", + config() { + return { ...conf }; + }, + }; +}