UNPKG

vike-cloudflare

Version:
378 lines (360 loc) 13.4 kB
// 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 };