@hono/vite-build 1.10.1 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @hono/vite-build
2
2
 
3
+ ## 1.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#340](https://github.com/honojs/vite-plugins/pull/340) [`1f2b2fc308fe39b3bf940b948199f5fe1c184d65`](https://github.com/honojs/vite-plugins/commit/1f2b2fc308fe39b3bf940b948199f5fe1c184d65) Thanks [@josiahwiebe](https://github.com/josiahwiebe)! - feat: configure multiple function output for Vercel
8
+
3
9
  ## 1.10.1
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -10,6 +10,7 @@ Here are the modules included:
10
10
  - `@hono/vite-build/bun`
11
11
  - `@hono/vite-build/node`
12
12
  - `@hono/vite-build/netlify-functions`
13
+ - `@hono/vite-build/vercel`
13
14
 
14
15
  ## Usage
15
16
 
@@ -125,6 +126,42 @@ export const defaultOptions = {
125
126
 
126
127
  This plugin generates `_routes.json` automatically. The automatic generation can be overridden by creating a `public/_routes.json`. See [Create a `_routes.json` file](https://developers.cloudflare.com/pages/functions/routing/#create-a-_routesjson-file) on Cloudflare Docs for more details.
127
128
 
129
+ ### Vercel
130
+
131
+ By default, the Vercel adapter emits a single function named `__hono` and a catch-all route.
132
+
133
+ To emit multiple functions, add the Vercel adapter multiple times in `plugins`.
134
+
135
+ ```ts
136
+ import { defineConfig } from 'vite'
137
+ import build from '@hono/vite-build/vercel'
138
+
139
+ export default defineConfig({
140
+ plugins: [
141
+ build({
142
+ entry: './src/server.ts',
143
+ vercel: {
144
+ name: 'api',
145
+ routes: [{ src: '^/api(?:/.*)?$' }],
146
+ },
147
+ }),
148
+ build({
149
+ entry: './src/auth.ts',
150
+ vercel: {
151
+ name: 'auth',
152
+ routes: [{ src: '^/auth(?:/.*)?$' }],
153
+ function: {
154
+ maxDuration: 30,
155
+ },
156
+ },
157
+ }),
158
+ ],
159
+ })
160
+ ```
161
+
162
+ If `vercel.routes` is omitted, the adapter generates `^/<function-name>(?:/.*)?$`.
163
+ If `vercel.name` is omitted, the adapter uses `__hono`.
164
+
128
165
  ## Example project
129
166
 
130
167
  `src/index.tsx`:
@@ -3,10 +3,18 @@ import { BuildOptions } from '../../base.js';
3
3
  import { VercelBuildConfigV3, VercelNodejsServerlessFunctionConfig } from './types.js';
4
4
  import '../../entry/index.js';
5
5
 
6
+ type VercelSourceRoute = Extract<NonNullable<VercelBuildConfigV3['routes']>[number], {
7
+ src: string;
8
+ }>;
9
+ type VercelRouteConfig = Array<Omit<VercelSourceRoute, 'dest'> & {
10
+ dest?: string;
11
+ }>;
6
12
  type VercelBuildOptions = {
7
13
  vercel?: {
8
14
  config?: VercelBuildConfigV3;
9
15
  function?: Partial<VercelNodejsServerlessFunctionConfig>;
16
+ name?: string;
17
+ routes?: VercelRouteConfig;
10
18
  };
11
19
  } & Omit<BuildOptions, 'output' | 'outputDir'>;
12
20
  declare const vercelBuildPlugin: (pluginOptions?: VercelBuildOptions) => Plugin;
@@ -1,9 +1,20 @@
1
- import { existsSync, mkdirSync } from "node:fs";
1
+ import { builtinModules } from "module";
2
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
2
3
  import { cp, writeFile } from "node:fs/promises";
3
4
  import { resolve } from "node:path";
4
- import buildPlugin from "../../base.js";
5
+ import { defaultOptions } from "../../base.js";
6
+ import { getEntryContent } from "../../entry/index.js";
5
7
  const BUNDLE_NAME = "index.js";
6
- const FUNCTION_NAME = "__hono";
8
+ const DEFAULT_FUNCTION_NAME = "__hono";
9
+ const VIRTUAL_ENTRY_PREFIX = "virtual:build-entry-module-vercel-";
10
+ const functionEntryHooks = {
11
+ entryContentAfterHooks: [
12
+ // eslint-disable-next-line quotes
13
+ () => "import { handle } from '@hono/node-server/vercel'"
14
+ ],
15
+ entryContentDefaultExportHook: (appName) => `export default handle(${appName})`
16
+ };
17
+ const configWriteQueues = /* @__PURE__ */ new Map();
7
18
  const writeJSON = (path, data) => {
8
19
  const dir = resolve(path, "..");
9
20
  if (!existsSync(dir)) {
@@ -11,6 +22,27 @@ const writeJSON = (path, data) => {
11
22
  }
12
23
  return writeFile(path, JSON.stringify(data));
13
24
  };
25
+ const readJSON = (path) => {
26
+ if (!existsSync(path)) {
27
+ return;
28
+ }
29
+ return JSON.parse(readFileSync(path, "utf-8"));
30
+ };
31
+ const enqueueConfigWrite = async (key, writeTask) => {
32
+ const previousTask = configWriteQueues.get(key);
33
+ if (previousTask) {
34
+ await previousTask;
35
+ }
36
+ const nextTask = writeTask();
37
+ configWriteQueues.set(key, nextTask);
38
+ try {
39
+ await nextTask;
40
+ } finally {
41
+ if (configWriteQueues.get(key) === nextTask) {
42
+ configWriteQueues.delete(key);
43
+ }
44
+ }
45
+ };
14
46
  const getRuntimeVersion = () => {
15
47
  try {
16
48
  const systemNodeVersion = process.versions.node.split(".")[0];
@@ -19,64 +51,160 @@ const getRuntimeVersion = () => {
19
51
  return "nodejs22.x";
20
52
  }
21
53
  };
54
+ const escapeRouteSegment = (value) => {
55
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
56
+ };
57
+ const getDefaultRoutePattern = (functionName) => {
58
+ return `^/${escapeRouteSegment(functionName)}(?:/.*)?$`;
59
+ };
60
+ const appendRouteIfMissing = (target, seen, route) => {
61
+ const key = JSON.stringify(route);
62
+ if (seen.has(key)) {
63
+ return;
64
+ }
65
+ seen.add(key);
66
+ target.push(route);
67
+ };
68
+ const getFunctionConfig = (config, functionConfig) => {
69
+ return {
70
+ ...functionConfig,
71
+ runtime: getRuntimeVersion(),
72
+ launcherType: "Nodejs",
73
+ handler: BUNDLE_NAME,
74
+ shouldAddHelpers: Boolean(functionConfig?.shouldAddHelpers),
75
+ shouldAddSourcemapSupport: Boolean(config.build.sourcemap),
76
+ supportsResponseStreaming: true
77
+ };
78
+ };
79
+ const getRoutesForFunction = (functionName, configuredRoutes) => {
80
+ if (configuredRoutes && configuredRoutes.length > 0) {
81
+ return configuredRoutes;
82
+ }
83
+ if (functionName === DEFAULT_FUNCTION_NAME) {
84
+ return [{ src: "/(.*)", dest: `/${DEFAULT_FUNCTION_NAME}` }];
85
+ }
86
+ return [{ src: getDefaultRoutePattern(functionName), dest: `/${functionName}` }];
87
+ };
88
+ const mergeVercelConfig = async (configPath, routesToAdd, configOverride, functionName) => {
89
+ const existingConfig = readJSON(configPath);
90
+ const mergedRoutes = [];
91
+ const seenRoutes = /* @__PURE__ */ new Set();
92
+ for (const route of existingConfig?.routes ?? []) {
93
+ appendRouteIfMissing(mergedRoutes, seenRoutes, route);
94
+ }
95
+ for (const route of configOverride?.routes ?? []) {
96
+ appendRouteIfMissing(mergedRoutes, seenRoutes, route);
97
+ }
98
+ appendRouteIfMissing(mergedRoutes, seenRoutes, { handle: "filesystem" });
99
+ for (const route of routesToAdd) {
100
+ appendRouteIfMissing(mergedRoutes, seenRoutes, {
101
+ ...route,
102
+ dest: route.dest ?? `/${functionName}`
103
+ });
104
+ }
105
+ const buildConfig = {
106
+ ...existingConfig,
107
+ ...configOverride,
108
+ version: 3,
109
+ routes: mergedRoutes
110
+ };
111
+ await writeJSON(configPath, buildConfig);
112
+ };
113
+ const copyStaticFiles = async (publicDirPath, outputDir) => {
114
+ if (!existsSync(publicDirPath)) {
115
+ return;
116
+ }
117
+ try {
118
+ await cp(publicDirPath, resolve(outputDir, "static"), {
119
+ recursive: true,
120
+ force: true
121
+ });
122
+ } catch (error) {
123
+ if (error.code !== "EEXIST") {
124
+ throw error;
125
+ }
126
+ }
127
+ };
22
128
  const vercelBuildPlugin = (pluginOptions) => {
23
129
  let config;
130
+ const functionName = pluginOptions?.vercel?.name ?? DEFAULT_FUNCTION_NAME;
131
+ if (!functionName) {
132
+ throw new Error("`vercel.name` is required and cannot be empty.");
133
+ }
134
+ const virtualEntryId = `${VIRTUAL_ENTRY_PREFIX}${functionName}`;
135
+ const resolvedVirtualEntryId = `\0${virtualEntryId}`;
24
136
  return {
25
- ...buildPlugin({
26
- ssrTarget: "node",
27
- output: `functions/${FUNCTION_NAME}.func/${BUNDLE_NAME}`,
28
- outputDir: ".vercel/output",
29
- ...{
30
- entryContentAfterHooks: [
31
- // eslint-disable-next-line quotes
32
- () => "import { handle } from '@hono/node-server/vercel'"
33
- ],
34
- entryContentDefaultExportHook: (appName) => `export default handle(${appName})`
35
- },
36
- ...pluginOptions
37
- }),
137
+ name: "@hono/vite-build/vercel",
138
+ apply: pluginOptions?.apply ?? defaultOptions.apply,
139
+ resolveId(id) {
140
+ if (id === virtualEntryId) {
141
+ return resolvedVirtualEntryId;
142
+ }
143
+ },
144
+ async load(id) {
145
+ if (id !== resolvedVirtualEntryId) {
146
+ return;
147
+ }
148
+ const entry = pluginOptions?.entry ?? defaultOptions.entry;
149
+ return await getEntryContent({
150
+ entry: Array.isArray(entry) ? entry : [entry],
151
+ entryContentBeforeHooks: pluginOptions?.entryContentBeforeHooks,
152
+ entryContentAfterHooks: pluginOptions?.entryContentAfterHooks ?? functionEntryHooks.entryContentAfterHooks,
153
+ entryContentDefaultExportHook: pluginOptions?.entryContentDefaultExportHook ?? functionEntryHooks.entryContentDefaultExportHook,
154
+ staticPaths: pluginOptions?.staticPaths,
155
+ preset: pluginOptions?.preset
156
+ });
157
+ },
38
158
  configResolved: (resolvedConfig) => {
39
159
  config = resolvedConfig;
40
160
  },
41
- writeBundle: async () => {
42
- const outputDir = resolve(config.root, config.build.outDir);
43
- const functionDir = resolve(outputDir, "functions", `${FUNCTION_NAME}.func`);
44
- const buildConfig = {
45
- ...pluginOptions?.vercel?.config,
46
- version: 3,
47
- routes: [
48
- ...pluginOptions?.vercel?.config?.routes ?? [],
49
- {
50
- handle: "filesystem"
51
- },
52
- {
53
- src: "/(.*)",
54
- dest: `/${FUNCTION_NAME}`
161
+ config: async () => {
162
+ return {
163
+ ssr: {
164
+ external: pluginOptions?.external ?? defaultOptions.external,
165
+ noExternal: true,
166
+ target: "node"
167
+ },
168
+ build: {
169
+ outDir: ".vercel/output",
170
+ emptyOutDir: pluginOptions?.emptyOutDir ?? defaultOptions.emptyOutDir,
171
+ minify: pluginOptions?.minify ?? defaultOptions.minify,
172
+ ssr: true,
173
+ rollupOptions: {
174
+ external: [...builtinModules, /^node:/],
175
+ input: {
176
+ [functionName]: virtualEntryId
177
+ },
178
+ output: {
179
+ entryFileNames: `functions/[name].func/${BUNDLE_NAME}`
180
+ }
55
181
  }
56
- ]
57
- };
58
- const functionConfig = {
59
- ...pluginOptions?.vercel?.function,
60
- runtime: getRuntimeVersion(),
61
- launcherType: "Nodejs",
62
- handler: BUNDLE_NAME,
63
- shouldAddHelpers: Boolean(pluginOptions?.vercel?.function?.shouldAddHelpers),
64
- shouldAddSourcemapSupport: Boolean(config.build.sourcemap),
65
- supportsResponseStreaming: true
182
+ }
66
183
  };
184
+ },
185
+ writeBundle: async () => {
186
+ const outputDir = resolve(config.root, config.build.outDir);
187
+ const functionDir = resolve(outputDir, "functions", `${functionName}.func`);
188
+ const configPath = resolve(outputDir, "config.json");
67
189
  const publicDirPath = resolve(config.root, config.publicDir);
190
+ const routesToAdd = getRoutesForFunction(functionName, pluginOptions?.vercel?.routes);
191
+ const functionConfig = getFunctionConfig(config, pluginOptions?.vercel?.function);
192
+ await copyStaticFiles(publicDirPath, outputDir);
68
193
  await Promise.all([
69
- // Copy static files to the .vercel/output/static directory
70
- ...existsSync(publicDirPath) ? [cp(publicDirPath, resolve(outputDir, "static"), { recursive: true })] : [],
71
- // Write the all necessary config files
72
- writeJSON(resolve(outputDir, "config.json"), buildConfig),
73
194
  writeJSON(resolve(functionDir, ".vc-config.json"), functionConfig),
74
195
  writeJSON(resolve(functionDir, "package.json"), {
75
196
  type: "module"
76
197
  })
77
198
  ]);
78
- },
79
- name: "@hono/vite-build/vercel"
199
+ await enqueueConfigWrite(configPath, async () => {
200
+ await mergeVercelConfig(
201
+ configPath,
202
+ routesToAdd,
203
+ pluginOptions?.vercel?.config,
204
+ functionName
205
+ );
206
+ });
207
+ }
80
208
  };
81
209
  };
82
210
  var vercel_default = vercelBuildPlugin;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hono/vite-build",
3
3
  "description": "Vite plugin to build your Hono app",
4
- "version": "1.10.1",
4
+ "version": "1.11.0",
5
5
  "types": "dist/index.d.ts",
6
6
  "module": "dist/index.js",
7
7
  "type": "module",