From 4e5bbdb2518dc493d33f2d128597bca36c996d1a Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 16 Oct 2025 18:46:19 +0900 Subject: [PATCH 1/8] feat(vite): add `experimental.assetsImport` option --- package.json | 1 + pnpm-lock.yaml | 15 +++++++++++++++ src/build/vite/plugin.ts | 11 +++++++++++ src/build/vite/prod.ts | 3 +++ src/build/vite/types.ts | 5 +++++ 5 files changed, 35 insertions(+) diff --git a/package.json b/package.json index 75bf57e0aa..d0065fbbe7 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "undici": "^7.11.0" }, "dependencies": { + "@hiogawa/vite-plugin-fullstack": "^0.0.3", "consola": "^3.4.2", "cookie-es": "^2.0.0", "crossws": "^0.4.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8ae3bc981..7bd0294369 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,9 @@ importers: .: dependencies: + '@hiogawa/vite-plugin-fullstack': + specifier: ^0.0.3 + version: 0.0.3(vite@7.1.9(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1)) consola: specifier: ^3.4.2 version: 3.4.2 @@ -1051,6 +1054,11 @@ packages: peerDependencies: vue: ^3.2.0 + '@hiogawa/vite-plugin-fullstack@0.0.3': + resolution: {integrity: sha512-vFx72us18dB1w/C/okzYLulPWc+YYp3FLK8LNxC88fDq+vcXuwAutpItq3oqDjZ9kCdDaiA0HiZ1R3xcL8vovA==} + peerDependencies: + vite: ^7.0.0 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -7207,6 +7215,13 @@ snapshots: '@tanstack/vue-virtual': 3.13.12(vue@3.5.22(typescript@5.9.3)) vue: 3.5.22(typescript@5.9.3) + '@hiogawa/vite-plugin-fullstack@0.0.3(vite@7.1.9(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))': + dependencies: + magic-string: 0.30.19 + srvx: 0.8.15 + strip-literal: 3.1.0 + vite: 7.1.9(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1) + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': diff --git a/src/build/vite/plugin.ts b/src/build/vite/plugin.ts index c7864269f8..995cc18bbb 100644 --- a/src/build/vite/plugin.ts +++ b/src/build/vite/plugin.ts @@ -18,6 +18,7 @@ import { defu } from "defu"; import { prettyPath } from "../../utils/fs"; import { NitroDevApp } from "../../dev/app"; import { nitroPreviewPlugin } from "./preview"; +import { assetsPlugin } from "@hiogawa/vite-plugin-fullstack" // https://vite.dev/guide/api-environment-plugins // https://vite.dev/guide/api-environment-frameworks.html @@ -296,6 +297,16 @@ function nitroPlugin(ctx: NitroPluginContext): VitePlugin[] { }, }, }, + ...ctx.pluginConfig.experimental?.assetsImport ? [ + assetsPlugin(), + { + name: "nitro:patch-assets-plugin", + configResolved(config) { + const plugin = config.plugins.find(p => p.name === 'fullstack:assets'); + ctx._buildApp = (plugin?.buildApp as any).handler + }, + } satisfies VitePlugin, + ] : [], ]; } diff --git a/src/build/vite/prod.ts b/src/build/vite/prod.ts index c59d21a406..960ccf16d6 100644 --- a/src/build/vite/prod.ts +++ b/src/build/vite/prod.ts @@ -76,6 +76,9 @@ export async function buildEnvironments( } } + // call postponed buildApp hook of other plugins + await ctx._buildApp?.(); + // ---------------------------------------------- // Stage 2: Build Nitro // ---------------------------------------------- diff --git a/src/build/vite/types.ts b/src/build/vite/types.ts index 8ff01c173d..94e3773f42 100644 --- a/src/build/vite/types.ts +++ b/src/build/vite/types.ts @@ -34,6 +34,10 @@ export interface NitroPluginConfig { * @note This is unsafe if plugins rely on temporary files on the filesystem. */ virtualBundle?: boolean; + /** + * @experimental Enable `?assets` import proposed by https://github.com/vitejs/vite/discussions/20913 + */ + assetsImport?: boolean }; } @@ -75,4 +79,5 @@ export interface NitroPluginContext { _publicDistDir?: string; _entryPoints: Record; _serviceBundles: Record; + _buildApp?: () => Promise; } From f9fd69f26db8142173f8dc25dce5fe0fbe39022b Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 16 Oct 2025 18:50:25 +0900 Subject: [PATCH 2/8] fix: delete buildApp --- src/build/vite/plugin.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/build/vite/plugin.ts b/src/build/vite/plugin.ts index 995cc18bbb..87188f323d 100644 --- a/src/build/vite/plugin.ts +++ b/src/build/vite/plugin.ts @@ -304,6 +304,7 @@ function nitroPlugin(ctx: NitroPluginContext): VitePlugin[] { configResolved(config) { const plugin = config.plugins.find(p => p.name === 'fullstack:assets'); ctx._buildApp = (plugin?.buildApp as any).handler + delete plugin?.buildApp; }, } satisfies VitePlugin, ] : [], From e9c0cffbf2e07b528807b19881397c69d7cd7b05 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 16 Oct 2025 23:34:00 +0900 Subject: [PATCH 3/8] fix: fix buildApp + fix types --- build.config.ts | 7 + examples/vite-assets/package.json | 21 ++ examples/vite-assets/src/counter.tsx | 8 + examples/vite-assets/src/entry-client.tsx | 8 + examples/vite-assets/src/entry-server.tsx | 41 +++ examples/vite-assets/src/styles.css | 7 + examples/vite-assets/tsconfig.json | 32 ++ examples/vite-assets/vite.config.ts | 16 + package.json | 2 +- pnpm-lock.yaml | 392 +++++++++++++++++++++- src/build/vite/plugin.ts | 28 +- src/build/vite/prod.ts | 2 +- src/build/vite/types.ts | 6 +- 13 files changed, 549 insertions(+), 21 deletions(-) create mode 100644 examples/vite-assets/package.json create mode 100644 examples/vite-assets/src/counter.tsx create mode 100644 examples/vite-assets/src/entry-client.tsx create mode 100644 examples/vite-assets/src/entry-server.tsx create mode 100644 examples/vite-assets/src/styles.css create mode 100644 examples/vite-assets/tsconfig.json create mode 100644 examples/vite-assets/vite.config.ts diff --git a/build.config.ts b/build.config.ts index ee83e9e682..4f36298548 100644 --- a/build.config.ts +++ b/build.config.ts @@ -6,6 +6,7 @@ import { defineBuildConfig } from "unbuild"; import { resolveModulePath } from "exsolve"; import { traceNodeModules } from "nf3"; +import { appendFileSync } from "node:fs"; const srcDir = fileURLToPath(new URL("src", import.meta.url)); const libDir = fileURLToPath(new URL("lib", import.meta.url)); @@ -82,6 +83,12 @@ export default defineBuildConfig({ } await rm(file); } + + // expose "?assets" import type through `nitro/vite` + appendFileSync( + "dist/vite.d.mts", + '\nimport type {} from "@hiogawa/vite-plugin-fullstack/types";\n' + ); }, }, externals: [ diff --git a/examples/vite-assets/package.json b/examples/vite-assets/package.json new file mode 100644 index 0000000000..18036929e1 --- /dev/null +++ b/examples/vite-assets/package.json @@ -0,0 +1,21 @@ +{ + "name": "nitro-playground", + "version": "1.0.0", + "type": "module", + "scripts": { + "build": "vite build", + "preview": "vite preview", + "dev": "vite dev" + }, + "devDependencies": { + "@preact/preset-vite": "^2.10.2", + "@tailwindcss/vite": "^4.1.14", + "nitro": "npm:nitro-nightly", + "tailwindcss": "^4.1.14", + "vite": "^7.1.8" + }, + "dependencies": { + "preact": "^10.27.2", + "preact-render-to-string": "^6.6.2" + } +} diff --git a/examples/vite-assets/src/counter.tsx b/examples/vite-assets/src/counter.tsx new file mode 100644 index 0000000000..d5953dccab --- /dev/null +++ b/examples/vite-assets/src/counter.tsx @@ -0,0 +1,8 @@ +import { useState } from "preact/hooks"; + +export function Counter() { + const [count, setCount] = useState(0); + return ( + + ); +} diff --git a/examples/vite-assets/src/entry-client.tsx b/examples/vite-assets/src/entry-client.tsx new file mode 100644 index 0000000000..182d2f8e14 --- /dev/null +++ b/examples/vite-assets/src/entry-client.tsx @@ -0,0 +1,8 @@ +import { hydrate } from "preact"; +import { Counter } from "./counter"; + +function main() { + hydrate(, document.querySelector("#counter")!); +} + +main(); diff --git a/examples/vite-assets/src/entry-server.tsx b/examples/vite-assets/src/entry-server.tsx new file mode 100644 index 0000000000..676f3790c3 --- /dev/null +++ b/examples/vite-assets/src/entry-server.tsx @@ -0,0 +1,41 @@ +import "./styles.css"; +import { renderToReadableStream } from "preact-render-to-string/stream"; +import clientAssets from "./entry-client?assets=client"; +import serverAssets from "./entry-server?assets=ssr"; +import { Counter } from "./counter"; + +export default { + async fetch(request: Request) { + const url = new URL(request.url); + const htmlStream = renderToReadableStream(); + return new Response(htmlStream, { + headers: { "Content-Type": "text/html;charset=utf-8" }, + }); + }, +}; + +function Root(props: { url: URL }) { + const assets = clientAssets.merge(serverAssets); + return ( + + + + Vite Assets Example + {assets.css.map((attr) => ( + + ))} + {assets.js.map((attr) => ( + + ))} +