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 };
+ },
+ };
+}