vike-cloudflare
Version:
Cloudflare adapter for Vike
378 lines (360 loc) • 13.4 kB
JavaScript
// src/plugins/build.ts
import { normalizePath } from "vite";
// src/plugins/const.ts
var NAME = "vike-cloudflare";
var WORKER_JS_NAME = "_worker.js";
var WORKER_NAME = "cloudflare-worker";
var ROUTES_JSON_NAME = "_routes.json";
var isWin = process.platform === "win32";
var isCI = Boolean(process.env.CI);
var virtualUserEntryId = "virtual:vike-cloudflare:user-entry";
var resolvedVirtualUserEntryId = `${virtualUserEntryId}-resolved`;
var virtualProdEntryId = "virtual:vike-cloudflare:prod-entry";
var resolvedVirtualProdEntryId = `\0${virtualProdEntryId}`;
var virtualEntryAuto = "virtual:vike-cloudflare:auto-entry";
// src/plugins/build.ts
import { cp, mkdir, readdir, rm, symlink, writeFile } from "node:fs/promises";
import { builtinModules } from "node:module";
import { dirname, isAbsolute, join, posix, relative } from "node:path";
import { prerender } from "vike/api";
import { getVikeConfig as getVikeConfig2 } from "vike/plugin";
// src/plugins/utils/resolveServerConfig.ts
import { getVikeConfig } from "vike/plugin";
import { resolveServerConfig } from "vike-server/api";
// src/assert.ts
function assert(condition, message) {
if (condition) {
return;
}
throw new Error(message);
}
// src/plugins/utils/resolveServerConfig.ts
function getUserServerConfig(config) {
const vike = getVikeConfig(config);
const servers = resolveServerConfig(vike.config.server);
if (servers.length <= 1) return;
const server = servers[0];
assert(server);
return server;
}
// src/plugins/build.ts
function buildPlugin() {
let resolvedConfig;
let shouldPrerender = false;
return {
name: NAME,
enforce: "post",
applyToEnvironment(env) {
return env.name === "ssr";
},
async configResolved(config) {
resolvedConfig = config;
const vike = getVikeConfig2(config);
assert(vike, "[Bug] Reach out to a maintainer");
shouldPrerender = isPrerenderEnabled(vike);
},
config() {
return {
esbuild: {
ignoreAnnotations: false,
treeShaking: true,
minifySyntax: true
},
build: {
target: "es2022",
rollupOptions: {
external: [...builtinModules, /^node:/],
treeshake: {
preset: "smallest",
moduleSideEffects: "no-external"
}
}
}
};
},
options(inputOptions) {
inputOptions.input ??= {};
assert(
typeof inputOptions.input === "object" && !Array.isArray(inputOptions.input),
`[${NAME}] input should be an object. Aborting`
);
inputOptions.input[WORKER_NAME] = virtualProdEntryId;
const server = getUserServerConfig(this.environment.config);
if (server) {
inputOptions.input["cloudflare-server-entry"] = virtualUserEntryId;
}
},
writeBundle: {
order: "post",
sequential: true,
async handler(opts, bundle) {
const outCloudflare = getOutDir(resolvedConfig, "cloudflare");
const outClient = getOutDir(resolvedConfig, "client");
const outServer = getOutDir(resolvedConfig, "server");
await rm(outCloudflare, { recursive: true, force: true });
await mkdir(outCloudflare, { recursive: true });
let staticRoutes = [];
for (const file of await readdir(outClient, {
withFileTypes: true
})) {
if (file.isDirectory()) {
staticRoutes.push(`/${file.name}/*`);
} else {
staticRoutes.push(`/${file.name}`);
}
await symlinkOrCopy(join(outClient, file.name), join(outCloudflare, file.name));
}
await symlinkOrCopy(outServer, join(outCloudflare, "server"));
if (shouldPrerender) {
const filePaths = await prerenderPages();
const relPaths = filePaths.map((path) => relative(outClient, path));
for (const relPath of relPaths) {
await symlinkOrCopy(join(outClient, relPath), join(outCloudflare, relPath));
}
staticRoutes = relPaths.map(normalizePath).map((m) => `/${m.endsWith(".html") ? m.slice(0, -5) : m}`).map((m) => m.endsWith("/index") ? m.slice(0, -5) : m);
}
await writeFile(
join(outCloudflare, ROUTES_JSON_NAME),
JSON.stringify(
{
version: 1,
include: ["/*"],
exclude: staticRoutes
},
void 0,
2
),
"utf-8"
);
const res = Object.entries(bundle).find(([_, value]) => {
return value.type === "chunk" && value.isEntry && value.name === WORKER_NAME;
});
if (!res) {
throw new Error(`Cannot find ${WORKER_NAME} entry`);
}
const [chunkPath] = res;
await writeFile(
join(outCloudflare, WORKER_JS_NAME),
`import handler from "./server/${chunkPath}";
export default handler;
`,
"utf-8"
);
}
}
};
}
async function symlinkOrCopy(target, path) {
assert(isAbsolute(target), `[${NAME}] target should be an absolute path. Aborting`);
assert(isAbsolute(path), `[${NAME}] path should be an absolute path. Aborting`);
if (isWin || isCI) {
await cp(target, path, {
dereference: true,
force: true,
recursive: true
});
} else {
const parent = dirname(path);
await mkdir(parent, { recursive: true }).catch(() => {
});
await symlink(posix.relative(parent, target), path);
}
}
function getOutDir(config, force) {
const p = join(config.root, normalizePath(config.build.outDir));
if (!force) return p;
return join(dirname(p), force);
}
async function prerenderPages() {
const filePaths = [];
await prerender({
// biome-ignore lint/suspicious/noExplicitAny: TODO
async onPagePrerender(page) {
const result = page._prerenderResult;
filePaths.push(result.filePath);
await mkdir(dirname(result.filePath), { recursive: true }).catch(() => {
});
await writeFile(result.filePath, result.fileContent, "utf-8");
}
});
return filePaths;
}
function isPrerenderEnabled(vike) {
return isPrerenderValueEnabling(vike.config.prerender) || Object.values(vike.pages).some((page) => isPrerenderValueEnabling(page.config.prerender));
}
function isPrerenderValueEnabling(prerender2) {
const val = prerender2?.[0];
if (isObject(val)) return val.enable === void 0 || val.enable === true;
return val === true;
}
function isObject(val) {
return typeof val === "object" && val !== null;
}
// src/plugins/define.ts
function definePlugin() {
return {
name: `${NAME}:define`,
apply: "build",
config() {
return {
define: {
"process.env.NODE_ENV": JSON.stringify("production")
}
};
}
};
}
// src/plugins/optional.ts
function optionalPlugin() {
return {
name: `${NAME}:optional`,
async resolveId(id, importer, options) {
if (id.startsWith("@hattip/adapter-cloudflare-workers")) {
const dep = await this.resolve(id, importer, options);
if (!dep) {
throw new Error('Please install the following missing package: "@hattip/adapter-cloudflare-workers"');
}
}
}
};
}
// src/plugins/resolve-conditions.ts
var cloudflareBuiltInModules = [
"cloudflare:email",
"cloudflare:sockets",
"cloudflare:workers",
"cloudflare:workflows"
];
function resolveConditionsPlugin() {
return {
name: `${NAME}:resolve-conditions`,
enforce: "post",
configEnvironment(name, config, env) {
assert(config.consumer);
if (config.consumer !== "server") return;
const isDev = env.command === "serve";
return isDev ? {
resolve: {
noExternal: ["vike-cloudflare"]
}
} : {
resolve: {
noExternal: true,
// https://github.com/cloudflare/workers-sdk/blob/515de6ab40ed6154a2e6579ff90b14b304809609/packages/wrangler/src/deployment-bundle/bundle.ts#L37
conditions: ["workerd", "worker", "browser", "development|production"],
builtins: [...cloudflareBuiltInModules]
}
};
}
};
}
// raw-loader:../assets/hattip.js?raw
var hattip_default = 'import "virtual:@brillout/vite-plugin-server-entry:serverEntry";\nimport cloudflareWorkersAdapter from "@hattip/adapter-cloudflare-workers/no-static";\nimport handler from "virtual:vike-cloudflare:user-entry";\n\nexport default {\n fetch: cloudflareWorkersAdapter(handler),\n};\n';
// raw-loader:../assets/hono.js?raw
var hono_default = 'import "virtual:@brillout/vite-plugin-server-entry:serverEntry";\nimport { Hono } from "vike-cloudflare/hono";\nimport app from "virtual:vike-cloudflare:user-entry";\n\nconst worker = new Hono();\n\nworker.route("/", app);\nworker.notFound(app.notFoundHandler);\n\nexport default worker;\n';
// raw-loader:../assets/hono-dev.js?raw
var hono_dev_default = 'import { apply, Hono } from "vike-cloudflare/hono";\nimport { serve } from "vike-cloudflare/hono/serve";\n\nfunction startServer() {\n const app = new Hono();\n const port = process.env.PORT || 3000;\n\n apply(app);\n\n return serve(app, { port: +port });\n}\n\nexport default startServer();\n';
// raw-loader:../assets/vike.js?raw
var vike_default = '/// <reference lib="webworker" />\nimport "virtual:@brillout/vite-plugin-server-entry:serverEntry";\nimport { renderPage } from "vike/server";\n\n/**\n * @param url {string}\n * @param ctx {{ env: any, ctx: any }}\n * @returns {Promise<Response>}\n */\nasync function handleSsr(url, ctx) {\n const pageContextInit = {\n urlOriginal: url,\n fetch,\n ...ctx,\n };\n const pageContext = await renderPage(pageContextInit);\n const { httpResponse } = pageContext;\n const { statusCode: status, headers } = httpResponse;\n\n return new Response(httpResponse.getReadableWebStream(), {\n status,\n headers,\n });\n}\n\nexport default {\n /**\n * @param request {Request}\n * @param env {any}\n * @param ctx {any}\n * @returns {Promise<Response>}\n */\n async fetch(request, env, ctx) {\n return handleSsr(request.url, { env, ctx });\n },\n};\n';
// src/assets.ts
function getAsset(kind) {
switch (kind) {
case "hono": {
return hono_default;
}
case "hono-dev": {
return hono_dev_default;
}
case "hattip": {
return hattip_default;
}
default:
return vike_default;
}
}
// src/plugins/entries.ts
function entriesPlugin() {
const resolvedPlugins = /* @__PURE__ */ new Map();
return [
{
name: `${NAME}:resolve-entries:pre`,
enforce: "pre",
apply: "build",
async resolveId(id, importer, opts) {
if (id in idsToServers) {
const resolved = await this.resolve(id, importer, opts);
if (resolved) {
resolvedPlugins.set(resolved.id, idsToServers[id]);
}
}
}
},
{
name: `${NAME}:resolve-entries:prod`,
apply: "build",
async resolveId(id, importer, opts) {
if (id === virtualEntryAuto || id === virtualProdEntryId) {
return resolvedVirtualProdEntryId;
}
},
async load(id) {
if (id === resolvedVirtualProdEntryId) {
const server = getUserServerConfig(this.environment.config);
if (server) {
const loaded = await this.load({ id: server.entry.index, resolveDependencies: true });
const graph = /* @__PURE__ */ new Set([...loaded.importedIdResolutions, ...loaded.dynamicallyImportedIdResolutions]);
let found;
for (const imported of graph.values()) {
found = resolvedPlugins.get(imported.id);
if (found) break;
if (imported.external) continue;
const sub = await this.load({ id: imported.id, resolveDependencies: true });
for (const imp of [...sub.importedIdResolutions, ...sub.dynamicallyImportedIdResolutions]) {
graph.add(imp);
}
}
assert(found, `[${NAME}] Cannot find "vike-cloudflare/hattip" or "vike-cloudflare/hono" in server entry`);
return getAsset(found);
}
return getAsset("hono");
}
}
},
{
name: `${NAME}:resolve-entries:user`,
async resolveId(id) {
if (id === virtualEntryAuto || id === virtualUserEntryId || id === resolvedVirtualUserEntryId) {
const server = getUserServerConfig(this.environment.config);
if (server) {
const resolved = await this.resolve(server.entry.index);
assert(resolved, `[${NAME}] Cannot resolve ${server.entry.index}`);
return resolved;
}
return resolvedVirtualUserEntryId;
}
},
async load(id) {
if (id === resolvedVirtualUserEntryId) {
const server = getUserServerConfig(this.environment.config);
if (server) {
assert(false);
}
return getAsset("hono-dev");
}
}
}
];
}
var idsToServers = {
"vike-cloudflare/hono": "hono",
"vike-server/hono": "hono",
"vike-cloudflare/hattip": "hattip",
"vike-server/hattip": "hattip"
};
// src/plugins/index.ts
var pages = () => {
return [definePlugin(), resolveConditionsPlugin(), entriesPlugin(), buildPlugin(), optionalPlugin()];
};
export {
pages
};