@hono/vite-build 1.0.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/README.md ADDED
@@ -0,0 +1,221 @@
1
+ # @hono/vite-build
2
+
3
+ `@hono/vite-build` is a set of Vite plugins for building Hono applications with Vite. It supports multiple runtimes and platforms, allowing you to build a project that includes JavaScript files for these platforms from a Hono app.
4
+
5
+ Here are the modules included:
6
+
7
+ - `@hono/vite-build/base`
8
+ - `@hono/vite-build/cloudflare-pages`
9
+ - `@hono/vite-build/cloudflare-workers`
10
+ - `@hono/vite-build/bun`
11
+ - `@hono/vite-build/node`
12
+
13
+ ## Usage
14
+
15
+ ### Install
16
+
17
+ You can install `vite` and `@hono/vite-build` via the npm.
18
+
19
+ ```bash
20
+ npm i -D vite @hono/vite-build
21
+ ```
22
+
23
+ Or you can install them with Bun.
24
+
25
+ ```bash
26
+ bun add -D vite @hono/vite-build
27
+ ```
28
+
29
+ ### Settings
30
+
31
+ Add `"type": "module"` to your package.json. Then, create `vite.config.ts` and edit it. The following is for Bun.
32
+
33
+ ```ts
34
+ import { defineConfig } from 'vite'
35
+ import build from '@hono/vite-build/bun'
36
+ // import build from '@hono/vite-build/cloudflare-pages'
37
+ // import build from '@hono/vite-build/cloudflare-workers'
38
+ // import build from '@hono/vite-build/node'
39
+
40
+ export default defineConfig({
41
+ plugins: [
42
+ build({
43
+ // Defaults are `src/index.ts`,`./src/index.tsx`,`./app/server.ts`
44
+ entry: './src/index.tsx',
45
+ }),
46
+ ],
47
+ })
48
+ ```
49
+
50
+ ### Build
51
+
52
+ Just run `vite build`.
53
+
54
+ ```bash
55
+ npm exec vite build
56
+ ```
57
+
58
+ Or
59
+
60
+ ```bash
61
+ bunx --bun vite build
62
+ ```
63
+
64
+ ### Run
65
+
66
+ Run with the command on your runtime. For examples:
67
+
68
+ Cloudflare Pages:
69
+
70
+ ```bash
71
+ wrangler pages dev ./dist
72
+ ```
73
+
74
+ Bun:
75
+
76
+ ```bash
77
+ cd ./dist
78
+ bun run ./index.js
79
+ ```
80
+
81
+ Node.js:
82
+
83
+ ```bash
84
+ cd ./dist
85
+ node ./index.js
86
+ ```
87
+
88
+ ## Common Options
89
+
90
+ ```ts
91
+ type BuildOptions = {
92
+ entry?: string | string[]
93
+ output?: string
94
+ outputDir?: string
95
+ external?: string[]
96
+ minify?: boolean
97
+ emptyOutDir?: boolean
98
+ }
99
+ ```
100
+
101
+ Default values:
102
+
103
+ ```ts
104
+ export const defaultOptions = {
105
+ entry: ['src/index.ts', './src/index.tsx', './app/server.ts'],
106
+ output: 'index.js',
107
+ outputDir: './dist',
108
+ external: [],
109
+ minify: true,
110
+ emptyOutDir: false,
111
+ staticPaths: [],
112
+ }
113
+ ```
114
+
115
+ ## Platform specific things
116
+
117
+ ### Cloudflare Pages
118
+
119
+ 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.
120
+
121
+ ## Example project
122
+
123
+ `src/index.tsx`:
124
+
125
+ ```tsx
126
+ import { Hono } from 'hono'
127
+
128
+ const app = new Hono()
129
+
130
+ app.get('/', (c) => {
131
+ return c.html(
132
+ <html>
133
+ <head>
134
+ <link href='/static/style.css' rel='stylesheet' />
135
+ </head>
136
+ <body>
137
+ <h1>Hello!</h1>
138
+ </body>
139
+ </html>
140
+ )
141
+ })
142
+
143
+ export default app
144
+ ```
145
+
146
+ `public/static/style.css`:
147
+
148
+ ```css
149
+ h1 {
150
+ font-family: Arial, Helvetica, sans-serif;
151
+ }
152
+ ```
153
+
154
+ The project with those file will be built to the following files with `@hono/vite-build/bun`:
155
+
156
+ ```txt
157
+ dist
158
+ ├── index.js
159
+ └── static
160
+ └── style.css
161
+ ```
162
+
163
+ ## Build a client
164
+
165
+ If you also want to build a client-side script, you can configure it as follows.
166
+
167
+ ```ts
168
+ export default defineConfig(({ mode }) => {
169
+ if (mode === 'client') {
170
+ return {
171
+ build: {
172
+ rollupOptions: {
173
+ input: './src/client.ts',
174
+ output: {
175
+ dir: './dist/static',
176
+ entryFileNames: 'client.js',
177
+ },
178
+ },
179
+ copyPublicDir: false,
180
+ },
181
+ }
182
+ } else {
183
+ return {
184
+ plugins: [build()],
185
+ }
186
+ }
187
+ })
188
+ ```
189
+
190
+ The build command:
191
+
192
+ ```bash
193
+ vite build --mode client && vite build
194
+ ```
195
+
196
+ `import.meta.env.PROD` is helpful in detecting whether it is in development or production mode if you are using it on a Vite dev server.
197
+
198
+ ```tsx
199
+ app.get('/', (c) => {
200
+ return c.html(
201
+ <html>
202
+ <head>
203
+ {import.meta.env.PROD ? (
204
+ <script type='module' src='/static/client.js'></script>
205
+ ) : (
206
+ <script type='module' src='/src/client.ts'></script>
207
+ )}
208
+ </head>
209
+ <body>Hello!</body>
210
+ </html>
211
+ )
212
+ })
213
+ ```
214
+
215
+ ## Authors
216
+
217
+ - Yusuke Wada <https://github.com/yusukebe>
218
+
219
+ ## License
220
+
221
+ MIT
@@ -0,0 +1,10 @@
1
+ import { Plugin } from 'vite';
2
+ import { BuildOptions } from '../../base.js';
3
+ import '../../entry/index.js';
4
+
5
+ type BunBuildOptions = {
6
+ staticRoot?: string | undefined;
7
+ } & BuildOptions;
8
+ declare const bunBuildPlugin: (pluginOptions?: BunBuildOptions) => Plugin;
9
+
10
+ export { BunBuildOptions, bunBuildPlugin as default };
@@ -0,0 +1,26 @@
1
+ import buildPlugin from "../../base.js";
2
+ import { serveStaticHook } from "../../entry/serve-static.js";
3
+ const bunBuildPlugin = (pluginOptions) => {
4
+ return {
5
+ ...buildPlugin({
6
+ ...{
7
+ entryContentBeforeHooks: [
8
+ async (appName, options) => {
9
+ let code = "import { serveStatic } from 'hono/bun'\n";
10
+ code += serveStaticHook(appName, {
11
+ filePaths: options?.staticPaths,
12
+ root: pluginOptions?.staticRoot
13
+ });
14
+ return code;
15
+ }
16
+ ]
17
+ },
18
+ ...pluginOptions
19
+ }),
20
+ name: "@hono/vite-build/bun"
21
+ };
22
+ };
23
+ var bun_default = bunBuildPlugin;
24
+ export {
25
+ bun_default as default
26
+ };
@@ -0,0 +1,8 @@
1
+ import { Plugin } from 'vite';
2
+ import { BuildOptions } from '../../base.js';
3
+ import '../../entry/index.js';
4
+
5
+ type CloudflarePagesBuildOptions = BuildOptions;
6
+ declare const cloudflarePagesBuildPlugin: (pluginOptions?: CloudflarePagesBuildOptions) => Plugin;
7
+
8
+ export { CloudflarePagesBuildOptions, cloudflarePagesBuildPlugin as default };
@@ -0,0 +1,53 @@
1
+ import { readdir, writeFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import buildPlugin, { defaultOptions } from "../../base.js";
4
+ const WORKER_JS_NAME = "_worker.js";
5
+ const ROUTES_JSON_NAME = "_routes.json";
6
+ const cloudflarePagesBuildPlugin = (pluginOptions) => {
7
+ let config;
8
+ const staticPaths = [];
9
+ return {
10
+ ...buildPlugin({
11
+ ...pluginOptions,
12
+ output: WORKER_JS_NAME
13
+ }),
14
+ configResolved: async (resolvedConfig) => {
15
+ config = resolvedConfig;
16
+ },
17
+ writeBundle: async () => {
18
+ const paths = await readdir(resolve(config.root, config.build.outDir), {
19
+ withFileTypes: true
20
+ });
21
+ if (paths.some((p) => p.name === ROUTES_JSON_NAME)) {
22
+ return;
23
+ } else {
24
+ paths.forEach((p) => {
25
+ if (p.isDirectory()) {
26
+ staticPaths.push(`/${p.name}/*`);
27
+ } else {
28
+ if (p.name === WORKER_JS_NAME) {
29
+ return;
30
+ }
31
+ staticPaths.push(`/${p.name}`);
32
+ }
33
+ });
34
+ const staticRoutes = {
35
+ version: 1,
36
+ include: ["/*"],
37
+ exclude: staticPaths
38
+ };
39
+ const path = resolve(
40
+ config.root,
41
+ pluginOptions?.outputDir ?? defaultOptions.outputDir,
42
+ "_routes.json"
43
+ );
44
+ await writeFile(path, JSON.stringify(staticRoutes));
45
+ }
46
+ },
47
+ name: "@hono/vite-build/cloudflare-pages"
48
+ };
49
+ };
50
+ var cloudflare_pages_default = cloudflarePagesBuildPlugin;
51
+ export {
52
+ cloudflare_pages_default as default
53
+ };
@@ -0,0 +1,8 @@
1
+ import { Plugin } from 'vite';
2
+ import { BuildOptions } from '../../base.js';
3
+ import '../../entry/index.js';
4
+
5
+ type CloudflareWorkersBuildOptions = BuildOptions;
6
+ declare const cloudflareWorkersBuildPlugin: (pluginOptions?: CloudflareWorkersBuildOptions) => Plugin;
7
+
8
+ export { CloudflareWorkersBuildOptions, cloudflareWorkersBuildPlugin as default };
@@ -0,0 +1,13 @@
1
+ import buildPlugin from "../../base.js";
2
+ const cloudflareWorkersBuildPlugin = (pluginOptions) => {
3
+ return {
4
+ ...buildPlugin({
5
+ ...pluginOptions
6
+ }),
7
+ name: "@hono/vite-build/cloudflare-workers"
8
+ };
9
+ };
10
+ var cloudflare_workers_default = cloudflareWorkersBuildPlugin;
11
+ export {
12
+ cloudflare_workers_default as default
13
+ };
@@ -0,0 +1,10 @@
1
+ import { Plugin } from 'vite';
2
+ import { BuildOptions } from '../../base.js';
3
+ import '../../entry/index.js';
4
+
5
+ type NodeBuildOptions = {
6
+ staticRoot?: string | undefined;
7
+ } & BuildOptions;
8
+ declare const nodeBuildPlugin: (pluginOptions?: NodeBuildOptions) => Plugin;
9
+
10
+ export { NodeBuildOptions, nodeBuildPlugin as default };
@@ -0,0 +1,33 @@
1
+ import buildPlugin from "../../base.js";
2
+ import { serveStaticHook } from "../../entry/serve-static.js";
3
+ const nodeBuildPlugin = (pluginOptions) => {
4
+ return {
5
+ ...buildPlugin({
6
+ ...{
7
+ entryContentBeforeHooks: [
8
+ async (appName, options) => {
9
+ let code = "import { serveStatic } from '@hono/node-server/serve-static'\n";
10
+ code += serveStaticHook(appName, {
11
+ filePaths: options?.staticPaths,
12
+ root: pluginOptions?.staticRoot
13
+ });
14
+ return code;
15
+ }
16
+ ],
17
+ entryContentAfterHooks: [
18
+ async (appName) => {
19
+ let code = "import { serve } from '@hono/node-server'\n";
20
+ code += `serve(${appName})`;
21
+ return code;
22
+ }
23
+ ]
24
+ },
25
+ ...pluginOptions
26
+ }),
27
+ name: "@hono/vite-build/node"
28
+ };
29
+ };
30
+ var node_default = nodeBuildPlugin;
31
+ export {
32
+ node_default as default
33
+ };
package/dist/base.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { UserConfig, ConfigEnv, Plugin } from 'vite';
2
+ import { GetEntryContentOptions } from './entry/index.js';
3
+
4
+ type BuildOptions = {
5
+ /**
6
+ * @default ['src/index.ts', './src/index.tsx', './app/server.ts']
7
+ */
8
+ entry?: string | string[];
9
+ /**
10
+ * @default './dist'
11
+ */
12
+ output?: string;
13
+ outputDir?: string;
14
+ external?: string[];
15
+ /**
16
+ * @default true
17
+ */
18
+ minify?: boolean;
19
+ emptyOutDir?: boolean;
20
+ apply?: ((this: void, config: UserConfig, env: ConfigEnv) => boolean) | undefined;
21
+ } & Omit<GetEntryContentOptions, 'entry'>;
22
+ declare const defaultOptions: Required<Omit<BuildOptions, 'entryContentAfterHooks' | 'entryContentBeforeHooks'>>;
23
+ declare const buildPlugin: (options: BuildOptions) => Plugin;
24
+
25
+ export { BuildOptions, buildPlugin as default, defaultOptions };
package/dist/base.js ADDED
@@ -0,0 +1,100 @@
1
+ import { builtinModules } from "module";
2
+ import { readdirSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { getEntryContent } from "./entry/index.js";
5
+ const defaultOptions = {
6
+ entry: ["src/index.ts", "./src/index.tsx", "./app/server.ts"],
7
+ output: "index.js",
8
+ outputDir: "./dist",
9
+ external: [],
10
+ minify: true,
11
+ emptyOutDir: false,
12
+ apply: (_config, { command, mode }) => {
13
+ if (command === "build" && mode !== "client") {
14
+ return true;
15
+ }
16
+ return false;
17
+ },
18
+ staticPaths: []
19
+ };
20
+ const buildPlugin = (options) => {
21
+ const virtualEntryId = "virtual:build-entry-module";
22
+ const resolvedVirtualEntryId = "\0" + virtualEntryId;
23
+ let config;
24
+ const output = options.output ?? defaultOptions.output;
25
+ return {
26
+ name: "@hono/vite-build",
27
+ configResolved: async (resolvedConfig) => {
28
+ config = resolvedConfig;
29
+ },
30
+ resolveId(id) {
31
+ if (id === virtualEntryId) {
32
+ return resolvedVirtualEntryId;
33
+ }
34
+ },
35
+ async load(id) {
36
+ if (id === resolvedVirtualEntryId) {
37
+ let staticPaths = [];
38
+ const direntPaths = [];
39
+ try {
40
+ const publicDirPaths = readdirSync(resolve(config.root, config.publicDir), {
41
+ withFileTypes: true
42
+ });
43
+ direntPaths.push(...publicDirPaths);
44
+ const buildOutDirPaths = readdirSync(resolve(config.root, config.build.outDir), {
45
+ withFileTypes: true
46
+ });
47
+ direntPaths.push(...buildOutDirPaths);
48
+ } catch {
49
+ }
50
+ const uniqueStaticPaths = /* @__PURE__ */ new Set();
51
+ direntPaths.forEach((p) => {
52
+ if (p.isDirectory()) {
53
+ uniqueStaticPaths.add(`/${p.name}/*`);
54
+ } else {
55
+ if (p.name === output) {
56
+ return;
57
+ }
58
+ uniqueStaticPaths.add(`/${p.name}`);
59
+ }
60
+ });
61
+ staticPaths = Array.from(uniqueStaticPaths);
62
+ const entry = options.entry ?? defaultOptions.entry;
63
+ return await getEntryContent({
64
+ entry: Array.isArray(entry) ? entry : [entry],
65
+ entryContentBeforeHooks: options.entryContentBeforeHooks,
66
+ entryContentAfterHooks: options.entryContentAfterHooks,
67
+ staticPaths
68
+ });
69
+ }
70
+ },
71
+ apply: options?.apply ?? defaultOptions.apply,
72
+ config: async () => {
73
+ return {
74
+ ssr: {
75
+ external: options?.external ?? defaultOptions.external,
76
+ noExternal: true,
77
+ target: "webworker"
78
+ },
79
+ build: {
80
+ outDir: options?.outputDir ?? defaultOptions.outputDir,
81
+ emptyOutDir: options?.emptyOutDir ?? defaultOptions.emptyOutDir,
82
+ minify: options?.minify ?? defaultOptions.minify,
83
+ ssr: true,
84
+ rollupOptions: {
85
+ external: [...builtinModules, /^node:/],
86
+ input: virtualEntryId,
87
+ output: {
88
+ entryFileNames: output
89
+ }
90
+ }
91
+ }
92
+ };
93
+ }
94
+ };
95
+ };
96
+ var base_default = buildPlugin;
97
+ export {
98
+ base_default as default,
99
+ defaultOptions
100
+ };
@@ -0,0 +1,13 @@
1
+ type EntryContentHookOptions = {
2
+ staticPaths: string[];
3
+ };
4
+ type EntryContentHook = (appName: string, options?: EntryContentHookOptions) => string | Promise<string>;
5
+ type GetEntryContentOptions = {
6
+ entry: string[];
7
+ entryContentBeforeHooks?: EntryContentHook[];
8
+ entryContentAfterHooks?: EntryContentHook[];
9
+ staticPaths?: string[];
10
+ };
11
+ declare const getEntryContent: (options: GetEntryContentOptions) => Promise<string>;
12
+
13
+ export { EntryContentHook, EntryContentHookOptions, GetEntryContentOptions, getEntryContent };
@@ -0,0 +1,53 @@
1
+ import { normalize } from "node:path";
2
+ const normalizePaths = (paths) => {
3
+ return paths.map((p) => {
4
+ let normalizedPath = normalize(p).replace(/\\/g, "/");
5
+ if (normalizedPath.startsWith("./")) {
6
+ normalizedPath = normalizedPath.substring(2);
7
+ }
8
+ return "/" + normalizedPath;
9
+ });
10
+ };
11
+ const getEntryContent = async (options) => {
12
+ const staticPaths = options.staticPaths ?? [""];
13
+ const globStr = normalizePaths(options.entry).map((e) => `'${e}'`).join(",");
14
+ const hooksToString = async (appName, hooks) => {
15
+ if (hooks) {
16
+ const str = (await Promise.all(
17
+ hooks.map((hook) => {
18
+ return hook(appName, {
19
+ staticPaths
20
+ });
21
+ })
22
+ )).join("\n");
23
+ return str;
24
+ }
25
+ return "";
26
+ };
27
+ const appStr = `const modules = import.meta.glob([${globStr}], { import: 'default', eager: true })
28
+ let added = false
29
+ for (const [, app] of Object.entries(modules)) {
30
+ if (app) {
31
+ mainApp.route('/', app)
32
+ mainApp.notFound(app.notFoundHandler)
33
+ added = true
34
+ }
35
+ }
36
+ if (!added) {
37
+ throw new Error("Can't import modules from [${globStr}]")
38
+ }`;
39
+ const mainAppStr = `import { Hono } from 'hono'
40
+ const mainApp = new Hono()
41
+
42
+ ${await hooksToString("mainApp", options.entryContentBeforeHooks)}
43
+
44
+ ${appStr}
45
+
46
+ ${await hooksToString("mainApp", options.entryContentAfterHooks)}
47
+
48
+ export default mainApp`;
49
+ return mainAppStr;
50
+ };
51
+ export {
52
+ getEntryContent
53
+ };
@@ -0,0 +1,7 @@
1
+ type ServeStaticHookOptions = {
2
+ filePaths?: string[];
3
+ root?: string;
4
+ };
5
+ declare const serveStaticHook: (appName: string, options: ServeStaticHookOptions) => string;
6
+
7
+ export { serveStaticHook };
@@ -0,0 +1,11 @@
1
+ const serveStaticHook = (appName, options) => {
2
+ let code = "";
3
+ for (const path of options.filePaths ?? []) {
4
+ code += `${appName}.use('${path}', serveStatic({ root: '${options.root ?? "./"}' }))
5
+ `;
6
+ }
7
+ return code;
8
+ };
9
+ export {
10
+ serveStaticHook
11
+ };
@@ -0,0 +1,8 @@
1
+ import buildPlugin from './base.js';
2
+ export { defaultOptions } from './base.js';
3
+ import 'vite';
4
+ import './entry/index.js';
5
+
6
+
7
+
8
+ export { buildPlugin as default };
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ import { defaultOptions } from "./base.js";
2
+ import basePlugin from "./base.js";
3
+ var src_default = basePlugin;
4
+ export {
5
+ src_default as default,
6
+ defaultOptions
7
+ };
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "@hono/vite-build",
3
+ "description": "Vite plugin to build your Hono app",
4
+ "version": "1.0.0",
5
+ "types": "dist/index.d.ts",
6
+ "module": "dist/index.js",
7
+ "type": "module",
8
+ "scripts": {
9
+ "test": "vitest --run",
10
+ "build": "rimraf dist && tsup && publint",
11
+ "watch": "tsup --watch",
12
+ "prerelease": "yarn build",
13
+ "release": "yarn publish"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ },
23
+ "./bun": {
24
+ "types": "./dist/adapter/bun/index.d.ts",
25
+ "import": "./dist/adapter/bun/index.js"
26
+ },
27
+ "./node": {
28
+ "types": "./dist/adapter/node/index.d.ts",
29
+ "import": "./dist/adapter/node/index.js"
30
+ },
31
+ "./cloudflare-pages": {
32
+ "types": "./dist/adapter/cloudflare-pages/index.d.ts",
33
+ "import": "./dist/adapter/cloudflare-pages/index.js"
34
+ },
35
+ "./cloudflare-workers": {
36
+ "types": "./dist/adapter/cloudflare-workers/index.d.ts",
37
+ "import": "./dist/adapter/cloudflare-workers/index.js"
38
+ }
39
+ },
40
+ "typesVersions": {
41
+ "*": {
42
+ "types": [
43
+ "./dist/types"
44
+ ],
45
+ "bun": [
46
+ "./dist/adapter/bun/index.d.ts"
47
+ ],
48
+ "node": [
49
+ "./dist/adapter/node/index.d.ts"
50
+ ],
51
+ "cloudflare-pages": [
52
+ "./dist/adapter/cloudflare-pages/index.d.ts"
53
+ ],
54
+ "cloudflare-workers": [
55
+ "./dist/adapter/cloudflare-workers/index.d.ts"
56
+ ]
57
+ }
58
+ },
59
+ "author": "Yusuke Wada <yusuke@kamawada.com> (https://github.com/yusukebe)",
60
+ "license": "MIT",
61
+ "repository": {
62
+ "type": "git",
63
+ "url": "https://github.com/honojs/vite-plugins.git"
64
+ },
65
+ "publishConfig": {
66
+ "registry": "https://registry.npmjs.org",
67
+ "access": "public"
68
+ },
69
+ "homepage": "https://github.com/honojs/vite-plugins",
70
+ "devDependencies": {
71
+ "glob": "^10.3.10",
72
+ "hono": "^4.6.1",
73
+ "publint": "^0.1.12",
74
+ "rimraf": "^5.0.1",
75
+ "tsup": "^7.2.0",
76
+ "vite": "^5.4.5",
77
+ "vitest": "^2.1.1"
78
+ },
79
+ "peerDependencies": {
80
+ "hono": "*"
81
+ },
82
+ "engines": {
83
+ "node": ">=18.14.1"
84
+ }
85
+ }