init
This commit is contained in:
155
plugins/ssrPlugin.ts
Normal file
155
plugins/ssrPlugin.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { Plugin } from "vite";
|
||||
export function createVirtualPlugin(name: string, load: Plugin["load"]) {
|
||||
name = "virtual:" + name;
|
||||
return {
|
||||
name,
|
||||
resolveId(source, _importer, _options) {
|
||||
if (source === name || source.startsWith(`${name}?`)) {
|
||||
return `\0${source}`;
|
||||
}
|
||||
return;
|
||||
},
|
||||
load(id, options) {
|
||||
if (id === `\0${name}` || id.startsWith(`\0${name}?`)) {
|
||||
return (load as any).apply(this, [id, options]);
|
||||
}
|
||||
},
|
||||
} satisfies Plugin;
|
||||
}
|
||||
export function clientFirstBuild(): Plugin {
|
||||
return {
|
||||
name: "client-first-build",
|
||||
config(config) {
|
||||
config.builder ??= {};
|
||||
config.builder.buildApp = async (builder) => {
|
||||
const clientEnvironment = builder.environments.client;
|
||||
const workerEnvironments = Object.keys(builder.environments)
|
||||
.filter((name) => name !== "client")
|
||||
.map((name) => builder.environments[name]);
|
||||
// console.log('Client First Build Plugin: Starting builds...', workerEnvironments)
|
||||
// Client build first
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (clientEnvironment) {
|
||||
// clientEnvironment.config.build.outDir = "dist/client";
|
||||
// console.log("Client First Build Plugin: Building client...", Object.keys());
|
||||
await builder.build(clientEnvironment);
|
||||
}
|
||||
// console.log("Client First Build Plugin: Client build complete.", workerEnvironments);
|
||||
// Then worker builds
|
||||
for (const workerEnv of workerEnvironments) {
|
||||
await builder.build(workerEnv);
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
export function injectManifest(): Plugin {
|
||||
let clientOutDir = "dist/client";
|
||||
|
||||
return {
|
||||
name: "inject-manifest",
|
||||
config(config) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const viteConfig = config as any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
clientOutDir =
|
||||
viteConfig.environments?.client?.build?.outDir ?? "dist/client";
|
||||
},
|
||||
async transform(code, id, options) {
|
||||
// Only transform in SSR environment (non-client)
|
||||
if (!options?.ssr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only transform files that contain the placeholder
|
||||
if (!code.includes("__VITE_MANIFEST_CONTENT__")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read manifest from client build output
|
||||
const manifestPath = path.resolve(
|
||||
process.cwd(),
|
||||
clientOutDir,
|
||||
".vite/manifest.json"
|
||||
);
|
||||
let manifestContent: string | undefined;
|
||||
try {
|
||||
manifestContent = await this.fs
|
||||
.readFile(manifestPath)
|
||||
.then((data) => data.toString());
|
||||
} catch {
|
||||
// Manifest not found
|
||||
}
|
||||
|
||||
if (!manifestContent) return;
|
||||
|
||||
// Replace placeholder string with actual manifest data
|
||||
// Format: { "__manifest__": { default: <manifest> } } to match the Object.entries loop
|
||||
const newCode = code.replace(
|
||||
/"__VITE_MANIFEST_CONTENT__"/g,
|
||||
`{ "__manifest__": { default: ${manifestContent} } }`
|
||||
);
|
||||
|
||||
if (newCode !== code) {
|
||||
return { code: newCode, map: null };
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
export default function ssrPlugin(): Plugin[] {
|
||||
// const { hotReload: hotReloadOption = true, entry: entryOption = {} } = options
|
||||
|
||||
const plugins: Plugin[] = [];
|
||||
|
||||
plugins.push(clientFirstBuild());
|
||||
plugins.push({
|
||||
name: "ssr-auto-entry",
|
||||
config(config) {
|
||||
config.define = config.define || {};
|
||||
},
|
||||
resolveId(id, importer, options) {
|
||||
if (!id.startsWith('@httpClientAdapter')) return
|
||||
const pwd = process.cwd()
|
||||
console.log('Resolving httpClientAdapter in', pwd, 'for', {id, importer, options})
|
||||
return path.resolve(
|
||||
__dirname,
|
||||
options?.ssr
|
||||
? pwd+"/src/api/httpClientAdapter.server.ts"
|
||||
: pwd+"/src/api/httpClientAdapter.client.ts"
|
||||
);
|
||||
},
|
||||
async configResolved(config) {
|
||||
const viteConfig = config as any;
|
||||
|
||||
if (!viteConfig.environments) {
|
||||
viteConfig.environments = {};
|
||||
}
|
||||
if (!viteConfig.environments.client) {
|
||||
viteConfig.environments.client = {};
|
||||
}
|
||||
if (!viteConfig.environments.client.build) {
|
||||
viteConfig.environments.client.build = {};
|
||||
}
|
||||
|
||||
const clientBuild = viteConfig.environments.client.build;
|
||||
clientBuild.manifest = true;
|
||||
clientBuild.rollupOptions = clientBuild.rollupOptions || {};
|
||||
clientBuild.rollupOptions.input = "src/client.ts";
|
||||
if (!viteConfig.environments.ssr) {
|
||||
const manifestPath = path.join(clientBuild.outDir as string, '.vite/manifest.json')
|
||||
try {
|
||||
const resolvedPath = path.resolve(process.cwd(), manifestPath)
|
||||
const manifestContent = readFileSync(resolvedPath, 'utf-8')
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
config.define['import.meta.env.VITE_MANIFEST_CONTENT'] = JSON.stringify(manifestContent)
|
||||
} catch {}
|
||||
}
|
||||
},
|
||||
});
|
||||
plugins.push(injectManifest());
|
||||
|
||||
return plugins;
|
||||
}
|
||||
116
plugins/vite-plugin-ssr-middleware.ts
Normal file
116
plugins/vite-plugin-ssr-middleware.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import type { Connect, Plugin } from "vite";
|
||||
import { name as packageName } from "../package.json";
|
||||
import { createMiddleware } from "@hattip/adapter-node";
|
||||
import { pathToFileURL } from "url";
|
||||
|
||||
|
||||
export function vitePluginSsrMiddleware({
|
||||
entry,
|
||||
preview,
|
||||
mode = "ssrLoadModule",
|
||||
}: {
|
||||
entry: string;
|
||||
preview?: string;
|
||||
mode?: "ssrLoadModule" | "ModuleRunner" | "ModuleRunner-HMR";
|
||||
}): Plugin {
|
||||
return {
|
||||
name: packageName,
|
||||
|
||||
apply(config, env) {
|
||||
// skip client build
|
||||
return Boolean(env.command === "serve" || config.build?.ssr);
|
||||
},
|
||||
|
||||
config(config, env) {
|
||||
if (env.command === "serve") {
|
||||
return {
|
||||
// disable builtin HTML middleware, which would rewrite `req.url` to "/index.html"
|
||||
appType: "custom",
|
||||
};
|
||||
}
|
||||
if (env.command === "build" && config.build?.ssr) {
|
||||
return {
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: entry,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return;
|
||||
},
|
||||
|
||||
async configureServer(server) {
|
||||
let loadModule = server.ssrLoadModule;
|
||||
if (mode === "ModuleRunner" || mode === "ModuleRunner-HMR") {
|
||||
const { createServerModuleRunner } = await import("vite");
|
||||
const runner = createServerModuleRunner(server.environments.ssr, {
|
||||
hmr: mode === "ModuleRunner-HMR" ? undefined : false,
|
||||
});
|
||||
loadModule = (id: string) => runner.import(id);
|
||||
}
|
||||
|
||||
const handler: Connect.NextHandleFunction = async (req, res, next) => {
|
||||
// expose ViteDevServer via request
|
||||
Object.defineProperty(req, "viteDevServer", { value: server });
|
||||
|
||||
try {
|
||||
const mod = await loadModule(entry);
|
||||
await createMiddleware((ctx) => mod["default"].fetch(ctx.request))(req, res, next);
|
||||
// await mod["default"](req, res, next);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
};
|
||||
return () => server.middlewares.use(handler);
|
||||
},
|
||||
|
||||
async configurePreviewServer(server) {
|
||||
if (preview) {
|
||||
const mod = await import( pathToFileURL(preview).href);
|
||||
return () => server.middlewares.use(createMiddleware((ctx) => mod["default"].fetch(ctx.request)));
|
||||
}
|
||||
return;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// minimal logger inspired by
|
||||
// https://github.com/koajs/logger
|
||||
// https://github.com/honojs/hono/blob/25beca878f2662fedd84ed3fbf80c6a515609cea/src/middleware/logger/index.ts
|
||||
|
||||
export function vitePluginLogger(): Plugin {
|
||||
return {
|
||||
name: vitePluginLogger.name,
|
||||
configureServer(server) {
|
||||
return () => server.middlewares.use(loggerMiddleware());
|
||||
},
|
||||
configurePreviewServer(server) {
|
||||
return () => server.middlewares.use(loggerMiddleware());
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function loggerMiddleware(): Connect.NextHandleFunction {
|
||||
return (req, res, next) => {
|
||||
const url = new URL(req.originalUrl!, "https://test.local");
|
||||
console.log(" -->", req.method, url.pathname);
|
||||
const startTime = Date.now();
|
||||
res.once("close", () => {
|
||||
console.log(
|
||||
" <--",
|
||||
req.method,
|
||||
url.pathname,
|
||||
res.statusCode,
|
||||
formatDuration(Date.now() - startTime),
|
||||
);
|
||||
});
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function formatDuration(ms: number) {
|
||||
return ms < 1000 ? `${Math.floor(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
|
||||
}
|
||||
Reference in New Issue
Block a user