diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 414aed740..87907c280 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,16 +25,6 @@ jobs: - run: pnpm tsc - run: pnpm test - test-fullstack: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/setup - with: - build: false - - run: pnpm -C packages/fullstack build - - run: pnpm -C packages/fullstack test-e2e - test-node-loader-cloudflare: runs-on: ubuntu-latest steps: @@ -42,7 +32,6 @@ jobs: - uses: ./.github/actions/setup with: build: false - - run: pnpm -C packages/fullstack build - run: pnpm -C packages/node-loader-cloudflare build - run: pnpm -C packages/node-loader-cloudflare test-e2e diff --git a/.github/workflows/pkg-pr-new.yml b/.github/workflows/pkg-pr-new.yml index a4cb3a562..c0c2e9d31 100644 --- a/.github/workflows/pkg-pr-new.yml +++ b/.github/workflows/pkg-pr-new.yml @@ -53,6 +53,5 @@ jobs: packages/pre-bundle-new-url \ packages/server-asset \ packages/nitro \ - packages/fullstack \ packages/node-loader-cloudflare \ packages/ssr-css diff --git a/packages/fullstack/CHANGELOG.md b/packages/fullstack/CHANGELOG.md deleted file mode 100644 index 0d271f57b..000000000 --- a/packages/fullstack/CHANGELOG.md +++ /dev/null @@ -1,105 +0,0 @@ -# Changelog - -## v0.0.9 (2025-12-19) - -- perf: use plugin hook filters ([#1328](https://github.com/hi-ogawa/vite-plugins/pull/1328)) - -## v0.0.8 (2025-12-17) - -- build: inline runtime ([#1327](https://github.com/hi-ogawa/vite-plugins/pull/1327)) - -## v0.0.7 (2025-12-17) - -- feat: support custom base URLs ([#1325](https://github.com/hi-ogawa/vite-plugins/pull/1325)) - -## v0.0.6 (2025-12-17) - -- refactor: use virtual runtime entry ([#1297](https://github.com/hi-ogawa/vite-plugins/pull/1297)) - -## v0.0.5 (2025-10-16) - -- feat: expose `ViteBuilder.writeAssetsManifest` ([#1288](https://github.com/hi-ogawa/vite-plugins/pull/1288)) -- feat: add `clientBuildFallback` option ([#1289](https://github.com/hi-ogawa/vite-plugins/pull/1289)) - -## v0.0.4 (2025-10-16) - -- feat: provide ambient types through plugin import ([#1286](https://github.com/hi-ogawa/vite-plugins/pull/1286)) -- feat: expose `Assets.merge` method ([#1285](https://github.com/hi-ogawa/vite-plugins/pull/1285)) -- chore: server hmr in remix 3 ([#1280](https://github.com/hi-ogawa/vite-plugins/pull/1280)) -- chore: update version to 0.0.3 ([#1277](https://github.com/hi-ogawa/vite-plugins/pull/1277)) - -## v0.0.3 (2025-10-15) - -- fix: skip `@vite/client` patch on latest vite ([#1275](https://github.com/hi-ogawa/vite-plugins/pull/1275)) -- fix: fix vite peerDependencies range ([#1261](https://github.com/hi-ogawa/vite-plugins/pull/1261)) -- chore: remix frame demo ([#1273](https://github.com/hi-ogawa/vite-plugins/pull/1273)) -- chore: remix hydrated component demo ([#1272](https://github.com/hi-ogawa/vite-plugins/pull/1272)) -- chore: use `/islands/` directory convention ([#1265](https://github.com/hi-ogawa/vite-plugins/pull/1265)) -- docs: polish proposal ([#1259](https://github.com/hi-ogawa/vite-plugins/pull/1259)) - -## v0.0.2 (2025-10-08) - -- fix: support `cssCodeSplit: false` ([#1255](https://github.com/hi-ogawa/vite-plugins/pull/1255)) -- fix: fix `collectCss` not traversing module due to non awaited promise ([#1254](https://github.com/hi-ogawa/vite-plugins/pull/1254)) -- fix: fix css module hmr ([#1250](https://github.com/hi-ogawa/vite-plugins/pull/1250)) -- chore: nav active link in `examples/island` ([#1253](https://github.com/hi-ogawa/vite-plugins/pull/1253)) -- docs: add comprehensive "How It Works" documentation ([#1252](https://github.com/hi-ogawa/vite-plugins/pull/1252)) -- docs: tweak readme ([#1247](https://github.com/hi-ogawa/vite-plugins/pull/1247)) - -## v0.0.1 (2025-10-07) - -- fix: avoid `addWatchFile` for virtual invalidation ([#1239](https://github.com/hi-ogawa/vite-plugins/pull/1239)) -- fix: skip assets module itself for css collection ([#1240](https://github.com/hi-ogawa/vite-plugins/pull/1240)) -- fix: fix `?assets` dts typing ([#1236](https://github.com/hi-ogawa/vite-plugins/pull/1236)) -- fix: use `preserveSignature: exports-only` for client assets entry ([#1234](https://github.com/hi-ogawa/vite-plugins/pull/1234)) -- fix: ensure module is analyzed during `collectCss` ([#1227](https://github.com/hi-ogawa/vite-plugins/pull/1227)) -- fix: remove `?assets` import chunk from client build ([#1221](https://github.com/hi-ogawa/vite-plugins/pull/1221)) -- fix: rename options ([#1229](https://github.com/hi-ogawa/vite-plugins/pull/1229)) -- refactor: simplify assets import virual ([#1238](https://github.com/hi-ogawa/vite-plugins/pull/1238)) -- chore: data fetching example ([#1242](https://github.com/hi-ogawa/vite-plugins/pull/1242)) -- chore: island example ([#1225](https://github.com/hi-ogawa/vite-plugins/pull/1225)) -- chore: tweak vue-router example ([#1223](https://github.com/hi-ogawa/vite-plugins/pull/1223)) -- test: add `import.meta.glob` example ([#1224](https://github.com/hi-ogawa/vite-plugins/pull/1224)) - -## v0.0.0 (2025-10-05) - -- docs: tweak heading ([#1218](https://github.com/hi-ogawa/vite-plugins/pull/1218)) - -## v0.0.0-alpha.3 (2025-10-04) - -- feat: support `?assets=client` and `?assets=ssr` ([#1214](https://github.com/hi-ogawa/vite-plugins/pull/1214)) -- feat: support `?assets` query ([#1208](https://github.com/hi-ogawa/vite-plugins/pull/1208)) -- fix: remove `transformeRequest` during `collectCss` ([#1212](https://github.com/hi-ogawa/vite-plugins/pull/1212)) -- docs: `?assets` import ([#1215](https://github.com/hi-ogawa/vite-plugins/pull/1215)) -- chore: use unhead to simplify vue-router ([#1216](https://github.com/hi-ogawa/vite-plugins/pull/1216)) -- chore: simplify vue-router ssr head example ([#1210](https://github.com/hi-ogawa/vite-plugins/pull/1210)) -- chore: simplify react-router ssr head example ([#1209](https://github.com/hi-ogawa/vite-plugins/pull/1209)) -- chore: tweak vue-router example ([#1205](https://github.com/hi-ogawa/vite-plugins/pull/1205)) - -## v0.0.0-alpha.2 (2025-10-04) - -- feat!: universal route assets as default ([#1196](https://github.com/hi-ogawa/vite-plugins/pull/1196)) -- fix: merge `asEntry` properly ([#1200](https://github.com/hi-ogawa/vite-plugins/pull/1200)) -- fix: ensure watch file for import with queries ([#1197](https://github.com/hi-ogawa/vite-plugins/pull/1197)) -- fix: remove client-fallback from output ([#1198](https://github.com/hi-ogawa/vite-plugins/pull/1198)) -- fix: avoid `addWatchFile` for assets virtual ([#1194](https://github.com/hi-ogawa/vite-plugins/pull/1194)) -- fix: enable `emitAssets` by default ([#1186](https://github.com/hi-ogawa/vite-plugins/pull/1186)) -- docs: typo ([#1195](https://github.com/hi-ogawa/vite-plugins/pull/1195)) -- test: test react-router ([#1188](https://github.com/hi-ogawa/vite-plugins/pull/1188)) -- test: test vue-router ([#1187](https://github.com/hi-ogawa/vite-plugins/pull/1187)) -- chore: tweak vue-router example ([#1202](https://github.com/hi-ogawa/vite-plugins/pull/1202)) -- chore: move `reactHmrPreamblePlugin` to each example ([#1201](https://github.com/hi-ogawa/vite-plugins/pull/1201)) -- chore: add ssg example ([#1184](https://github.com/hi-ogawa/vite-plugins/pull/1184)) - -## v0.0.0-alpha.1 (2025-10-03) - -- fix: fix `asEntry` option ([#1174](https://github.com/hi-ogawa/vite-plugins/pull/1174)) -- fix: enable `emitAssets` by default ([#1176](https://github.com/hi-ogawa/vite-plugins/pull/1176)) -- chore: remove `universal` option ([#1181](https://github.com/hi-ogawa/vite-plugins/pull/1181)) -- chore: standalone examples ([#1180](https://github.com/hi-ogawa/vite-plugins/pull/1180)) -- test: cloudflare plugin ([#1177](https://github.com/hi-ogawa/vite-plugins/pull/1177)) -- test: more e2e ([#1173](https://github.com/hi-ogawa/vite-plugins/pull/1173)) - -## v0.0.0-alpha.0 (2025-10-02) - -- prototype `import.meta.vite.assets` API ([#1168](https://github.com/hi-ogawa/vite-plugins/pull/1168)) diff --git a/packages/fullstack/HOW_IT_WORKS.md b/packages/fullstack/HOW_IT_WORKS.md deleted file mode 100644 index 615555493..000000000 --- a/packages/fullstack/HOW_IT_WORKS.md +++ /dev/null @@ -1,210 +0,0 @@ -# How It Works - -This document explains the internal architecture and implementation details of `@hiogawa/vite-plugin-fullstack`. - -## Table of Contents - -- [Overview](#overview) -- [Query Import System](#query-import-system) -- [Virtual Module System](#virtual-module-system) -- [Dev Mode: CSS Collection](#dev-mode-css-collection) -- [Build Mode: Asset Manifest](#build-mode-asset-manifest) -- [Hot Module Replacement](#hot-module-replacement) - -## Overview - -The plugin provides a `?assets` query import API for accessing build assets information: - -```js -import assets from "./page.js?assets"; -import clientAssets from "./client.js?assets=client"; -import ssrAssets from "./server.js?assets=ssr"; -``` - -The implementation differs between dev and build modes: -- **Dev mode**: Dynamic CSS collection via module graph traversal -- **Build mode**: Static asset manifest generated during build - -## Query Import System - -### `?assets` Query Variations - -The plugin supports three variations: - -```js -// Universal route (both client and current environment) -import assets from "./page.js?assets"; - -// Client assets only (with entry flag for server-accessing-client case) -import assets from "./page.js?assets=client"; - -// Specific environment assets -import assets from "./page.js?assets=ssr"; -``` - -### Resolution Logic - -The `fullstack:assets-query` plugin intercepts `?assets` imports: - -1. **Server environments**: Generates code based on query value: - - `?assets=client` → Single environment import - - `?assets=ssr` → Single environment import - - `?assets` → Merged client + current environment - -2. **Client environment**: Returns empty assets (client doesn't need this info) - ```js - return `\0virtual:fullstack/empty-assets`; - ``` - -## Virtual Module System - -### Virtual Module Flow - -When you import `page.js?assets`, the plugin resolves it to a virtual module that loads the assets information. In dev mode, it returns the CSS collected via module graph traversal. In build mode, it returns a reference to the static assets manifest. - -### `processAssetsImport()` Implementation - -This core function has different behavior in dev vs build: - -**Dev Mode:** -```js -{ - entry: "/src/entry.client.tsx", // Only for client environment - js: [], // Always empty in dev - css: [ // Collected via module graph on server. Empty for `?assets=client` - { href: "/src/styles.css", "data-vite-dev-id": "..." } - ] -} -``` - -**Build Mode:** - -Returns a reference to the static manifest entry (see [Build Mode: Asset Manifest](#build-mode-asset-manifest) for how the manifest is generated): - -```js -__assets_manifest["ssr"]["/src/entry.client.tsx"] -// { -// entry: "/assets/index-abc123.js", // Entry chunk file name -// js: [ // Preload chunks -// { href: "/assets/chunk-def456.js" } -// ], -// css: [ // CSS files -// { href: "/assets/style-789xyz.css" } -// ] -// } -``` - -## Dev Mode: CSS Collection - -In dev mode, the plugin traverses the module graph to find all CSS dependencies of a given entry module. It recursively visits all imported modules, collecting any CSS imports it encounters. To ensure all imports are discovered, modules are eagerly transformed if not already processed. The traversal skips special CSS queries like `?url`, `?inline`, and `?raw`, as well as virtual modules and `?assets` queries themselves to avoid circular dependencies. - -## Build Mode: Asset Manifest - -### Build Process Flow - -1. **Tracking Phase** (during server build): - ```js - // Server code references client assets - import clientAssets from "./entry.client.js?assets=client"; - - // Plugin tracks this in importAssetsMetaMap: - importAssetsMetaMap["client"]["/entry.client.js"] = { - id: "/entry.client.js", - key: "entry.client.js", // Relative path for machine-independent builds - importerEnvironment: "ssr", - isEntry: true - }; - ``` - -2. **Dynamic Entry Injection** (`buildStart` hook): - - When building the client environment, the plugin emits tracked modules as entry chunks: - ```js - if (environment.name === "client") { - for (const meta of importAssetsMetaMap["client"]) { - if (meta.isEntry) { - this.emitFile({ type: "chunk", id: meta.id, preserveSignature: "exports-only" }); - } - } - } - ``` - -3. **Dependency Collection** (`buildApp` hook): - - After all environments are built, collect dependencies for each tracked module: - ```js - function collectAssetDeps(bundle) { - // For each chunk, recursively collect: - // - All imported chunks (JS dependencies) - // - All imported CSS files (viteMetadata.importedCss) - return { js: [...], css: [...] }; - } - ``` - -4. **Manifest Generation**: - ```js - // __fullstack_assets_manifest.js - export default { - "client": { - "entry.client.js": { - entry: "/assets/entry.client-abc123.js", - js: [ - { href: "/assets/chunk-def456.js" } - ], - css: [ - { href: "/assets/style-789xyz.css" } - ] - } - }, - "ssr": { - "page.js": { - js: [], - css: [ - { href: "/assets/page-style-abc.css" } - ] - } - } - }; - ``` - -5. **Asset Copying**: - - CSS assets generated by server builds are copied to the client output directory - - This ensures all CSS is served from the client's public directory - -### Manifest Access at Runtime - -Server code accesses the manifest via external import: - -```js -// Generated virtual module -import __assets_manifest from "virtual:fullstack/assets-manifest"; -export default __assets_manifest["ssr"]["page.js"]; -``` - -During `renderChunk`, the plugin rewrites the import to a relative path: -```js -"virtual:fullstack/assets-manifest" → "./__fullstack_assets_manifest.js" -``` - -## Hot Module Replacement - -### SSR-Injected CSS HMR - -Vite's HMR assumes all CSS is injected via `import`, not via SSR-rendered `` tags. The plugin patches Vite's client-side HMR to track SSR-injected CSS links and prevent duplicate style injection. When CSS updates, it removes the old `` tags with matching `data-vite-dev-id` attributes instead of trying to update them as ` diff --git a/packages/fullstack/examples/vue-router/src/framework/entry.client.ts b/packages/fullstack/examples/vue-router/src/framework/entry.client.ts deleted file mode 100644 index e790154c4..000000000 --- a/packages/fullstack/examples/vue-router/src/framework/entry.client.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createSSRApp } from "vue"; -import { RouterView, createRouter, createWebHistory } from "vue-router"; -import { routes } from "../routes"; - -async function main() { - const app = createSSRApp(RouterView); - - const router = createRouter({ - history: createWebHistory(), - routes, - }); - app.use(router); - - await router.isReady(); - app.mount("#root"); - - if (import.meta.hot) { - // TODO - import.meta.hot.on("fullstack:update", (e) => { - console.log("[fullstack:update]", e); - window.location.reload(); - }); - } -} - -main(); diff --git a/packages/fullstack/examples/vue-router/src/framework/entry.server.ts b/packages/fullstack/examples/vue-router/src/framework/entry.server.ts deleted file mode 100644 index 05a9325e9..000000000 --- a/packages/fullstack/examples/vue-router/src/framework/entry.server.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { createHead, transformHtmlTemplate } from "unhead/server"; -import { createSSRApp } from "vue"; -import { RouterView, createMemoryHistory, createRouter } from "vue-router"; -import { renderToString } from "vue/server-renderer"; -import { routes } from "../routes"; -import clientEntry from "./entry.client.ts?assets=client"; - -async function handler(request: Request): Promise { - // setup app - const app = createSSRApp(RouterView); - - // setup vue-router - // https://github.com/nuxt/nuxt/blob/766806c8d90015873f86c3f103b09803bd214258/packages/nuxt/src/pages/runtime/plugins/router.ts - const router = createRouter({ - history: createMemoryHistory(), - routes, - }); - app.use(router); - - const url = new URL(request.url); - const href = url.href.slice(url.origin.length); - await router.push(href); - await router.isReady(); - - // collect assets from current route - const assets = clientEntry.merge( - ...(await Promise.all( - router.currentRoute.value.matched - .map((to) => to.meta.assets) - .filter(Boolean) - .map((fn) => fn!().then((m) => m.default)), - )), - ); - const head = createHead(); - head.push({ - link: [ - ...assets.css.map((attrs) => ({ rel: "stylesheet", ...attrs })), - ...assets.js.map((attrs) => ({ rel: "modulepreload", ...attrs })), - ], - script: [{ type: "module", src: clientEntry.entry }], - }); - - // SSR - const ssrStream = await renderToString(app); - - // inject to HTML shell with head tags - let htmlStream = `\ - - - - - - Vue Router Custom Framework - - -
${ssrStream}
- - -`; - htmlStream = await transformHtmlTemplate(head, htmlStream); - - return new Response(htmlStream, { - headers: { - "Content-Type": "text/html;charset=utf-8", - }, - }); -} - -export default { - fetch: handler, -}; - -if (import.meta.hot) { - import.meta.hot.accept(); -} diff --git a/packages/fullstack/examples/vue-router/src/framework/types.d.ts b/packages/fullstack/examples/vue-router/src/framework/types.d.ts deleted file mode 100644 index ab39e4318..000000000 --- a/packages/fullstack/examples/vue-router/src/framework/types.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// https://router.vuejs.org/guide/advanced/meta.html#TypeScript -import "vue-router"; - -export {}; - -declare module "vue-router" { - interface RouteMeta { - assets?: () => Promise; - } -} diff --git a/packages/fullstack/examples/vue-router/src/pages/about.ts b/packages/fullstack/examples/vue-router/src/pages/about.ts deleted file mode 100644 index 3a4eb4943..000000000 --- a/packages/fullstack/examples/vue-router/src/pages/about.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { defineComponent, h } from "vue"; - -export default defineComponent(() => { - return () => { - return h("main", [ - h("h1", "About"), - h("div", { class: "card" }, [ - h( - "p", - "This is a simple Vue Router demo app built with Vite Plugin Fullstack.", - ), - h("p", "It demonstrates basic routing and server-side rendering."), - ]), - ]); - }; -}); diff --git a/packages/fullstack/examples/vue-router/src/pages/index.vue b/packages/fullstack/examples/vue-router/src/pages/index.vue deleted file mode 100644 index a3e222e57..000000000 --- a/packages/fullstack/examples/vue-router/src/pages/index.vue +++ /dev/null @@ -1,51 +0,0 @@ - - - - - diff --git a/packages/fullstack/examples/vue-router/src/pages/not-found.vue b/packages/fullstack/examples/vue-router/src/pages/not-found.vue deleted file mode 100644 index 199773af2..000000000 --- a/packages/fullstack/examples/vue-router/src/pages/not-found.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/packages/fullstack/examples/vue-router/src/routes.ts b/packages/fullstack/examples/vue-router/src/routes.ts deleted file mode 100644 index 3aa0f2e03..000000000 --- a/packages/fullstack/examples/vue-router/src/routes.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { RouteRecordRaw } from "vue-router"; - -// custom framework may employ fs router convention to reduce boilerplace. -export const routes: RouteRecordRaw[] = [ - { - path: "/", - name: "app", - component: () => import("./app.vue"), - meta: { - assets: () => import("./app.vue?assets"), - }, - children: [ - { - path: "/", - name: "home", - component: () => import("./pages/index.vue"), - meta: { - assets: () => import("./pages/index.vue?assets"), - }, - }, - { - path: "/about", - name: "about", - component: () => import("./pages/about.ts"), - meta: { - assets: () => import("./pages/about.ts?assets"), - }, - }, - { - path: "/:catchAll(.*)", - name: "not-found", - component: () => import("./pages/not-found.vue"), - meta: { - assets: () => import("./pages/not-found.vue?assets"), - }, - }, - ], - }, -]; diff --git a/packages/fullstack/examples/vue-router/src/styles.css b/packages/fullstack/examples/vue-router/src/styles.css deleted file mode 100644 index 1dd200ab9..000000000 --- a/packages/fullstack/examples/vue-router/src/styles.css +++ /dev/null @@ -1,49 +0,0 @@ -* { - box-sizing: border-box; -} - -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - background: #f5f5f5; - color: #333; -} - -main { - max-width: 800px; - margin: 0 auto; - padding: 2rem; -} - -h1 { - font-size: 2.5rem; - margin-bottom: 0.5rem; -} - -.card { - background: white; - border-radius: 8px; - padding: 2rem; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); - margin: 2rem 0; -} - -button { - background: rgb(83, 91, 242); - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 4px; - font-size: 1rem; - cursor: pointer; -} - -button:hover { - background: #535bf2; -} - -.subtitle { - color: #666; - font-size: 1.1rem; - margin-bottom: 2rem; -} diff --git a/packages/fullstack/examples/vue-router/tsconfig.json b/packages/fullstack/examples/vue-router/tsconfig.json deleted file mode 100644 index e7bee27aa..000000000 --- a/packages/fullstack/examples/vue-router/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "include": ["src", "*.ts", "src/**/*.vue"], - "compilerOptions": { - "jsx": "react-jsx", - "erasableSyntaxOnly": true, - "allowImportingTsExtensions": true, - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "skipLibCheck": true, - "verbatimModuleSyntax": true, - "noEmit": true, - "moduleResolution": "Bundler", - "module": "ESNext", - "target": "ESNext", - "lib": ["ESNext", "DOM", "DOM.Iterable"], - "types": ["vite/client"] - } -} diff --git a/packages/fullstack/examples/vue-router/vite.config.ts b/packages/fullstack/examples/vue-router/vite.config.ts deleted file mode 100644 index 9dd6df322..000000000 --- a/packages/fullstack/examples/vue-router/vite.config.ts +++ /dev/null @@ -1,113 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import { Readable } from "node:stream"; -import { pathToFileURL } from "node:url"; -import fullstack from "@hiogawa/vite-plugin-fullstack"; -import vue from "@vitejs/plugin-vue"; -import { type Plugin, type ResolvedConfig, defineConfig } from "vite"; -import devtoolsJson from "vite-plugin-devtools-json"; - -export default defineConfig((_env) => ({ - clearScreen: false, - plugins: [ - // import("vite-plugin-inspect").then((m) => m.default()), - patchVueExclude(vue(), /\?assets/), - devtoolsJson(), - fullstack(), - !!process.env.TEST_SSG && - ssgPlugin({ - paths: ["/", "/about"], - }), - ], - optimizeDeps: { - entries: ["src/framework/entry.client.ts"], - }, - environments: { - client: { - build: { - outDir: "./dist/client", - }, - }, - ssr: { - build: { - outDir: "./dist/ssr", - rollupOptions: { - input: { - index: "./src/framework/entry.server.ts", - }, - }, - }, - }, - }, - builder: { - async buildApp(builder) { - await builder.build(builder.environments["ssr"]!); - await builder.build(builder.environments["client"]!); - }, - }, -})); - -// workaround https://github.com/vitejs/vite-plugin-vue/issues/677 -function patchVueExclude(plugin: Plugin, exclude: RegExp) { - const original = (plugin.transform as any).handler; - (plugin.transform as any).handler = function (this: any, ...args: any[]) { - if (exclude.test(args[1])) return; - return original.call(this, ...args); - }; - return plugin; -} - -type SsgPluginOptions = { - paths: string[]; -}; - -function ssgPlugin(pluginOpts: SsgPluginOptions): Plugin { - return { - name: "ssg", - buildApp: { - order: "post", - async handler(builder) { - await renderStatic(pluginOpts, builder.config); - }, - }, - }; -} - -async function renderStatic( - pluginOpts: SsgPluginOptions, - config: ResolvedConfig, -) { - console.log("[ssg] started"); - - // import server entry - const entryPath = path.join(config.environments.ssr.build.outDir, "index.js"); - const entry: typeof import("./src/framework/entry.server") = await import( - pathToFileURL(entryPath).href - ); - - // render html - const baseDir = config.environments.client.build.outDir; - for (const staticPatch of pluginOpts.paths) { - config.logger.info(" -> " + staticPatch); - const response = await entry.default.fetch( - new Request(new URL(staticPatch, "http://ssg.local")), - ); - await writeFileStream( - path.join(baseDir, normalizeHtmlFilePath(staticPatch)), - response.body!, - ); - } - console.log("[ssg] finished in (TODO) ms"); -} - -async function writeFileStream(filePath: string, stream: ReadableStream) { - await fs.promises.mkdir(path.dirname(filePath), { recursive: true }); - await fs.promises.writeFile(filePath, Readable.fromWeb(stream as any)); -} - -function normalizeHtmlFilePath(p: string) { - if (p.endsWith("/")) { - return p + "index.html"; - } - return p + ".html"; -} diff --git a/packages/fullstack/examples/vue-router/vue.d.ts b/packages/fullstack/examples/vue-router/vue.d.ts deleted file mode 100644 index daba9b9ec..000000000 --- a/packages/fullstack/examples/vue-router/vue.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare module "*.vue" { - import { DefineComponent } from "vue"; - const component: DefineComponent<{}, {}, any>; - export default component; -} diff --git a/packages/fullstack/notes.md b/packages/fullstack/notes.md deleted file mode 100644 index bda494eaf..000000000 --- a/packages/fullstack/notes.md +++ /dev/null @@ -1,279 +0,0 @@ - -## TODO - -### MVP - -- [ ] mvp API design + implementation - - [x] client entry on server - - [x] client assets / dependencies on server - - [x] server css on server -- [ ] examples - - [x] `create-vite-extra`-like ssr example https://github.com/bluwy/create-vite-extra/ - - [x] `@cloudflare/vite-plugin` - - [ ] `nitro/vite` examples https://github.com/nitrojs/vite-examples - - [x] island - - [x] client only - - [ ] ssr - - [ ] fresh - - [ ] router library - - [x] react router - - [x] vue router - - scoped css hmr is broken :( - because of `&lang.css` vs `&lang.css=` difference? - it looks like new css link is returned as `text/javascript`? - workarounded by forcing `&lang.css` via middleware. -- [ ] e2e -- [ ] docs -- [ ] RFC on vite discussion - -### Future - -- API - - add client entry dynamically - - `transformIndexHtml` on server - - handle server change event (e.g. reload / refetch) - - deduplicate client and server css on build when css code split differs - - treat css on server like client reference? -- create custom integration of router libraries - - react router - - vue router -- test it on ecosystem framework - - `fresh` (server css) - - -WIP - -This plugin provides primitives to make building ssr application on Vite simpler. - -## Features - -- Framework agnostics -- ... - -## Example - -```js -import { defineConfig } from "vite"; -import fullstack from "@hiogawa/vite-plugin-fullstack" - -export default defineConfig({ - plugins: [ - fullstack(), - ], - environments: { - client: { - build: { - rollupOptions: { - input: { - index: "./src/entry.client.tsx", - }, - }, - }, - }, - ssr: { - build: { - rollupOptions: { - input: { - index: "./src/entry.server.tsx", - }, - }, - }, - } - } -}); -``` - -## Example with Nitro plugin - -```js -import { defineConfig } from "vite"; -import nitro from "nitro/vite" -import fullstack from "@hiogawa/vite-plugin-fullstack" - -export default defineConfig({ - plugins: [ - nitro(), - fullstack(), - ], -}); -``` - -## Ideas - -- handle css on server -- access client asset on server runtime - - js and css dependencies (e.g. modulepreload) -- `transformIndexHtml` compat/alternative -- cjs module runner -- "use client-entry"? -- dynamic entry injection? -- request handle convention (same as nitro) -- logger -- ssg primitive (may delegate to nitro?) -- multi platform deployment (delgate to nitro) - -## Brainstorming - -- design - -```js -import.meta.vite.ssrAssets("/page.tsx"); -import.meta.vite.ssrAssets(); // self-referencing -import.meta.vite.entryAssets("index"); // by entry name - -import.meta.assetDeps("ssr", { entryName: "index" }); -import.meta.assetDeps("entry:index", { environment: "ssr", entry }); -import.meta.assetDeps("entry:index", { environment: "client" }); -import.meta.assetsManifest(); - -type SsrAssetsInfo = { - js: string[]; - css: string[]; -} -type ClientEntry = { - entry: string; - js: string[]; - css: string[]; -} -type ClientAssetsInfo = { - entry: string; - js: string[]; - css: string[]; -} - -// separate by environments? -type AssetsInfo = { - css: string[]; - environments: { - ssr?: { - css: string[]; - }, - client?: { - entry: string; - js: string[]; - css: string[]; - }, - } -} - -// use cases -// - entry.server.js references entry.client.js -import.meta.vite.assets({ import: "/entry.client.js", clientOnly: true }) - -// - universal route file references its assets -import.meta.vite.assets({ ssrOnly: true }) - -// - island references its assets -import.meta.vite.assets({ import: "", clientOnly: true }) -``` - -## Questions - -- doesn't this handle only initial render (ssr)? how about preloading assets on client side navigation? - - This is not needed since Vite optimizes client dynamic import. https://vite.dev/guide/features.html#async-chunk-loading-optimization - - -## Initial assumptions - -- environments: client, ssr - -## Target 1 - -ssr/client universal route (e.g. React router, Vue router, etc.) - -- routes.js - - framework plugin can auto generate with server loader splitting etc. - -```js -const routes = { - "/": () => import("routes/index.js"), - "/about": () => import("routes/about.js"), -} -``` - -- entry.server.js - -```js -import.meta.vite.entryAssets("/entry.client.js") -handleRequest(request, routes) -``` - -- entry.client.js - -```js -hydrate(document, routes); -``` - -- routes/index.js - -```js -export default function Page() { - // ... -} - -// manually or automatically inject by framework plugin -export const assets = import.meta.vite.ssrAssets(); -``` - -## Target 2 - -ssr only route + client (ssr-optional) island (e.g. Astro, Fresh) - -- routes.js - -```js -const routes = { - "/": () => import("routes/index.js"), - "/about": () => import("routes/about.js"), -} -``` - -- entry.server.js - -```js -handleRequest(request, routes) -``` - -- routes/index.js - -```js -export default function Page() { - // framework specific head injection - const assets = import.meta.vite.ssrAssets(); -} -``` - -- island.js - - framework can apply transform to implement island - -```js -export function Island() { -} -``` - -## Stages - -- migrate minimal examples - - https://github.com/bluwy/create-vite-extra/ - - https://github.com/nitrojs/vite-examples -- create custom integration of router libraries - - react router - - vue router -- integrate with ecosystem plugins - - `@cloudflare/vite-plugin` - - `nitro/vite` - - `fresh` - -## Examples - -- `create-vite-extra` https://github.com/bluwy/create-vite-extra/ -- `@cloudflare/vite-plugin` -- `nitro/vite` -- react router -- vue router -- `@remix-run/fetch-router` https://github.com/remix-run/remix/tree/main/packages/fetch-router - -## References - -- https://github.com/vitejs/vite/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22feat%3A%20ssr%22 - - https://github.com/vitejs/vite/issues/16515 diff --git a/packages/fullstack/package.json b/packages/fullstack/package.json deleted file mode 100644 index 2c123b6ae..000000000 --- a/packages/fullstack/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "@hiogawa/vite-plugin-fullstack", - "version": "0.0.9", - "homepage": "https://github.com/hi-ogawa/vite-plugins/tree/main/packages/fullstack", - "repository": { - "type": "git", - "url": "git+https://github.com/hi-ogawa/vite-plugins.git", - "directory": "packages/fullstack" - }, - "license": "MIT", - "type": "module", - "exports": { - "./package.json": "./package.json", - "./types": "./types/index.d.ts", - ".": "./dist/index.js", - "./*": "./dist/*.js" - }, - "files": ["dist", "types"], - "scripts": { - "dev": "tsdown --sourcemap --watch src", - "build": "tsdown", - "prepack": "tsdown --clean", - "test-e2e": "playwright test" - }, - "dependencies": { - "@rolldown/pluginutils": "1.0.0-beta.55", - "magic-string": "^0.30.17", - "srvx": "^0.8.7", - "strip-literal": "^3.1.0" - }, - "peerDependencies": { - "vite": "^7.0.0" - } -} diff --git a/packages/fullstack/playwright.config.ts b/packages/fullstack/playwright.config.ts deleted file mode 100644 index 4ae51df49..000000000 --- a/packages/fullstack/playwright.config.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { defineConfig, devices } from "@playwright/test"; - -export default defineConfig({ - testDir: "e2e", - use: { - trace: "on-all-retries", - screenshot: "only-on-failure", - }, - projects: [ - { - name: "chromium", - use: { - ...devices["Desktop Chrome"], - viewport: null, - deviceScaleFactor: undefined, - }, - }, - ], - workers: 1, - retries: process.env.CI ? 2 : 0, - forbidOnly: !!process.env.CI, - reporter: process.env.CI ? [["list"], ["github"]] : "list", -}) as any; diff --git a/packages/fullstack/src/index.ts b/packages/fullstack/src/index.ts deleted file mode 100644 index 18499feb2..000000000 --- a/packages/fullstack/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default, serverHandlerPlugin, assetsPlugin } from "./plugin.ts"; diff --git a/packages/fullstack/src/plugin.ts b/packages/fullstack/src/plugin.ts deleted file mode 100644 index e773497da..000000000 --- a/packages/fullstack/src/plugin.ts +++ /dev/null @@ -1,922 +0,0 @@ -import assert from "node:assert/strict"; -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { exactRegex } from "@rolldown/pluginutils"; -import MagicString from "magic-string"; -import { toNodeHandler } from "srvx/node"; -import { stripLiteral } from "strip-literal"; -import { - DevEnvironment, - type EnvironmentModuleNode, - type Plugin, - type ResolvedConfig, - type Rollup, - type ViteBuilder, - type ViteDevServer, - isCSSRequest, - isRunnableDevEnvironment, - normalizePath, -} from "vite"; -import type { - ImportAssetsOptions, - ImportAssetsResultRaw, -} from "../types/shared"; -import { - parseAssetsVirtual, - parseIdQuery, - toAssetsVirtual, -} from "./plugins/shared"; -import { - createVirtualPlugin, - getEntrySource, - hashString, - normalizeRelativePath, -} from "./plugins/utils"; -import { - directRequestRE, - evalValue, - normalizeViteImportAnalysisUrl, -} from "./plugins/vite-utils"; - -type FullstackPluginOptions = { - /** - * @default true - */ - serverHandler?: boolean; - /** - * @default ["ssr"] - */ - serverEnvironments?: string[]; - /** - * @experimental - */ - experimental?: { - /** - * @default true - */ - devEagerTransform?: boolean; - /** - * @default true - */ - clientBuildFallback?: boolean; - }; -}; - -type ImportAssetsMeta = { - id: string; - key: string; - importerEnvironment: string; - isEntry: boolean; -}; - -export default function vitePluginFullstack( - pluginOpts?: FullstackPluginOptions, -): Plugin[] { - return [...serverHandlerPlugin(pluginOpts), ...assetsPlugin(pluginOpts)]; -} - -export function serverHandlerPlugin( - pluginOpts?: FullstackPluginOptions, -): Plugin[] { - return [ - { - name: "fullstack:server-handler", - apply: () => pluginOpts?.serverHandler !== false, - config(userConfig, _env) { - return { - appType: userConfig.appType ?? "custom", - }; - }, - configureServer(server) { - const name = (pluginOpts?.serverEnvironments ?? ["ssr"])[0]!; - const environment = server.environments[name]!; - assert(isRunnableDevEnvironment(environment)); - const runner = environment.runner; - return () => { - server.middlewares.use(async (req, res, next) => { - try { - const source = getEntrySource(environment.config); - const mod = await runner.import(source); - req.url = req.originalUrl ?? req.url; - await toNodeHandler(mod.default.fetch)(req, res); - } catch (e) { - next(e); - } - }); - }; - }, - }, - ]; -} - -export function assetsPlugin(pluginOpts?: FullstackPluginOptions): Plugin[] { - let server: ViteDevServer; - let resolvedConfig: ResolvedConfig; - const importAssetsMetaMap: { - [environment: string]: { [id: string]: ImportAssetsMeta }; - } = {}; - const bundleMap: { [environment: string]: Rollup.OutputBundle } = {}; - - async function processAssetsImport( - ctx: Rollup.PluginContext, - id: string, - options: { - environment: string; - isEntry: boolean; - }, - ) { - if (ctx.environment.mode === "dev") { - const result: ImportAssetsResultRaw = { - entry: undefined, // defined only on client - js: [], // always empty - css: [], // defined only on server - }; - const environment = server.environments[options.environment]; - assert(environment, `Unknown environment: ${options.environment}`); - if (options.environment === "client") { - result.entry = assetsURLDev( - normalizeViteImportAnalysisUrl(environment, id).slice(1), - resolvedConfig, - ); - } - if (environment.name !== "client") { - const collected = await collectCss(environment, id, { - eager: pluginOpts?.experimental?.devEagerTransform ?? true, - }); - result.css = collected.hrefs.map((href, i) => ({ - href: assetsURLDev(href.slice(1), resolvedConfig), - "data-vite-dev-id": collected.ids[i], - })); - } - return JSON.stringify(result); - } else { - const map = (importAssetsMetaMap[options.environment] ??= {}); - const meta: ImportAssetsMeta = { - id, - // normalize key to have machine-independent build output - key: path.relative(resolvedConfig.root, id), - importerEnvironment: ctx.environment.name, - isEntry: !!(map[id]?.isEntry || options.isEntry), - }; - map[id] = meta; - return `__assets_manifest[${JSON.stringify(options.environment)}][${JSON.stringify(meta.key)}]`; - } - } - - let writeAssetsManifestCalled = false; - async function writeAssetsManifest(builder: ViteBuilder) { - if (writeAssetsManifestCalled) return; - writeAssetsManifestCalled = true; - - // build manifest of imported assets - const manifest: BuildAssetsManifest = {}; - for (const [environmentName, metas] of Object.entries( - importAssetsMetaMap, - )) { - const bundle = bundleMap[environmentName]!; - const assetDepsMap = collectAssetDeps(bundle); - for (const [id, meta] of Object.entries(metas)) { - const found = assetDepsMap[id]; - if (!found) { - builder.config.logger.error( - `[vite-plugin-fullstack] failed to find built chunk for ${meta.id} imported by ${meta.importerEnvironment} environment`, - ); - return; - } - const result: BuildAssetsManifestRaw = { - js: [], - css: [], - }; - const { chunk, deps } = found; - if (environmentName === "client") { - result.entry = assetsURL(chunk.fileName, builder.config); - result.js = deps.js.map((fileName) => ({ - href: assetsURL(fileName, builder.config), - })); - } - result.css = deps.css.map((fileName) => ({ - href: assetsURL(fileName, builder.config), - })); - - // add single css when `cssCodeSplit: false` - // https://github.com/vitejs/vite/blob/3a92bc79b306a01b8aaf37f80b2239eaf6e488e7/packages/vite/src/node/plugins/css.ts#L999-L1011 - if (!builder.environments[environmentName]!.config.build.cssCodeSplit) { - const singleCss = Object.values(bundle).find( - (v) => - v.type === "asset" && v.originalFileNames.includes("style.css"), - ); - if (singleCss) { - result.css.push({ - href: assetsURL(singleCss.fileName, builder.config), - }); - } - } - - (manifest[environmentName] ??= {})[meta.key] = result; - } - } - - // write manifest to importer environments - const importerEnvironments = new Set( - Object.values(importAssetsMetaMap) - .flatMap((metas) => Object.values(metas)) - .flatMap((meta) => meta.importerEnvironment), - ); - for (const environmentName of importerEnvironments) { - const outDir = builder.environments[environmentName]!.config.build.outDir; - fs.writeFileSync( - path.join(outDir, BUILD_ASSETS_MANIFEST_NAME), - `export default ${serializeValueWithRuntime(manifest)};`, - ); - - // copy assets to client (mainly for server css) - const clientOutDir = builder.environments["client"]!.config.build.outDir; - for (const asset of Object.values(bundleMap[environmentName]!)) { - if (asset.type === "asset") { - const srcFile = path.join(outDir, asset.fileName); - const destFile = path.join(clientOutDir, asset.fileName); - fs.mkdirSync(path.dirname(destFile), { recursive: true }); - fs.copyFileSync(srcFile, destFile); - } - } - } - } - - return [ - { - name: "fullstack:assets", - // TODO: support non shared build? - sharedDuringBuild: true, - configureServer(server_) { - server = server_; - }, - configResolved(config) { - resolvedConfig = config; - }, - configEnvironment(name) { - const serverEnvironments = pluginOpts?.serverEnvironments ?? ["ssr"]; - if (serverEnvironments.includes(name)) { - return { - build: { - emitAssets: true, - }, - }; - } - }, - /** - * [Transform input] - * const assets = import.meta.vite.assets(...) - * - * [Transform output] - * import __assets_xxx from "virtual:fullstack/assets?..." - * const assets = __assets_xxx - */ - transform: { - filter: { code: /import\.meta\.vite\.assets\(/ }, - async handler(code, id, _options) { - const output = new MagicString(code); - const strippedCode = stripLiteral(code); - - const newImports = new Set(); - - for (const match of code.matchAll( - /import\.meta\.vite\.assets\(([\s\S]*?)\)/dg, - )) { - const [start, end] = match.indices![0]!; - - // skip if inside comment or string literal - if ( - !strippedCode - .slice(start, end) - .includes("import.meta.vite.assets") - ) { - continue; - } - - // No-op on client since vite build handles preload/css for dynamic import on client. - // https://vite.dev/guide/features.html#async-chunk-loading-optimization - if (this.environment.name === "client") { - const replacement = `(${JSON.stringify(EMPTY_ASSETS)})`; - output.update(start, end, replacement); - continue; - } - - const argCode = match[1]!.trim(); - const options = { - import: id, - environment: undefined, - asEntry: false, - } satisfies ImportAssetsOptions; - if (argCode) { - const argValue = evalValue(argCode); - Object.assign(options, argValue); - } - - // when `environment` is omitted, import both client and - // current environment (i.e. treat is as universal route) - const environments = options.environment - ? [options.environment] - : ["client", this.environment.name]; - const importedNames: string[] = []; - for (const environment of environments) { - const importSource = toAssetsVirtual({ - import: options.import, - importer: id, - environment, - entry: options.asEntry ? "1" : "", - }); - const hash = hashString(importSource); - const importedName = `__assets_${hash}`; - newImports.add( - `;import ${importedName} from ${JSON.stringify(importSource)};\n`, - ); - importedNames.push(importedName); - } - let replacement = importedNames[0]!; - if (importedNames.length > 1) { - newImports.add( - `;import * as __assets_runtime from "virtual:fullstack/runtime";\n`, - ); - replacement = `__assets_runtime.mergeAssets(${importedNames.join(", ")})`; - } - output.update(start, end, `(${replacement})`); - } - - if (output.hasChanged()) { - // add virtual imports at the end so that other imports are already processed - // and css already exists in server module graph. - // TODO: forgot to do this on `@vitejs/plugin-rsc` - // TODO: nop, we probably want to eagerly transform anyways. - for (const newImport of newImports) { - output.append(newImport); - } - return { - code: output.toString(), - map: output.generateMap({ hires: "boundary" }), - }; - } - }, - }, - resolveId: { - filter: { id: /^virtual:fullstack\// }, - handler(source) { - if (source === "virtual:fullstack/runtime") { - return "\0" + source; - } - if (source.startsWith("virtual:fullstack/assets?")) { - return "\0" + source; - } - if (source === "virtual:fullstack/assets-manifest") { - assert.notEqual(this.environment.name, "client"); - assert.equal(this.environment.mode, "build"); - return { id: source, external: true }; - } - }, - }, - load: { - filter: { id: /^\0virtual:fullstack\// }, - async handler(id) { - if (id === "\0virtual:fullstack/runtime") { - return fs.readFileSync( - path.join(import.meta.dirname, "runtime.js"), - "utf-8", - ); - } - - const parsed = parseAssetsVirtual(id); - if (!parsed) return; - assert.notEqual(this.environment.name, "client"); - - // TODO: we probably shouldn't resolve client file on other environment. - // we could avoid this by another virtual but it's possible only for dev. - const resolved = await this.resolve(parsed.import, parsed.importer); - assert(resolved, `Failed to resolve: ${parsed.import}`); - - const s = new MagicString(""); - const code = await processAssetsImport(this, resolved.id, { - environment: parsed.environment, - isEntry: !!parsed.entry, - }); - s.append(`export default ${code};\n`); - if (this.environment.mode === "build") { - s.prepend( - `import __assets_manifest from "virtual:fullstack/assets-manifest";\n`, - ); - } - return s.toString(); - }, - }, - // non-client builds can load assets manifest as external - renderChunk(code, chunk) { - if (code.includes("virtual:fullstack/assets-manifest")) { - const replacement = normalizeRelativePath( - path.relative( - path.join(chunk.fileName, ".."), - BUILD_ASSETS_MANIFEST_NAME, - ), - ); - code = code.replaceAll( - "virtual:fullstack/assets-manifest", - () => replacement, - ); - return { code }; - } - return; - }, - writeBundle(_options, bundle) { - bundleMap[this.environment.name] = bundle; - }, - buildStart() { - // dynamically add client entry during build - if ( - this.environment.mode == "build" && - this.environment.name === "client" - ) { - const metas = importAssetsMetaMap["client"]; - if (metas) { - for (const meta of Object.values(importAssetsMetaMap["client"]!)) { - if (meta.isEntry) { - this.emitFile({ - type: "chunk", - id: meta.id, - preserveSignature: "exports-only", - }); - } - } - } - } - }, - buildApp: { - order: "pre", - async handler(builder) { - // expose writeAssetsManifest to builder - builder.writeAssetsManifest = async () => { - await writeAssetsManifest(builder); - }; - }, - }, - }, - { - name: "fullstack:write-assets-manifest-post", - buildApp: { - order: "post", - async handler(builder) { - // ensure this is called at least once - await builder.writeAssetsManifest(); - }, - }, - }, - { - name: "fullstack:assets-query", - sharedDuringBuild: true, - resolveId: { - order: "pre", - filter: { id: /[?&]assets/ }, - handler(source) { - const { query } = parseIdQuery(source); - const value = query["assets"]; - if (typeof value !== "undefined") { - if (this.environment.name === "client") { - return `\0virtual:fullstack/empty-assets`; - } - } - }, - }, - load: { - filter: { id: [/^\0virtual:fullstack\/empty-assets$/, /[?&]assets/] }, - async handler(id) { - if (id === "\0virtual:fullstack/empty-assets") { - return `export default ${JSON.stringify(EMPTY_ASSETS)}`; - } - const { filename, query } = parseIdQuery(id); - const value = query["assets"]; - if (typeof value !== "undefined") { - // implement different semantics depending on query - // assets=client => { environment: "client", isEntry: true } - // assets=ssr => { environment: "ssr", isEntry: false } - // assets => { environment: "client", isEntry: false }, { environment: , isEntry: false } - const s = new MagicString(""); - const codes: string[] = []; - if (value) { - const code = await processAssetsImport(this, filename, { - environment: value, - isEntry: value === "client", - }); - codes.push(code); - } else { - const code1 = await processAssetsImport(this, filename, { - environment: "client", - isEntry: false, - }); - const code2 = await processAssetsImport(this, filename, { - environment: this.environment.name, - isEntry: false, - }); - codes.push(code1, code2); - } - s.append(` -import * as __assets_runtime from "virtual:fullstack/runtime";\n -export default __assets_runtime.mergeAssets(${codes.join(", ")}); -`); - if (this.environment.mode === "build") { - s.prepend( - `import __assets_manifest from "virtual:fullstack/assets-manifest";\n`, - ); - } - return { - code: s.toString(), - moduleSideEffects: false, - }; - } - }, - }, - // NOTE: - // manually invalidate instead of automatic module graph based invalidation via `addWatchFile`. - // context: - // - https://github.com/hi-ogawa/vite-plugins/issues/1233 - // - https://github.com/vitejs/vite-plugin-react/pull/847 - hotUpdate(ctx) { - if (this.environment.name === "rsc") { - const mods = collectModuleDependents(ctx.modules); - for (const mod of mods) { - if (mod.id) { - const ids = [ - `${mod.id}?assets`, - `${mod.id}?assets=client`, - `${mod.id}?assets=${this.environment.name}`, - ]; - for (const id of ids) { - invalidteModuleById(this.environment, id); - } - } - } - } - }, - }, - // ensure at least one client build input to prevent Vite - // from looking for index.html and breaking build - { - ...createVirtualPlugin("fullstack/client-fallback", () => "export {}"), - configEnvironment: { - order: "post", - handler(name, config, _env) { - if (name === "client") { - const clientBuildFallback = - pluginOpts?.experimental?.clientBuildFallback ?? true; - if (clientBuildFallback && !config.build?.rollupOptions?.input) { - return { - build: { - rollupOptions: { - input: { - __fallback: "virtual:fullstack/client-fallback", - }, - }, - }, - }; - } - } - }, - }, - generateBundle(_optoins, bundle) { - if (this.environment.name !== "client") return; - for (const [k, v] of Object.entries(bundle)) { - if (v.type === "chunk" && v.name === "__fallback") { - delete bundle[k]; - } - } - }, - }, - patchViteClientPlugin(), - patchVueScopeCssHmr(), - patchCssLinkSelfAccept(), - ]; -} - -const EMPTY_ASSETS: ImportAssetsResultRaw = { - js: [], - css: [], -}; - -const BUILD_ASSETS_MANIFEST_NAME = "__fullstack_assets_manifest.js"; - -async function collectCss( - environment: DevEnvironment, - entryId: string, - options: { eager: boolean }, -) { - const visited = new Set(); - const cssIds = new Set(); - - async function recurse(id: string) { - if ( - visited.has(id) || - parseAssetsVirtual(id) || - "assets" in parseIdQuery(id).query - ) { - return; - } - visited.add(id); - const mod = environment.moduleGraph.getModuleById(id); - if (!mod) return; - if (options.eager && !mod?.transformResult) { - try { - await environment.transformRequest(id); - } catch (e) { - console.error(`[collectCss] Failed to transform '${id}'`, e); - } - } - // TODO: should skip dynamic imports? but no such metadata in dev module graph. - for (const next of mod?.importedModules ?? []) { - if (next.id) { - if (isCSSRequest(next.id)) { - if (hasSpecialCssQuery(next.id)) { - continue; - } - cssIds.add(next.id); - } else { - await recurse(next.id); - } - } - } - } - - await recurse(entryId); - - // this doesn't include ?t= query so that RSC won't keep adding styles. - const hrefs = [...cssIds].map((id) => - normalizeViteImportAnalysisUrl(environment, id), - ); - return { ids: [...cssIds], hrefs }; -} - -function invalidteModuleById(environment: DevEnvironment, id: string) { - const mod = environment.moduleGraph.getModuleById(id); - if (mod) { - environment.moduleGraph.invalidateModule(mod); - } - return mod; -} - -function collectModuleDependents(mods: EnvironmentModuleNode[]) { - const visited = new Set(); - function recurse(mod: EnvironmentModuleNode) { - if (visited.has(mod)) return; - visited.add(mod); - for (const importer of mod.importers) { - recurse(importer); - } - } - for (const mod of mods) { - recurse(mod); - } - return [...visited]; -} - -function hasSpecialCssQuery(id: string): boolean { - return /[?&](url|inline|raw)(\b|=|&|$)/.test(id); -} - -// Internal type for build manifest that supports BuildAssetURL for dynamic URL generation. -// This differs from ImportAssetsResultRaw which is exported and used at runtime with string URLs only. -type BuildAssetsManifestRaw = { - entry?: BuildAssetURL; - js: { href: BuildAssetURL }[]; - css: { href: BuildAssetURL; "data-vite-dev-id"?: string }[]; -}; - -type BuildAssetsManifest = { - [environment: string]: { - [import_: string]: BuildAssetsManifestRaw; - }; -}; - -type AssetDeps = { - js: string[]; - css: string[]; -}; - -type AssetDepsMap = { - [id: string]: { chunk: Rollup.OutputChunk; deps: AssetDeps }; -}; - -function collectAssetDeps(bundle: Rollup.OutputBundle) { - const chunkToDeps = new Map(); - for (const chunk of Object.values(bundle)) { - if (chunk.type === "chunk") { - chunkToDeps.set(chunk, collectAssetDepsInner(chunk.fileName, bundle)); - } - } - const idToDeps: AssetDepsMap = {}; - for (const [chunk, deps] of chunkToDeps.entries()) { - for (const id of chunk.moduleIds) { - idToDeps[id] = { chunk, deps }; - } - } - return idToDeps; -} - -function collectAssetDepsInner( - fileName: string, - bundle: Rollup.OutputBundle, -): AssetDeps { - const visited = new Set(); - const css: string[] = []; - - function recurse(k: string) { - if (visited.has(k)) return; - visited.add(k); - const v = bundle[k]; - assert(v, `Not found '${k}' in the bundle`); - if (v.type === "chunk") { - css.push(...(v.viteMetadata?.importedCss ?? [])); - for (const k2 of v.imports) { - // server external imports is not in bundle - if (k2 in bundle) { - recurse(k2); - } - } - } - } - - recurse(fileName); - return { - js: [...visited], - css: [...new Set(css)], - }; -} - -function patchViteClientPlugin(): Plugin { - const viteClientPath = normalizePath( - fileURLToPath(import.meta.resolve("vite/dist/client/client.mjs")), - ); - - function endIndexOf(code: string, searchValue: string) { - const i = code.lastIndexOf(searchValue); - return i === -1 ? i : i + searchValue.length; - } - - return { - name: "fullstack:patch-vite-client", - transform: { - filter: { id: exactRegex(viteClientPath) }, - handler(code, id) { - if (id === viteClientPath) { - // skip for latest vite https://github.com/vitejs/vite/pull/20767 - if (code.includes("linkSheetsMap")) return; - - const s = new MagicString(code); - s.prependLeft( - code.indexOf("const sheetsMap"), - `\ -const linkSheetsMap = new Map(); -document - .querySelectorAll('link[rel="stylesheet"][data-vite-dev-id]') - .forEach((el) => { - linkSheetsMap.set(el.getAttribute('data-vite-dev-id'), el) - }); -`, - ); - s.appendLeft( - endIndexOf(code, `function updateStyle(id, content) {`), - `if (linkSheetsMap.has(id)) { return }`, - ); - s.appendLeft( - endIndexOf(code, `function removeStyle(id) {`), - ` -const link = linkSheetsMap.get(id); -if (link) { - document - .querySelectorAll( - 'link[rel="stylesheet"][data-vite-dev-id]', - ) - .forEach((el) => { - if (el.getAttribute('data-vite-dev-id') === id) { - el.remove() - } - }) - linkSheetsMap.delete(id) -} -`, - ); - return s.toString(); - } - }, - }, - }; -} - -// TODO: upstream? -// Vite client HMR requests scoped css link stylesheet with `lang.css=` instead of `lang.css`, -// which seems to cause response to be `content-type: text/javascript` even though response text is raw css. -// This middleware rewrites url so that Vite will add `text/css`. -function patchVueScopeCssHmr(): Plugin { - return { - name: "fullstack:patch-vue-scoped-css-hmr", - configureServer(server) { - server.middlewares.use((req, _res, next) => { - if ( - req.headers.accept?.includes("text/css") && - req.url?.includes("&lang.css=") - ) { - req.url = req.url.replace("&lang.css=", "?lang.css"); - } - next(); - }); - }, - }; -} - -// TODO: upstream? -// force self accepting "?direct" css (injected via SSR ``) to avoid full reload. -// this should only apply to css modules -// https://github.com/vitejs/vite/blob/84079a84ad94de4c1ef4f1bdb2ab448ff2c01196/packages/vite/src/node/plugins/css.ts#L1096 -function patchCssLinkSelfAccept(): Plugin { - return { - name: "fullstack:patch-css-link-self-accept", - apply: "serve", - transform: { - order: "post", - handler(_code, id, _options) { - if ( - this.environment.name === "client" && - this.environment.mode === "dev" && - isCSSRequest(id) && - directRequestRE.test(id) - ) { - const mod = this.environment.moduleGraph.getModuleById(id); - if (mod && !mod.isSelfAccepting) { - mod.isSelfAccepting = true; - } - } - }, - }, - }; -} - -// advanced base option support ported from @vitejs/plugin-rsc -// https://github.com/vitejs/vite-plugin-react/pull/612 - -type BuildAssetURL = string | BuildAssetsURLWithRuntime; - -class BuildAssetsURLWithRuntime { - constructor(public runtime: string) {} -} - -function serializeValueWithRuntime(value: BuildAssetsManifest) { - const replacements: [string, string][] = []; - let result = JSON.stringify( - value, - (_key, value) => { - if (value instanceof BuildAssetsURLWithRuntime) { - const placeholder = `__runtime_placeholder_${replacements.length}__`; - replacements.push([placeholder, value.runtime]); - return placeholder; - } - - return value; - }, - 2, - ); - - for (const [placeholder, runtime] of replacements) { - result = result.replace(`"${placeholder}"`, runtime); - } - - return result; -} - -function assetsURL(url: string, config: ResolvedConfig): BuildAssetURL { - if ( - config.command === "build" && - typeof config.experimental?.renderBuiltUrl === "function" - ) { - // https://github.com/vitejs/vite/blob/bdde0f9e5077ca1a21a04eefc30abad055047226/packages/vite/src/node/build.ts#L1369 - const result = config.experimental.renderBuiltUrl(url, { - type: "asset", - hostType: "js", - ssr: true, - hostId: "", - }); - - if (typeof result === "object") { - if (result.runtime) { - return new BuildAssetsURLWithRuntime(result.runtime); - } - assert( - !result.relative, - '"result.relative" not supported on renderBuiltUrl() for fullstack plugin', - ); - } else if (result) { - return result satisfies string; - } - } - - // https://github.com/vitejs/vite/blob/2a7473cfed96237711cda9f736465c84d442ddef/packages/vite/src/node/plugins/importAnalysisBuild.ts#L222-L230 - return config.base + url; -} - -function assetsURLDev(url: string, config: ResolvedConfig): string { - // https://github.com/vitejs/vite/blob/2a7473cfed96237711cda9f736465c84d442ddef/packages/vite/src/node/plugins/importAnalysisBuild.ts#L222-L230 - return config.base + url; -} diff --git a/packages/fullstack/src/plugins/shared.ts b/packages/fullstack/src/plugins/shared.ts deleted file mode 100644 index abc5a133e..000000000 --- a/packages/fullstack/src/plugins/shared.ts +++ /dev/null @@ -1,29 +0,0 @@ -// https://github.com/vitejs/vite-plugin-vue/blob/06931b1ea2b9299267374cb8eb4db27c0626774a/packages/plugin-vue/src/utils/query.ts#L13 -export function parseIdQuery(id: string): { - filename: string; - query: { - [k: string]: string; - }; -} { - if (!id.includes("?")) return { filename: id, query: {} }; - const [filename, rawQuery] = id.split(`?`, 2) as [string, string]; - const query = Object.fromEntries(new URLSearchParams(rawQuery)); - return { filename, query }; -} - -export type AssetsVirtual = { - import: string; - importer: string; - environment: string; - entry: string; -}; - -export function toAssetsVirtual(options: AssetsVirtual) { - return `virtual:fullstack/assets?${new URLSearchParams(options)}&lang.js`; -} - -export function parseAssetsVirtual(id: string): AssetsVirtual | undefined { - if (id.startsWith("\0virtual:fullstack/assets?")) { - return parseIdQuery(id).query as any; - } -} diff --git a/packages/fullstack/src/plugins/utils.ts b/packages/fullstack/src/plugins/utils.ts deleted file mode 100644 index 1dbe5ac66..000000000 --- a/packages/fullstack/src/plugins/utils.ts +++ /dev/null @@ -1,111 +0,0 @@ -import assert from "node:assert"; -import { createHash } from "node:crypto"; -import { exactRegex } from "@rolldown/pluginutils"; -import { - type Plugin, - type ResolvedConfig, - type Rollup, - normalizePath, -} from "vite"; - -export function sortObject(o: T) { - return Object.fromEntries( - Object.entries(o).sort(([a], [b]) => a.localeCompare(b)), - ) as T; -} - -// Rethrow transform error through `this.error` with `error.pos` -export function withRollupError any>( - ctx: Rollup.TransformPluginContext, - f: F, -): F { - function processError(e: any): never { - if (e && typeof e === "object" && typeof e.pos === "number") { - return ctx.error(e, e.pos); - } - throw e; - } - return function (this: any, ...args: any[]) { - try { - const result = f.apply(this, args); - if (result instanceof Promise) { - return result.catch((e: any) => processError(e)); - } - return result; - } catch (e: any) { - processError(e); - } - } as F; -} - -export function createVirtualPlugin( - name: string, - load: Plugin["load"], -): Plugin { - name = "virtual:" + name; - return { - name: `rsc:virtual-${name}`, - resolveId: { - filter: { id: exactRegex(name) }, - handler(source, _importer, _options) { - return source === name ? "\0" + name : undefined; - }, - }, - load: { - filter: { id: exactRegex("\0" + name) }, - handler(id, options) { - return (load as Function).apply(this, [id, options]); - }, - }, - }; -} - -export function normalizeRelativePath(s: string): string { - s = normalizePath(s); - return s[0] === "." ? s : "./" + s; -} - -export function getEntrySource( - config: Pick, - name: string = "index", -): string { - const input = config.build.rollupOptions.input; - if (typeof input === "string") { - return input; - } - assert( - typeof input === "object" && - !Array.isArray(input) && - name in input && - typeof input[name] === "string", - `[vite-rsc:getEntrySource] expected 'build.rollupOptions.input' to be an object with a '${name}' property that is a string, but got ${JSON.stringify(input)}`, - ); - return input[name]; -} - -export function hashString(v: string): string { - return createHash("sha256").update(v).digest().toString("hex").slice(0, 12); -} - -// normalize server entry exports to align with server runtimes -// https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/ -// https://srvx.h3.dev/guide -// https://vercel.com/docs/functions/functions-api-reference?framework=other#fetch-web-standard -// https://github.com/jacob-ebey/rsbuild-rsc-playground/blob/eb1a54afa49cbc5ff93c315744d7754d5ed63498/plugin/fetch-server.ts#L59-L79 -export function getFetchHandlerExport(exports: object): any { - if ("default" in exports) { - const default_ = exports.default; - if ( - default_ && - typeof default_ === "object" && - "fetch" in default_ && - typeof default_.fetch === "function" - ) { - return default_.fetch; - } - if (typeof default_ === "function") { - return default_; - } - } - throw new Error("Invalid server handler entry"); -} diff --git a/packages/fullstack/src/plugins/vite-utils.ts b/packages/fullstack/src/plugins/vite-utils.ts deleted file mode 100644 index 77f13964f..000000000 --- a/packages/fullstack/src/plugins/vite-utils.ts +++ /dev/null @@ -1,167 +0,0 @@ -// misc utilities copied from vite - -import fs from "node:fs"; -import path from "node:path"; -import { stripVTControlCharacters as strip } from "node:util"; -import type { DevEnvironment, ErrorPayload, Rollup } from "vite"; - -export const VALID_ID_PREFIX = `/@id/`; - -export const NULL_BYTE_PLACEHOLDER = `__x00__`; - -export const FS_PREFIX = `/@fs/`; - -export function wrapId(id: string): string { - return id.startsWith(VALID_ID_PREFIX) - ? id - : VALID_ID_PREFIX + id.replace("\0", NULL_BYTE_PLACEHOLDER); -} - -export function unwrapId(id: string): string { - return id.startsWith(VALID_ID_PREFIX) - ? id.slice(VALID_ID_PREFIX.length).replace(NULL_BYTE_PLACEHOLDER, "\0") - : id; -} - -export function withTrailingSlash(path: string): string { - if (path[path.length - 1] !== "/") { - return `${path}/`; - } - return path; -} - -const postfixRE = /[?#].*$/; -export function cleanUrl(url: string): string { - return url.replace(postfixRE, ""); -} - -export function splitFileAndPostfix(path: string): { - file: string; - postfix: string; -} { - const file = cleanUrl(path); - return { file, postfix: path.slice(file.length) }; -} - -const windowsSlashRE = /\\/g; -export function slash(p: string): string { - return p.replace(windowsSlashRE, "/"); -} - -const isWindows = - typeof process !== "undefined" && process.platform === "win32"; - -export function injectQuery(url: string, queryToInject: string): string { - const { file, postfix } = splitFileAndPostfix(url); - const normalizedFile = isWindows ? slash(file) : file; - return `${normalizedFile}?${queryToInject}${ - postfix[0] === "?" ? `&${postfix.slice(1)}` : /* hash only */ postfix - }`; -} - -export function joinUrlSegments(a: string, b: string): string { - if (!a || !b) { - return a || b || ""; - } - if (a.endsWith("/")) { - a = a.substring(0, a.length - 1); - } - if (b[0] !== "/") { - b = "/" + b; - } - return a + b; -} - -export function normalizeResolvedIdToUrl( - environment: DevEnvironment, - url: string, - resolved: Rollup.PartialResolvedId, -): string { - const root = environment.config.root; - const depsOptimizer = environment.depsOptimizer; - - // normalize all imports into resolved URLs - // e.g. `import 'foo'` -> `import '/@fs/.../node_modules/foo/index.js'` - if (resolved.id.startsWith(withTrailingSlash(root))) { - // in root: infer short absolute path from root - url = resolved.id.slice(root.length); - } else if ( - depsOptimizer?.isOptimizedDepFile(resolved.id) || - // vite-plugin-react isn't following the leading \0 virtual module convention. - // This is a temporary hack to avoid expensive fs checks for React apps. - // We'll remove this as soon we're able to fix the react plugins. - (resolved.id !== "/@react-refresh" && - path.isAbsolute(resolved.id) && - fs.existsSync(cleanUrl(resolved.id))) - ) { - // an optimized deps may not yet exists in the filesystem, or - // a regular file exists but is out of root: rewrite to absolute /@fs/ paths - url = path.posix.join(FS_PREFIX, resolved.id); - } else { - url = resolved.id; - } - - // if the resolved id is not a valid browser import specifier, - // prefix it to make it valid. We will strip this before feeding it - // back into the transform pipeline - if (url[0] !== "." && url[0] !== "/") { - url = wrapId(resolved.id); - } - - return url; -} - -export function normalizeViteImportAnalysisUrl( - environment: DevEnvironment, - id: string, -): string { - let url = normalizeResolvedIdToUrl(environment, id, { id }); - - // https://github.com/vitejs/vite/blob/c18ce868c4d70873406e9f7d1b2d0a03264d2168/packages/vite/src/node/plugins/importAnalysis.ts#L416 - if (environment.config.consumer === "client") { - const mod = environment.moduleGraph.getModuleById(id); - if (mod && mod.lastHMRTimestamp > 0) { - url = injectQuery(url, `t=${mod.lastHMRTimestamp}`); - } - } - - return url; -} - -// error formatting -// https://github.com/vitejs/vite/blob/8033e5bf8d3ff43995d0620490ed8739c59171dd/packages/vite/src/node/server/middlewares/error.ts#L11 - -type RollupError = Rollup.RollupError; - -export function prepareError(err: Error | RollupError): ErrorPayload["err"] { - // only copy the information we need and avoid serializing unnecessary - // properties, since some errors may attach full objects (e.g. PostCSS) - return { - message: strip(err.message), - stack: strip(cleanStack(err.stack || "")), - id: (err as RollupError).id, - frame: strip((err as RollupError).frame || ""), - plugin: (err as RollupError).plugin, - pluginCode: (err as RollupError).pluginCode?.toString(), - loc: (err as RollupError).loc, - }; -} - -function cleanStack(stack: string) { - return stack - .split(/\n/) - .filter((l) => /^\s*at/.test(l)) - .join("\n"); -} - -// https://github.com/vitejs/vite/blob/ea9aed7ebcb7f4be542bd2a384cbcb5a1e7b31bd/packages/vite/src/node/utils.ts#L1469-L1475 -export function evalValue(rawValue: string): T { - const fn = new Function(` - var console, exports, global, module, process, require - return (\n${rawValue}\n) - `); - return fn(); -} - -// https://github.com/vitejs/vite/blob/84079a84ad94de4c1ef4f1bdb2ab448ff2c01196/packages/vite/src/node/utils.ts#L321 -export const directRequestRE: RegExp = /(\?|&)direct=?(?:&|$)/; diff --git a/packages/fullstack/src/runtime.ts b/packages/fullstack/src/runtime.ts deleted file mode 100644 index 56d14c263..000000000 --- a/packages/fullstack/src/runtime.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { - ImportAssetsResult, - ImportAssetsResultRaw, -} from "../types/shared"; - -export type { ImportAssetsResult }; - -export function mergeAssets( - ...args: ImportAssetsResultRaw[] -): ImportAssetsResult { - const js = uniqBy( - args.flatMap((h) => h.js), - (a) => a.href, - ); - const css = uniqBy( - args.flatMap((h) => h.css), - (a) => a.href, - ); - const entry = args.filter((arg) => arg.entry)?.[0]?.entry; - const raw: ImportAssetsResultRaw = { entry, js, css }; - return { ...raw, merge: (...args) => mergeAssets(raw, ...args) }; -} - -// merging is cumbersome because of `data-vite-dev-id` :( -function uniqBy(array: T[], key: (item: T) => string): T[] { - const seen = new Set(); - return array.filter((item) => { - const k = key(item); - if (seen.has(k)) { - return false; - } - seen.add(k); - return true; - }); -} diff --git a/packages/fullstack/tsconfig.json b/packages/fullstack/tsconfig.json deleted file mode 100644 index 6a1a380d8..000000000 --- a/packages/fullstack/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src", "types", "*.ts"], - "compilerOptions": { - "allowImportingTsExtensions": true, - "noPropertyAccessFromIndexSignature": false, - "noImplicitReturns": false, - "checkJs": false, - "declaration": true, - "isolatedDeclarations": true, - "jsx": "react-jsx" - } -} diff --git a/packages/fullstack/tsdown.config.ts b/packages/fullstack/tsdown.config.ts deleted file mode 100644 index 154f59291..000000000 --- a/packages/fullstack/tsdown.config.ts +++ /dev/null @@ -1,28 +0,0 @@ -import fs from "node:fs"; -import { readFile, writeFile } from "node:fs/promises"; -import { defineConfig } from "tsdown"; - -export default defineConfig({ - entry: ["src/index.ts", "src/runtime.ts"], - format: ["esm"], - dts: { - sourcemap: process.argv.slice(2).includes("--sourcemap"), - }, - hooks: { - async "build:done"() { - fs.appendFileSync( - "./dist/index.d.ts", - `\nimport type {} from "@hiogawa/vite-plugin-fullstack/types";\n`, - ); - // inline file content as raw string to allow downstream package `nitro` to bundle this plugin package - let pluginBundle = await readFile("dist/index.js", "utf-8"); - await writeFile( - "dist/index.js", - pluginBundle.replace( - `fs.readFileSync(path.join(import.meta.dirname, "runtime.js"), "utf-8")`, - `\`${await readFile("dist/runtime.js", "utf-8")}\``, - ), - ); - }, - }, -}) as any; diff --git a/packages/fullstack/types/index.d.ts b/packages/fullstack/types/index.d.ts deleted file mode 100644 index 8333b6d15..000000000 --- a/packages/fullstack/types/index.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -/// - -import type { ImportAssetsOptions, ImportAssetsResult } from "./shared.ts"; - -declare global { - interface ImportMeta { - readonly vite: { - /** @deprecated use `?assets` query import instead */ - assets(options?: ImportAssetsOptions): ImportAssetsResult; - }; - } -} - -declare module "vite" { - interface ViteBuilder { - /** - * The plugin injects this method to allow flexible build pipeline - * for downstream integrations. This will be automatically called during the plugin - * post `buildApp` hook when it was not called by users. - */ - writeAssetsManifest(): Promise; - } -} diff --git a/packages/fullstack/types/query.d.ts b/packages/fullstack/types/query.d.ts deleted file mode 100644 index 07a4fd763..000000000 --- a/packages/fullstack/types/query.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -type Assets = import("./shared").ImportAssetsResult; - -declare module "*?assets" { - const assets: Assets; - export default assets; -} - -declare module "*?assets=client" { - const assets: Assets; - export default assets; -} - -declare module "*?assets=ssr" { - const assets: Assets; - export default assets; -} diff --git a/packages/fullstack/types/shared.ts b/packages/fullstack/types/shared.ts deleted file mode 100644 index d18e46671..000000000 --- a/packages/fullstack/types/shared.ts +++ /dev/null @@ -1,36 +0,0 @@ -// TODO: probably we can narrow the use cases to simply the API: -// -// 1. assets({ import: "..", clientEntry: true }) => entry, js, css -// - server entry referencing client entry -// - client only island -// -// 2. assets({ import: "..", universal: true }) => js, css -// - universal route -// -// 3. assets({ import: ".." }) => css -// - css assets for server only route -// -export type ImportAssetsOptions = { - import?: string; - environment?: string; // TODO: can we remove in favor of asEntry and universal? - asEntry?: boolean; - // universal?: boolean; -}; - -// TODO: rename to just Assets? -export type ImportAssetsResult = ImportAssetsResultRaw & { - merge(...args: ImportAssetsResultRaw[]): ImportAssetsResult; -}; - -export type ImportAssetsResultRaw = { - entry?: string; - js: { href: string }[]; - css: CssLinkAttributes[]; -}; - -// TODO: do we ever need