├── .changeset ├── README.md └── config.json ├── .github └── workflows │ ├── ci-build.yml │ ├── ci-cloudflare-pages.yml │ ├── ci-dev-server.yml │ ├── ci-ssg.yml │ └── release.yml ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── .yarn ├── plugins │ └── @yarnpkg │ │ └── plugin-workspace-tools.cjs └── releases │ └── yarn-3.6.4.cjs ├── .yarnrc.yml ├── README.md ├── eslint.config.mjs ├── package.json ├── packages ├── build │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── adapter │ │ │ ├── bun │ │ │ │ └── index.ts │ │ │ ├── cloudflare-pages │ │ │ │ └── index.ts │ │ │ ├── cloudflare-workers │ │ │ │ └── index.ts │ │ │ ├── deno │ │ │ │ └── index.ts │ │ │ ├── netlify-functions │ │ │ │ └── index.ts │ │ │ ├── node │ │ │ │ └── index.ts │ │ │ └── vercel │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ ├── base.ts │ │ ├── entry │ │ │ ├── index.ts │ │ │ └── serve-static.ts │ │ └── index.ts │ ├── test │ │ ├── .gitignore │ │ ├── adapter.test.ts │ │ ├── basic.test.ts │ │ └── mocks │ │ │ ├── app-static-files │ │ │ ├── public-routes-json │ │ │ │ └── _routes.json │ │ │ ├── public │ │ │ │ ├── foo.txt │ │ │ │ └── js │ │ │ │ │ └── client.js │ │ │ └── src │ │ │ │ ├── server-fetch-with-handlers.ts │ │ │ │ ├── server-fetch-with-handlers2.ts │ │ │ │ └── server.ts │ │ │ └── app │ │ │ └── src │ │ │ ├── server-fetch.ts │ │ │ └── server.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vitest.config.ts ├── cloudflare-pages │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── cloudflare-pages.ts │ │ ├── entry.ts │ │ └── index.ts │ ├── test │ │ ├── cloudflare-pages.test.ts │ │ └── project │ │ │ ├── app │ │ │ └── server.ts │ │ │ ├── public-routes-json │ │ │ └── _routes.json │ │ │ └── public │ │ │ ├── favicon.ico │ │ │ └── static │ │ │ └── foo.txt │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── tsup.config.ts ├── dev-server │ ├── CHANGELOG.md │ ├── README.md │ ├── e2e-bun │ │ ├── e2e.test.ts │ │ ├── mock │ │ │ └── app.ts │ │ ├── playwright.config.ts │ │ ├── public │ │ │ └── static │ │ │ │ └── hello.json │ │ └── vite.config.ts │ ├── e2e │ │ ├── e2e.test.ts │ │ ├── mock │ │ │ └── worker.ts │ │ ├── playwright.config.ts │ │ ├── public │ │ │ └── hono-logo.png │ │ ├── vite.config.ts │ │ └── wrangler.toml │ ├── package.json │ ├── src │ │ ├── adapter │ │ │ ├── bun.ts │ │ │ ├── cloudflare.ts │ │ │ └── node.ts │ │ ├── dev-server.test.ts │ │ ├── dev-server.ts │ │ ├── index.ts │ │ └── types.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vitest.config.ts └── ssg │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ ├── index.ts │ └── ssg.ts │ ├── test │ ├── app.ts │ ├── sample.js │ └── ssg.test.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── tsup.config.ts ├── tsconfig.base.json └── yarn.lock /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { 6 | "repo": "honojs/vite-plugins" 7 | } 8 | ], 9 | "commit": false, 10 | "linked": [], 11 | "access": "public", 12 | "baseBranch": "main", 13 | "updateInternalDependencies": "patch", 14 | "ignore": [] 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/ci-build.yml: -------------------------------------------------------------------------------- 1 | name: ci-build 2 | on: 3 | push: 4 | branches: [main] 5 | paths: 6 | - 'packages/build/**' 7 | pull_request: 8 | branches: ['*'] 9 | paths: 10 | - 'packages/build/**' 11 | 12 | jobs: 13 | ci: 14 | runs-on: ubuntu-latest 15 | defaults: 16 | run: 17 | working-directory: ./packages/build 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: 20.x 23 | - run: yarn install 24 | - run: yarn build 25 | - run: yarn test 26 | 27 | ci-windows: 28 | runs-on: windows-latest 29 | defaults: 30 | run: 31 | working-directory: ./packages/build 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: actions/setup-node@v4 35 | with: 36 | node-version: 20.x 37 | - run: yarn install 38 | - run: yarn test 39 | -------------------------------------------------------------------------------- /.github/workflows/ci-cloudflare-pages.yml: -------------------------------------------------------------------------------- 1 | name: ci-cloudflare-pages 2 | on: 3 | push: 4 | branches: [main] 5 | paths: 6 | - 'packages/cloudflare-pages/**' 7 | pull_request: 8 | branches: ['*'] 9 | paths: 10 | - 'packages/cloudflare-pages/**' 11 | 12 | jobs: 13 | ci: 14 | runs-on: ubuntu-latest 15 | defaults: 16 | run: 17 | working-directory: ./packages/cloudflare-pages 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: 20.x 23 | - run: yarn install 24 | - run: yarn build 25 | - run: yarn test 26 | 27 | ci-windows: 28 | runs-on: windows-latest 29 | defaults: 30 | run: 31 | working-directory: ./packages/cloudflare-pages 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: actions/setup-node@v4 35 | with: 36 | node-version: 20.x 37 | - run: yarn install 38 | - run: yarn test 39 | -------------------------------------------------------------------------------- /.github/workflows/ci-dev-server.yml: -------------------------------------------------------------------------------- 1 | name: ci-dev-server 2 | on: 3 | push: 4 | branches: [main] 5 | paths: 6 | - 'packages/dev-server/**' 7 | pull_request: 8 | branches: ['*'] 9 | paths: 10 | - 'packages/dev-server/**' 11 | 12 | jobs: 13 | ci: 14 | runs-on: ubuntu-latest 15 | defaults: 16 | run: 17 | working-directory: ./packages/dev-server 18 | strategy: 19 | matrix: 20 | node: [20, 22] 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions/setup-node@v3 24 | with: 25 | node-version: ${{ matrix.node }} 26 | - run: yarn install 27 | - run: yarn build 28 | - run: npx playwright install --with-deps 29 | - run: yarn test 30 | ci-bun: 31 | runs-on: ubuntu-latest 32 | defaults: 33 | run: 34 | working-directory: ./packages/dev-server 35 | steps: 36 | - uses: actions/checkout@v4 37 | - uses: oven-sh/setup-bun@v1 38 | with: 39 | bun-version: '1.0.25' 40 | - run: bun install 41 | - run: npx playwright install --with-deps 42 | - run: bun run test:e2e:bun 43 | -------------------------------------------------------------------------------- /.github/workflows/ci-ssg.yml: -------------------------------------------------------------------------------- 1 | name: ci-ssg 2 | on: 3 | push: 4 | branches: [main] 5 | paths: 6 | - 'packages/ssg/**' 7 | pull_request: 8 | branches: ['*'] 9 | paths: 10 | - 'packages/ssg/**' 11 | 12 | jobs: 13 | ci: 14 | runs-on: ubuntu-latest 15 | defaults: 16 | run: 17 | working-directory: ./packages/ssg 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: 20.x 23 | - run: yarn install 24 | - run: yarn build 25 | - run: yarn test 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Repo 16 | uses: actions/checkout@v4 17 | 18 | - name: Setup Node.js 20.x 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: 20.x 22 | 23 | - name: Install Dependencies 24 | run: yarn install --frozen-lockfile 25 | 26 | - name: Build 27 | run: yarn build 28 | 29 | - name: Create Release Pull Request or Publish to npm 30 | id: changesets 31 | uses: changesets/action@v1 32 | with: 33 | publish: yarn changeset publish 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | package-lock.json 4 | *.tgz 5 | sandbox 6 | test-results 7 | playwright-report 8 | .hono 9 | 10 | # yarn 11 | .yarn/* 12 | !.yarn/patches 13 | !.yarn/plugins 14 | !.yarn/releases 15 | !.yarn/sdks 16 | !.yarn/versions 17 | yarn-error.log -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "trailingComma": "es5", 4 | "tabWidth": 2, 5 | "semi": false, 6 | "singleQuote": true, 7 | "jsxSingleQuote": true, 8 | "endOfLine": "lf" 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": false, 3 | "eslint.validate": [ 4 | "javascript", 5 | "javascriptreact", 6 | "typescript", 7 | "typescriptreact" 8 | ], 9 | "editor.codeActionsOnSave": { 10 | "source.fixAll.eslint": "explicit" 11 | } 12 | } -------------------------------------------------------------------------------- /.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | //prettier-ignore 3 | module.exports = { 4 | name: "@yarnpkg/plugin-workspace-tools", 5 | factory: function (require) { 6 | var plugin=(()=>{var yr=Object.create;var we=Object.defineProperty;var _r=Object.getOwnPropertyDescriptor;var Er=Object.getOwnPropertyNames;var br=Object.getPrototypeOf,xr=Object.prototype.hasOwnProperty;var W=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(r,t)=>(typeof require<"u"?require:r)[t]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw new Error('Dynamic require of "'+e+'" is not supported')});var q=(e,r)=>()=>(r||e((r={exports:{}}).exports,r),r.exports),Cr=(e,r)=>{for(var t in r)we(e,t,{get:r[t],enumerable:!0})},Je=(e,r,t,n)=>{if(r&&typeof r=="object"||typeof r=="function")for(let s of Er(r))!xr.call(e,s)&&s!==t&&we(e,s,{get:()=>r[s],enumerable:!(n=_r(r,s))||n.enumerable});return e};var Be=(e,r,t)=>(t=e!=null?yr(br(e)):{},Je(r||!e||!e.__esModule?we(t,"default",{value:e,enumerable:!0}):t,e)),wr=e=>Je(we({},"__esModule",{value:!0}),e);var ve=q(ee=>{"use strict";ee.isInteger=e=>typeof e=="number"?Number.isInteger(e):typeof e=="string"&&e.trim()!==""?Number.isInteger(Number(e)):!1;ee.find=(e,r)=>e.nodes.find(t=>t.type===r);ee.exceedsLimit=(e,r,t=1,n)=>n===!1||!ee.isInteger(e)||!ee.isInteger(r)?!1:(Number(r)-Number(e))/Number(t)>=n;ee.escapeNode=(e,r=0,t)=>{let n=e.nodes[r];!n||(t&&n.type===t||n.type==="open"||n.type==="close")&&n.escaped!==!0&&(n.value="\\"+n.value,n.escaped=!0)};ee.encloseBrace=e=>e.type!=="brace"?!1:e.commas>>0+e.ranges>>0===0?(e.invalid=!0,!0):!1;ee.isInvalidBrace=e=>e.type!=="brace"?!1:e.invalid===!0||e.dollar?!0:e.commas>>0+e.ranges>>0===0||e.open!==!0||e.close!==!0?(e.invalid=!0,!0):!1;ee.isOpenOrClose=e=>e.type==="open"||e.type==="close"?!0:e.open===!0||e.close===!0;ee.reduce=e=>e.reduce((r,t)=>(t.type==="text"&&r.push(t.value),t.type==="range"&&(t.type="text"),r),[]);ee.flatten=(...e)=>{let r=[],t=n=>{for(let s=0;s{"use strict";var tt=ve();rt.exports=(e,r={})=>{let t=(n,s={})=>{let i=r.escapeInvalid&&tt.isInvalidBrace(s),a=n.invalid===!0&&r.escapeInvalid===!0,c="";if(n.value)return(i||a)&&tt.isOpenOrClose(n)?"\\"+n.value:n.value;if(n.value)return n.value;if(n.nodes)for(let p of n.nodes)c+=t(p);return c};return t(e)}});var st=q((Vn,nt)=>{"use strict";nt.exports=function(e){return typeof e=="number"?e-e===0:typeof e=="string"&&e.trim()!==""?Number.isFinite?Number.isFinite(+e):isFinite(+e):!1}});var ht=q((Jn,pt)=>{"use strict";var at=st(),le=(e,r,t)=>{if(at(e)===!1)throw new TypeError("toRegexRange: expected the first argument to be a number");if(r===void 0||e===r)return String(e);if(at(r)===!1)throw new TypeError("toRegexRange: expected the second argument to be a number.");let n={relaxZeros:!0,...t};typeof n.strictZeros=="boolean"&&(n.relaxZeros=n.strictZeros===!1);let s=String(n.relaxZeros),i=String(n.shorthand),a=String(n.capture),c=String(n.wrap),p=e+":"+r+"="+s+i+a+c;if(le.cache.hasOwnProperty(p))return le.cache[p].result;let m=Math.min(e,r),h=Math.max(e,r);if(Math.abs(m-h)===1){let y=e+"|"+r;return n.capture?`(${y})`:n.wrap===!1?y:`(?:${y})`}let R=ft(e)||ft(r),f={min:e,max:r,a:m,b:h},$=[],_=[];if(R&&(f.isPadded=R,f.maxLen=String(f.max).length),m<0){let y=h<0?Math.abs(h):1;_=it(y,Math.abs(m),f,n),m=f.a=0}return h>=0&&($=it(m,h,f,n)),f.negatives=_,f.positives=$,f.result=Sr(_,$,n),n.capture===!0?f.result=`(${f.result})`:n.wrap!==!1&&$.length+_.length>1&&(f.result=`(?:${f.result})`),le.cache[p]=f,f.result};function Sr(e,r,t){let n=Pe(e,r,"-",!1,t)||[],s=Pe(r,e,"",!1,t)||[],i=Pe(e,r,"-?",!0,t)||[];return n.concat(i).concat(s).join("|")}function vr(e,r){let t=1,n=1,s=ut(e,t),i=new Set([r]);for(;e<=s&&s<=r;)i.add(s),t+=1,s=ut(e,t);for(s=ct(r+1,n)-1;e1&&c.count.pop(),c.count.push(h.count[0]),c.string=c.pattern+lt(c.count),a=m+1;continue}t.isPadded&&(R=Lr(m,t,n)),h.string=R+h.pattern+lt(h.count),i.push(h),a=m+1,c=h}return i}function Pe(e,r,t,n,s){let i=[];for(let a of e){let{string:c}=a;!n&&!ot(r,"string",c)&&i.push(t+c),n&&ot(r,"string",c)&&i.push(t+c)}return i}function $r(e,r){let t=[];for(let n=0;nr?1:r>e?-1:0}function ot(e,r,t){return e.some(n=>n[r]===t)}function ut(e,r){return Number(String(e).slice(0,-r)+"9".repeat(r))}function ct(e,r){return e-e%Math.pow(10,r)}function lt(e){let[r=0,t=""]=e;return t||r>1?`{${r+(t?","+t:"")}}`:""}function kr(e,r,t){return`[${e}${r-e===1?"":"-"}${r}]`}function ft(e){return/^-?(0+)\d/.test(e)}function Lr(e,r,t){if(!r.isPadded)return e;let n=Math.abs(r.maxLen-String(e).length),s=t.relaxZeros!==!1;switch(n){case 0:return"";case 1:return s?"0?":"0";case 2:return s?"0{0,2}":"00";default:return s?`0{0,${n}}`:`0{${n}}`}}le.cache={};le.clearCache=()=>le.cache={};pt.exports=le});var Ue=q((es,Et)=>{"use strict";var Or=W("util"),At=ht(),dt=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),Nr=e=>r=>e===!0?Number(r):String(r),Me=e=>typeof e=="number"||typeof e=="string"&&e!=="",Ae=e=>Number.isInteger(+e),De=e=>{let r=`${e}`,t=-1;if(r[0]==="-"&&(r=r.slice(1)),r==="0")return!1;for(;r[++t]==="0";);return t>0},Ir=(e,r,t)=>typeof e=="string"||typeof r=="string"?!0:t.stringify===!0,Br=(e,r,t)=>{if(r>0){let n=e[0]==="-"?"-":"";n&&(e=e.slice(1)),e=n+e.padStart(n?r-1:r,"0")}return t===!1?String(e):e},gt=(e,r)=>{let t=e[0]==="-"?"-":"";for(t&&(e=e.slice(1),r--);e.length{e.negatives.sort((a,c)=>ac?1:0),e.positives.sort((a,c)=>ac?1:0);let t=r.capture?"":"?:",n="",s="",i;return e.positives.length&&(n=e.positives.join("|")),e.negatives.length&&(s=`-(${t}${e.negatives.join("|")})`),n&&s?i=`${n}|${s}`:i=n||s,r.wrap?`(${t}${i})`:i},mt=(e,r,t,n)=>{if(t)return At(e,r,{wrap:!1,...n});let s=String.fromCharCode(e);if(e===r)return s;let i=String.fromCharCode(r);return`[${s}-${i}]`},Rt=(e,r,t)=>{if(Array.isArray(e)){let n=t.wrap===!0,s=t.capture?"":"?:";return n?`(${s}${e.join("|")})`:e.join("|")}return At(e,r,t)},yt=(...e)=>new RangeError("Invalid range arguments: "+Or.inspect(...e)),_t=(e,r,t)=>{if(t.strictRanges===!0)throw yt([e,r]);return[]},Mr=(e,r)=>{if(r.strictRanges===!0)throw new TypeError(`Expected step "${e}" to be a number`);return[]},Dr=(e,r,t=1,n={})=>{let s=Number(e),i=Number(r);if(!Number.isInteger(s)||!Number.isInteger(i)){if(n.strictRanges===!0)throw yt([e,r]);return[]}s===0&&(s=0),i===0&&(i=0);let a=s>i,c=String(e),p=String(r),m=String(t);t=Math.max(Math.abs(t),1);let h=De(c)||De(p)||De(m),R=h?Math.max(c.length,p.length,m.length):0,f=h===!1&&Ir(e,r,n)===!1,$=n.transform||Nr(f);if(n.toRegex&&t===1)return mt(gt(e,R),gt(r,R),!0,n);let _={negatives:[],positives:[]},y=T=>_[T<0?"negatives":"positives"].push(Math.abs(T)),E=[],S=0;for(;a?s>=i:s<=i;)n.toRegex===!0&&t>1?y(s):E.push(Br($(s,S),R,f)),s=a?s-t:s+t,S++;return n.toRegex===!0?t>1?Pr(_,n):Rt(E,null,{wrap:!1,...n}):E},Ur=(e,r,t=1,n={})=>{if(!Ae(e)&&e.length>1||!Ae(r)&&r.length>1)return _t(e,r,n);let s=n.transform||(f=>String.fromCharCode(f)),i=`${e}`.charCodeAt(0),a=`${r}`.charCodeAt(0),c=i>a,p=Math.min(i,a),m=Math.max(i,a);if(n.toRegex&&t===1)return mt(p,m,!1,n);let h=[],R=0;for(;c?i>=a:i<=a;)h.push(s(i,R)),i=c?i-t:i+t,R++;return n.toRegex===!0?Rt(h,null,{wrap:!1,options:n}):h},$e=(e,r,t,n={})=>{if(r==null&&Me(e))return[e];if(!Me(e)||!Me(r))return _t(e,r,n);if(typeof t=="function")return $e(e,r,1,{transform:t});if(dt(t))return $e(e,r,0,t);let s={...n};return s.capture===!0&&(s.wrap=!0),t=t||s.step||1,Ae(t)?Ae(e)&&Ae(r)?Dr(e,r,t,s):Ur(e,r,Math.max(Math.abs(t),1),s):t!=null&&!dt(t)?Mr(t,s):$e(e,r,1,t)};Et.exports=$e});var Ct=q((ts,xt)=>{"use strict";var Gr=Ue(),bt=ve(),qr=(e,r={})=>{let t=(n,s={})=>{let i=bt.isInvalidBrace(s),a=n.invalid===!0&&r.escapeInvalid===!0,c=i===!0||a===!0,p=r.escapeInvalid===!0?"\\":"",m="";if(n.isOpen===!0||n.isClose===!0)return p+n.value;if(n.type==="open")return c?p+n.value:"(";if(n.type==="close")return c?p+n.value:")";if(n.type==="comma")return n.prev.type==="comma"?"":c?n.value:"|";if(n.value)return n.value;if(n.nodes&&n.ranges>0){let h=bt.reduce(n.nodes),R=Gr(...h,{...r,wrap:!1,toRegex:!0});if(R.length!==0)return h.length>1&&R.length>1?`(${R})`:R}if(n.nodes)for(let h of n.nodes)m+=t(h,n);return m};return t(e)};xt.exports=qr});var vt=q((rs,St)=>{"use strict";var Kr=Ue(),wt=He(),he=ve(),fe=(e="",r="",t=!1)=>{let n=[];if(e=[].concat(e),r=[].concat(r),!r.length)return e;if(!e.length)return t?he.flatten(r).map(s=>`{${s}}`):r;for(let s of e)if(Array.isArray(s))for(let i of s)n.push(fe(i,r,t));else for(let i of r)t===!0&&typeof i=="string"&&(i=`{${i}}`),n.push(Array.isArray(i)?fe(s,i,t):s+i);return he.flatten(n)},Wr=(e,r={})=>{let t=r.rangeLimit===void 0?1e3:r.rangeLimit,n=(s,i={})=>{s.queue=[];let a=i,c=i.queue;for(;a.type!=="brace"&&a.type!=="root"&&a.parent;)a=a.parent,c=a.queue;if(s.invalid||s.dollar){c.push(fe(c.pop(),wt(s,r)));return}if(s.type==="brace"&&s.invalid!==!0&&s.nodes.length===2){c.push(fe(c.pop(),["{}"]));return}if(s.nodes&&s.ranges>0){let R=he.reduce(s.nodes);if(he.exceedsLimit(...R,r.step,t))throw new RangeError("expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.");let f=Kr(...R,r);f.length===0&&(f=wt(s,r)),c.push(fe(c.pop(),f)),s.nodes=[];return}let p=he.encloseBrace(s),m=s.queue,h=s;for(;h.type!=="brace"&&h.type!=="root"&&h.parent;)h=h.parent,m=h.queue;for(let R=0;R{"use strict";Ht.exports={MAX_LENGTH:1024*64,CHAR_0:"0",CHAR_9:"9",CHAR_UPPERCASE_A:"A",CHAR_LOWERCASE_A:"a",CHAR_UPPERCASE_Z:"Z",CHAR_LOWERCASE_Z:"z",CHAR_LEFT_PARENTHESES:"(",CHAR_RIGHT_PARENTHESES:")",CHAR_ASTERISK:"*",CHAR_AMPERSAND:"&",CHAR_AT:"@",CHAR_BACKSLASH:"\\",CHAR_BACKTICK:"`",CHAR_CARRIAGE_RETURN:"\r",CHAR_CIRCUMFLEX_ACCENT:"^",CHAR_COLON:":",CHAR_COMMA:",",CHAR_DOLLAR:"$",CHAR_DOT:".",CHAR_DOUBLE_QUOTE:'"',CHAR_EQUAL:"=",CHAR_EXCLAMATION_MARK:"!",CHAR_FORM_FEED:"\f",CHAR_FORWARD_SLASH:"/",CHAR_HASH:"#",CHAR_HYPHEN_MINUS:"-",CHAR_LEFT_ANGLE_BRACKET:"<",CHAR_LEFT_CURLY_BRACE:"{",CHAR_LEFT_SQUARE_BRACKET:"[",CHAR_LINE_FEED:` 7 | `,CHAR_NO_BREAK_SPACE:"\xA0",CHAR_PERCENT:"%",CHAR_PLUS:"+",CHAR_QUESTION_MARK:"?",CHAR_RIGHT_ANGLE_BRACKET:">",CHAR_RIGHT_CURLY_BRACE:"}",CHAR_RIGHT_SQUARE_BRACKET:"]",CHAR_SEMICOLON:";",CHAR_SINGLE_QUOTE:"'",CHAR_SPACE:" ",CHAR_TAB:" ",CHAR_UNDERSCORE:"_",CHAR_VERTICAL_LINE:"|",CHAR_ZERO_WIDTH_NOBREAK_SPACE:"\uFEFF"}});var Nt=q((ss,Ot)=>{"use strict";var jr=He(),{MAX_LENGTH:Tt,CHAR_BACKSLASH:Ge,CHAR_BACKTICK:Fr,CHAR_COMMA:Qr,CHAR_DOT:Xr,CHAR_LEFT_PARENTHESES:Zr,CHAR_RIGHT_PARENTHESES:Yr,CHAR_LEFT_CURLY_BRACE:zr,CHAR_RIGHT_CURLY_BRACE:Vr,CHAR_LEFT_SQUARE_BRACKET:kt,CHAR_RIGHT_SQUARE_BRACKET:Lt,CHAR_DOUBLE_QUOTE:Jr,CHAR_SINGLE_QUOTE:en,CHAR_NO_BREAK_SPACE:tn,CHAR_ZERO_WIDTH_NOBREAK_SPACE:rn}=$t(),nn=(e,r={})=>{if(typeof e!="string")throw new TypeError("Expected a string");let t=r||{},n=typeof t.maxLength=="number"?Math.min(Tt,t.maxLength):Tt;if(e.length>n)throw new SyntaxError(`Input length (${e.length}), exceeds max characters (${n})`);let s={type:"root",input:e,nodes:[]},i=[s],a=s,c=s,p=0,m=e.length,h=0,R=0,f,$={},_=()=>e[h++],y=E=>{if(E.type==="text"&&c.type==="dot"&&(c.type="text"),c&&c.type==="text"&&E.type==="text"){c.value+=E.value;return}return a.nodes.push(E),E.parent=a,E.prev=c,c=E,E};for(y({type:"bos"});h0){if(a.ranges>0){a.ranges=0;let E=a.nodes.shift();a.nodes=[E,{type:"text",value:jr(a)}]}y({type:"comma",value:f}),a.commas++;continue}if(f===Xr&&R>0&&a.commas===0){let E=a.nodes;if(R===0||E.length===0){y({type:"text",value:f});continue}if(c.type==="dot"){if(a.range=[],c.value+=f,c.type="range",a.nodes.length!==3&&a.nodes.length!==5){a.invalid=!0,a.ranges=0,c.type="text";continue}a.ranges++,a.args=[];continue}if(c.type==="range"){E.pop();let S=E[E.length-1];S.value+=c.value+f,c=S,a.ranges--;continue}y({type:"dot",value:f});continue}y({type:"text",value:f})}do if(a=i.pop(),a.type!=="root"){a.nodes.forEach(T=>{T.nodes||(T.type==="open"&&(T.isOpen=!0),T.type==="close"&&(T.isClose=!0),T.nodes||(T.type="text"),T.invalid=!0)});let E=i[i.length-1],S=E.nodes.indexOf(a);E.nodes.splice(S,1,...a.nodes)}while(i.length>0);return y({type:"eos"}),s};Ot.exports=nn});var Pt=q((as,Bt)=>{"use strict";var It=He(),sn=Ct(),an=vt(),on=Nt(),Z=(e,r={})=>{let t=[];if(Array.isArray(e))for(let n of e){let s=Z.create(n,r);Array.isArray(s)?t.push(...s):t.push(s)}else t=[].concat(Z.create(e,r));return r&&r.expand===!0&&r.nodupes===!0&&(t=[...new Set(t)]),t};Z.parse=(e,r={})=>on(e,r);Z.stringify=(e,r={})=>It(typeof e=="string"?Z.parse(e,r):e,r);Z.compile=(e,r={})=>(typeof e=="string"&&(e=Z.parse(e,r)),sn(e,r));Z.expand=(e,r={})=>{typeof e=="string"&&(e=Z.parse(e,r));let t=an(e,r);return r.noempty===!0&&(t=t.filter(Boolean)),r.nodupes===!0&&(t=[...new Set(t)]),t};Z.create=(e,r={})=>e===""||e.length<3?[e]:r.expand!==!0?Z.compile(e,r):Z.expand(e,r);Bt.exports=Z});var me=q((is,qt)=>{"use strict";var un=W("path"),se="\\\\/",Mt=`[^${se}]`,ie="\\.",cn="\\+",ln="\\?",Te="\\/",fn="(?=.)",Dt="[^/]",qe=`(?:${Te}|$)`,Ut=`(?:^|${Te})`,Ke=`${ie}{1,2}${qe}`,pn=`(?!${ie})`,hn=`(?!${Ut}${Ke})`,dn=`(?!${ie}{0,1}${qe})`,gn=`(?!${Ke})`,An=`[^.${Te}]`,mn=`${Dt}*?`,Gt={DOT_LITERAL:ie,PLUS_LITERAL:cn,QMARK_LITERAL:ln,SLASH_LITERAL:Te,ONE_CHAR:fn,QMARK:Dt,END_ANCHOR:qe,DOTS_SLASH:Ke,NO_DOT:pn,NO_DOTS:hn,NO_DOT_SLASH:dn,NO_DOTS_SLASH:gn,QMARK_NO_DOT:An,STAR:mn,START_ANCHOR:Ut},Rn={...Gt,SLASH_LITERAL:`[${se}]`,QMARK:Mt,STAR:`${Mt}*?`,DOTS_SLASH:`${ie}{1,2}(?:[${se}]|$)`,NO_DOT:`(?!${ie})`,NO_DOTS:`(?!(?:^|[${se}])${ie}{1,2}(?:[${se}]|$))`,NO_DOT_SLASH:`(?!${ie}{0,1}(?:[${se}]|$))`,NO_DOTS_SLASH:`(?!${ie}{1,2}(?:[${se}]|$))`,QMARK_NO_DOT:`[^.${se}]`,START_ANCHOR:`(?:^|[${se}])`,END_ANCHOR:`(?:[${se}]|$)`},yn={alnum:"a-zA-Z0-9",alpha:"a-zA-Z",ascii:"\\x00-\\x7F",blank:" \\t",cntrl:"\\x00-\\x1F\\x7F",digit:"0-9",graph:"\\x21-\\x7E",lower:"a-z",print:"\\x20-\\x7E ",punct:"\\-!\"#$%&'()\\*+,./:;<=>?@[\\]^_`{|}~",space:" \\t\\r\\n\\v\\f",upper:"A-Z",word:"A-Za-z0-9_",xdigit:"A-Fa-f0-9"};qt.exports={MAX_LENGTH:1024*64,POSIX_REGEX_SOURCE:yn,REGEX_BACKSLASH:/\\(?![*+?^${}(|)[\]])/g,REGEX_NON_SPECIAL_CHARS:/^[^@![\].,$*+?^{}()|\\/]+/,REGEX_SPECIAL_CHARS:/[-*+?.^${}(|)[\]]/,REGEX_SPECIAL_CHARS_BACKREF:/(\\?)((\W)(\3*))/g,REGEX_SPECIAL_CHARS_GLOBAL:/([-*+?.^${}(|)[\]])/g,REGEX_REMOVE_BACKSLASH:/(?:\[.*?[^\\]\]|\\(?=.))/g,REPLACEMENTS:{"***":"*","**/**":"**","**/**/**":"**"},CHAR_0:48,CHAR_9:57,CHAR_UPPERCASE_A:65,CHAR_LOWERCASE_A:97,CHAR_UPPERCASE_Z:90,CHAR_LOWERCASE_Z:122,CHAR_LEFT_PARENTHESES:40,CHAR_RIGHT_PARENTHESES:41,CHAR_ASTERISK:42,CHAR_AMPERSAND:38,CHAR_AT:64,CHAR_BACKWARD_SLASH:92,CHAR_CARRIAGE_RETURN:13,CHAR_CIRCUMFLEX_ACCENT:94,CHAR_COLON:58,CHAR_COMMA:44,CHAR_DOT:46,CHAR_DOUBLE_QUOTE:34,CHAR_EQUAL:61,CHAR_EXCLAMATION_MARK:33,CHAR_FORM_FEED:12,CHAR_FORWARD_SLASH:47,CHAR_GRAVE_ACCENT:96,CHAR_HASH:35,CHAR_HYPHEN_MINUS:45,CHAR_LEFT_ANGLE_BRACKET:60,CHAR_LEFT_CURLY_BRACE:123,CHAR_LEFT_SQUARE_BRACKET:91,CHAR_LINE_FEED:10,CHAR_NO_BREAK_SPACE:160,CHAR_PERCENT:37,CHAR_PLUS:43,CHAR_QUESTION_MARK:63,CHAR_RIGHT_ANGLE_BRACKET:62,CHAR_RIGHT_CURLY_BRACE:125,CHAR_RIGHT_SQUARE_BRACKET:93,CHAR_SEMICOLON:59,CHAR_SINGLE_QUOTE:39,CHAR_SPACE:32,CHAR_TAB:9,CHAR_UNDERSCORE:95,CHAR_VERTICAL_LINE:124,CHAR_ZERO_WIDTH_NOBREAK_SPACE:65279,SEP:un.sep,extglobChars(e){return{"!":{type:"negate",open:"(?:(?!(?:",close:`))${e.STAR})`},"?":{type:"qmark",open:"(?:",close:")?"},"+":{type:"plus",open:"(?:",close:")+"},"*":{type:"star",open:"(?:",close:")*"},"@":{type:"at",open:"(?:",close:")"}}},globChars(e){return e===!0?Rn:Gt}}});var Re=q(Q=>{"use strict";var _n=W("path"),En=process.platform==="win32",{REGEX_BACKSLASH:bn,REGEX_REMOVE_BACKSLASH:xn,REGEX_SPECIAL_CHARS:Cn,REGEX_SPECIAL_CHARS_GLOBAL:wn}=me();Q.isObject=e=>e!==null&&typeof e=="object"&&!Array.isArray(e);Q.hasRegexChars=e=>Cn.test(e);Q.isRegexChar=e=>e.length===1&&Q.hasRegexChars(e);Q.escapeRegex=e=>e.replace(wn,"\\$1");Q.toPosixSlashes=e=>e.replace(bn,"/");Q.removeBackslashes=e=>e.replace(xn,r=>r==="\\"?"":r);Q.supportsLookbehinds=()=>{let e=process.version.slice(1).split(".").map(Number);return e.length===3&&e[0]>=9||e[0]===8&&e[1]>=10};Q.isWindows=e=>e&&typeof e.windows=="boolean"?e.windows:En===!0||_n.sep==="\\";Q.escapeLast=(e,r,t)=>{let n=e.lastIndexOf(r,t);return n===-1?e:e[n-1]==="\\"?Q.escapeLast(e,r,n-1):`${e.slice(0,n)}\\${e.slice(n)}`};Q.removePrefix=(e,r={})=>{let t=e;return t.startsWith("./")&&(t=t.slice(2),r.prefix="./"),t};Q.wrapOutput=(e,r={},t={})=>{let n=t.contains?"":"^",s=t.contains?"":"$",i=`${n}(?:${e})${s}`;return r.negated===!0&&(i=`(?:^(?!${i}).*$)`),i}});var Yt=q((us,Zt)=>{"use strict";var Kt=Re(),{CHAR_ASTERISK:We,CHAR_AT:Sn,CHAR_BACKWARD_SLASH:ye,CHAR_COMMA:vn,CHAR_DOT:je,CHAR_EXCLAMATION_MARK:Fe,CHAR_FORWARD_SLASH:Xt,CHAR_LEFT_CURLY_BRACE:Qe,CHAR_LEFT_PARENTHESES:Xe,CHAR_LEFT_SQUARE_BRACKET:Hn,CHAR_PLUS:$n,CHAR_QUESTION_MARK:Wt,CHAR_RIGHT_CURLY_BRACE:Tn,CHAR_RIGHT_PARENTHESES:jt,CHAR_RIGHT_SQUARE_BRACKET:kn}=me(),Ft=e=>e===Xt||e===ye,Qt=e=>{e.isPrefix!==!0&&(e.depth=e.isGlobstar?1/0:1)},Ln=(e,r)=>{let t=r||{},n=e.length-1,s=t.parts===!0||t.scanToEnd===!0,i=[],a=[],c=[],p=e,m=-1,h=0,R=0,f=!1,$=!1,_=!1,y=!1,E=!1,S=!1,T=!1,L=!1,z=!1,I=!1,re=0,K,g,v={value:"",depth:0,isGlob:!1},k=()=>m>=n,l=()=>p.charCodeAt(m+1),H=()=>(K=g,p.charCodeAt(++m));for(;m0&&(B=p.slice(0,h),p=p.slice(h),R-=h),w&&_===!0&&R>0?(w=p.slice(0,R),o=p.slice(R)):_===!0?(w="",o=p):w=p,w&&w!==""&&w!=="/"&&w!==p&&Ft(w.charCodeAt(w.length-1))&&(w=w.slice(0,-1)),t.unescape===!0&&(o&&(o=Kt.removeBackslashes(o)),w&&T===!0&&(w=Kt.removeBackslashes(w)));let u={prefix:B,input:e,start:h,base:w,glob:o,isBrace:f,isBracket:$,isGlob:_,isExtglob:y,isGlobstar:E,negated:L,negatedExtglob:z};if(t.tokens===!0&&(u.maxDepth=0,Ft(g)||a.push(v),u.tokens=a),t.parts===!0||t.tokens===!0){let P;for(let b=0;b{"use strict";var ke=me(),Y=Re(),{MAX_LENGTH:Le,POSIX_REGEX_SOURCE:On,REGEX_NON_SPECIAL_CHARS:Nn,REGEX_SPECIAL_CHARS_BACKREF:In,REPLACEMENTS:zt}=ke,Bn=(e,r)=>{if(typeof r.expandRange=="function")return r.expandRange(...e,r);e.sort();let t=`[${e.join("-")}]`;try{new RegExp(t)}catch{return e.map(s=>Y.escapeRegex(s)).join("..")}return t},de=(e,r)=>`Missing ${e}: "${r}" - use "\\\\${r}" to match literal characters`,Vt=(e,r)=>{if(typeof e!="string")throw new TypeError("Expected a string");e=zt[e]||e;let t={...r},n=typeof t.maxLength=="number"?Math.min(Le,t.maxLength):Le,s=e.length;if(s>n)throw new SyntaxError(`Input length: ${s}, exceeds maximum allowed length: ${n}`);let i={type:"bos",value:"",output:t.prepend||""},a=[i],c=t.capture?"":"?:",p=Y.isWindows(r),m=ke.globChars(p),h=ke.extglobChars(m),{DOT_LITERAL:R,PLUS_LITERAL:f,SLASH_LITERAL:$,ONE_CHAR:_,DOTS_SLASH:y,NO_DOT:E,NO_DOT_SLASH:S,NO_DOTS_SLASH:T,QMARK:L,QMARK_NO_DOT:z,STAR:I,START_ANCHOR:re}=m,K=A=>`(${c}(?:(?!${re}${A.dot?y:R}).)*?)`,g=t.dot?"":E,v=t.dot?L:z,k=t.bash===!0?K(t):I;t.capture&&(k=`(${k})`),typeof t.noext=="boolean"&&(t.noextglob=t.noext);let l={input:e,index:-1,start:0,dot:t.dot===!0,consumed:"",output:"",prefix:"",backtrack:!1,negated:!1,brackets:0,braces:0,parens:0,quotes:0,globstar:!1,tokens:a};e=Y.removePrefix(e,l),s=e.length;let H=[],w=[],B=[],o=i,u,P=()=>l.index===s-1,b=l.peek=(A=1)=>e[l.index+A],V=l.advance=()=>e[++l.index]||"",J=()=>e.slice(l.index+1),X=(A="",O=0)=>{l.consumed+=A,l.index+=O},Ee=A=>{l.output+=A.output!=null?A.output:A.value,X(A.value)},mr=()=>{let A=1;for(;b()==="!"&&(b(2)!=="("||b(3)==="?");)V(),l.start++,A++;return A%2===0?!1:(l.negated=!0,l.start++,!0)},be=A=>{l[A]++,B.push(A)},oe=A=>{l[A]--,B.pop()},C=A=>{if(o.type==="globstar"){let O=l.braces>0&&(A.type==="comma"||A.type==="brace"),d=A.extglob===!0||H.length&&(A.type==="pipe"||A.type==="paren");A.type!=="slash"&&A.type!=="paren"&&!O&&!d&&(l.output=l.output.slice(0,-o.output.length),o.type="star",o.value="*",o.output=k,l.output+=o.output)}if(H.length&&A.type!=="paren"&&(H[H.length-1].inner+=A.value),(A.value||A.output)&&Ee(A),o&&o.type==="text"&&A.type==="text"){o.value+=A.value,o.output=(o.output||"")+A.value;return}A.prev=o,a.push(A),o=A},xe=(A,O)=>{let d={...h[O],conditions:1,inner:""};d.prev=o,d.parens=l.parens,d.output=l.output;let x=(t.capture?"(":"")+d.open;be("parens"),C({type:A,value:O,output:l.output?"":_}),C({type:"paren",extglob:!0,value:V(),output:x}),H.push(d)},Rr=A=>{let O=A.close+(t.capture?")":""),d;if(A.type==="negate"){let x=k;A.inner&&A.inner.length>1&&A.inner.includes("/")&&(x=K(t)),(x!==k||P()||/^\)+$/.test(J()))&&(O=A.close=`)$))${x}`),A.inner.includes("*")&&(d=J())&&/^\.[^\\/.]+$/.test(d)&&(O=A.close=`)${d})${x})`),A.prev.type==="bos"&&(l.negatedExtglob=!0)}C({type:"paren",extglob:!0,value:u,output:O}),oe("parens")};if(t.fastpaths!==!1&&!/(^[*!]|[/()[\]{}"])/.test(e)){let A=!1,O=e.replace(In,(d,x,M,j,G,Ie)=>j==="\\"?(A=!0,d):j==="?"?x?x+j+(G?L.repeat(G.length):""):Ie===0?v+(G?L.repeat(G.length):""):L.repeat(M.length):j==="."?R.repeat(M.length):j==="*"?x?x+j+(G?k:""):k:x?d:`\\${d}`);return A===!0&&(t.unescape===!0?O=O.replace(/\\/g,""):O=O.replace(/\\+/g,d=>d.length%2===0?"\\\\":d?"\\":"")),O===e&&t.contains===!0?(l.output=e,l):(l.output=Y.wrapOutput(O,l,r),l)}for(;!P();){if(u=V(),u==="\0")continue;if(u==="\\"){let d=b();if(d==="/"&&t.bash!==!0||d==="."||d===";")continue;if(!d){u+="\\",C({type:"text",value:u});continue}let x=/^\\+/.exec(J()),M=0;if(x&&x[0].length>2&&(M=x[0].length,l.index+=M,M%2!==0&&(u+="\\")),t.unescape===!0?u=V():u+=V(),l.brackets===0){C({type:"text",value:u});continue}}if(l.brackets>0&&(u!=="]"||o.value==="["||o.value==="[^")){if(t.posix!==!1&&u===":"){let d=o.value.slice(1);if(d.includes("[")&&(o.posix=!0,d.includes(":"))){let x=o.value.lastIndexOf("["),M=o.value.slice(0,x),j=o.value.slice(x+2),G=On[j];if(G){o.value=M+G,l.backtrack=!0,V(),!i.output&&a.indexOf(o)===1&&(i.output=_);continue}}}(u==="["&&b()!==":"||u==="-"&&b()==="]")&&(u=`\\${u}`),u==="]"&&(o.value==="["||o.value==="[^")&&(u=`\\${u}`),t.posix===!0&&u==="!"&&o.value==="["&&(u="^"),o.value+=u,Ee({value:u});continue}if(l.quotes===1&&u!=='"'){u=Y.escapeRegex(u),o.value+=u,Ee({value:u});continue}if(u==='"'){l.quotes=l.quotes===1?0:1,t.keepQuotes===!0&&C({type:"text",value:u});continue}if(u==="("){be("parens"),C({type:"paren",value:u});continue}if(u===")"){if(l.parens===0&&t.strictBrackets===!0)throw new SyntaxError(de("opening","("));let d=H[H.length-1];if(d&&l.parens===d.parens+1){Rr(H.pop());continue}C({type:"paren",value:u,output:l.parens?")":"\\)"}),oe("parens");continue}if(u==="["){if(t.nobracket===!0||!J().includes("]")){if(t.nobracket!==!0&&t.strictBrackets===!0)throw new SyntaxError(de("closing","]"));u=`\\${u}`}else be("brackets");C({type:"bracket",value:u});continue}if(u==="]"){if(t.nobracket===!0||o&&o.type==="bracket"&&o.value.length===1){C({type:"text",value:u,output:`\\${u}`});continue}if(l.brackets===0){if(t.strictBrackets===!0)throw new SyntaxError(de("opening","["));C({type:"text",value:u,output:`\\${u}`});continue}oe("brackets");let d=o.value.slice(1);if(o.posix!==!0&&d[0]==="^"&&!d.includes("/")&&(u=`/${u}`),o.value+=u,Ee({value:u}),t.literalBrackets===!1||Y.hasRegexChars(d))continue;let x=Y.escapeRegex(o.value);if(l.output=l.output.slice(0,-o.value.length),t.literalBrackets===!0){l.output+=x,o.value=x;continue}o.value=`(${c}${x}|${o.value})`,l.output+=o.value;continue}if(u==="{"&&t.nobrace!==!0){be("braces");let d={type:"brace",value:u,output:"(",outputIndex:l.output.length,tokensIndex:l.tokens.length};w.push(d),C(d);continue}if(u==="}"){let d=w[w.length-1];if(t.nobrace===!0||!d){C({type:"text",value:u,output:u});continue}let x=")";if(d.dots===!0){let M=a.slice(),j=[];for(let G=M.length-1;G>=0&&(a.pop(),M[G].type!=="brace");G--)M[G].type!=="dots"&&j.unshift(M[G].value);x=Bn(j,t),l.backtrack=!0}if(d.comma!==!0&&d.dots!==!0){let M=l.output.slice(0,d.outputIndex),j=l.tokens.slice(d.tokensIndex);d.value=d.output="\\{",u=x="\\}",l.output=M;for(let G of j)l.output+=G.output||G.value}C({type:"brace",value:u,output:x}),oe("braces"),w.pop();continue}if(u==="|"){H.length>0&&H[H.length-1].conditions++,C({type:"text",value:u});continue}if(u===","){let d=u,x=w[w.length-1];x&&B[B.length-1]==="braces"&&(x.comma=!0,d="|"),C({type:"comma",value:u,output:d});continue}if(u==="/"){if(o.type==="dot"&&l.index===l.start+1){l.start=l.index+1,l.consumed="",l.output="",a.pop(),o=i;continue}C({type:"slash",value:u,output:$});continue}if(u==="."){if(l.braces>0&&o.type==="dot"){o.value==="."&&(o.output=R);let d=w[w.length-1];o.type="dots",o.output+=u,o.value+=u,d.dots=!0;continue}if(l.braces+l.parens===0&&o.type!=="bos"&&o.type!=="slash"){C({type:"text",value:u,output:R});continue}C({type:"dot",value:u,output:R});continue}if(u==="?"){if(!(o&&o.value==="(")&&t.noextglob!==!0&&b()==="("&&b(2)!=="?"){xe("qmark",u);continue}if(o&&o.type==="paren"){let x=b(),M=u;if(x==="<"&&!Y.supportsLookbehinds())throw new Error("Node.js v10 or higher is required for regex lookbehinds");(o.value==="("&&!/[!=<:]/.test(x)||x==="<"&&!/<([!=]|\w+>)/.test(J()))&&(M=`\\${u}`),C({type:"text",value:u,output:M});continue}if(t.dot!==!0&&(o.type==="slash"||o.type==="bos")){C({type:"qmark",value:u,output:z});continue}C({type:"qmark",value:u,output:L});continue}if(u==="!"){if(t.noextglob!==!0&&b()==="("&&(b(2)!=="?"||!/[!=<:]/.test(b(3)))){xe("negate",u);continue}if(t.nonegate!==!0&&l.index===0){mr();continue}}if(u==="+"){if(t.noextglob!==!0&&b()==="("&&b(2)!=="?"){xe("plus",u);continue}if(o&&o.value==="("||t.regex===!1){C({type:"plus",value:u,output:f});continue}if(o&&(o.type==="bracket"||o.type==="paren"||o.type==="brace")||l.parens>0){C({type:"plus",value:u});continue}C({type:"plus",value:f});continue}if(u==="@"){if(t.noextglob!==!0&&b()==="("&&b(2)!=="?"){C({type:"at",extglob:!0,value:u,output:""});continue}C({type:"text",value:u});continue}if(u!=="*"){(u==="$"||u==="^")&&(u=`\\${u}`);let d=Nn.exec(J());d&&(u+=d[0],l.index+=d[0].length),C({type:"text",value:u});continue}if(o&&(o.type==="globstar"||o.star===!0)){o.type="star",o.star=!0,o.value+=u,o.output=k,l.backtrack=!0,l.globstar=!0,X(u);continue}let A=J();if(t.noextglob!==!0&&/^\([^?]/.test(A)){xe("star",u);continue}if(o.type==="star"){if(t.noglobstar===!0){X(u);continue}let d=o.prev,x=d.prev,M=d.type==="slash"||d.type==="bos",j=x&&(x.type==="star"||x.type==="globstar");if(t.bash===!0&&(!M||A[0]&&A[0]!=="/")){C({type:"star",value:u,output:""});continue}let G=l.braces>0&&(d.type==="comma"||d.type==="brace"),Ie=H.length&&(d.type==="pipe"||d.type==="paren");if(!M&&d.type!=="paren"&&!G&&!Ie){C({type:"star",value:u,output:""});continue}for(;A.slice(0,3)==="/**";){let Ce=e[l.index+4];if(Ce&&Ce!=="/")break;A=A.slice(3),X("/**",3)}if(d.type==="bos"&&P()){o.type="globstar",o.value+=u,o.output=K(t),l.output=o.output,l.globstar=!0,X(u);continue}if(d.type==="slash"&&d.prev.type!=="bos"&&!j&&P()){l.output=l.output.slice(0,-(d.output+o.output).length),d.output=`(?:${d.output}`,o.type="globstar",o.output=K(t)+(t.strictSlashes?")":"|$)"),o.value+=u,l.globstar=!0,l.output+=d.output+o.output,X(u);continue}if(d.type==="slash"&&d.prev.type!=="bos"&&A[0]==="/"){let Ce=A[1]!==void 0?"|$":"";l.output=l.output.slice(0,-(d.output+o.output).length),d.output=`(?:${d.output}`,o.type="globstar",o.output=`${K(t)}${$}|${$}${Ce})`,o.value+=u,l.output+=d.output+o.output,l.globstar=!0,X(u+V()),C({type:"slash",value:"/",output:""});continue}if(d.type==="bos"&&A[0]==="/"){o.type="globstar",o.value+=u,o.output=`(?:^|${$}|${K(t)}${$})`,l.output=o.output,l.globstar=!0,X(u+V()),C({type:"slash",value:"/",output:""});continue}l.output=l.output.slice(0,-o.output.length),o.type="globstar",o.output=K(t),o.value+=u,l.output+=o.output,l.globstar=!0,X(u);continue}let O={type:"star",value:u,output:k};if(t.bash===!0){O.output=".*?",(o.type==="bos"||o.type==="slash")&&(O.output=g+O.output),C(O);continue}if(o&&(o.type==="bracket"||o.type==="paren")&&t.regex===!0){O.output=u,C(O);continue}(l.index===l.start||o.type==="slash"||o.type==="dot")&&(o.type==="dot"?(l.output+=S,o.output+=S):t.dot===!0?(l.output+=T,o.output+=T):(l.output+=g,o.output+=g),b()!=="*"&&(l.output+=_,o.output+=_)),C(O)}for(;l.brackets>0;){if(t.strictBrackets===!0)throw new SyntaxError(de("closing","]"));l.output=Y.escapeLast(l.output,"["),oe("brackets")}for(;l.parens>0;){if(t.strictBrackets===!0)throw new SyntaxError(de("closing",")"));l.output=Y.escapeLast(l.output,"("),oe("parens")}for(;l.braces>0;){if(t.strictBrackets===!0)throw new SyntaxError(de("closing","}"));l.output=Y.escapeLast(l.output,"{"),oe("braces")}if(t.strictSlashes!==!0&&(o.type==="star"||o.type==="bracket")&&C({type:"maybe_slash",value:"",output:`${$}?`}),l.backtrack===!0){l.output="";for(let A of l.tokens)l.output+=A.output!=null?A.output:A.value,A.suffix&&(l.output+=A.suffix)}return l};Vt.fastpaths=(e,r)=>{let t={...r},n=typeof t.maxLength=="number"?Math.min(Le,t.maxLength):Le,s=e.length;if(s>n)throw new SyntaxError(`Input length: ${s}, exceeds maximum allowed length: ${n}`);e=zt[e]||e;let i=Y.isWindows(r),{DOT_LITERAL:a,SLASH_LITERAL:c,ONE_CHAR:p,DOTS_SLASH:m,NO_DOT:h,NO_DOTS:R,NO_DOTS_SLASH:f,STAR:$,START_ANCHOR:_}=ke.globChars(i),y=t.dot?R:h,E=t.dot?f:h,S=t.capture?"":"?:",T={negated:!1,prefix:""},L=t.bash===!0?".*?":$;t.capture&&(L=`(${L})`);let z=g=>g.noglobstar===!0?L:`(${S}(?:(?!${_}${g.dot?m:a}).)*?)`,I=g=>{switch(g){case"*":return`${y}${p}${L}`;case".*":return`${a}${p}${L}`;case"*.*":return`${y}${L}${a}${p}${L}`;case"*/*":return`${y}${L}${c}${p}${E}${L}`;case"**":return y+z(t);case"**/*":return`(?:${y}${z(t)}${c})?${E}${p}${L}`;case"**/*.*":return`(?:${y}${z(t)}${c})?${E}${L}${a}${p}${L}`;case"**/.*":return`(?:${y}${z(t)}${c})?${a}${p}${L}`;default:{let v=/^(.*?)\.(\w+)$/.exec(g);if(!v)return;let k=I(v[1]);return k?k+a+v[2]:void 0}}},re=Y.removePrefix(e,T),K=I(re);return K&&t.strictSlashes!==!0&&(K+=`${c}?`),K};Jt.exports=Vt});var rr=q((ls,tr)=>{"use strict";var Pn=W("path"),Mn=Yt(),Ze=er(),Ye=Re(),Dn=me(),Un=e=>e&&typeof e=="object"&&!Array.isArray(e),D=(e,r,t=!1)=>{if(Array.isArray(e)){let h=e.map(f=>D(f,r,t));return f=>{for(let $ of h){let _=$(f);if(_)return _}return!1}}let n=Un(e)&&e.tokens&&e.input;if(e===""||typeof e!="string"&&!n)throw new TypeError("Expected pattern to be a non-empty string");let s=r||{},i=Ye.isWindows(r),a=n?D.compileRe(e,r):D.makeRe(e,r,!1,!0),c=a.state;delete a.state;let p=()=>!1;if(s.ignore){let h={...r,ignore:null,onMatch:null,onResult:null};p=D(s.ignore,h,t)}let m=(h,R=!1)=>{let{isMatch:f,match:$,output:_}=D.test(h,a,r,{glob:e,posix:i}),y={glob:e,state:c,regex:a,posix:i,input:h,output:_,match:$,isMatch:f};return typeof s.onResult=="function"&&s.onResult(y),f===!1?(y.isMatch=!1,R?y:!1):p(h)?(typeof s.onIgnore=="function"&&s.onIgnore(y),y.isMatch=!1,R?y:!1):(typeof s.onMatch=="function"&&s.onMatch(y),R?y:!0)};return t&&(m.state=c),m};D.test=(e,r,t,{glob:n,posix:s}={})=>{if(typeof e!="string")throw new TypeError("Expected input to be a string");if(e==="")return{isMatch:!1,output:""};let i=t||{},a=i.format||(s?Ye.toPosixSlashes:null),c=e===n,p=c&&a?a(e):e;return c===!1&&(p=a?a(e):e,c=p===n),(c===!1||i.capture===!0)&&(i.matchBase===!0||i.basename===!0?c=D.matchBase(e,r,t,s):c=r.exec(p)),{isMatch:Boolean(c),match:c,output:p}};D.matchBase=(e,r,t,n=Ye.isWindows(t))=>(r instanceof RegExp?r:D.makeRe(r,t)).test(Pn.basename(e));D.isMatch=(e,r,t)=>D(r,t)(e);D.parse=(e,r)=>Array.isArray(e)?e.map(t=>D.parse(t,r)):Ze(e,{...r,fastpaths:!1});D.scan=(e,r)=>Mn(e,r);D.compileRe=(e,r,t=!1,n=!1)=>{if(t===!0)return e.output;let s=r||{},i=s.contains?"":"^",a=s.contains?"":"$",c=`${i}(?:${e.output})${a}`;e&&e.negated===!0&&(c=`^(?!${c}).*$`);let p=D.toRegex(c,r);return n===!0&&(p.state=e),p};D.makeRe=(e,r={},t=!1,n=!1)=>{if(!e||typeof e!="string")throw new TypeError("Expected a non-empty string");let s={negated:!1,fastpaths:!0};return r.fastpaths!==!1&&(e[0]==="."||e[0]==="*")&&(s.output=Ze.fastpaths(e,r)),s.output||(s=Ze(e,r)),D.compileRe(s,r,t,n)};D.toRegex=(e,r)=>{try{let t=r||{};return new RegExp(e,t.flags||(t.nocase?"i":""))}catch(t){if(r&&r.debug===!0)throw t;return/$^/}};D.constants=Dn;tr.exports=D});var sr=q((fs,nr)=>{"use strict";nr.exports=rr()});var cr=q((ps,ur)=>{"use strict";var ir=W("util"),or=Pt(),ae=sr(),ze=Re(),ar=e=>e===""||e==="./",N=(e,r,t)=>{r=[].concat(r),e=[].concat(e);let n=new Set,s=new Set,i=new Set,a=0,c=h=>{i.add(h.output),t&&t.onResult&&t.onResult(h)};for(let h=0;h!n.has(h));if(t&&m.length===0){if(t.failglob===!0)throw new Error(`No matches found for "${r.join(", ")}"`);if(t.nonull===!0||t.nullglob===!0)return t.unescape?r.map(h=>h.replace(/\\/g,"")):r}return m};N.match=N;N.matcher=(e,r)=>ae(e,r);N.isMatch=(e,r,t)=>ae(r,t)(e);N.any=N.isMatch;N.not=(e,r,t={})=>{r=[].concat(r).map(String);let n=new Set,s=[],a=N(e,r,{...t,onResult:c=>{t.onResult&&t.onResult(c),s.push(c.output)}});for(let c of s)a.includes(c)||n.add(c);return[...n]};N.contains=(e,r,t)=>{if(typeof e!="string")throw new TypeError(`Expected a string: "${ir.inspect(e)}"`);if(Array.isArray(r))return r.some(n=>N.contains(e,n,t));if(typeof r=="string"){if(ar(e)||ar(r))return!1;if(e.includes(r)||e.startsWith("./")&&e.slice(2).includes(r))return!0}return N.isMatch(e,r,{...t,contains:!0})};N.matchKeys=(e,r,t)=>{if(!ze.isObject(e))throw new TypeError("Expected the first argument to be an object");let n=N(Object.keys(e),r,t),s={};for(let i of n)s[i]=e[i];return s};N.some=(e,r,t)=>{let n=[].concat(e);for(let s of[].concat(r)){let i=ae(String(s),t);if(n.some(a=>i(a)))return!0}return!1};N.every=(e,r,t)=>{let n=[].concat(e);for(let s of[].concat(r)){let i=ae(String(s),t);if(!n.every(a=>i(a)))return!1}return!0};N.all=(e,r,t)=>{if(typeof e!="string")throw new TypeError(`Expected a string: "${ir.inspect(e)}"`);return[].concat(r).every(n=>ae(n,t)(e))};N.capture=(e,r,t)=>{let n=ze.isWindows(t),i=ae.makeRe(String(e),{...t,capture:!0}).exec(n?ze.toPosixSlashes(r):r);if(i)return i.slice(1).map(a=>a===void 0?"":a)};N.makeRe=(...e)=>ae.makeRe(...e);N.scan=(...e)=>ae.scan(...e);N.parse=(e,r)=>{let t=[];for(let n of[].concat(e||[]))for(let s of or(String(n),r))t.push(ae.parse(s,r));return t};N.braces=(e,r)=>{if(typeof e!="string")throw new TypeError("Expected a string");return r&&r.nobrace===!0||!/\{.*\}/.test(e)?[e]:or(e,r)};N.braceExpand=(e,r)=>{if(typeof e!="string")throw new TypeError("Expected a string");return N.braces(e,{...r,expand:!0})};ur.exports=N});var fr=q((hs,lr)=>{"use strict";lr.exports=(e,...r)=>new Promise(t=>{t(e(...r))})});var hr=q((ds,Ve)=>{"use strict";var Gn=fr(),pr=e=>{if(e<1)throw new TypeError("Expected `concurrency` to be a number from 1 and up");let r=[],t=0,n=()=>{t--,r.length>0&&r.shift()()},s=(c,p,...m)=>{t++;let h=Gn(c,...m);p(h),h.then(n,n)},i=(c,p,...m)=>{tnew Promise(m=>i(c,m,...p));return Object.defineProperties(a,{activeCount:{get:()=>t},pendingCount:{get:()=>r.length}}),a};Ve.exports=pr;Ve.exports.default=pr});var jn={};Cr(jn,{default:()=>Wn});var Se=W("@yarnpkg/cli"),ne=W("@yarnpkg/core"),et=W("@yarnpkg/core"),ue=W("clipanion"),ce=class extends Se.BaseCommand{constructor(){super(...arguments);this.json=ue.Option.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.production=ue.Option.Boolean("--production",!1,{description:"Only install regular dependencies by omitting dev dependencies"});this.all=ue.Option.Boolean("-A,--all",!1,{description:"Install the entire project"});this.workspaces=ue.Option.Rest()}async execute(){let t=await ne.Configuration.find(this.context.cwd,this.context.plugins),{project:n,workspace:s}=await ne.Project.find(t,this.context.cwd),i=await ne.Cache.find(t);await n.restoreInstallState({restoreResolutions:!1});let a;if(this.all)a=new Set(n.workspaces);else if(this.workspaces.length===0){if(!s)throw new Se.WorkspaceRequiredError(n.cwd,this.context.cwd);a=new Set([s])}else a=new Set(this.workspaces.map(p=>n.getWorkspaceByIdent(et.structUtils.parseIdent(p))));for(let p of a)for(let m of this.production?["dependencies"]:ne.Manifest.hardDependencies)for(let h of p.manifest.getForScope(m).values()){let R=n.tryWorkspaceByDescriptor(h);R!==null&&a.add(R)}for(let p of n.workspaces)a.has(p)?this.production&&p.manifest.devDependencies.clear():(p.manifest.installConfig=p.manifest.installConfig||{},p.manifest.installConfig.selfReferences=!1,p.manifest.dependencies.clear(),p.manifest.devDependencies.clear(),p.manifest.peerDependencies.clear(),p.manifest.scripts.clear());return(await ne.StreamReport.start({configuration:t,json:this.json,stdout:this.context.stdout,includeLogs:!0},async p=>{await n.install({cache:i,report:p,persistProject:!1})})).exitCode()}};ce.paths=[["workspaces","focus"]],ce.usage=ue.Command.Usage({category:"Workspace-related commands",description:"install a single workspace and its dependencies",details:"\n This command will run an install as if the specified workspaces (and all other workspaces they depend on) were the only ones in the project. If no workspaces are explicitly listed, the active one will be assumed.\n\n Note that this command is only very moderately useful when using zero-installs, since the cache will contain all the packages anyway - meaning that the only difference between a full install and a focused install would just be a few extra lines in the `.pnp.cjs` file, at the cost of introducing an extra complexity.\n\n If the `-A,--all` flag is set, the entire project will be installed. Combine with `--production` to replicate the old `yarn install --production`.\n "});var Ne=W("@yarnpkg/cli"),ge=W("@yarnpkg/core"),_e=W("@yarnpkg/core"),F=W("@yarnpkg/core"),gr=W("@yarnpkg/plugin-git"),U=W("clipanion"),Oe=Be(cr()),Ar=Be(hr()),te=Be(W("typanion")),pe=class extends Ne.BaseCommand{constructor(){super(...arguments);this.recursive=U.Option.Boolean("-R,--recursive",!1,{description:"Find packages via dependencies/devDependencies instead of using the workspaces field"});this.from=U.Option.Array("--from",[],{description:"An array of glob pattern idents from which to base any recursion"});this.all=U.Option.Boolean("-A,--all",!1,{description:"Run the command on all workspaces of a project"});this.verbose=U.Option.Boolean("-v,--verbose",!1,{description:"Prefix each output line with the name of the originating workspace"});this.parallel=U.Option.Boolean("-p,--parallel",!1,{description:"Run the commands in parallel"});this.interlaced=U.Option.Boolean("-i,--interlaced",!1,{description:"Print the output of commands in real-time instead of buffering it"});this.jobs=U.Option.String("-j,--jobs",{description:"The maximum number of parallel tasks that the execution will be limited to; or `unlimited`",validator:te.isOneOf([te.isEnum(["unlimited"]),te.applyCascade(te.isNumber(),[te.isInteger(),te.isAtLeast(1)])])});this.topological=U.Option.Boolean("-t,--topological",!1,{description:"Run the command after all workspaces it depends on (regular) have finished"});this.topologicalDev=U.Option.Boolean("--topological-dev",!1,{description:"Run the command after all workspaces it depends on (regular + dev) have finished"});this.include=U.Option.Array("--include",[],{description:"An array of glob pattern idents; only matching workspaces will be traversed"});this.exclude=U.Option.Array("--exclude",[],{description:"An array of glob pattern idents; matching workspaces won't be traversed"});this.publicOnly=U.Option.Boolean("--no-private",{description:"Avoid running the command on private workspaces"});this.since=U.Option.String("--since",{description:"Only include workspaces that have been changed since the specified ref.",tolerateBoolean:!0});this.commandName=U.Option.String();this.args=U.Option.Proxy()}async execute(){let t=await ge.Configuration.find(this.context.cwd,this.context.plugins),{project:n,workspace:s}=await ge.Project.find(t,this.context.cwd);if(!this.all&&!s)throw new Ne.WorkspaceRequiredError(n.cwd,this.context.cwd);await n.restoreInstallState();let i=this.cli.process([this.commandName,...this.args]),a=i.path.length===1&&i.path[0]==="run"&&typeof i.scriptName<"u"?i.scriptName:null;if(i.path.length===0)throw new U.UsageError("Invalid subcommand name for iteration - use the 'run' keyword if you wish to execute a script");let c=this.all?n.topLevelWorkspace:s,p=this.since?Array.from(await gr.gitUtils.fetchChangedWorkspaces({ref:this.since,project:n})):[c,...this.from.length>0?c.getRecursiveWorkspaceChildren():[]],m=g=>Oe.default.isMatch(F.structUtils.stringifyIdent(g.locator),this.from),h=this.from.length>0?p.filter(m):p,R=new Set([...h,...h.map(g=>[...this.recursive?this.since?g.getRecursiveWorkspaceDependents():g.getRecursiveWorkspaceDependencies():g.getRecursiveWorkspaceChildren()]).flat()]),f=[],$=!1;if(a!=null&&a.includes(":")){for(let g of n.workspaces)if(g.manifest.scripts.has(a)&&($=!$,$===!1))break}for(let g of R)a&&!g.manifest.scripts.has(a)&&!$&&!(await ge.scriptUtils.getWorkspaceAccessibleBinaries(g)).has(a)||a===process.env.npm_lifecycle_event&&g.cwd===s.cwd||this.include.length>0&&!Oe.default.isMatch(F.structUtils.stringifyIdent(g.locator),this.include)||this.exclude.length>0&&Oe.default.isMatch(F.structUtils.stringifyIdent(g.locator),this.exclude)||this.publicOnly&&g.manifest.private===!0||f.push(g);let _=this.parallel?this.jobs==="unlimited"?1/0:Number(this.jobs)||Math.ceil(F.nodeUtils.availableParallelism()/2):1,y=_===1?!1:this.parallel,E=y?this.interlaced:!0,S=(0,Ar.default)(_),T=new Map,L=new Set,z=0,I=null,re=!1,K=await _e.StreamReport.start({configuration:t,stdout:this.context.stdout,includePrefix:!1},async g=>{let v=async(k,{commandIndex:l})=>{if(re)return-1;!y&&this.verbose&&l>1&&g.reportSeparator();let H=qn(k,{configuration:t,verbose:this.verbose,commandIndex:l}),[w,B]=dr(g,{prefix:H,interlaced:E}),[o,u]=dr(g,{prefix:H,interlaced:E});try{this.verbose&&g.reportInfo(null,`${H} Process started`);let P=Date.now(),b=await this.cli.run([this.commandName,...this.args],{cwd:k.cwd,stdout:w,stderr:o})||0;w.end(),o.end(),await B,await u;let V=Date.now();if(this.verbose){let J=t.get("enableTimers")?`, completed in ${F.formatUtils.pretty(t,V-P,F.formatUtils.Type.DURATION)}`:"";g.reportInfo(null,`${H} Process exited (exit code ${b})${J}`)}return b===130&&(re=!0,I=b),b}catch(P){throw w.end(),o.end(),await B,await u,P}};for(let k of f)T.set(k.anchoredLocator.locatorHash,k);for(;T.size>0&&!g.hasErrors();){let k=[];for(let[w,B]of T){if(L.has(B.anchoredDescriptor.descriptorHash))continue;let o=!0;if(this.topological||this.topologicalDev){let u=this.topologicalDev?new Map([...B.manifest.dependencies,...B.manifest.devDependencies]):B.manifest.dependencies;for(let P of u.values()){let b=n.tryWorkspaceByDescriptor(P);if(o=b===null||!T.has(b.anchoredLocator.locatorHash),!o)break}}if(!!o&&(L.add(B.anchoredDescriptor.descriptorHash),k.push(S(async()=>{let u=await v(B,{commandIndex:++z});return T.delete(w),L.delete(B.anchoredDescriptor.descriptorHash),u})),!y))break}if(k.length===0){let w=Array.from(T.values()).map(B=>F.structUtils.prettyLocator(t,B.anchoredLocator)).join(", ");g.reportError(_e.MessageName.CYCLIC_DEPENDENCIES,`Dependency cycle detected (${w})`);return}let H=(await Promise.all(k)).find(w=>w!==0);I===null&&(I=typeof H<"u"?1:I),(this.topological||this.topologicalDev)&&typeof H<"u"&&g.reportError(_e.MessageName.UNNAMED,"The command failed for workspaces that are depended upon by other workspaces; can't satisfy the dependency graph")}});return I!==null?I:K.exitCode()}};pe.paths=[["workspaces","foreach"]],pe.usage=U.Command.Usage({category:"Workspace-related commands",description:"run a command on all workspaces",details:"\n This command will run a given sub-command on current and all its descendant workspaces. Various flags can alter the exact behavior of the command:\n\n - If `-p,--parallel` is set, the commands will be ran in parallel; they'll by default be limited to a number of parallel tasks roughly equal to half your core number, but that can be overridden via `-j,--jobs`, or disabled by setting `-j unlimited`.\n\n - If `-p,--parallel` and `-i,--interlaced` are both set, Yarn will print the lines from the output as it receives them. If `-i,--interlaced` wasn't set, it would instead buffer the output from each process and print the resulting buffers only after their source processes have exited.\n\n - If `-t,--topological` is set, Yarn will only run the command after all workspaces that it depends on through the `dependencies` field have successfully finished executing. If `--topological-dev` is set, both the `dependencies` and `devDependencies` fields will be considered when figuring out the wait points.\n\n - If `-A,--all` is set, Yarn will run the command on all the workspaces of a project. By default yarn runs the command only on current and all its descendant workspaces.\n\n - If `-R,--recursive` is set, Yarn will find workspaces to run the command on by recursively evaluating `dependencies` and `devDependencies` fields, instead of looking at the `workspaces` fields.\n\n - If `--from` is set, Yarn will use the packages matching the 'from' glob as the starting point for any recursive search.\n\n - If `--since` is set, Yarn will only run the command on workspaces that have been modified since the specified ref. By default Yarn will use the refs specified by the `changesetBaseRefs` configuration option.\n\n - The command may apply to only some workspaces through the use of `--include` which acts as a whitelist. The `--exclude` flag will do the opposite and will be a list of packages that mustn't execute the script. Both flags accept glob patterns (if valid Idents and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them.\n\n Adding the `-v,--verbose` flag will cause Yarn to print more information; in particular the name of the workspace that generated the output will be printed at the front of each line.\n\n If the command is `run` and the script being run does not exist the child workspace will be skipped without error.\n ",examples:[["Publish current and all descendant packages","yarn workspaces foreach npm publish --tolerate-republish"],["Run build script on current and all descendant packages","yarn workspaces foreach run build"],["Run build script on current and all descendant packages in parallel, building package dependencies first","yarn workspaces foreach -pt run build"],["Run build script on several packages and all their dependencies, building dependencies first","yarn workspaces foreach -ptR --from '{workspace-a,workspace-b}' run build"]]});function dr(e,{prefix:r,interlaced:t}){let n=e.createStreamReporter(r),s=new F.miscUtils.DefaultStream;s.pipe(n,{end:!1}),s.on("finish",()=>{n.end()});let i=new Promise(c=>{n.on("finish",()=>{c(s.active)})});if(t)return[s,i];let a=new F.miscUtils.BufferStream;return a.pipe(s,{end:!1}),a.on("finish",()=>{s.end()}),[a,i]}function qn(e,{configuration:r,commandIndex:t,verbose:n}){if(!n)return null;let i=`[${F.structUtils.stringifyIdent(e.locator)}]:`,a=["#2E86AB","#A23B72","#F18F01","#C73E1D","#CCE2A3"],c=a[t%a.length];return F.formatUtils.pretty(r,i,c)}var Kn={commands:[ce,pe]},Wn=Kn;return wr(jn);})(); 8 | /*! 9 | * fill-range 10 | * 11 | * Copyright (c) 2014-present, Jon Schlinkert. 12 | * Licensed under the MIT License. 13 | */ 14 | /*! 15 | * is-number 16 | * 17 | * Copyright (c) 2014-present, Jon Schlinkert. 18 | * Released under the MIT License. 19 | */ 20 | /*! 21 | * to-regex-range 22 | * 23 | * Copyright (c) 2015-present, Jon Schlinkert. 24 | * Released under the MIT License. 25 | */ 26 | return plugin; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 5 | spec: "@yarnpkg/plugin-workspace-tools" 6 | 7 | yarnPath: .yarn/releases/yarn-3.6.4.cjs 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vite Plugins for Hono 2 | 3 | This is a monorepo managing Vite Plugins for Hono. 4 | 5 | ## Available Plugins 6 | 7 | - [@hono/vite-dev-server](./packages/dev-server/) 8 | - [@hono/vite-build](./packages/build/) 9 | - [@hono/vite-ssg](./packages/ssg/) 10 | 11 | ## How to contribute 12 | 13 | The specific flow is as follows 14 | 15 | 1. Fork this repository. 16 | 2. Write your plugins. 17 | 3. Create the pull request. 18 | 19 | We use [changesets](https://github.com/changesets/changesets) to manage releases and CHANGELOG. 20 | Run the following command at the top level to describe any changes. 21 | 22 | ```sh 23 | yarn changeset 24 | ``` 25 | 26 | ## Authors 27 | 28 | - Yusuke Wada 29 | 30 | ## License 31 | 32 | MIT 33 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import baseConfig from '@hono/eslint-config' 2 | 3 | export default [...baseConfig] 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hono-vite-plugins-monorepo", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "packageManager": "yarn@3.6.4", 7 | "workspaces": [ 8 | "packages/*" 9 | ], 10 | "engines": { 11 | "node": ">=18.14.1" 12 | }, 13 | "scripts": { 14 | "build": "yarn workspaces foreach run build", 15 | "lint": "eslint 'packages/**/*.{ts,tsx}'", 16 | "lint:fix": "eslint --fix 'packages/**/*.{ts,tsx}'", 17 | "format": "prettier --check 'packages/**/*.{ts,tsx}'", 18 | "format:fix": "prettier --write 'packages/**/*.{ts,tsx}'" 19 | }, 20 | "devDependencies": { 21 | "@changesets/changelog-github": "^0.4.8", 22 | "@changesets/cli": "^2.26.2", 23 | "@hono/eslint-config": "^1.0.1", 24 | "eslint": "^9.10.0", 25 | "prettier": "^3.2.4", 26 | "typescript": "^5.2.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/build/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @hono/vite-build 2 | 3 | ## 1.6.2 4 | 5 | ### Patch Changes 6 | 7 | - [#266](https://github.com/honojs/vite-plugins/pull/266) [`7e51cc7f29d0d64dc4bd9c27e0f6c5491ccba5c8`](https://github.com/honojs/vite-plugins/commit/7e51cc7f29d0d64dc4bd9c27e0f6c5491ccba5c8) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: fix the option types of Cloudflare Workers adapter 8 | 9 | ## 1.6.1 10 | 11 | ### Patch Changes 12 | 13 | - [#261](https://github.com/honojs/vite-plugins/pull/261) [`c116701a2423eb9882de6559223304b66305a281`](https://github.com/honojs/vite-plugins/commit/c116701a2423eb9882de6559223304b66305a281) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: correct `CloudflareWorkersBuildOptions` type 14 | 15 | ## 1.6.0 16 | 17 | ### Minor Changes 18 | 19 | - [#254](https://github.com/honojs/vite-plugins/pull/254) [`ee00f8b93a480ab332245c6d661b8001f24028e2`](https://github.com/honojs/vite-plugins/commit/ee00f8b93a480ab332245c6d661b8001f24028e2) Thanks [@yusukebe](https://github.com/yusukebe)! - feat: export `defaultOptions` from Cloudflare Workers adpater 20 | 21 | ## 1.5.0 22 | 23 | ### Minor Changes 24 | 25 | - [#247](https://github.com/honojs/vite-plugins/pull/247) [`e7f58050840cf2f6a1ec446e3e4e89b0b0a85014`](https://github.com/honojs/vite-plugins/commit/e7f58050840cf2f6a1ec446e3e4e89b0b0a85014) Thanks [@toga4](https://github.com/toga4)! - feat(build): support non-fetch handlers for Cloudflare Workers 26 | 27 | ## 1.4.0 28 | 29 | ### Minor Changes 30 | 31 | - [#241](https://github.com/honojs/vite-plugins/pull/241) [`314c66da2b656d4705c4d0636cd1623b643dbd61`](https://github.com/honojs/vite-plugins/commit/314c66da2b656d4705c4d0636cd1623b643dbd61) Thanks [@justblender](https://github.com/justblender)! - Added a new Vercel build adapter. 32 | 33 | This adapter can be imported from `@hono/vite-build/vercel` and will compile 34 | your Hono app to comply with the specification requirements of the Vercel Build Output API. 35 | 36 | Please note that this adapter produces output suitable only for Vercel Serverless Functions. 37 | It does not support the Edge Runtime, which appears to be gradually phased out in favor of Vercel's Fluid compute architecture. 38 | 39 | The default export will have the `@hono/node-server/vercel` adapter applied to it. 40 | 41 | ### Patch Changes 42 | 43 | - [#244](https://github.com/honojs/vite-plugins/pull/244) [`2d8d6d202a106de6049febc524c29ec24f6911b9`](https://github.com/honojs/vite-plugins/commit/2d8d6d202a106de6049febc524c29ec24f6911b9) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: enable `staticPaths` option 44 | 45 | ## 1.3.1 46 | 47 | ### Patch Changes 48 | 49 | - [#242](https://github.com/honojs/vite-plugins/pull/242) [`88ca94493ebb39cafe0d42bb741cce870ef58c68`](https://github.com/honojs/vite-plugins/commit/88ca94493ebb39cafe0d42bb741cce870ef58c68) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: support `fetch` export 50 | 51 | ## 1.3.0 52 | 53 | ### Minor Changes 54 | 55 | - [#218](https://github.com/honojs/vite-plugins/pull/218) [`65e2f768a26d0665aaa05f60abf36bb66a6b6fa9`](https://github.com/honojs/vite-plugins/commit/65e2f768a26d0665aaa05f60abf36bb66a6b6fa9) Thanks [@chadxz](https://github.com/chadxz)! - Added a new Netlify Functions build adapter. 56 | 57 | This adapter can be imported from `@hono/vite-build/netlify-functions` and will 58 | compile your Hono app to comply with the requirements of the Netlify Functions 59 | runtime. 60 | 61 | - The default export will have the `hono/netlify` adapter applied to it. 62 | - A `config` object will be exported, setting the function path to `'/*'` and 63 | `preferStatic` to `true`. 64 | 65 | Please note, this is for the Netlify Functions runtime, not the Netlify Edge 66 | Functions runtime. 67 | 68 | Example: 69 | 70 | ```ts 71 | // vite.config.ts 72 | import { defineConfig } from 'vite' 73 | import devServer from '@hono/vite-dev-server' 74 | import build from '@hono/vite-build/netlify-functions' 75 | 76 | export default defineConfig({ 77 | plugins: [ 78 | devServer({ 79 | entry: './src/index.ts', 80 | }), 81 | build({ 82 | entry: './src/index.ts', 83 | output: 'functions/server/index.js', 84 | }), 85 | ], 86 | }) 87 | ``` 88 | 89 | If you also have a `public/publish` directory for your assets that should be 90 | published to the corresponding Netlify site, then after running a build, you 91 | would end up with a directory structure like: 92 | 93 | ``` 94 | dist/ 95 | functions/ 96 | server/ 97 | index.js 98 | publish/ 99 | robots.txt 100 | .... 101 | ``` 102 | 103 | then you can use a netlify.toml that looks like: 104 | 105 | ```toml 106 | # https://ntl.fyi/file-based-build-config 107 | [build] 108 | command = "vite build" 109 | functions = "dist/functions" 110 | publish = "dist/publish" 111 | ``` 112 | 113 | ## 1.2.1 114 | 115 | ### Patch Changes 116 | 117 | - [#204](https://github.com/honojs/vite-plugins/pull/204) [`adcdd9ad7a3c7ef6a828dfa1210ba5d08eadc576`](https://github.com/honojs/vite-plugins/commit/adcdd9ad7a3c7ef6a828dfa1210ba5d08eadc576) Thanks [@jonz94](https://github.com/jonz94)! - fix(build): remove `console.log` 118 | 119 | ## 1.2.0 120 | 121 | ### Minor Changes 122 | 123 | - [#198](https://github.com/honojs/vite-plugins/pull/198) [`f08c6586018c0da828158ec252be4d889f8c32e8`](https://github.com/honojs/vite-plugins/commit/f08c6586018c0da828158ec252be4d889f8c32e8) Thanks [@yusukebe](https://github.com/yusukebe)! - feat: Node.js adapter supports `port` option 124 | 125 | ## 1.1.1 126 | 127 | ### Patch Changes 128 | 129 | - [#196](https://github.com/honojs/vite-plugins/pull/196) [`ead8c3255f2d7fb68084b8d30c3fbe9fcaabb3ec`](https://github.com/honojs/vite-plugins/commit/ead8c3255f2d7fb68084b8d30c3fbe9fcaabb3ec) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: support latest `hono` 130 | 131 | ## 1.1.0 132 | 133 | ### Minor Changes 134 | 135 | - [#181](https://github.com/honojs/vite-plugins/pull/181) [`fc15f718c0172f84748f8717f53abba40470baed`](https://github.com/honojs/vite-plugins/commit/fc15f718c0172f84748f8717f53abba40470baed) Thanks [@nakasyou](https://github.com/nakasyou)! - Added Deno adapter 136 | 137 | ## 1.0.0 138 | 139 | ### Major Changes 140 | 141 | - [#177](https://github.com/honojs/vite-plugins/pull/177) [`1ceb95757f1151e9f08cebd992447fb67b470957`](https://github.com/honojs/vite-plugins/commit/1ceb95757f1151e9f08cebd992447fb67b470957) Thanks [@yusukebe](https://github.com/yusukebe)! - Initial release 142 | -------------------------------------------------------------------------------- /packages/build/README.md: -------------------------------------------------------------------------------- 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 | - `@hono/vite-build/netlify-functions` 13 | 14 | ## Usage 15 | 16 | ### Install 17 | 18 | You can install `vite` and `@hono/vite-build` via the npm. 19 | 20 | ```bash 21 | npm i -D vite @hono/vite-build 22 | ``` 23 | 24 | Or you can install them with Bun. 25 | 26 | ```bash 27 | bun add -D vite @hono/vite-build 28 | ``` 29 | 30 | ### Settings 31 | 32 | Add `"type": "module"` to your package.json. Then, create `vite.config.ts` and edit it. The following is for Bun. 33 | 34 | ```ts 35 | import { defineConfig } from 'vite' 36 | import build from '@hono/vite-build/bun' 37 | // import build from '@hono/vite-build/cloudflare-pages' 38 | // import build from '@hono/vite-build/cloudflare-workers' 39 | // import build from '@hono/vite-build/node' 40 | // import build from '@hono/vite-build/netlify-functions' 41 | // import build from '@hono/vite-build/vercel' 42 | 43 | export default defineConfig({ 44 | plugins: [ 45 | build({ 46 | // Defaults are `src/index.ts`,`./src/index.tsx`,`./app/server.ts` 47 | entry: './src/index.tsx', 48 | // port option is only for Node.js adapter. Default is 3000 49 | port: 3001, 50 | }), 51 | ], 52 | }) 53 | ``` 54 | 55 | ### Build 56 | 57 | Just run `vite build`. 58 | 59 | ```bash 60 | npm exec vite build 61 | ``` 62 | 63 | Or 64 | 65 | ```bash 66 | bunx --bun vite build 67 | ``` 68 | 69 | ### Run 70 | 71 | Run with the command on your runtime. For examples: 72 | 73 | Cloudflare Pages: 74 | 75 | ```bash 76 | wrangler pages dev ./dist 77 | ``` 78 | 79 | Bun: 80 | 81 | ```bash 82 | cd ./dist 83 | bun run ./index.js 84 | ``` 85 | 86 | Node.js: 87 | 88 | ```bash 89 | cd ./dist 90 | node ./index.js 91 | ``` 92 | 93 | ## Common Options 94 | 95 | ```ts 96 | type BuildOptions = { 97 | entry?: string | string[] 98 | output?: string 99 | outputDir?: string 100 | external?: string[] 101 | minify?: boolean 102 | emptyOutDir?: boolean 103 | } 104 | ``` 105 | 106 | Default values: 107 | 108 | ```ts 109 | export const defaultOptions = { 110 | entry: ['src/index.ts', './src/index.tsx', './app/server.ts'], 111 | output: 'index.js', 112 | outputDir: './dist', 113 | external: [], 114 | minify: true, 115 | emptyOutDir: false, 116 | staticPaths: [], 117 | } 118 | ``` 119 | 120 | ## Platform specific things 121 | 122 | ### Cloudflare Pages 123 | 124 | 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. 125 | 126 | ## Example project 127 | 128 | `src/index.tsx`: 129 | 130 | ```tsx 131 | import { Hono } from 'hono' 132 | 133 | const app = new Hono() 134 | 135 | app.get('/', (c) => { 136 | return c.html( 137 | 138 | 139 | 140 | 141 | 142 |

Hello!

143 | 144 | 145 | ) 146 | }) 147 | 148 | export default app 149 | ``` 150 | 151 | `public/static/style.css`: 152 | 153 | ```css 154 | h1 { 155 | font-family: Arial, Helvetica, sans-serif; 156 | } 157 | ``` 158 | 159 | The project with those file will be built to the following files with `@hono/vite-build/bun`: 160 | 161 | ```txt 162 | dist 163 | ├── index.js 164 | └── static 165 | └── style.css 166 | ``` 167 | 168 | ## Build a client 169 | 170 | If you also want to build a client-side script, you can configure it as follows. 171 | 172 | ```ts 173 | export default defineConfig(({ mode }) => { 174 | if (mode === 'client') { 175 | return { 176 | build: { 177 | rollupOptions: { 178 | input: './src/client.ts', 179 | output: { 180 | dir: './dist/static', 181 | entryFileNames: 'client.js', 182 | }, 183 | }, 184 | copyPublicDir: false, 185 | }, 186 | } 187 | } else { 188 | return { 189 | plugins: [build()], 190 | } 191 | } 192 | }) 193 | ``` 194 | 195 | The build command: 196 | 197 | ```bash 198 | vite build --mode client && vite build 199 | ``` 200 | 201 | `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. 202 | 203 | ```tsx 204 | app.get('/', (c) => { 205 | return c.html( 206 | 207 | 208 | {import.meta.env.PROD ? ( 209 | 210 | ) : ( 211 | 212 | )} 213 | 214 | Hello! 215 | 216 | ) 217 | }) 218 | ``` 219 | 220 | ## Example 221 | 222 | You can see the example project here - [hono-vite-jsx](https://github.com/honojs/examples/tree/main/hono-vite-jsx) 223 | 224 | ## Authors 225 | 226 | - Yusuke Wada 227 | 228 | ## License 229 | 230 | MIT 231 | -------------------------------------------------------------------------------- /packages/build/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hono/vite-build", 3 | "description": "Vite plugin to build your Hono app", 4 | "version": "1.6.2", 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 | "./netlify-functions": { 40 | "types": "./dist/adapter/netlify-functions/index.d.ts", 41 | "import": "./dist/adapter/netlify-functions/index.js" 42 | }, 43 | "./deno": { 44 | "types": "./dist/adapter/deno/index.d.ts", 45 | "import": "./dist/adapter/deno/index.js" 46 | }, 47 | "./vercel": { 48 | "types": "./dist/adapter/vercel/index.d.ts", 49 | "import": "./dist/adapter/vercel/index.js" 50 | } 51 | }, 52 | "typesVersions": { 53 | "*": { 54 | "types": [ 55 | "./dist/types" 56 | ], 57 | "bun": [ 58 | "./dist/adapter/bun/index.d.ts" 59 | ], 60 | "node": [ 61 | "./dist/adapter/node/index.d.ts" 62 | ], 63 | "cloudflare-pages": [ 64 | "./dist/adapter/cloudflare-pages/index.d.ts" 65 | ], 66 | "cloudflare-workers": [ 67 | "./dist/adapter/cloudflare-workers/index.d.ts" 68 | ], 69 | "netlify-functions": [ 70 | "./dist/adapter/netlify-functions/index.d.ts" 71 | ], 72 | "deno": [ 73 | "./dist/adapter/deno/index.d.ts" 74 | ], 75 | "vercel": [ 76 | "./dist/adapter/vercel/index.d.ts" 77 | ] 78 | } 79 | }, 80 | "author": "Yusuke Wada (https://github.com/yusukebe)", 81 | "license": "MIT", 82 | "repository": { 83 | "type": "git", 84 | "url": "https://github.com/honojs/vite-plugins.git" 85 | }, 86 | "publishConfig": { 87 | "registry": "https://registry.npmjs.org", 88 | "access": "public" 89 | }, 90 | "homepage": "https://github.com/honojs/vite-plugins", 91 | "devDependencies": { 92 | "glob": "^10.3.10", 93 | "hono": "^4.6.12", 94 | "publint": "^0.1.12", 95 | "rimraf": "^5.0.1", 96 | "tsup": "^7.2.0", 97 | "vite": "^6.1.1", 98 | "vitest": "^2.1.1" 99 | }, 100 | "peerDependencies": { 101 | "hono": "*" 102 | }, 103 | "engines": { 104 | "node": ">=18.14.1" 105 | } 106 | } -------------------------------------------------------------------------------- /packages/build/src/adapter/bun/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'vite' 2 | import type { BuildOptions } from '../../base.js' 3 | import buildPlugin from '../../base.js' 4 | import { serveStaticHook } from '../../entry/serve-static.js' 5 | 6 | export type BunBuildOptions = { 7 | staticRoot?: string | undefined 8 | } & BuildOptions 9 | 10 | const bunBuildPlugin = (pluginOptions?: BunBuildOptions): Plugin => { 11 | return { 12 | ...buildPlugin({ 13 | ...{ 14 | entryContentBeforeHooks: [ 15 | async (appName, options) => { 16 | // eslint-disable-next-line quotes 17 | let code = "import { serveStatic } from 'hono/bun'\n" 18 | code += serveStaticHook(appName, { 19 | filePaths: options?.staticPaths, 20 | root: pluginOptions?.staticRoot, 21 | }) 22 | return code 23 | }, 24 | ], 25 | }, 26 | ...pluginOptions, 27 | }), 28 | name: '@hono/vite-build/bun', 29 | } 30 | } 31 | 32 | export default bunBuildPlugin 33 | -------------------------------------------------------------------------------- /packages/build/src/adapter/cloudflare-pages/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin, ResolvedConfig } from 'vite' 2 | import { readdir, writeFile } from 'node:fs/promises' 3 | import { resolve } from 'node:path' 4 | import type { BuildOptions } from '../../base.js' 5 | import buildPlugin, { defaultOptions } from '../../base.js' 6 | 7 | export type CloudflarePagesBuildOptions = BuildOptions 8 | 9 | const WORKER_JS_NAME = '_worker.js' 10 | const ROUTES_JSON_NAME = '_routes.json' 11 | 12 | type StaticRoutes = { version: number; include: string[]; exclude: string[] } 13 | 14 | const cloudflarePagesBuildPlugin = (pluginOptions?: CloudflarePagesBuildOptions): Plugin => { 15 | let config: ResolvedConfig 16 | const staticPaths: string[] = [] 17 | 18 | return { 19 | ...buildPlugin({ 20 | ...pluginOptions, 21 | output: WORKER_JS_NAME, 22 | }), 23 | configResolved: async (resolvedConfig) => { 24 | config = resolvedConfig 25 | }, 26 | writeBundle: async () => { 27 | const paths = await readdir(resolve(config.root, config.build.outDir), { 28 | withFileTypes: true, 29 | }) 30 | // If _routes.json already exists, don't create it 31 | if (paths.some((p) => p.name === ROUTES_JSON_NAME)) { 32 | return 33 | } else { 34 | paths.forEach((p) => { 35 | if (p.isDirectory()) { 36 | staticPaths.push(`/${p.name}/*`) 37 | } else { 38 | if (p.name === WORKER_JS_NAME) { 39 | return 40 | } 41 | staticPaths.push(`/${p.name}`) 42 | } 43 | }) 44 | const staticRoutes: StaticRoutes = { 45 | version: 1, 46 | include: ['/*'], 47 | exclude: staticPaths, 48 | } 49 | const path = resolve( 50 | config.root, 51 | pluginOptions?.outputDir ?? defaultOptions.outputDir, 52 | '_routes.json' 53 | ) 54 | await writeFile(path, JSON.stringify(staticRoutes)) 55 | } 56 | }, 57 | name: '@hono/vite-build/cloudflare-pages', 58 | } 59 | } 60 | 61 | export default cloudflarePagesBuildPlugin 62 | -------------------------------------------------------------------------------- /packages/build/src/adapter/cloudflare-workers/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'vite' 2 | import type { BuildOptions } from '../../base.js' 3 | import buildPlugin, { defaultOptions as baseDefaultOptions } from '../../base.js' 4 | import type { GetEntryContentOptions } from '../../entry/index.js' 5 | 6 | export type CloudflareWorkersBuildOptions = BuildOptions & 7 | Pick 8 | 9 | export const defaultOptions: CloudflareWorkersBuildOptions = { 10 | ...baseDefaultOptions, 11 | 12 | entryContentAfterHooks: [ 13 | () => ` 14 | const merged = {} 15 | const definedHandlers = new Set() 16 | for (const [file, app] of Object.entries(modules)) { 17 | for (const [key, handler] of Object.entries(app)) { 18 | if (key !== 'fetch') { 19 | if (definedHandlers.has(key)) { 20 | throw new Error(\`Handler "\${key}" is defined in multiple entry files. Please ensure each handler (except fetch) is defined only once.\`); 21 | } 22 | definedHandlers.add(key) 23 | merged[key] = handler 24 | } 25 | } 26 | } 27 | `, 28 | ], 29 | entryContentDefaultExportHook: (appName) => 30 | `export default { ...merged, fetch: ${appName}.fetch }`, 31 | } 32 | 33 | const cloudflareWorkersBuildPlugin = (pluginOptions?: CloudflareWorkersBuildOptions): Plugin => { 34 | return { 35 | ...buildPlugin({ 36 | ...pluginOptions, 37 | entryContentAfterHooks: 38 | pluginOptions?.entryContentAfterHooks ?? defaultOptions.entryContentAfterHooks, 39 | entryContentDefaultExportHook: 40 | pluginOptions?.entryContentDefaultExportHook ?? 41 | defaultOptions.entryContentDefaultExportHook, 42 | }), 43 | name: '@hono/vite-build/cloudflare-workers', 44 | } 45 | } 46 | 47 | export default cloudflareWorkersBuildPlugin 48 | -------------------------------------------------------------------------------- /packages/build/src/adapter/deno/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'vite' 2 | import type { BuildOptions } from '../../base.js' 3 | import buildPlugin from '../../base.js' 4 | import { serveStaticHook } from '../../entry/serve-static.js' 5 | 6 | export type DenoBuildOptions = { 7 | staticRoot?: string | undefined 8 | } & BuildOptions 9 | 10 | const denoBuildPlugin = (pluginOptions?: DenoBuildOptions): Plugin => { 11 | return { 12 | ...buildPlugin({ 13 | ...{ 14 | entryContentBeforeHooks: [ 15 | async (appName, options) => { 16 | // eslint-disable-next-line quotes 17 | let code = "import { serveStatic } from 'hono/deno'\n" 18 | code += serveStaticHook(appName, { 19 | filePaths: options?.staticPaths, 20 | root: pluginOptions?.staticRoot, 21 | }) 22 | return code 23 | }, 24 | ], 25 | }, 26 | ...pluginOptions, 27 | }), 28 | name: '@hono/vite-build/deno', 29 | } 30 | } 31 | 32 | export default denoBuildPlugin 33 | -------------------------------------------------------------------------------- /packages/build/src/adapter/netlify-functions/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'vite' 2 | import type { BuildOptions } from '../../base.js' 3 | import buildPlugin from '../../base.js' 4 | 5 | export type NetlifyFunctionsBuildOptions = BuildOptions 6 | 7 | export default function netlifyFunctionsBuildPlugin( 8 | pluginOptions?: NetlifyFunctionsBuildOptions 9 | ): Plugin { 10 | return { 11 | ...buildPlugin({ 12 | ...{ 13 | entryContentBeforeHooks: [() => 'import { handle } from "hono/netlify"'], 14 | entryContentAfterHooks: [() => 'export const config = { path: "/*", preferStatic: true }'], 15 | entryContentDefaultExportHook: (appName) => `export default handle(${appName})`, 16 | }, 17 | ...pluginOptions, 18 | }), 19 | name: '@hono/vite-build/netlify-functions', 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/build/src/adapter/node/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'vite' 2 | import type { BuildOptions } from '../../base.js' 3 | import buildPlugin from '../../base.js' 4 | import { serveStaticHook } from '../../entry/serve-static.js' 5 | 6 | export type NodeBuildOptions = { 7 | staticRoot?: string | undefined 8 | port?: number | undefined 9 | } & BuildOptions 10 | 11 | const nodeBuildPlugin = (pluginOptions?: NodeBuildOptions): Plugin => { 12 | const port = pluginOptions?.port ?? 3000 13 | return { 14 | ...buildPlugin({ 15 | ...{ 16 | entryContentBeforeHooks: [ 17 | async (appName, options) => { 18 | // eslint-disable-next-line quotes 19 | let code = "import { serveStatic } from '@hono/node-server/serve-static'\n" 20 | code += serveStaticHook(appName, { 21 | filePaths: options?.staticPaths, 22 | root: pluginOptions?.staticRoot, 23 | }) 24 | return code 25 | }, 26 | ], 27 | entryContentAfterHooks: [ 28 | async (appName) => { 29 | // eslint-disable-next-line quotes 30 | let code = "import { serve } from '@hono/node-server'\n" 31 | code += `serve({ fetch: ${appName}.fetch, port: ${port.toString()} })` 32 | return code 33 | }, 34 | ], 35 | }, 36 | ...pluginOptions, 37 | }), 38 | name: '@hono/vite-build/node', 39 | } 40 | } 41 | 42 | export default nodeBuildPlugin 43 | -------------------------------------------------------------------------------- /packages/build/src/adapter/vercel/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin, ResolvedConfig } from 'vite' 2 | import { existsSync, mkdirSync } from 'node:fs' 3 | import { cp, writeFile } from 'node:fs/promises' 4 | import { resolve } from 'node:path' 5 | import type { BuildOptions } from '../../base.js' 6 | import buildPlugin from '../../base.js' 7 | import type { VercelBuildConfigV3, VercelServerlessFunctionConfig } from './types.js' 8 | 9 | export type VercelBuildOptions = { 10 | vercel?: { 11 | config?: VercelBuildConfigV3 12 | function?: VercelServerlessFunctionConfig 13 | } 14 | } & Omit 15 | 16 | const BUNDLE_NAME = 'index.js' 17 | const FUNCTION_NAME = '__hono' 18 | 19 | const writeJSON = (path: string, data: Record) => { 20 | const dir = resolve(path, '..') 21 | if (!existsSync(dir)) { 22 | mkdirSync(dir, { recursive: true }) 23 | } 24 | return writeFile(path, JSON.stringify(data)) 25 | } 26 | 27 | const getRuntimeVersion = () => { 28 | try { 29 | const systemNodeVersion = process.versions.node.split('.')[0] 30 | return `nodejs${Number(systemNodeVersion)}.x` as const 31 | } catch { 32 | return 'nodejs22.x' as const 33 | } 34 | } 35 | 36 | const vercelBuildPlugin = (pluginOptions?: VercelBuildOptions): Plugin => { 37 | let config: ResolvedConfig 38 | 39 | return { 40 | ...buildPlugin({ 41 | output: `functions/${FUNCTION_NAME}.func/${BUNDLE_NAME}`, 42 | outputDir: '.vercel/output', 43 | ...{ 44 | entryContentAfterHooks: [ 45 | // eslint-disable-next-line quotes 46 | () => "import { handle } from '@hono/node-server/vercel'", 47 | ], 48 | entryContentDefaultExportHook: (appName) => `export default handle(${appName})`, 49 | }, 50 | ...pluginOptions, 51 | }), 52 | configResolved: (resolvedConfig) => { 53 | config = resolvedConfig 54 | }, 55 | writeBundle: async () => { 56 | const outputDir = resolve(config.root, config.build.outDir) 57 | const functionDir = resolve(outputDir, 'functions', `${FUNCTION_NAME}.func`) 58 | 59 | const buildConfig: VercelBuildConfigV3 = { 60 | ...pluginOptions?.vercel?.config, 61 | version: 3, 62 | routes: [ 63 | ...(pluginOptions?.vercel?.config?.routes ?? []), 64 | { 65 | handle: 'filesystem', 66 | }, 67 | { 68 | src: '/(.*)', 69 | dest: `/${FUNCTION_NAME}`, 70 | }, 71 | ], 72 | } 73 | 74 | const functionConfig: VercelServerlessFunctionConfig = { 75 | ...pluginOptions?.vercel?.function, 76 | runtime: getRuntimeVersion(), 77 | launcherType: 'Nodejs', 78 | handler: BUNDLE_NAME, 79 | shouldAddHelpers: true, 80 | shouldAddSourcemapSupport: Boolean(config.build.sourcemap), 81 | supportsResponseStreaming: true, 82 | } 83 | 84 | await Promise.all([ 85 | // Copy static files to the .vercel/output/static directory 86 | cp(resolve(config.root, config.publicDir), resolve(outputDir, 'static'), { 87 | recursive: true, 88 | }), 89 | // Write the all necessary config files 90 | writeJSON(resolve(outputDir, 'config.json'), buildConfig), 91 | writeJSON(resolve(functionDir, '.vc-config.json'), functionConfig), 92 | writeJSON(resolve(functionDir, 'package.json'), { 93 | type: 'module', 94 | }), 95 | ]) 96 | }, 97 | name: '@hono/vite-build/vercel', 98 | } 99 | } 100 | 101 | export default vercelBuildPlugin 102 | -------------------------------------------------------------------------------- /packages/build/src/adapter/vercel/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Main configuration type for Vercel Build Output API v3. 3 | * This type represents the root configuration object that should be output in the `.vercel/output/config.json` file. 4 | * @see https://vercel.com/docs/build-output-api/configuration 5 | */ 6 | export type VercelBuildConfigV3 = { 7 | /** Version identifier for the Build Output API. Must be 3. */ 8 | version?: 3 9 | /** 10 | * Array of routing rules to handle incoming requests. 11 | * Routes are evaluated in order, where the first matching route will be applied. 12 | */ 13 | routes?: Route[] 14 | /** 15 | * Configuration for Vercel's Image Optimization feature. 16 | * Defines how images should be optimized, cached, and served. 17 | * @see https://vercel.com/docs/build-output-api/configuration#images 18 | */ 19 | images?: ImagesConfig 20 | /** 21 | * Custom domain wildcard configurations for internationalization. 22 | * Maps domain names to values that can be referenced by the routes configuration. 23 | * @see https://vercel.com/docs/build-output-api/configuration#wildcard 24 | */ 25 | wildcard?: WildCard[] 26 | /** 27 | * File-specific overrides for static files in the `.vercel/output/static` directory. 28 | * Allows overriding Content-Type headers and URL paths for static files. 29 | * @see https://vercel.com/docs/build-output-api/configuration#overrides 30 | */ 31 | overrides?: Record 32 | /** 33 | * Array of file paths or glob patterns to be cached between builds. 34 | * Only relevant when Vercel is building from source code. 35 | * @see https://vercel.com/docs/build-output-api/configuration#cache 36 | */ 37 | cache?: string[] 38 | /** 39 | * Scheduled tasks configuration for production deployments. 40 | * Defines API routes that should be invoked on a schedule. 41 | * @see https://vercel.com/docs/build-output-api/configuration#crons 42 | */ 43 | crons?: Cron[] 44 | } 45 | 46 | /** 47 | * Route configuration that can either be a Source route or a Handler route. 48 | * Source routes match incoming requests, while Handler routes define special behaviors. 49 | */ 50 | type Route = Source | Handler 51 | 52 | /** 53 | * Source route configuration for matching and handling incoming requests. 54 | * Provides detailed control over request matching and response handling. 55 | */ 56 | type Source = { 57 | /** Regular expression pattern to match incoming request paths */ 58 | src: string 59 | /** Path to rewrite or redirect the matched request to */ 60 | dest?: string 61 | /** Custom HTTP headers to add to the response */ 62 | headers?: Record 63 | /** Array of HTTP methods this route should match */ 64 | methods?: string[] 65 | /** When true, matching will continue even after this route matches */ 66 | continue?: boolean 67 | /** When true, the src pattern will be matched case-sensitively */ 68 | caseSensitive?: boolean 69 | /** Additional validation flag for route matching */ 70 | check?: boolean 71 | /** HTTP status code to return (e.g., 308 for redirects) */ 72 | status?: number 73 | /** Conditions that must be present in the request for the route to match */ 74 | has?: Array 75 | /** Conditions that must be absent from the request for the route to match */ 76 | missing?: Array 77 | /** Configuration for locale-based routing and redirects */ 78 | locale?: Locale 79 | /** Raw source patterns used by middleware */ 80 | middlewareRawSrc?: string[] 81 | /** Path to the middleware implementation file */ 82 | middlewarePath?: string 83 | } 84 | 85 | /** 86 | * Locale configuration for internationalization routing. 87 | * Used to configure language-specific redirects and preferences. 88 | */ 89 | type Locale = { 90 | /** Mapping of locale codes to their redirect destinations */ 91 | redirect?: Record 92 | /** Name of the cookie used to store the user's locale preference */ 93 | cookie?: string 94 | } 95 | 96 | /** 97 | * Host-based condition for route matching. 98 | * Used to match requests based on the Host header. 99 | */ 100 | type HostHasField = { 101 | /** Identifies this as a host matching condition */ 102 | type: 'host' 103 | /** Pattern to match against the Host header */ 104 | value: string 105 | } 106 | 107 | /** 108 | * Header-based condition for route matching. 109 | * Used to match requests based on HTTP headers. 110 | */ 111 | type HeaderHasField = { 112 | /** Identifies this as a header matching condition */ 113 | type: 'header' 114 | /** Name of the header to match */ 115 | key: string 116 | /** Optional value the header should match */ 117 | value?: string 118 | } 119 | 120 | /** 121 | * Cookie-based condition for route matching. 122 | * Used to match requests based on cookie values. 123 | */ 124 | type CookieHasField = { 125 | /** Identifies this as a cookie matching condition */ 126 | type: 'cookie' 127 | /** Name of the cookie to match */ 128 | key: string 129 | /** Optional value the cookie should match */ 130 | value?: string 131 | } 132 | 133 | /** 134 | * Query parameter condition for route matching. 135 | * Used to match requests based on query string parameters. 136 | */ 137 | type QueryHasField = { 138 | /** Identifies this as a query parameter matching condition */ 139 | type: 'query' 140 | /** Name of the query parameter to match */ 141 | key: string 142 | /** Optional value the query parameter should match */ 143 | value?: string 144 | } 145 | 146 | /** 147 | * Special handler phases for request processing. 148 | * Defines when and how requests should be processed in the routing pipeline. 149 | */ 150 | type HandleValue = 151 | | 'rewrite' // Rewrites the request URL 152 | | 'filesystem' // Checks for matches after filesystem misses 153 | | 'resource' // Handles the request as a static resource 154 | | 'miss' // Processes after any filesystem miss 155 | | 'hit' // Handles successful cache hits 156 | | 'error' // Processes after errors (500, 404, etc.) 157 | 158 | /** 159 | * Handler route configuration for special request processing phases. 160 | * Used to define behavior at specific points in the request lifecycle. 161 | */ 162 | type Handler = { 163 | /** The type of handler to process the request */ 164 | handle: HandleValue 165 | /** Optional pattern to match against the request path */ 166 | src?: string 167 | /** Optional path to handle the request with */ 168 | dest?: string 169 | /** HTTP status code to return in the response */ 170 | status?: number 171 | } 172 | 173 | /** 174 | * Supported image formats for the Image Optimization API. 175 | * @see https://vercel.com/docs/build-output-api/configuration#images 176 | */ 177 | type ImageFormat = 'image/avif' | 'image/webp' 178 | 179 | /** 180 | * Configuration for remote image sources in Image Optimization. 181 | * Defines patterns for matching and processing external images. 182 | */ 183 | type RemotePattern = { 184 | /** Protocol allowed for remote images (http or https) */ 185 | protocol?: 'http' | 'https' 186 | /** Hostname pattern that remote images must match */ 187 | hostname: string 188 | /** Optional port number for remote image URLs */ 189 | port?: string 190 | /** Path pattern that remote image URLs must match */ 191 | pathname?: string 192 | /** Search query pattern that remote image URLs must match */ 193 | search?: string 194 | } 195 | 196 | /** 197 | * Configuration for local image patterns in Image Optimization. 198 | * Defines patterns for matching and processing local images. 199 | */ 200 | type LocalPattern = { 201 | /** Path pattern that local images must match */ 202 | pathname?: string 203 | /** Search query pattern that local image URLs must match */ 204 | search?: string 205 | } 206 | 207 | /** 208 | * Configuration for Vercel's Image Optimization feature. 209 | * @see https://vercel.com/docs/build-output-api/configuration#images 210 | */ 211 | type ImagesConfig = { 212 | /** Array of allowed image widths for resizing */ 213 | sizes: number[] 214 | /** Array of allowed domains for remote images */ 215 | domains: string[] 216 | /** Patterns for matching remote image sources */ 217 | remotePatterns?: RemotePattern[] 218 | /** Patterns for matching local image sources */ 219 | localPatterns?: LocalPattern[] 220 | /** Array of allowed quality values for image optimization */ 221 | qualities?: number[] 222 | /** Minimum time (in seconds) to cache optimized images */ 223 | minimumCacheTTL?: number 224 | /** Array of supported output formats for optimization */ 225 | formats?: ImageFormat[] 226 | /** Whether to allow processing of SVG images (use with caution) */ 227 | dangerouslyAllowSVG?: boolean 228 | /** Content Security Policy for optimized images */ 229 | contentSecurityPolicy?: string 230 | /** Content-Disposition header type for image responses */ 231 | contentDispositionType?: string 232 | } 233 | 234 | /** 235 | * Configuration for custom domain wildcards. 236 | * Used for internationalization and dynamic routing based on domains. 237 | * @see https://vercel.com/docs/build-output-api/configuration#wildcard 238 | */ 239 | type WildCard = { 240 | /** Domain name to match for this wildcard configuration */ 241 | domain: string 242 | /** Value to use when this wildcard matches (available as $wildcard in routes) */ 243 | value: string 244 | } 245 | 246 | /** 247 | * Configuration for path or content-type overrides of static files. 248 | * @see https://vercel.com/docs/build-output-api/configuration#overrides 249 | */ 250 | type Override = { 251 | /** URL path where the static file will be accessible */ 252 | path?: string 253 | /** Content-Type header value for the static file */ 254 | contentType?: string 255 | } 256 | 257 | /** 258 | * Configuration for scheduled tasks (Cron Jobs). 259 | * @see https://vercel.com/docs/build-output-api/configuration#crons 260 | */ 261 | type Cron = { 262 | /** Path to the API route that handles the cron job */ 263 | path: string 264 | /** Cron schedule expression (e.g., "0 0 * * *" for daily at midnight) */ 265 | schedule: string 266 | } 267 | 268 | /** 269 | * Configuration for a serverless function in Vercel. 270 | * This type **partially** represents the configuration object that should be output in the `.vercel/output/functions//config.json` file. 271 | * @see https://vercel.com/docs/build-output-api/primitives#serverless-function-configuration 272 | */ 273 | export type VercelServerlessFunctionConfig = { 274 | /** Indicates the initial file where code will be executed for the Serverless Function. */ 275 | handler?: string 276 | /** Specifies which "launcher" will be used to execute the Serverless Function */ 277 | launcherType?: 'Nodejs' 278 | /** Specifies which "runtime" will be used to execute the Serverless Function, only Node.js is supported currently */ 279 | runtime?: `nodejs${number}.x` 280 | /** The amount of memory allocated to the function in MB */ 281 | memory?: number 282 | /** The maximum duration of the function in seconds */ 283 | maxDuration?: number 284 | /** The regions the function is available in */ 285 | regions?: string[] 286 | /** Whether the function supports response streaming */ 287 | supportsResponseStreaming?: boolean 288 | /** Enables request and response helpers methods */ 289 | shouldAddHelpers?: boolean 290 | /** Enables source map generation */ 291 | shouldAddSourcemapSupport?: boolean 292 | } 293 | -------------------------------------------------------------------------------- /packages/build/src/base.ts: -------------------------------------------------------------------------------- 1 | import type { ConfigEnv, Plugin, ResolvedConfig, UserConfig } from 'vite' 2 | import { builtinModules } from 'module' 3 | import { readdirSync } from 'node:fs' 4 | import { resolve } from 'node:path' 5 | import { getEntryContent } from './entry/index.js' 6 | import type { GetEntryContentOptions } from './entry/index.js' 7 | 8 | export type BuildOptions = { 9 | /** 10 | * @default ['src/index.ts', './src/index.tsx', './app/server.ts'] 11 | */ 12 | entry?: string | string[] 13 | /** 14 | * @default './dist' 15 | */ 16 | output?: string 17 | outputDir?: string 18 | external?: string[] 19 | /** 20 | * @default true 21 | */ 22 | minify?: boolean 23 | emptyOutDir?: boolean 24 | apply?: ((this: void, config: UserConfig, env: ConfigEnv) => boolean) | undefined 25 | } & Omit 26 | 27 | export const defaultOptions: Required< 28 | Omit< 29 | BuildOptions, 30 | 'entryContentAfterHooks' | 'entryContentBeforeHooks' | 'entryContentDefaultExportHook' 31 | > 32 | > = { 33 | entry: ['src/index.ts', './src/index.tsx', './app/server.ts'], 34 | output: 'index.js', 35 | outputDir: './dist', 36 | external: [], 37 | minify: true, 38 | emptyOutDir: false, 39 | apply: (_config, { command, mode }) => { 40 | if (command === 'build' && mode !== 'client') { 41 | return true 42 | } 43 | return false 44 | }, 45 | staticPaths: [], 46 | } 47 | 48 | const buildPlugin = (options: BuildOptions): Plugin => { 49 | const virtualEntryId = 'virtual:build-entry-module' 50 | const resolvedVirtualEntryId = '\0' + virtualEntryId 51 | let config: ResolvedConfig 52 | const output = options.output ?? defaultOptions.output 53 | 54 | return { 55 | name: '@hono/vite-build', 56 | configResolved: async (resolvedConfig) => { 57 | config = resolvedConfig 58 | }, 59 | resolveId(id) { 60 | if (id === virtualEntryId) { 61 | return resolvedVirtualEntryId 62 | } 63 | }, 64 | async load(id) { 65 | if (id === resolvedVirtualEntryId) { 66 | const staticPaths: string[] = options.staticPaths ?? [] 67 | const direntPaths = [] 68 | try { 69 | const publicDirPaths = readdirSync(resolve(config.root, config.publicDir), { 70 | withFileTypes: true, 71 | }) 72 | direntPaths.push(...publicDirPaths) 73 | const buildOutDirPaths = readdirSync(resolve(config.root, config.build.outDir), { 74 | withFileTypes: true, 75 | }) 76 | direntPaths.push(...buildOutDirPaths) 77 | } catch {} 78 | 79 | const uniqueStaticPaths = new Set() 80 | 81 | direntPaths.forEach((p) => { 82 | if (p.isDirectory()) { 83 | uniqueStaticPaths.add(`/${p.name}/*`) 84 | } else { 85 | if (p.name === output) { 86 | return 87 | } 88 | uniqueStaticPaths.add(`/${p.name}`) 89 | } 90 | }) 91 | 92 | staticPaths.push(...Array.from(uniqueStaticPaths)) 93 | 94 | const entry = options.entry ?? defaultOptions.entry 95 | return await getEntryContent({ 96 | entry: Array.isArray(entry) ? entry : [entry], 97 | entryContentBeforeHooks: options.entryContentBeforeHooks, 98 | entryContentAfterHooks: options.entryContentAfterHooks, 99 | entryContentDefaultExportHook: options.entryContentDefaultExportHook, 100 | staticPaths, 101 | }) 102 | } 103 | }, 104 | apply: options?.apply ?? defaultOptions.apply, 105 | config: async (): Promise => { 106 | return { 107 | ssr: { 108 | external: options?.external ?? defaultOptions.external, 109 | noExternal: true, 110 | target: 'webworker', 111 | }, 112 | build: { 113 | outDir: options?.outputDir ?? defaultOptions.outputDir, 114 | emptyOutDir: options?.emptyOutDir ?? defaultOptions.emptyOutDir, 115 | minify: options?.minify ?? defaultOptions.minify, 116 | ssr: true, 117 | rollupOptions: { 118 | external: [...builtinModules, /^node:/], 119 | input: virtualEntryId, 120 | output: { 121 | entryFileNames: output, 122 | }, 123 | }, 124 | }, 125 | } 126 | }, 127 | } 128 | } 129 | 130 | export default buildPlugin 131 | -------------------------------------------------------------------------------- /packages/build/src/entry/index.ts: -------------------------------------------------------------------------------- 1 | import { normalize } from 'node:path' 2 | 3 | export type EntryContentHookOptions = { 4 | staticPaths: string[] 5 | } 6 | 7 | export type EntryContentHook = ( 8 | appName: string, 9 | options?: EntryContentHookOptions 10 | ) => string | Promise 11 | 12 | export type GetEntryContentOptions = { 13 | entry: string[] 14 | entryContentBeforeHooks?: EntryContentHook[] 15 | entryContentAfterHooks?: EntryContentHook[] 16 | /** 17 | * Explicitly specify the default export for the app. Make sure your export 18 | * incorporates the app passed as the `appName` argument. 19 | * 20 | * @default `export default ${appName}` 21 | */ 22 | entryContentDefaultExportHook?: EntryContentHook 23 | staticPaths?: string[] 24 | } 25 | 26 | const normalizePaths = (paths: string[]) => { 27 | return paths.map((p) => { 28 | let normalizedPath = normalize(p).replace(/\\/g, '/') 29 | if (normalizedPath.startsWith('./')) { 30 | normalizedPath = normalizedPath.substring(2) 31 | } 32 | return '/' + normalizedPath 33 | }) 34 | } 35 | 36 | export const getEntryContent = async (options: GetEntryContentOptions) => { 37 | const staticPaths = options.staticPaths ?? [''] 38 | const globStr = normalizePaths(options.entry) 39 | .map((e) => `'${e}'`) 40 | .join(',') 41 | 42 | const hooksToString = async (appName: string, hooks?: EntryContentHook[]) => { 43 | if (hooks) { 44 | const str = ( 45 | await Promise.all( 46 | hooks.map((hook) => { 47 | return hook(appName, { 48 | staticPaths, 49 | }) 50 | }) 51 | ) 52 | ).join('\n') 53 | return str 54 | } 55 | return '' 56 | } 57 | 58 | const appStr = `const modules = import.meta.glob([${globStr}], { import: 'default', eager: true }) 59 | let added = false 60 | for (const [, app] of Object.entries(modules)) { 61 | if (app) { 62 | mainApp.all('*', (c) => { 63 | let executionCtx 64 | try { 65 | executionCtx = c.executionCtx 66 | } catch {} 67 | return app.fetch(c.req.raw, c.env, executionCtx) 68 | }) 69 | mainApp.notFound((c) => { 70 | let executionCtx 71 | try { 72 | executionCtx = c.executionCtx 73 | } catch {} 74 | return app.fetch(c.req.raw, c.env, executionCtx) 75 | }) 76 | added = true 77 | } 78 | } 79 | if (!added) { 80 | throw new Error("Can't import modules from [${globStr}]") 81 | }` 82 | 83 | const defaultExportHook = 84 | options.entryContentDefaultExportHook ?? (() => 'export default mainApp') 85 | 86 | return `import { Hono } from 'hono' 87 | const mainApp = new Hono() 88 | 89 | ${await hooksToString('mainApp', options.entryContentBeforeHooks)} 90 | 91 | ${appStr} 92 | 93 | ${await hooksToString('mainApp', options.entryContentAfterHooks)} 94 | 95 | ${await hooksToString('mainApp', [defaultExportHook])}` 96 | } 97 | -------------------------------------------------------------------------------- /packages/build/src/entry/serve-static.ts: -------------------------------------------------------------------------------- 1 | type ServeStaticHookOptions = { 2 | filePaths?: string[] 3 | root?: string 4 | } 5 | 6 | export const serveStaticHook = (appName: string, options: ServeStaticHookOptions) => { 7 | let code = '' 8 | for (const path of options.filePaths ?? []) { 9 | code += `${appName}.use('${path}', serveStatic({ root: '${options.root ?? './'}' }))\n` 10 | } 11 | return code 12 | } 13 | -------------------------------------------------------------------------------- /packages/build/src/index.ts: -------------------------------------------------------------------------------- 1 | import basePlugin, { defaultOptions } from './base.js' 2 | export { defaultOptions } 3 | 4 | export default basePlugin 5 | -------------------------------------------------------------------------------- /packages/build/test/.gitignore: -------------------------------------------------------------------------------- 1 | mocks/app-static-files/customDir -------------------------------------------------------------------------------- /packages/build/test/adapter.test.ts: -------------------------------------------------------------------------------- 1 | import { build } from 'vite' 2 | import { existsSync, readFileSync, rmSync } from 'node:fs' 3 | import bunBuildPlugin from '../src/adapter/bun' 4 | import cloudflarePagesPlugin from '../src/adapter/cloudflare-pages' 5 | import cloudflareWorkersPlugin from '../src/adapter/cloudflare-workers' 6 | import denoBuildPlugin from '../src/adapter/deno' 7 | import netlifyFunctionsPlugin from '../src/adapter/netlify-functions' 8 | import nodeBuildPlugin from '../src/adapter/node' 9 | import vercelBuildPlugin from '../src/adapter/vercel' 10 | 11 | describe('Build Plugin with Bun Adapter', () => { 12 | const testDir = './test/mocks/app-static-files' 13 | const entry = './src/server.ts' 14 | 15 | afterEach(() => { 16 | rmSync(`${testDir}/dist`, { recursive: true, force: true }) 17 | }) 18 | 19 | it('Should build the project correctly with the plugin', async () => { 20 | const outputFile = `${testDir}/dist/index.js` 21 | 22 | await build({ 23 | root: testDir, 24 | plugins: [ 25 | bunBuildPlugin({ 26 | entry, 27 | minify: false, 28 | staticPaths: ['/static/*'], 29 | }), 30 | ], 31 | }) 32 | 33 | expect(existsSync(outputFile)).toBe(true) 34 | 35 | const output = readFileSync(outputFile, 'utf-8') 36 | expect(output).toContain('Hello World') 37 | expect(output).toContain('use("/foo.txt"') 38 | expect(output).toContain('use("/js/*"') 39 | expect(output).toContain('use("/static/*", serveStatic({ root: "./" }))') 40 | 41 | const outputFooTxt = readFileSync(`${testDir}/dist/foo.txt`, 'utf-8') 42 | expect(outputFooTxt).toContain('foo') 43 | 44 | const outputJsClientJs = readFileSync(`${testDir}/dist/js/client.js`, 'utf-8') 45 | // eslint-disable-next-line quotes 46 | expect(outputJsClientJs).toContain("console.log('foo')") 47 | }) 48 | }) 49 | 50 | describe('Build Plugin with Netlify Functions Adapter', () => { 51 | const testDir = './test/mocks/app-static-files' 52 | const entry = './src/server.ts' 53 | 54 | afterEach(() => { 55 | rmSync(`${testDir}/dist`, { recursive: true, force: true }) 56 | }) 57 | 58 | it('Should build the project correctly with the plugin', async () => { 59 | const outputFile = `${testDir}/dist/index.js` 60 | 61 | await build({ 62 | root: testDir, 63 | plugins: [ 64 | netlifyFunctionsPlugin({ 65 | entry, 66 | minify: false, 67 | }), 68 | ], 69 | }) 70 | 71 | expect(existsSync(outputFile)).toBe(true) 72 | 73 | const output = readFileSync(outputFile, 'utf-8') 74 | expect(output).toContain('Hello World') 75 | expect(output).toContain('{ path: "/*", preferStatic: true }') 76 | expect(output).toContain('handle(mainApp)') 77 | 78 | const outputFooTxt = readFileSync(`${testDir}/dist/foo.txt`, 'utf-8') 79 | expect(outputFooTxt).toContain('foo') 80 | 81 | const outputJsClientJs = readFileSync(`${testDir}/dist/js/client.js`, 'utf-8') 82 | // eslint-disable-next-line quotes 83 | expect(outputJsClientJs).toContain("console.log('foo')") 84 | }) 85 | }) 86 | 87 | describe('Build Plugin with Cloudflare Pages Adapter', () => { 88 | const testDir = './test/mocks/app-static-files' 89 | 90 | afterEach(() => { 91 | rmSync(`${testDir}/dist`, { recursive: true, force: true }) 92 | }) 93 | 94 | it('Should build the project correctly with the plugin', async () => { 95 | const outputFile = `${testDir}/dist/_worker.js` 96 | const routesFile = `${testDir}/dist/_routes.json` 97 | 98 | await build({ 99 | root: testDir, 100 | plugins: [ 101 | cloudflarePagesPlugin({ 102 | entry: 'src/server.ts', 103 | }), 104 | ], 105 | }) 106 | 107 | expect(existsSync(outputFile)).toBe(true) 108 | expect(existsSync(routesFile)).toBe(true) 109 | 110 | const output = readFileSync(outputFile, 'utf-8') 111 | expect(output).toContain('Hello World') 112 | 113 | const routes = readFileSync(routesFile, 'utf-8') 114 | expect(routes).toContain('{"version":1,"include":["/*"],"exclude":["/foo.txt","/js/*"]}') 115 | }) 116 | 117 | it('Should build the project correctly with custom output directory', async () => { 118 | const outputFile = `${testDir}/customDir/_worker.js` 119 | const routesFile = `${testDir}/customDir/_routes.json` 120 | 121 | await build({ 122 | root: testDir, 123 | plugins: [ 124 | cloudflarePagesPlugin({ 125 | outputDir: 'customDir', 126 | entry: 'src/server.ts', 127 | }), 128 | ], 129 | build: { 130 | emptyOutDir: true, 131 | }, 132 | }) 133 | 134 | expect(existsSync(outputFile)).toBe(true) 135 | expect(existsSync(routesFile)).toBe(true) 136 | 137 | const output = readFileSync(outputFile, 'utf-8') 138 | expect(output).toContain('Hello World') 139 | 140 | const routes = readFileSync(routesFile, 'utf-8') 141 | expect(routes).toContain('{"version":1,"include":["/*"],"exclude":["/foo.txt","/js/*"]}') 142 | }) 143 | 144 | it('Should not create a new _routes.json when _routes.json on output directory.', async () => { 145 | const outputFile = `${testDir}/dist/_worker.js` 146 | const routesFile = `${testDir}/dist/_routes.json` 147 | 148 | await build({ 149 | publicDir: 'public-routes-json', 150 | root: testDir, 151 | plugins: [ 152 | cloudflarePagesPlugin({ 153 | entry: 'src/server.ts', 154 | }), 155 | ], 156 | }) 157 | 158 | expect(existsSync(outputFile)).toBe(true) 159 | expect(existsSync(routesFile)).toBe(true) 160 | 161 | const output = readFileSync(outputFile, 'utf-8') 162 | expect(output).toContain('Hello World') 163 | 164 | const routes = readFileSync(routesFile, 'utf-8') 165 | expect(routes).toContain('{"version":1,"include":["/"],"exclude":["/customRoute"]}') 166 | }) 167 | }) 168 | 169 | describe('Build Plugin with Cloudflare Workers Adapter with single entry file', () => { 170 | const testDir = './test/mocks/app-static-files' 171 | const outputFile = `${testDir}/dist/index.js` 172 | 173 | afterAll(() => { 174 | rmSync(`${testDir}/dist`, { recursive: true, force: true }) 175 | }) 176 | 177 | it('Should build the project correctly with the plugin', async () => { 178 | await build({ 179 | root: testDir, 180 | plugins: [ 181 | cloudflareWorkersPlugin({ 182 | entry: './src/server-fetch-with-handlers.ts', 183 | }), 184 | ], 185 | }) 186 | 187 | expect(existsSync(outputFile)).toBe(true) 188 | 189 | const output = readFileSync(outputFile, 'utf-8') 190 | expect(output).toContain('Hello World') 191 | }) 192 | 193 | it('Should return correct result from the scheduled handler in the output file', async () => { 194 | const module = await import(outputFile) 195 | const app = module.default 196 | 197 | const result = app.scheduled() 198 | expect(result).toBe('Hello World') 199 | }) 200 | }) 201 | 202 | describe('Build Plugin with Cloudflare Workers Adapter with multiple entry files', () => { 203 | const testDir = './test/mocks/app-static-files' 204 | const outputFile = 'index-multiple.js' 205 | const outputPath = `${testDir}/dist/${outputFile}` 206 | 207 | afterAll(() => { 208 | rmSync(`${testDir}/dist`, { recursive: true, force: true }) 209 | }) 210 | 211 | it('Should build the project correctly with the plugin', async () => { 212 | await build({ 213 | root: testDir, 214 | plugins: [ 215 | cloudflareWorkersPlugin({ 216 | entry: ['./src/server-fetch-with-handlers.ts', './src/server-fetch-with-handlers2.ts'], 217 | output: outputFile, 218 | }), 219 | ], 220 | }) 221 | expect(existsSync(outputPath)).toBe(true) 222 | 223 | const output = readFileSync(outputPath, 'utf-8') 224 | expect(output).toContain('Hello World') 225 | }) 226 | 227 | it('Should cause a runtime error when the same handler is registered more than once', async () => { 228 | expect(import(outputPath)).rejects.toThrow(/scheduled/) 229 | }) 230 | }) 231 | 232 | describe('Build Plugin with Deno Adapter', () => { 233 | const testDir = './test/mocks/app-static-files' 234 | const entry = './src/server.ts' 235 | 236 | afterEach(() => { 237 | rmSync(`${testDir}/dist`, { recursive: true, force: true }) 238 | }) 239 | 240 | it('Should build the project correctly with the plugin', async () => { 241 | const outputFile = `${testDir}/dist/index.js` 242 | 243 | await build({ 244 | root: testDir, 245 | plugins: [ 246 | denoBuildPlugin({ 247 | entry, 248 | minify: false, 249 | staticPaths: ['/static/*'], 250 | }), 251 | ], 252 | }) 253 | 254 | expect(existsSync(outputFile)).toBe(true) 255 | 256 | const output = readFileSync(outputFile, 'utf-8') 257 | expect(output).toContain('Hello World') 258 | expect(output).toContain('use("/foo.txt"') 259 | expect(output).toContain('use("/js/*"') 260 | expect(output).toContain('use("/static/*", serveStatic({ root: "./" }))') 261 | 262 | const outputFooTxt = readFileSync(`${testDir}/dist/foo.txt`, 'utf-8') 263 | expect(outputFooTxt).toContain('foo') 264 | 265 | const outputJsClientJs = readFileSync(`${testDir}/dist/js/client.js`, 'utf-8') 266 | // eslint-disable-next-line quotes 267 | expect(outputJsClientJs).toContain("console.log('foo')") 268 | }) 269 | }) 270 | 271 | describe('Build Plugin with Node.js Adapter', () => { 272 | const testDir = './test/mocks/app-static-files' 273 | const entry = './src/server.ts' 274 | 275 | afterEach(() => { 276 | rmSync(`${testDir}/dist`, { recursive: true, force: true }) 277 | }) 278 | 279 | it('Should build the project correctly with the plugin', async () => { 280 | const outputFile = `${testDir}/dist/index.js` 281 | 282 | await build({ 283 | root: testDir, 284 | plugins: [ 285 | nodeBuildPlugin({ 286 | entry, 287 | minify: false, 288 | staticPaths: ['/static/*'], 289 | port: 3001, 290 | }), 291 | ], 292 | }) 293 | 294 | expect(existsSync(outputFile)).toBe(true) 295 | 296 | const output = readFileSync(outputFile, 'utf-8') 297 | expect(output).toContain('Hello World') 298 | expect(output).toContain('use("/foo.txt"') 299 | expect(output).toContain('use("/js/*"') 300 | expect(output).toContain('use("/static/*", serveStatic({ root: "./" }))') 301 | expect(output).toContain('serve({ fetch: mainApp.fetch, port: 3001 })') 302 | 303 | const outputFooTxt = readFileSync(`${testDir}/dist/foo.txt`, 'utf-8') 304 | expect(outputFooTxt).toContain('foo') 305 | 306 | const outputJsClientJs = readFileSync(`${testDir}/dist/js/client.js`, 'utf-8') 307 | // eslint-disable-next-line quotes 308 | expect(outputJsClientJs).toContain("console.log('foo')") 309 | }) 310 | }) 311 | 312 | describe('Build Plugin with Vercel Adapter', () => { 313 | const testDir = './test/mocks/app-static-files' 314 | const vercelDir = `${testDir}/.vercel` 315 | const entry = './src/server.ts' 316 | 317 | afterEach(() => { 318 | rmSync(vercelDir, { recursive: true, force: true }) 319 | }) 320 | 321 | it('Should build the project correctly with the plugin', async () => { 322 | const outputFile = `${vercelDir}/output/functions/__hono.func/index.js` 323 | const configFile = `${vercelDir}/output/config.json` 324 | 325 | await build({ 326 | root: testDir, 327 | plugins: [ 328 | vercelBuildPlugin({ 329 | entry, 330 | minify: false, 331 | }), 332 | ], 333 | }) 334 | 335 | expect(existsSync(outputFile)).toBe(true) 336 | expect(existsSync(configFile)).toBe(true) 337 | 338 | const output = readFileSync(outputFile, 'utf-8') 339 | expect(output).toContain('Hello World') 340 | 341 | const routes = readFileSync(configFile, 'utf-8') 342 | expect(routes).toContain( 343 | '{"version":3,"routes":[{"handle":"filesystem"},{"src":"/(.*)","dest":"/__hono"}]}' 344 | ) 345 | 346 | const outputFooTxt = readFileSync(`${vercelDir}/output/static/foo.txt`, 'utf-8') 347 | expect(outputFooTxt).toContain('foo') 348 | 349 | const outputJsClientJs = readFileSync(`${vercelDir}/output/static/js/client.js`, 'utf-8') 350 | // eslint-disable-next-line quotes 351 | expect(outputJsClientJs).toContain("console.log('foo')") 352 | }) 353 | }) 354 | -------------------------------------------------------------------------------- /packages/build/test/basic.test.ts: -------------------------------------------------------------------------------- 1 | import { build } from 'vite' 2 | import { existsSync, readFileSync, rmSync } from 'node:fs' 3 | import buildPlugin from '../src/base' 4 | 5 | const cases = [ 6 | { 7 | name: 'default export', 8 | entry: './src/server.ts', 9 | output: 'index.js', 10 | }, 11 | { 12 | name: 'with fetch export', 13 | entry: './src/server-fetch.ts', 14 | output: 'server-fetch.js', 15 | }, 16 | ] 17 | 18 | describe.each(cases)('Build Plugin - $name', ({ entry, output }) => { 19 | const testDir = './test/mocks/app' 20 | const outputFile = `${testDir}/dist/${output}` 21 | 22 | afterAll(() => { 23 | rmSync(`${testDir}/dist`, { recursive: true, force: true }) 24 | }) 25 | 26 | it('Should build the project correctly with the plugin', async () => { 27 | await build({ 28 | root: testDir, 29 | plugins: [ 30 | buildPlugin({ 31 | entry, 32 | output, 33 | }), 34 | ], 35 | }) 36 | 37 | expect(existsSync(outputFile)).toBe(true) 38 | 39 | const outputContent = readFileSync(outputFile, 'utf-8') 40 | expect(outputContent).toContain('Hello World') 41 | }) 42 | 43 | it('Should return correct responses from the output file', async () => { 44 | const module = await import(outputFile) 45 | const app = module.default 46 | 47 | let res = await app.request('/') 48 | expect(res.status).toBe(200) 49 | 50 | res = await app.request('/not-found') 51 | expect(res.status).toBe(404) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /packages/build/test/mocks/app-static-files/public-routes-json/_routes.json: -------------------------------------------------------------------------------- 1 | {"version":1,"include":["/"],"exclude":["/customRoute"]} -------------------------------------------------------------------------------- /packages/build/test/mocks/app-static-files/public/foo.txt: -------------------------------------------------------------------------------- 1 | foo -------------------------------------------------------------------------------- /packages/build/test/mocks/app-static-files/public/js/client.js: -------------------------------------------------------------------------------- 1 | console.log('foo') 2 | -------------------------------------------------------------------------------- /packages/build/test/mocks/app-static-files/src/server-fetch-with-handlers.ts: -------------------------------------------------------------------------------- 1 | import app from './server' 2 | 3 | export default { 4 | fetch: app.fetch, 5 | scheduled: function () { 6 | return 'Hello World' 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /packages/build/test/mocks/app-static-files/src/server-fetch-with-handlers2.ts: -------------------------------------------------------------------------------- 1 | import app from './server' 2 | 3 | export default { 4 | fetch: app.fetch, 5 | scheduled: function () { 6 | return 'Hello World 2' 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /packages/build/test/mocks/app-static-files/src/server.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from 'hono' 2 | 3 | const app = new Hono() 4 | 5 | app.get('/', (c) => c.text('Hello World')) 6 | 7 | export default app 8 | -------------------------------------------------------------------------------- /packages/build/test/mocks/app/src/server-fetch.ts: -------------------------------------------------------------------------------- 1 | import app from './server' 2 | 3 | export default { 4 | fetch: app.fetch, 5 | } 6 | -------------------------------------------------------------------------------- /packages/build/test/mocks/app/src/server.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from 'hono' 2 | 3 | const app = new Hono() 4 | 5 | app.get('/', (c) => c.text('Hello World')) 6 | 7 | export default app 8 | -------------------------------------------------------------------------------- /packages/build/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src/" 5 | }, 6 | "include": [ 7 | "src/**/*.ts" 8 | ], 9 | "exclude": [ 10 | "src/**/*.test.ts" 11 | ] 12 | } -------------------------------------------------------------------------------- /packages/build/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": [ 4 | "src", 5 | "test" 6 | ], 7 | "compilerOptions": { 8 | "module": "ES2022", 9 | "target": "ES2022", 10 | "types": [ 11 | "vitest/globals", 12 | "vite/client" 13 | ] 14 | }, 15 | } -------------------------------------------------------------------------------- /packages/build/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { glob } from 'glob' 2 | import { defineConfig } from 'tsup' 3 | 4 | const entryPoints = glob.sync('./src/**/*.+(ts|tsx|json)', { 5 | ignore: ['./src/**/*.test.+(ts|tsx)'], 6 | }) 7 | 8 | export default defineConfig({ 9 | entry: entryPoints, 10 | dts: true, 11 | tsconfig: './tsconfig.build.json', 12 | splitting: false, 13 | minify: false, 14 | format: ['esm'], 15 | bundle: false, 16 | platform: 'node', 17 | }) 18 | -------------------------------------------------------------------------------- /packages/build/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | }, 7 | }) 8 | -------------------------------------------------------------------------------- /packages/cloudflare-pages/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @hono/vite-cloudflare-pages 2 | 3 | ## 0.4.2 4 | 5 | ### Patch Changes 6 | 7 | - [#153](https://github.com/honojs/vite-plugins/pull/153) [`6fb01bfa65b53c9236d45a0106250e4444a6c8f7`](https://github.com/honojs/vite-plugins/commit/6fb01bfa65b53c9236d45a0106250e4444a6c8f7) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: add `target` option as `webworker` 8 | 9 | ## 0.4.1 10 | 11 | ### Patch Changes 12 | 13 | - [#146](https://github.com/honojs/vite-plugins/pull/146) [`05476646e6d9029f30075c7eefe6425507ce2336`](https://github.com/honojs/vite-plugins/commit/05476646e6d9029f30075c7eefe6425507ce2336) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: add `apply` option 14 | 15 | ## 0.4.0 16 | 17 | ### Minor Changes 18 | 19 | - [#135](https://github.com/honojs/vite-plugins/pull/135) [`25bfb6170ecb7b9aec827a2b0e4838c8e6e7a78d`](https://github.com/honojs/vite-plugins/commit/25bfb6170ecb7b9aec827a2b0e4838c8e6e7a78d) Thanks [@ryuapp](https://github.com/ryuapp)! - feat: creating a `public/_routes.json` will override the automatic generation 20 | 21 | ## 0.3.0 22 | 23 | ### Minor Changes 24 | 25 | - [#121](https://github.com/honojs/vite-plugins/pull/121) [`cf3afbf4548008e6b2f6bcb97c4222d02c2713d5`](https://github.com/honojs/vite-plugins/commit/cf3afbf4548008e6b2f6bcb97c4222d02c2713d5) Thanks [@yusukebe](https://github.com/yusukebe)! - feat: detect static files automatically 26 | 27 | ## 0.2.5 28 | 29 | ### Patch Changes 30 | 31 | - [#75](https://github.com/honojs/vite-plugins/pull/75) [`50e640a64c0a9384145603d0fdc51b5c90cc19f5`](https://github.com/honojs/vite-plugins/commit/50e640a64c0a9384145603d0fdc51b5c90cc19f5) Thanks [@ninebolt6](https://github.com/ninebolt6)! - fix: respect `outputDir` option 32 | 33 | ## 0.2.4 34 | 35 | ### Patch Changes 36 | 37 | - [#73](https://github.com/honojs/vite-plugins/pull/73) [`8c02a9a425ef1eb37044ed59f2a8d4fdc0c9a37c`](https://github.com/honojs/vite-plugins/commit/8c02a9a425ef1eb37044ed59f2a8d4fdc0c9a37c) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: support windows 38 | 39 | - [#73](https://github.com/honojs/vite-plugins/pull/73) [`8c02a9a425ef1eb37044ed59f2a8d4fdc0c9a37c`](https://github.com/honojs/vite-plugins/commit/8c02a9a425ef1eb37044ed59f2a8d4fdc0c9a37c) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: support windows 40 | 41 | ## 0.2.3 42 | 43 | ### Patch Changes 44 | 45 | - [#69](https://github.com/honojs/vite-plugins/pull/69) [`06d753152aec120d7df83e207e71cb6c31d3ca41`](https://github.com/honojs/vite-plugins/commit/06d753152aec120d7df83e207e71cb6c31d3ca41) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: `emptyOutDir` as `false` 46 | 47 | ## 0.2.2 48 | 49 | ### Patch Changes 50 | 51 | - [#64](https://github.com/honojs/vite-plugins/pull/64) [`47f3a1073036b8da2a1b405466e9a9b6d5ff9c1f`](https://github.com/honojs/vite-plugins/commit/47f3a1073036b8da2a1b405466e9a9b6d5ff9c1f) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: peerDependencies 52 | 53 | ## 0.2.1 54 | 55 | ### Patch Changes 56 | 57 | - [#61](https://github.com/honojs/vite-plugins/pull/61) [`6ccd2338adcee4d60597f7037a1a7be902a6bf91`](https://github.com/honojs/vite-plugins/commit/6ccd2338adcee4d60597f7037a1a7be902a6bf91) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: fix default entry path and throw error if not found 58 | 59 | ## 0.2.0 60 | 61 | ### Minor Changes 62 | 63 | - [#55](https://github.com/honojs/vite-plugins/pull/55) [`876e2585b08676ac18c7a1fd7934625b4606778c`](https://github.com/honojs/vite-plugins/commit/876e2585b08676ac18c7a1fd7934625b4606778c) Thanks [@yusukebe](https://github.com/yusukebe)! - feat: use virtual entry 64 | 65 | - [#57](https://github.com/honojs/vite-plugins/pull/57) [`26f48e937123960b9d462b5ae252c769c636b9d2`](https://github.com/honojs/vite-plugins/commit/26f48e937123960b9d462b5ae252c769c636b9d2) Thanks [@yusukebe](https://github.com/yusukebe)! - feat: supports multiple entries 66 | 67 | ## 0.1.2 68 | 69 | ### Patch Changes 70 | 71 | - [#39](https://github.com/honojs/vite-plugins/pull/39) [`1f8d212f0d194e90a7a4133d11d29667a69942ff`](https://github.com/honojs/vite-plugins/commit/1f8d212f0d194e90a7a4133d11d29667a69942ff) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: update `peerDependencies` 72 | 73 | ## 0.1.1 74 | 75 | ### Patch Changes 76 | 77 | - [#33](https://github.com/honojs/vite-plugins/pull/33) [`a13778d`](https://github.com/honojs/vite-plugins/commit/a13778df270c2c6031d830cb528ee55bcea92575) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: handle `notFoundHandler` 78 | -------------------------------------------------------------------------------- /packages/cloudflare-pages/README.md: -------------------------------------------------------------------------------- 1 | # @hono/vite-cloudflare-pages 2 | 3 | `@hono/vite-cloudflare-pages` is a Vite plugin to build your Hono application for Cloudflare Pages. 4 | 5 | > [!CAUTION] 6 | > 7 | > `@hono/vite-cloudflare-page` is deprecated. Use [`@hono/vite-build`](https://github.com/honojs/vite-plugins/tree/main/packages/build) instead of it. 8 | 9 | ## Usage 10 | 11 | ### Installation 12 | 13 | You can install `vite` and `@hono/vite-cloudflare-pages` via npm. 14 | 15 | ```plain 16 | npm i -D vite @hono/vite-cloudflare-pages 17 | ``` 18 | 19 | Or you can install them with Bun. 20 | 21 | ```plain 22 | bun add vite @hono/vite-cloudflare-pages 23 | ``` 24 | 25 | ### Settings 26 | 27 | Add `"type": "module"` to your `package.json`. Then, create `vite.config.ts` and edit it. 28 | 29 | ```ts 30 | import { defineConfig } from 'vite' 31 | import pages from '@hono/vite-cloudflare-pages' 32 | 33 | export default defineConfig({ 34 | plugins: [pages()], 35 | }) 36 | ``` 37 | 38 | ### Build 39 | 40 | Just run `vite build`. 41 | 42 | ```text 43 | npm exec vite build 44 | ``` 45 | 46 | Or 47 | 48 | ```text 49 | bunx --bun vite build 50 | ``` 51 | 52 | ### Deploy to Cloudflare Pages 53 | 54 | Run the `wrangler` command. 55 | 56 | ```text 57 | wrangler pages deploy ./dist 58 | ``` 59 | 60 | ## Options 61 | 62 | The options are below. 63 | 64 | ```ts 65 | type CloudflarePagesOptions = { 66 | entry?: string | string[] 67 | outputDir?: string 68 | external?: string[] 69 | minify?: boolean 70 | emptyOutDir?: boolean 71 | } 72 | ``` 73 | 74 | Default values: 75 | 76 | ```ts 77 | export const defaultOptions = { 78 | entry: ['./src/index.tsx', './app/server.ts'], 79 | outputDir: './dist', 80 | external: [], 81 | minify: true, 82 | emptyOutDir: false, 83 | } 84 | ``` 85 | 86 | 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. 87 | 88 | ## Build a client 89 | 90 | If you also want to build a client-side script, you can configure it as follows. 91 | 92 | ```ts 93 | export default defineConfig(({ mode }) => { 94 | if (mode === 'client') { 95 | return { 96 | build: { 97 | rollupOptions: { 98 | input: './src/client.ts', 99 | output: { 100 | dir: './dist/static', 101 | entryFileNames: 'client.js', 102 | }, 103 | }, 104 | copyPublicDir: false, 105 | }, 106 | } 107 | } else { 108 | return { 109 | plugins: [pages()], 110 | } 111 | } 112 | }) 113 | ``` 114 | 115 | The build command: 116 | 117 | ```text 118 | vite build --mode client && vite build 119 | ``` 120 | 121 | ## Authors 122 | 123 | - Yusuke Wada 124 | 125 | ## License 126 | 127 | MIT 128 | -------------------------------------------------------------------------------- /packages/cloudflare-pages/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hono/vite-cloudflare-pages", 3 | "description": "Vite plugin to build your Hono app for Cloudflare Pages", 4 | "version": "0.4.2", 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 | }, 24 | "typesVersions": { 25 | "*": { 26 | "types": [ 27 | "./dist/types" 28 | ] 29 | } 30 | }, 31 | "author": "Yusuke Wada (https://github.com/yusukebe)", 32 | "license": "MIT", 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/honojs/vite-plugins.git" 36 | }, 37 | "publishConfig": { 38 | "registry": "https://registry.npmjs.org", 39 | "access": "public" 40 | }, 41 | "homepage": "https://github.com/honojs/vite-plugins", 42 | "devDependencies": { 43 | "glob": "^10.3.10", 44 | "hono": "^4.2.7", 45 | "publint": "^0.1.12", 46 | "rimraf": "^5.0.1", 47 | "tsup": "^7.2.0", 48 | "vite": "^6.1.1", 49 | "vitest": "^1.2.1" 50 | }, 51 | "peerDependencies": { 52 | "hono": "*" 53 | }, 54 | "engines": { 55 | "node": ">=18.14.1" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/cloudflare-pages/src/cloudflare-pages.ts: -------------------------------------------------------------------------------- 1 | import type { ConfigEnv, Plugin, ResolvedConfig, UserConfig } from 'vite' 2 | import { builtinModules } from 'module' 3 | import { readdir, writeFile } from 'node:fs/promises' 4 | import { resolve } from 'node:path' 5 | import { getEntryContent } from './entry.js' 6 | 7 | type CloudflarePagesOptions = { 8 | /** 9 | * @default ['./src/index.tsx', './app/server.ts'] 10 | */ 11 | entry?: string | string[] 12 | /** 13 | * @default './dist' 14 | */ 15 | outputDir?: string 16 | external?: string[] 17 | /** 18 | * @default true 19 | */ 20 | minify?: boolean 21 | emptyOutDir?: boolean 22 | apply?: ((this: void, config: UserConfig, env: ConfigEnv) => boolean) | undefined 23 | } 24 | 25 | export const defaultOptions: Required> = { 26 | entry: ['./src/index.tsx', './app/server.ts'], 27 | outputDir: './dist', 28 | external: [], 29 | minify: true, 30 | emptyOutDir: false, 31 | apply: (_config, { command, mode }) => { 32 | if (command === 'build' && mode !== 'client') { 33 | return true 34 | } 35 | return false 36 | }, 37 | } 38 | 39 | const WORKER_JS_NAME = '_worker.js' 40 | const ROUTES_JSON_NAME = '_routes.json' 41 | 42 | type StaticRoutes = { version: number; include: string[]; exclude: string[] } 43 | 44 | export const cloudflarePagesPlugin = (options?: CloudflarePagesOptions): Plugin => { 45 | const virtualEntryId = 'virtual:cloudflare-pages-entry-module' 46 | const resolvedVirtualEntryId = '\0' + virtualEntryId 47 | let config: ResolvedConfig 48 | const staticPaths: string[] = [] 49 | 50 | return { 51 | name: '@hono/vite-cloudflare-pages', 52 | configResolved: async (resolvedConfig) => { 53 | config = resolvedConfig 54 | }, 55 | resolveId(id) { 56 | if (id === virtualEntryId) { 57 | return resolvedVirtualEntryId 58 | } 59 | }, 60 | async load(id) { 61 | if (id === resolvedVirtualEntryId) { 62 | return await getEntryContent({ 63 | entry: options?.entry 64 | ? Array.isArray(options.entry) 65 | ? options.entry 66 | : [options.entry] 67 | : [...defaultOptions.entry], 68 | }) 69 | } 70 | }, 71 | writeBundle: async () => { 72 | const paths = await readdir(resolve(config.root, config.build.outDir), { 73 | withFileTypes: true, 74 | }) 75 | // If _routes.json already exists, don't create it 76 | if (paths.some((p) => p.name === ROUTES_JSON_NAME)) { 77 | return 78 | } else { 79 | paths.forEach((p) => { 80 | if (p.isDirectory()) { 81 | staticPaths.push(`/${p.name}/*`) 82 | } else { 83 | if (p.name === WORKER_JS_NAME) { 84 | return 85 | } 86 | staticPaths.push(`/${p.name}`) 87 | } 88 | }) 89 | const staticRoutes: StaticRoutes = { 90 | version: 1, 91 | include: ['/*'], 92 | exclude: staticPaths, 93 | } 94 | const path = resolve( 95 | config.root, 96 | options?.outputDir ?? defaultOptions.outputDir, 97 | '_routes.json' 98 | ) 99 | await writeFile(path, JSON.stringify(staticRoutes)) 100 | } 101 | }, 102 | apply: options?.apply ?? defaultOptions.apply, 103 | config: async (): Promise => { 104 | return { 105 | ssr: { 106 | external: options?.external ?? defaultOptions.external, 107 | noExternal: true, 108 | target: 'webworker', 109 | }, 110 | build: { 111 | outDir: options?.outputDir ?? defaultOptions.outputDir, 112 | emptyOutDir: options?.emptyOutDir ?? defaultOptions.emptyOutDir, 113 | minify: options?.minify ?? defaultOptions.minify, 114 | ssr: true, 115 | rollupOptions: { 116 | external: [...builtinModules, /^node:/], 117 | input: virtualEntryId, 118 | output: { 119 | entryFileNames: WORKER_JS_NAME, 120 | }, 121 | }, 122 | }, 123 | } 124 | }, 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /packages/cloudflare-pages/src/entry.ts: -------------------------------------------------------------------------------- 1 | import { normalize } from 'node:path' 2 | 3 | export type Options = { 4 | entry: string[] 5 | } 6 | 7 | const normalizePaths = (paths: string[]) => { 8 | return paths.map((p) => { 9 | let normalizedPath = normalize(p).replace(/\\/g, '/') 10 | if (normalizedPath.startsWith('./')) { 11 | normalizedPath = normalizedPath.substring(2) 12 | } 13 | return '/' + normalizedPath 14 | }) 15 | } 16 | 17 | export const getEntryContent = async (options: Options) => { 18 | const globStr = normalizePaths(options.entry) 19 | .map((e) => `'${e}'`) 20 | .join(',') 21 | const appStr = `const modules = import.meta.glob([${globStr}], { import: 'default', eager: true }) 22 | let added = false 23 | for (const [, app] of Object.entries(modules)) { 24 | if (app) { 25 | worker.route('/', app) 26 | worker.notFound(app.notFoundHandler) 27 | added = true 28 | } 29 | } 30 | if (!added) { 31 | throw new Error("Can't import modules from [${globStr}]") 32 | } 33 | ` 34 | 35 | return `import { Hono } from 'hono' 36 | 37 | const worker = new Hono() 38 | 39 | ${appStr} 40 | 41 | export default worker` 42 | } 43 | -------------------------------------------------------------------------------- /packages/cloudflare-pages/src/index.ts: -------------------------------------------------------------------------------- 1 | import { cloudflarePagesPlugin, defaultOptions } from './cloudflare-pages.js' 2 | export { defaultOptions } 3 | export default cloudflarePagesPlugin 4 | -------------------------------------------------------------------------------- /packages/cloudflare-pages/test/cloudflare-pages.test.ts: -------------------------------------------------------------------------------- 1 | import { build } from 'vite' 2 | import { describe, it, expect, afterAll } from 'vitest' 3 | import * as fs from 'node:fs' 4 | import cloudflarePagesPlugin from '../src/index' 5 | 6 | describe('cloudflarePagesPlugin', () => { 7 | const testDir = './test/project' 8 | const entryFile = `${testDir}/app/server.ts` 9 | 10 | afterAll(() => { 11 | fs.rmSync(`${testDir}/dist`, { recursive: true, force: true }) 12 | }) 13 | 14 | it('Should build the project correctly with the plugin', async () => { 15 | const outputFile = `${testDir}/dist/_worker.js` 16 | const routesFile = `${testDir}/dist/_routes.json` 17 | 18 | expect(fs.existsSync(entryFile)).toBe(true) 19 | 20 | await build({ 21 | root: testDir, 22 | plugins: [cloudflarePagesPlugin()], 23 | }) 24 | 25 | expect(fs.existsSync(outputFile)).toBe(true) 26 | expect(fs.existsSync(routesFile)).toBe(true) 27 | 28 | const output = fs.readFileSync(outputFile, 'utf-8') 29 | expect(output).toContain('Hello World') 30 | 31 | const routes = fs.readFileSync(routesFile, 'utf-8') 32 | expect(routes).toContain( 33 | '{"version":1,"include":["/*"],"exclude":["/favicon.ico","/static/*"]}' 34 | ) 35 | }) 36 | 37 | it('Should build the project correctly with custom output directory', async () => { 38 | const outputFile = `${testDir}/customDir/_worker.js` 39 | const routesFile = `${testDir}/customDir/_routes.json` 40 | 41 | afterAll(() => { 42 | fs.rmSync(`${testDir}/customDir/`, { recursive: true, force: true }) 43 | }) 44 | 45 | expect(fs.existsSync(entryFile)).toBe(true) 46 | 47 | await build({ 48 | root: testDir, 49 | plugins: [ 50 | cloudflarePagesPlugin({ 51 | outputDir: 'customDir', 52 | }), 53 | ], 54 | build: { 55 | emptyOutDir: true, 56 | }, 57 | }) 58 | 59 | expect(fs.existsSync(outputFile)).toBe(true) 60 | expect(fs.existsSync(routesFile)).toBe(true) 61 | 62 | const output = fs.readFileSync(outputFile, 'utf-8') 63 | expect(output).toContain('Hello World') 64 | 65 | const routes = fs.readFileSync(routesFile, 'utf-8') 66 | expect(routes).toContain( 67 | '{"version":1,"include":["/*"],"exclude":["/favicon.ico","/static/*"]}' 68 | ) 69 | }) 70 | 71 | it('Should not create a new _routes.json when _routes.json on output directory.', async () => { 72 | const outputFile = `${testDir}/dist/_worker.js` 73 | const routesFile = `${testDir}/dist/_routes.json` 74 | 75 | expect(fs.existsSync(entryFile)).toBe(true) 76 | 77 | await build({ 78 | publicDir: 'public-routes-json', 79 | root: testDir, 80 | plugins: [cloudflarePagesPlugin()], 81 | }) 82 | 83 | expect(fs.existsSync(outputFile)).toBe(true) 84 | expect(fs.existsSync(routesFile)).toBe(true) 85 | 86 | const output = fs.readFileSync(outputFile, 'utf-8') 87 | expect(output).toContain('Hello World') 88 | 89 | const routes = fs.readFileSync(routesFile, 'utf-8') 90 | expect(routes).toContain('{"version":1,"include":["/"],"exclude":["/customRoute"]}') 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /packages/cloudflare-pages/test/project/app/server.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | fetch: () => new Response('Hello World'), 3 | } 4 | -------------------------------------------------------------------------------- /packages/cloudflare-pages/test/project/public-routes-json/_routes.json: -------------------------------------------------------------------------------- 1 | {"version":1,"include":["/"],"exclude":["/customRoute"]} -------------------------------------------------------------------------------- /packages/cloudflare-pages/test/project/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honojs/vite-plugins/64865d5a2cdfac287e89f67d5f3cb0d2f0f54186/packages/cloudflare-pages/test/project/public/favicon.ico -------------------------------------------------------------------------------- /packages/cloudflare-pages/test/project/public/static/foo.txt: -------------------------------------------------------------------------------- 1 | foo -------------------------------------------------------------------------------- /packages/cloudflare-pages/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src/" 5 | }, 6 | "include": [ 7 | "src/**/*.ts" 8 | ], 9 | "exclude": [ 10 | "src/**/*.test.ts" 11 | ] 12 | } -------------------------------------------------------------------------------- /packages/cloudflare-pages/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": [ 4 | "src", 5 | "test" 6 | ], 7 | "compilerOptions": { 8 | "module": "ES2022", 9 | "target": "ES2022", 10 | "types": [ 11 | "vite/client" 12 | ] 13 | }, 14 | } -------------------------------------------------------------------------------- /packages/cloudflare-pages/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { glob } from 'glob' 2 | import { defineConfig } from 'tsup' 3 | 4 | const entryPoints = glob.sync('./src/**/*.+(ts|tsx|json)', { 5 | ignore: ['./src/**/*.test.+(ts|tsx)'], 6 | }) 7 | 8 | export default defineConfig({ 9 | entry: entryPoints, 10 | dts: true, 11 | tsconfig: './tsconfig.build.json', 12 | splitting: false, 13 | minify: false, 14 | format: ['esm'], 15 | bundle: false, 16 | platform: 'node', 17 | }) 18 | -------------------------------------------------------------------------------- /packages/dev-server/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @hono/vite-dev-server 2 | 3 | ## 0.19.1 4 | 5 | ### Patch Changes 6 | 7 | - [#264](https://github.com/honojs/vite-plugins/pull/264) [`2eb66db9b43131e4b41417e2ddc31ebf16db383e`](https://github.com/honojs/vite-plugins/commit/2eb66db9b43131e4b41417e2ddc31ebf16db383e) Thanks [@zachnoble](https://github.com/zachnoble)! - chore: bump `@hono/node-server` to 1.14.2 8 | 9 | ## 0.19.0 10 | 11 | ### Minor Changes 12 | 13 | - [#235](https://github.com/honojs/vite-plugins/pull/235) [`367d2b088c14c29ae12701ab702305209b1959ce`](https://github.com/honojs/vite-plugins/commit/367d2b088c14c29ae12701ab702305209b1959ce) Thanks [@yusukebe](https://github.com/yusukebe)! - feat: add `handleHotUpdate` option 14 | 15 | ## 0.18.3 16 | 17 | ### Patch Changes 18 | 19 | - [#233](https://github.com/honojs/vite-plugins/pull/233) [`1dbcbb8654739f54beb0e4082b3639f4f4a9468f`](https://github.com/honojs/vite-plugins/commit/1dbcbb8654739f54beb0e4082b3639f4f4a9468f) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: enable HMR for client-side 20 | 21 | ## 0.18.2 22 | 23 | ### Patch Changes 24 | 25 | - [#231](https://github.com/honojs/vite-plugins/pull/231) [`eb196d52a7f2059540c64a9c0b94298d49a00b90`](https://github.com/honojs/vite-plugins/commit/eb196d52a7f2059540c64a9c0b94298d49a00b90) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: support hot reload for Vite6 26 | 27 | - [#228](https://github.com/honojs/vite-plugins/pull/228) [`5153b84779b279274836512f7172c53e5cc11ae7`](https://github.com/honojs/vite-plugins/commit/5153b84779b279274836512f7172c53e5cc11ae7) Thanks [@mo36924](https://github.com/mo36924)! - fix: Fixed problem with source map not working when reading entry files 28 | 29 | ## 0.18.1 30 | 31 | ### Patch Changes 32 | 33 | - [#212](https://github.com/honojs/vite-plugins/pull/212) [`01d28ca426646f4b75754767baeb41a11e0d8dfd`](https://github.com/honojs/vite-plugins/commit/01d28ca426646f4b75754767baeb41a11e0d8dfd) Thanks [@gobengo](https://github.com/gobengo)! - dev-server plugin getRequestListener fetchCallback now always returns Promise instead of sometimes returning Promise 34 | 35 | ## 0.18.0 36 | 37 | ### Minor Changes 38 | 39 | - [#202](https://github.com/honojs/vite-plugins/pull/202) [`3eae0ff4685f53067b32d78d1d9393bddce165eb`](https://github.com/honojs/vite-plugins/commit/3eae0ff4685f53067b32d78d1d9393bddce165eb) Thanks [@meck93](https://github.com/meck93)! - feat: add CSP support to vite's `injectClientScript` option 40 | 41 | ## 0.17.0 42 | 43 | ### Minor Changes 44 | 45 | - [#191](https://github.com/honojs/vite-plugins/pull/191) [`1a9b6851c01ef17c3129a4432db96f67724ab966`](https://github.com/honojs/vite-plugins/commit/1a9b6851c01ef17c3129a4432db96f67724ab966) Thanks [@yusukebe](https://github.com/yusukebe)! - feat: exlude files in `public` dir 46 | 47 | ## 0.16.0 48 | 49 | ### Minor Changes 50 | 51 | - [#175](https://github.com/honojs/vite-plugins/pull/175) [`c44f9391cf145192b3632c6eb71b15a8d5d3178b`](https://github.com/honojs/vite-plugins/commit/c44f9391cf145192b3632c6eb71b15a8d5d3178b) Thanks [@yusukebe](https://github.com/yusukebe)! - feat: add `loadModule` option 52 | 53 | ## 0.15.2 54 | 55 | ### Patch Changes 56 | 57 | - [#173](https://github.com/honojs/vite-plugins/pull/173) [`840e6da43ba95bbfde767246f8ee071d8235f166`](https://github.com/honojs/vite-plugins/commit/840e6da43ba95bbfde767246f8ee071d8235f166) Thanks [@komapotter](https://github.com/komapotter)! - fix(dev-server): Add null check and error handling for proxy disposal in cloudflareAdapter 58 | 59 | ## 0.15.1 60 | 61 | ### Patch Changes 62 | 63 | - [#170](https://github.com/honojs/vite-plugins/pull/170) [`4137d3fd60cb7189c485c7b73c46d64fb7adb0af`](https://github.com/honojs/vite-plugins/commit/4137d3fd60cb7189c485c7b73c46d64fb7adb0af) Thanks [@arisris](https://github.com/arisris)! - (fix) missing export for initial node, bun adapter in the package.json from the previous version 64 | 65 | ## 0.15.0 66 | 67 | ### Minor Changes 68 | 69 | - [#167](https://github.com/honojs/vite-plugins/pull/167) [`044a023d63d1ce903aa6bab132b16d0799265766`](https://github.com/honojs/vite-plugins/commit/044a023d63d1ce903aa6bab132b16d0799265766) Thanks [@arisris](https://github.com/arisris)! - Add initial nodejs adapter 70 | 71 | - [#166](https://github.com/honojs/vite-plugins/pull/166) [`714951ca854e949834f9b5375342684849f5c260`](https://github.com/honojs/vite-plugins/commit/714951ca854e949834f9b5375342684849f5c260) Thanks [@arisris](https://github.com/arisris)! - Add Initial bun adapter 72 | 73 | ### Patch Changes 74 | 75 | - [#165](https://github.com/honojs/vite-plugins/pull/165) [`0a59fddeaeae3cc1222779035c1f2b1c4753f1e3`](https://github.com/honojs/vite-plugins/commit/0a59fddeaeae3cc1222779035c1f2b1c4753f1e3) Thanks [@arisris](https://github.com/arisris)! - Remove miniflare from deps, and add it to peer optional. 76 | 77 | ## 0.14.0 78 | 79 | ### Minor Changes 80 | 81 | - [#161](https://github.com/honojs/vite-plugins/pull/161) [`6cb1002e01e7b7554b2efa873f8e46ca3d14c5fc`](https://github.com/honojs/vite-plugins/commit/6cb1002e01e7b7554b2efa873f8e46ca3d14c5fc) Thanks [@yusukebe](https://github.com/yusukebe)! - feat: support `cf` property in Cloudflare adapter 82 | 83 | ## 0.13.1 84 | 85 | ### Patch Changes 86 | 87 | - [#156](https://github.com/honojs/vite-plugins/pull/156) [`e62bf1b49d96cd2d59d98572a034bee7514cd00f`](https://github.com/honojs/vite-plugins/commit/e62bf1b49d96cd2d59d98572a034bee7514cd00f) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: set `navigator.userAgent` correctly 88 | 89 | ## 0.13.0 90 | 91 | ### Minor Changes 92 | 93 | - [#152](https://github.com/honojs/vite-plugins/pull/152) [`1782639ce0b9dcc802d3ecbb5f14dd614b80f708`](https://github.com/honojs/vite-plugins/commit/1782639ce0b9dcc802d3ecbb5f14dd614b80f708) Thanks [@yusukebe](https://github.com/yusukebe)! - feat: remove plugins 94 | 95 | ### Patch Changes 96 | 97 | - [#150](https://github.com/honojs/vite-plugins/pull/150) [`a0c634394cf6dde72df8408d475598ccf35a9bc5`](https://github.com/honojs/vite-plugins/commit/a0c634394cf6dde72df8408d475598ccf35a9bc5) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: add `navigator.userAgent` value for the cloudflare adapter 98 | 99 | ## 0.12.2 100 | 101 | ### Patch Changes 102 | 103 | - [#144](https://github.com/honojs/vite-plugins/pull/144) [`728099d899fab5ff81adb126fcd39e47e7f13051`](https://github.com/honojs/vite-plugins/commit/728099d899fab5ff81adb126fcd39e47e7f13051) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: handler cloudflare dispose error 104 | 105 | ## 0.12.1 106 | 107 | ### Patch Changes 108 | 109 | - [#138](https://github.com/honojs/vite-plugins/pull/138) [`6dd041a94b955331e1bff049c8b9572d7b554d06`](https://github.com/honojs/vite-plugins/commit/6dd041a94b955331e1bff049c8b9572d7b554d06) Thanks [@yusukebe](https://github.com/yusukebe)! - use `ssrLoadModule` instead of Runtime API 110 | 111 | ## 0.12.0 112 | 113 | ### Minor Changes 114 | 115 | - [#129](https://github.com/honojs/vite-plugins/pull/129) [`b4019f0f194d95a3e3d2a56aba649614b06a0135`](https://github.com/honojs/vite-plugins/commit/b4019f0f194d95a3e3d2a56aba649614b06a0135) Thanks [@alessandrojcm](https://github.com/alessandrojcm)! - Switched to executeEntrypoint instead of ssrLoadModule 116 | 117 | ### Patch Changes 118 | 119 | - [#130](https://github.com/honojs/vite-plugins/pull/130) [`5b728da65de4b5588de07fd3d3656cbc09bf62df`](https://github.com/honojs/vite-plugins/commit/5b728da65de4b5588de07fd3d3656cbc09bf62df) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: add caches which to do nothing to cloudflare adapter 120 | 121 | - [#127](https://github.com/honojs/vite-plugins/pull/127) [`ac6d6c4cc4089c5555ae539c7c5467779d15808a`](https://github.com/honojs/vite-plugins/commit/ac6d6c4cc4089c5555ae539c7c5467779d15808a) Thanks [@naporin0624](https://github.com/naporin0624)! - fixed typesVersions in `@hono/vite-dev-server/cloudflare` 122 | 123 | ## 0.11.1 124 | 125 | ### Patch Changes 126 | 127 | - [#122](https://github.com/honojs/vite-plugins/pull/122) [`d58ad511026840b2c5cef17a6121e022110a20c2`](https://github.com/honojs/vite-plugins/commit/d58ad511026840b2c5cef17a6121e022110a20c2) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: bump `@hono/node-server` 128 | 129 | ## 0.11.0 130 | 131 | ### Minor Changes 132 | 133 | - [#119](https://github.com/honojs/vite-plugins/pull/119) [`7365dc477f57ad2103d026b0e9b9fcbae9c1966b`](https://github.com/honojs/vite-plugins/commit/7365dc477f57ad2103d026b0e9b9fcbae9c1966b) Thanks [@yusukebe](https://github.com/yusukebe)! - feat: supports Bun 134 | 135 | ## 0.10.0 136 | 137 | ### Minor Changes 138 | 139 | - [#111](https://github.com/honojs/vite-plugins/pull/111) [`fcc98f92cd45df52ce34380414072d0b10e8c701`](https://github.com/honojs/vite-plugins/commit/fcc98f92cd45df52ce34380414072d0b10e8c701) Thanks [@tseijp](https://github.com/tseijp)! - Added `.mf` to `ignoreWatching` in `vite-plugin` to fix unnecessary server reloads on file changes. This update prevents the Vite server from restarting when `.mf` files are modified, improving development experience. 140 | 141 | ## 0.9.0 142 | 143 | ### Minor Changes 144 | 145 | - [#108](https://github.com/honojs/vite-plugins/pull/108) [`939eee515adf974f7491710f003383127e9d94a1`](https://github.com/honojs/vite-plugins/commit/939eee515adf974f7491710f003383127e9d94a1) Thanks [@yusukebe](https://github.com/yusukebe)! - feat: introduce Cloudflare Adapter 146 | 147 | ## 0.8.2 148 | 149 | ### Patch Changes 150 | 151 | - [#105](https://github.com/honojs/vite-plugins/pull/105) [`0f44e181b1391754e369a8e07354213ac59ddb75`](https://github.com/honojs/vite-plugins/commit/0f44e181b1391754e369a8e07354213ac59ddb75) Thanks [@emirotin](https://github.com/emirotin)! - Add CSS to server excludes 152 | 153 | ## 0.8.1 154 | 155 | ### Patch Changes 156 | 157 | - [#102](https://github.com/honojs/vite-plugins/pull/102) [`95564fe656d7e6ce590e7f9adf585837c0fe3736`](https://github.com/honojs/vite-plugins/commit/95564fe656d7e6ce590e7f9adf585837c0fe3736) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: add `/\?t\=\d+$/` to `exclude` 158 | 159 | - [#100](https://github.com/honojs/vite-plugins/pull/100) [`10b50e61a5d5586173705fb5f3fb37480b23e0d5`](https://github.com/honojs/vite-plugins/commit/10b50e61a5d5586173705fb5f3fb37480b23e0d5) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: bump `@hono/node-server` 160 | 161 | ## 0.8.0 162 | 163 | ### Minor Changes 164 | 165 | - [#96](https://github.com/honojs/vite-plugins/pull/96) [`88fe05f7a2e56f87e3ff12066104dcaee32372c5`](https://github.com/honojs/vite-plugins/commit/88fe05f7a2e56f87e3ff12066104dcaee32372c5) Thanks [@yusukebe](https://github.com/yusukebe)! - feat: add `ignoreWatching` option 166 | 167 | - [`6897fd999340a462935213a14f886566e264ec18`](https://github.com/honojs/vite-plugins/commit/6897fd999340a462935213a14f886566e264ec18) Thanks [@KaiSpencer](https://github.com/KaiSpencer)! - feat: add `adapter` and merge `env` 168 | 169 | ### Patch Changes 170 | 171 | - [#98](https://github.com/honojs/vite-plugins/pull/98) [`ff1f0b65c47db8b7776dcee693f95f19060560c9`](https://github.com/honojs/vite-plugins/commit/ff1f0b65c47db8b7776dcee693f95f19060560c9) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: fixed `.wrangler` path 172 | 173 | ## 0.7.1 174 | 175 | ### Patch Changes 176 | 177 | - [#90](https://github.com/honojs/vite-plugins/pull/90) [`73587e53d30eed82c4388ecfdc13ea5a1a019317`](https://github.com/honojs/vite-plugins/commit/73587e53d30eed82c4388ecfdc13ea5a1a019317) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: fix `plugin.env` type 178 | 179 | ## 0.7.0 180 | 181 | ### Minor Changes 182 | 183 | - [#86](https://github.com/honojs/vite-plugins/pull/86) [`f9e3545753c8a3cb8e90cb8123b070c0a8966de2`](https://github.com/honojs/vite-plugins/commit/f9e3545753c8a3cb8e90cb8123b070c0a8966de2) Thanks [@jxom](https://github.com/jxom)! - Added `export` property to `devServer`. 184 | 185 | ## 0.6.1 186 | 187 | ### Patch Changes 188 | 189 | - [#87](https://github.com/honojs/vite-plugins/pull/87) [`5d562cce8baf4f9ff405d6d15f654401c8b54b4d`](https://github.com/honojs/vite-plugins/commit/5d562cce8baf4f9ff405d6d15f654401c8b54b4d) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: bump `@hono/node-server` 190 | 191 | ## 0.6.0 192 | 193 | ### Minor Changes 194 | 195 | - [#82](https://github.com/honojs/vite-plugins/pull/82) [`930fa87eeaef0a342bf4c7be5db6a31a5a4d6b46`](https://github.com/honojs/vite-plugins/commit/930fa87eeaef0a342bf4c7be5db6a31a5a4d6b46) Thanks [@yusukebe](https://github.com/yusukebe)! - feat: bump `@hono/node-server` 196 | 197 | ## 0.5.1 198 | 199 | ### Patch Changes 200 | 201 | - [#77](https://github.com/honojs/vite-plugins/pull/77) [`b397fdd438081f4a2b98a042ac2f974c9b63c449`](https://github.com/honojs/vite-plugins/commit/b397fdd438081f4a2b98a042ac2f974c9b63c449) Thanks [@rutan](https://github.com/rutan)! - fix: initialize Miniflare only on the first run 202 | 203 | ## 0.5.0 204 | 205 | ### Minor Changes 206 | 207 | - [#63](https://github.com/honojs/vite-plugins/pull/63) [`10a7ab5da5e61cf314cc7566ddfa53552bf3172a`](https://github.com/honojs/vite-plugins/commit/10a7ab5da5e61cf314cc7566ddfa53552bf3172a) Thanks [@marbemac](https://github.com/marbemac)! - Leverage vite error handling. To leverage this, return client errors in development like in the example below: 208 | 209 | ```ts 210 | honoApp.get('*', async c => { 211 | try { 212 | // react, solid, etc 213 | const app = await renderClientApp(); 214 | 215 | return new Response(app, { headers: { 'Content-Type': 'text/html' } }); 216 | } catch (err: any) { 217 | // in dev, pass the error back to the vite dev server to display in the error overlay 218 | if (import.meta.env.DEV) return err; 219 | 220 | throw err; 221 | } 222 | }); 223 | ``` 224 | 225 | ## 0.4.1 226 | 227 | ### Patch Changes 228 | 229 | - [#64](https://github.com/honojs/vite-plugins/pull/64) [`47f3a1073036b8da2a1b405466e9a9b6d5ff9c1f`](https://github.com/honojs/vite-plugins/commit/47f3a1073036b8da2a1b405466e9a9b6d5ff9c1f) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: peerDependencies 230 | 231 | ## 0.4.0 232 | 233 | ### Minor Changes 234 | 235 | - [#53](https://github.com/honojs/vite-plugins/pull/53) [`4c0b96f908152ffc366ffba93a35e268008c0c4c`](https://github.com/honojs/vite-plugins/commit/4c0b96f908152ffc366ffba93a35e268008c0c4c) Thanks [@yusukebe](https://github.com/yusukebe)! - feat: introduce Plugins 236 | 237 | ## 0.3.5 238 | 239 | ### Patch Changes 240 | 241 | - [#51](https://github.com/honojs/vite-plugins/pull/51) [`296c6782531a0b6780154a47dd1ac7a78ed1b910`](https://github.com/honojs/vite-plugins/commit/296c6782531a0b6780154a47dd1ac7a78ed1b910) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: bump `@hono/node-server` 242 | 243 | ## 0.3.4 244 | 245 | ### Patch Changes 246 | 247 | - [#47](https://github.com/honojs/vite-plugins/pull/47) [`0a8c34da171294376186f3bc716a9620e24d37df`](https://github.com/honojs/vite-plugins/commit/0a8c34da171294376186f3bc716a9620e24d37df) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: use `toString()` for the error message 248 | 249 | ## 0.3.3 250 | 251 | ### Patch Changes 252 | 253 | - [#45](https://github.com/honojs/vite-plugins/pull/45) [`05b9841b69f140b52a08b08803d0f717a4caf025`](https://github.com/honojs/vite-plugins/commit/05b9841b69f140b52a08b08803d0f717a4caf025) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: handle invalid response 254 | 255 | ## 0.3.2 256 | 257 | ### Patch Changes 258 | 259 | - [#43](https://github.com/honojs/vite-plugins/pull/43) [`c955dcfc7c42cf33b0c7c470ebcdb5c1f130e2f8`](https://github.com/honojs/vite-plugins/commit/c955dcfc7c42cf33b0c7c470ebcdb5c1f130e2f8) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: don't crash when ssrLoadModule throw the error 260 | 261 | ## 0.3.1 262 | 263 | ### Patch Changes 264 | 265 | - [#39](https://github.com/honojs/vite-plugins/pull/39) [`1f8d212f0d194e90a7a4133d11d29667a69942ff`](https://github.com/honojs/vite-plugins/commit/1f8d212f0d194e90a7a4133d11d29667a69942ff) Thanks [@yusukebe](https://github.com/yusukebe)! - fix: update `peerDependencies` 266 | 267 | ## 0.3.0 268 | 269 | ### Minor Changes 270 | 271 | - [#30](https://github.com/honojs/vite-plugins/pull/30) [`12a8b15`](https://github.com/honojs/vite-plugins/commit/12a8b15f9d7347d622342e8dd0466ed1f56b0e4d) Thanks [@yusukebe](https://github.com/yusukebe)! - feat: introduce `env` 272 | 273 | - [#36](https://github.com/honojs/vite-plugins/pull/36) [`6de0805`](https://github.com/honojs/vite-plugins/commit/6de08051e79f3b3dbebb7943dcb401ef47875b67) Thanks [@yusukebe](https://github.com/yusukebe)! - feat: support glob-matching for exclude option 274 | -------------------------------------------------------------------------------- /packages/dev-server/README.md: -------------------------------------------------------------------------------- 1 | # @hono/vite-dev-server 2 | 3 | `@hono/vite-dev-server` is a Vite Plugin that provides a custom dev-server for `fetch`-based web applications like those using Hono. 4 | You can develop your application with Vite. It's fast. 5 | 6 | ## Features 7 | 8 | - Support any `fetch`-based applications. 9 | - Hono applications run on. 10 | - Fast by Vite. 11 | - HMR (Only for the client side). 12 | - Adapters are available, e.g., Cloudflare. 13 | - Also runs on Bun. 14 | 15 | ## Demo 16 | 17 | https://github.com/honojs/vite-plugins/assets/10682/a93ee4c5-2e1a-4b17-8bb2-64f955f2f0b0 18 | 19 | ## Supported applications 20 | 21 | You can run any application on `@hono/vite-dev-server` that uses `fetch` and is built with Web Standard APIs. The minimal application is the following. 22 | 23 | ```ts 24 | export default { 25 | fetch(_request: Request) { 26 | return new Response('Hello Vite!') 27 | }, 28 | } 29 | ``` 30 | 31 | This code can also run on Cloudflare Workers or Bun. 32 | And if you change the entry point, you can run on Deno, Vercel, Lagon, and other platforms. 33 | 34 | Hono is designed for `fetch`-based applications like this. 35 | 36 | ```ts 37 | import { Hono } from 'hono' 38 | 39 | const app = new Hono() 40 | 41 | app.get('/', (c) => c.text('Hello Vite!')) 42 | 43 | export default app 44 | ``` 45 | 46 | So, any Hono application will run on `@hono/vite-dev-server`. 47 | 48 | ## Usage 49 | 50 | ### Installation 51 | 52 | You can install `vite` and `@hono/vite-dev-server` via npm. 53 | 54 | ```bash 55 | npm i -D vite @hono/vite-dev-server 56 | ``` 57 | 58 | Or you can install them with Bun. 59 | 60 | ```bash 61 | bun add vite @hono/vite-dev-server 62 | ``` 63 | 64 | ### Settings 65 | 66 | Add `"type": "module"` to your `package.json`. Then, create `vite.config.ts` and edit it. 67 | 68 | ```ts 69 | import { defineConfig } from 'vite' 70 | import devServer from '@hono/vite-dev-server' 71 | 72 | export default defineConfig({ 73 | plugins: [ 74 | devServer({ 75 | entry: 'src/index.ts', // The file path of your application. 76 | }), 77 | ], 78 | }) 79 | ``` 80 | 81 | ### Development 82 | 83 | Just run `vite`. 84 | 85 | ```bash 86 | npm exec vite 87 | ``` 88 | 89 | Or 90 | 91 | ```bash 92 | bunx --bun vite 93 | ``` 94 | 95 | ## Options 96 | 97 | The options are below. 98 | 99 | ```ts 100 | export type DevServerOptions = { 101 | entry?: string 102 | export?: string 103 | injectClientScript?: boolean 104 | exclude?: (string | RegExp)[] 105 | ignoreWatching?: (string | RegExp)[] 106 | adapter?: { 107 | env?: Env 108 | onServerClose?: () => Promise 109 | } 110 | handleHotUpdate?: VitePlugin['handleHotUpdate'] 111 | } 112 | ``` 113 | 114 | Default values: 115 | 116 | ```ts 117 | export const defaultOptions: Required> = { 118 | entry: './src/index.ts', 119 | injectClientScript: true, 120 | exclude: [ 121 | /.*\.css$/, 122 | /.*\.ts$/, 123 | /.*\.tsx$/, 124 | /^\/@.+$/, 125 | /\?t\=\d+$/, 126 | /^\/favicon\.ico$/, 127 | /^\/static\/.+/, 128 | /^\/node_modules\/.*/, 129 | ], 130 | ignoreWatching: [/\.wrangler/], 131 | handleHotUpdate: ({ server, modules }) => { 132 | const isSSR = modules.some((mod) => mod._ssrModule) 133 | if (isSSR) { 134 | server.hot.send({ type: 'full-reload' }) 135 | return [] 136 | } 137 | }, 138 | } 139 | ``` 140 | 141 | ### `injectClientScript` 142 | 143 | If it's `true` and the response content type is "HTML", inject the script that enables Hot-reload. default is `true`. 144 | 145 | ### `exclude` 146 | 147 | The paths that are not served by the dev-server. 148 | 149 | If you have static files in `public/assets/*` and want to return them, exclude `/assets/*` as follows: 150 | 151 | ```ts 152 | import devServer, { defaultOptions } from '@hono/vite-dev-server' 153 | import { defineConfig } from 'vite' 154 | 155 | export default defineConfig({ 156 | plugins: [ 157 | devServer({ 158 | exclude: ['/assets/*', ...defaultOptions.exclude], 159 | }), 160 | ], 161 | }) 162 | ``` 163 | 164 | ### `ignoreWatching` 165 | 166 | You can add target directories for the server to watch. 167 | 168 | ### `adapter` 169 | 170 | You can pass the `env` value of a specified environment to the application. 171 | 172 | ## Adapter 173 | 174 | ### Cloudflare 175 | 176 | You can pass the Bindings specified in `wrangler.toml` to your application by using "Cloudflare Adapter". 177 | 178 | Install miniflare and wrangler to develop and deploy your cf project. 179 | 180 | ```bash 181 | npm i -D wrangler miniflare 182 | ``` 183 | 184 | ```ts 185 | import devServer from '@hono/vite-dev-server' 186 | import cloudflareAdapter from '@hono/vite-dev-server/cloudflare' 187 | import { defineConfig } from 'vite' 188 | 189 | export default defineConfig(async () => { 190 | return { 191 | plugins: [ 192 | devServer({ 193 | adapter: cloudflareAdapter, 194 | }), 195 | ], 196 | } 197 | }) 198 | ``` 199 | 200 | ### Node.js & Bun 201 | 202 | No additional dependencies needed. 203 | 204 | ```ts 205 | import devServer from '@hono/vite-dev-server' 206 | import nodeAdapter from '@hono/vite-dev-server/node' 207 | // OR 208 | // import bunAdapter from '@hono/vite-dev-server/bun' 209 | import { defineConfig } from 'vite' 210 | 211 | export default defineConfig(async () => { 212 | return { 213 | plugins: [ 214 | devServer({ 215 | adapter: nodeAdapter, 216 | // OR 217 | // adapter: bunAdapter, 218 | }), 219 | ], 220 | } 221 | }) 222 | ``` 223 | 224 | ## Client-side 225 | 226 | You can write client-side scripts and import them into your application using Vite's features. 227 | If `/src/client.ts` is the entry point, simply write it in the `script` tag. 228 | Additionally, `import.meta.env.PROD` is useful for detecting whether it's running on a dev server or in the build phase. 229 | 230 | ```tsx 231 | app.get('/', (c) => { 232 | return c.html( 233 | 234 | 235 | {import.meta.env.PROD ? ( 236 | 237 | ) : ( 238 | 239 | )} 240 | 241 | 242 |

Hello

243 | 244 | 245 | ) 246 | }) 247 | ``` 248 | 249 | In order to build the script properly, you can use the example config file `vite.config.ts` as shown below. 250 | 251 | ```ts 252 | import { defineConfig } from 'vite' 253 | import devServer from '@hono/vite-dev-server' 254 | 255 | export default defineConfig(({ mode }) => { 256 | if (mode === 'client') { 257 | return { 258 | build: { 259 | rollupOptions: { 260 | input: ['./app/client.ts'], 261 | output: { 262 | entryFileNames: 'static/client.js', 263 | chunkFileNames: 'static/assets/[name]-[hash].js', 264 | assetFileNames: 'static/assets/[name].[ext]', 265 | }, 266 | }, 267 | emptyOutDir: false, 268 | copyPublicDir: false, 269 | }, 270 | } 271 | } else { 272 | return { 273 | build: { 274 | minify: true, 275 | rollupOptions: { 276 | output: { 277 | entryFileNames: '_worker.js', 278 | }, 279 | }, 280 | }, 281 | plugins: [ 282 | devServer({ 283 | entry: './app/server.ts', 284 | }), 285 | ], 286 | } 287 | } 288 | }) 289 | ``` 290 | 291 | You can run the following command to build the client script. 292 | 293 | ```bash 294 | vite build --mode client 295 | ``` 296 | 297 | ## Frequently Asked Questions 298 | 299 | ### exports is not defined 300 | 301 | If you use a package that only supports CommonJS, you will encounter the error `exports is not defined`. 302 | 303 | ![exports is not defined](https://github.com/honojs/vite-plugins/assets/10682/6b36f8c2-50a8-4672-a177-4321f325a39f) 304 | 305 | In that case, specify the target package in `ssr.external` in `vite.config.ts`: 306 | 307 | ```ts 308 | export default defineConfig({ 309 | ssr: { 310 | external: ['react', 'react-dom'], 311 | }, 312 | plugins: [devServer()], 313 | }) 314 | ``` 315 | 316 | ### Importing Asset as URL is not working 317 | 318 | If you want to [import assets as URL](https://vitejs.dev/guide/assets) with the following code, the `logo` image may not be found. 319 | 320 | ```tsx 321 | import { Hono } from 'hono' 322 | 323 | import logo from './logo.png' 324 | 325 | const app = new Hono() 326 | 327 | app.get('/', async (c) => { 328 | return c.html( 329 | 330 | 331 | 332 | 333 | 334 | ) 335 | }) 336 | 337 | export default app 338 | ``` 339 | 340 | This is because `logo.png` is served from this dev-server, even though it is expected to be served from Vite itself. So to fix it, you can add `*.png` to the `exclude` option: 341 | 342 | ```ts 343 | import devServer, { defaultOptions } from '@hono/vite-dev-server' 344 | 345 | export default defineConfig({ 346 | plugins: [ 347 | devServer({ 348 | entry: 'src/index.tsx', 349 | exclude: [/.*\.png$/, ...defaultOptions.exclude], 350 | }), 351 | ], 352 | }) 353 | ``` 354 | 355 | ## Authors 356 | 357 | - Yusuke Wada 358 | 359 | ## License 360 | 361 | MIT 362 | -------------------------------------------------------------------------------- /packages/dev-server/e2e-bun/e2e.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | 3 | test('Should return 200 response', async ({ page }) => { 4 | const response = await page.goto('/') 5 | expect(response?.status()).toBe(200) 6 | 7 | const headers = response?.headers() ?? {} 8 | expect(headers['x-via']).toBe('vite') 9 | 10 | const content = await page.textContent('h1') 11 | expect(content).toBe('Hello Vite!') 12 | }) 13 | 14 | test('Should contain an injected script tag', async ({ page }) => { 15 | await page.goto('/') 16 | 17 | const lastScriptTag = await page.$('script:last-of-type') 18 | expect(lastScriptTag).not.toBeNull() 19 | 20 | const nonce = await lastScriptTag?.getAttribute('nonce') 21 | expect(nonce).toBeNull() 22 | 23 | const content = await lastScriptTag?.textContent() 24 | expect(content).toBe('import("/@vite/client")') 25 | }) 26 | 27 | test('Should contain an injected script tag with a nonce', async ({ page }) => { 28 | await page.goto('/with-nonce') 29 | 30 | const lastScriptTag = await page.$('script:last-of-type') 31 | expect(lastScriptTag).not.toBeNull() 32 | 33 | const nonce = await lastScriptTag?.getAttribute('nonce') 34 | expect(nonce).not.toBeNull() 35 | 36 | const content = await lastScriptTag?.textContent() 37 | expect(content).toBe('import("/@vite/client")') 38 | }) 39 | 40 | test('Should exclude the file specified in the config file', async ({ page }) => { 41 | let response = await page.goto('/file.ts') 42 | expect(response?.status()).toBe(404) 43 | 44 | response = await page.goto('/ends-in-ts') 45 | expect(response?.status()).toBe(200) 46 | 47 | response = await page.goto('/app/foo') 48 | expect(response?.status()).toBe(404) 49 | 50 | response = await page.goto('/favicon.ico') 51 | expect(response?.status()).toBe(404) 52 | 53 | response = await page.goto('/static/foo.png') 54 | expect(response?.status()).toBe(404) 55 | }) 56 | 57 | test('Should return 200 response - /stream', async ({ page }) => { 58 | const response = await page.goto('/stream') 59 | expect(response?.status()).toBe(200) 60 | 61 | const headers = response?.headers() ?? {} 62 | expect(headers['x-via']).toBe('vite') 63 | 64 | const content = await page.textContent('h1') 65 | expect(content).toBe('Hello Vite!') 66 | }) 67 | 68 | test('Should serve static files in `public/static`', async ({ page }) => { 69 | const response = await page.goto('/static/hello.json') 70 | expect(response?.status()).toBe(200) 71 | 72 | const data = await response?.json() 73 | expect(data['message']).toBe('Hello') 74 | }) 75 | 76 | test('Should return a vite error page - /invalid-response', async ({ page }) => { 77 | const response = await page.goto('/invalid-response') 78 | expect(response?.status()).toBe(500) 79 | expect(await response?.text()).toContain('ErrorOverlay') 80 | }) 81 | 82 | test('Should set `workerd` as a runtime key', async ({ page }) => { 83 | const res = await page.goto('/runtime') 84 | expect(res?.ok()).toBe(true) 85 | expect(await res?.text()).toBe('bun') 86 | }) 87 | -------------------------------------------------------------------------------- /packages/dev-server/e2e-bun/mock/app.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from 'hono' 2 | import { getRuntimeKey } from 'hono/adapter' 3 | 4 | const app = new Hono() 5 | 6 | app.get('/', (c) => { 7 | c.header('x-via', 'vite') 8 | return c.html('

Hello Vite!

') 9 | }) 10 | 11 | app.get('/with-nonce', (c) => { 12 | // eslint-disable-next-line quotes -- allowing using double-quotes bc of embedded single quotes 13 | c.header('content-security-policy', "script-src-elem 'self' 'nonce-ZMuLoN/taD7JZTUXfl5yvQ==';") 14 | return c.html('

Hello Vite!

') 15 | }) 16 | 17 | app.get('/file.ts', (c) => { 18 | return c.text('console.log("exclude me!")') 19 | }) 20 | 21 | app.get('/app/foo', (c) => { 22 | return c.html('

exclude me!

') 23 | }) 24 | 25 | app.get('/ends-in-ts', (c) => { 26 | return c.text('this should not be excluded') 27 | }) 28 | 29 | app.get('/favicon.ico', (c) => { 30 | return c.text('a good favicon') 31 | }) 32 | 33 | app.get('/static/foo.png', (c) => { 34 | return c.text('a good image') 35 | }) 36 | 37 | app.get('/stream', () => { 38 | const html = new TextEncoder().encode('

Hello Vite!

') 39 | const stream = new ReadableStream({ 40 | start(controller) { 41 | controller.enqueue(html) 42 | controller.close() 43 | }, 44 | }) 45 | return new Response(stream, { 46 | headers: { 'Content-Type': 'text/html', 'x-via': 'vite' }, 47 | }) 48 | }) 49 | 50 | // @ts-expect-error the response is string 51 | app.get('/invalid-response', () => { 52 | return '

Hello!

' 53 | }) 54 | 55 | app.get('/invalid-error-response', (c) => { 56 | try { 57 | // @ts-expect-error the variable does not exist, intentionally 58 | doesNotExist = true 59 | 60 | return c.html('

Hello Vite!

') 61 | } catch (err) { 62 | return err 63 | } 64 | }) 65 | 66 | app.get('/runtime', (c) => c.text(getRuntimeKey())) 67 | 68 | export default app 69 | -------------------------------------------------------------------------------- /packages/dev-server/e2e-bun/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test' 2 | 3 | export default defineConfig({ 4 | fullyParallel: true, 5 | forbidOnly: !!process.env.CI, 6 | retries: process.env.CI ? 2 : 0, 7 | workers: process.env.CI ? 1 : undefined, 8 | use: { 9 | baseURL: 'http://localhost:6173', 10 | }, 11 | projects: [ 12 | { 13 | name: 'chromium', 14 | use: { ...devices['Desktop Chrome'] }, 15 | timeout: 5000, 16 | retries: 2, 17 | }, 18 | ], 19 | webServer: { 20 | command: 'bun --bun vite --port 6173 -c ./vite.config.ts', 21 | port: 6173, 22 | reuseExistingServer: !process.env.CI, 23 | }, 24 | }) 25 | -------------------------------------------------------------------------------- /packages/dev-server/e2e-bun/public/static/hello.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "Hello" 3 | } -------------------------------------------------------------------------------- /packages/dev-server/e2e-bun/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import devServer, { defaultOptions } from '../src' 3 | 4 | export default defineConfig(async () => { 5 | return { 6 | plugins: [ 7 | devServer({ 8 | entry: './mock/app.ts', 9 | exclude: [...defaultOptions.exclude, '/app/**'], 10 | }), 11 | ], 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /packages/dev-server/e2e/e2e.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, request } from '@playwright/test' 2 | 3 | test('Should return 200 response', async ({ page }) => { 4 | const response = await page.goto('/') 5 | expect(response?.status()).toBe(200) 6 | 7 | const headers = response?.headers() ?? {} 8 | expect(headers['x-via']).toBe('vite') 9 | 10 | const content = await page.textContent('h1') 11 | expect(content).toBe('Hello Vite!') 12 | }) 13 | 14 | test('Should contain an injected script tag', async ({ page }) => { 15 | await page.goto('/') 16 | 17 | const lastScriptTag = await page.$('script:last-of-type') 18 | expect(lastScriptTag).not.toBeNull() 19 | 20 | const nonce = await lastScriptTag?.getAttribute('nonce') 21 | expect(nonce).toBeNull() 22 | 23 | const content = await lastScriptTag?.textContent() 24 | expect(content).toBe('import("/@vite/client")') 25 | }) 26 | 27 | test('Should contain an injected script tag with a nonce', async ({ page }) => { 28 | await page.goto('/with-nonce') 29 | 30 | const lastScriptTag = await page.$('script:last-of-type') 31 | expect(lastScriptTag).not.toBeNull() 32 | 33 | const nonce = await lastScriptTag?.getAttribute('nonce') 34 | expect(nonce).not.toBeNull() 35 | 36 | const content = await lastScriptTag?.textContent() 37 | expect(content).toBe('import("/@vite/client")') 38 | }) 39 | 40 | test('Should have Cloudflare bindings', async ({ page }) => { 41 | const response = await page.goto('/name') 42 | expect(response?.status()).toBe(200) 43 | 44 | const content = await page.textContent('h1') 45 | expect(content).toBe('My name is Hono') 46 | }) 47 | 48 | test('Should not throw an error if using `waitUntil`', async ({ page }) => { 49 | const response = await page.goto('/wait-until') 50 | expect(response?.status()).toBe(200) 51 | 52 | const content = await page.textContent('h1') 53 | expect(content).toBe('Hello Vite!') 54 | }) 55 | 56 | test('Should exclude the file specified in the config file', async ({ page }) => { 57 | let response = await page.goto('/file.ts') 58 | expect(response?.status()).toBe(404) 59 | 60 | response = await page.goto('/ends-in-ts') 61 | expect(response?.status()).toBe(200) 62 | 63 | response = await page.goto('/app/foo') 64 | expect(response?.status()).toBe(404) 65 | 66 | response = await page.goto('/favicon.ico') 67 | expect(response?.status()).toBe(404) 68 | 69 | response = await page.goto('/static/foo.png') 70 | expect(response?.status()).toBe(404) 71 | }) 72 | 73 | test('Should return 200 response - /stream', async ({ page }) => { 74 | const response = await page.goto('/stream') 75 | expect(response?.status()).toBe(200) 76 | 77 | const headers = response?.headers() ?? {} 78 | expect(headers['x-via']).toBe('vite') 79 | 80 | const content = await page.textContent('h1') 81 | expect(content).toBe('Hello Vite!') 82 | }) 83 | 84 | test('Should return a vite error page - /invalid-response', async ({ page }) => { 85 | const response = await page.goto('/invalid-response') 86 | expect(response?.status()).toBe(500) 87 | expect(await response?.text()).toContain('ErrorOverlay') 88 | }) 89 | 90 | test('Should return a vite error page with stack trace - /invalid-error-response', async ({ 91 | page, 92 | }) => { 93 | const response = await page.goto('/invalid-error-response') 94 | expect(response?.status()).toBe(500) 95 | expect(await response?.text()).toContain('e2e/mock/worker.ts') 96 | }) 97 | 98 | test('Should set `workerd` as a runtime key', async ({ page }) => { 99 | const res = await page.goto('/runtime') 100 | expect(res?.ok()).toBe(true) 101 | expect(await res?.text()).toBe('workerd') 102 | }) 103 | 104 | test('Should not throw an error if accessing the `caches`', async ({ page }) => { 105 | const res = await page.goto('/cache') 106 | expect(res?.ok()).toBe(true) 107 | expect(await res?.text()).toBe('first') 108 | const resCached = await page.goto('/cache') 109 | expect(resCached?.ok()).toBe(true) 110 | // Cache API provided by `getPlatformProxy` currently do nothing. 111 | // It does **not** return cached content. 112 | expect(await resCached?.text()).not.toBe('cached') 113 | }) 114 | 115 | test('Should set `cf` properties', async ({ page }) => { 116 | const res = await page.goto('/cf') 117 | expect(res?.ok()).toBe(true) 118 | expect(await res?.json()).toEqual({ cf: true }) 119 | }) 120 | 121 | test('Should return files in the public directory', async ({ page }) => { 122 | const res = await page.goto('/hono-logo.png') 123 | expect(res?.status()).toBe(200) 124 | }) 125 | 126 | test('Should not crash when receiving a HEAD request', async () => { 127 | const apiContext = await request.newContext() 128 | const response = await apiContext.fetch('/', { 129 | method: 'HEAD', 130 | }) 131 | 132 | expect(response.status()).toBe(200) 133 | expect(response.headers()['content-type']).toMatch(/^text\/html/) 134 | const body = await response.body() 135 | expect(body.length).toBe(0) 136 | await apiContext.dispose() 137 | }) 138 | -------------------------------------------------------------------------------- /packages/dev-server/e2e/mock/worker.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from 'hono' 2 | import { getRuntimeKey } from 'hono/adapter' 3 | 4 | const app = new Hono<{ 5 | Bindings: { 6 | NAME: string 7 | } 8 | }>() 9 | 10 | app.get('/', (c) => { 11 | c.header('x-via', 'vite') 12 | return c.html('

Hello Vite!

') 13 | }) 14 | 15 | app.get('/with-nonce', (c) => { 16 | // eslint-disable-next-line quotes -- allowing using double-quotes bc of embedded single quotes 17 | c.header('content-security-policy', "script-src-elem 'self' 'nonce-ZMuLoN/taD7JZTUXfl5yvQ==';") 18 | return c.html('

Hello Vite!

') 19 | }) 20 | 21 | app.get('/name', (c) => c.html(`

My name is ${c.env.NAME}

`)) 22 | 23 | app.get('/wait-until', (c) => { 24 | const fn = async () => {} 25 | c.executionCtx.waitUntil(fn()) 26 | return c.html('

Hello Vite!

') 27 | }) 28 | 29 | app.get('/file.ts', (c) => { 30 | return c.text('console.log("exclude me!")') 31 | }) 32 | 33 | app.get('/app/foo', (c) => { 34 | return c.html('

exclude me!

') 35 | }) 36 | 37 | app.get('/ends-in-ts', (c) => { 38 | return c.text('this should not be excluded') 39 | }) 40 | 41 | app.get('/favicon.ico', (c) => { 42 | return c.text('a good favicon') 43 | }) 44 | 45 | app.get('/static/foo.png', (c) => { 46 | return c.text('a good image') 47 | }) 48 | 49 | app.get('/stream', () => { 50 | const html = new TextEncoder().encode('

Hello Vite!

') 51 | const stream = new ReadableStream({ 52 | start(controller) { 53 | controller.enqueue(html) 54 | controller.close() 55 | }, 56 | }) 57 | return new Response(stream, { 58 | headers: { 'Content-Type': 'text/html', 'x-via': 'vite' }, 59 | }) 60 | }) 61 | 62 | // @ts-expect-error the response is string 63 | app.get('/invalid-response', () => { 64 | return '

Hello!

' 65 | }) 66 | 67 | app.get('/invalid-error-response', (c) => { 68 | try { 69 | // @ts-expect-error the variable does not exist, intentionally 70 | doesNotExist = true 71 | 72 | return c.html('

Hello Vite!

') 73 | } catch (err) { 74 | return err 75 | } 76 | }) 77 | 78 | app.get('/env', (c) => { 79 | return c.json({ env: c.env }) 80 | }) 81 | 82 | app.get('/runtime', (c) => c.text(getRuntimeKey())) 83 | 84 | app.get('/cache', async (c) => { 85 | const myCache = await caches.open('myCache') 86 | const res = await myCache.match(c.req.url) 87 | if (res) { 88 | return res 89 | } 90 | await myCache.put( 91 | c.req.url, 92 | new Response('cached', { 93 | headers: { 94 | 'Cache-Control': 's-maxage=10', 95 | }, 96 | }) 97 | ) 98 | return c.text('first') 99 | }) 100 | 101 | app.get('/cf', (c) => { 102 | return c.json({ 103 | // @ts-expect-error `Request.cf` is not typed 104 | cf: typeof c.req.raw.cf === 'object' ? true : false, 105 | }) 106 | }) 107 | 108 | export default app 109 | -------------------------------------------------------------------------------- /packages/dev-server/e2e/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test' 2 | 3 | export default defineConfig({ 4 | fullyParallel: true, 5 | forbidOnly: !!process.env.CI, 6 | retries: process.env.CI ? 2 : 0, 7 | workers: process.env.CI ? 1 : undefined, 8 | use: { 9 | baseURL: 'http://localhost:6173', 10 | }, 11 | projects: [ 12 | { 13 | name: 'chromium', 14 | use: { ...devices['Desktop Chrome'] }, 15 | timeout: 5000, 16 | retries: 2, 17 | }, 18 | ], 19 | webServer: { 20 | command: 'yarn vite --port 6173 -c ./vite.config.ts', 21 | port: 6173, 22 | reuseExistingServer: !process.env.CI, 23 | }, 24 | }) 25 | -------------------------------------------------------------------------------- /packages/dev-server/e2e/public/hono-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honojs/vite-plugins/64865d5a2cdfac287e89f67d5f3cb0d2f0f54186/packages/dev-server/e2e/public/hono-logo.png -------------------------------------------------------------------------------- /packages/dev-server/e2e/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import devServer, { defaultOptions } from '../src' 3 | import cloudflareAdapter from '../src/adapter/cloudflare' 4 | 5 | export default defineConfig(async () => { 6 | return { 7 | plugins: [ 8 | devServer({ 9 | entry: './mock/worker.ts', 10 | exclude: [...defaultOptions.exclude, '/app/**'], 11 | adapter: cloudflareAdapter, 12 | }), 13 | ], 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /packages/dev-server/e2e/wrangler.toml: -------------------------------------------------------------------------------- 1 | [vars] 2 | NAME = "Hono" 3 | -------------------------------------------------------------------------------- /packages/dev-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hono/vite-dev-server", 3 | "description": "Vite dev-server plugin for Hono", 4 | "version": "0.19.1", 5 | "types": "dist/index.d.ts", 6 | "module": "dist/index.js", 7 | "type": "module", 8 | "scripts": { 9 | "test:unit": "vitest --run ./src", 10 | "test:e2e": "playwright test -c e2e/playwright.config.ts e2e/e2e.test.ts", 11 | "test:e2e:bun": "playwright test -c e2e-bun/playwright.config.ts e2e-bun/e2e.test.ts", 12 | "test": "yarn test:unit && yarn test:e2e", 13 | "build": "rimraf dist && tsup --format esm,cjs --dts && publint", 14 | "watch": "tsup --watch", 15 | "prerelease": "yarn build && yarn test", 16 | "release": "yarn publish" 17 | }, 18 | "files": [ 19 | "dist" 20 | ], 21 | "exports": { 22 | ".": { 23 | "require": { 24 | "types": "./dist/index.d.cts", 25 | "default": "./dist/index.cjs" 26 | }, 27 | "import": { 28 | "types": "./dist/index.d.ts", 29 | "default": "./dist/index.js" 30 | } 31 | }, 32 | "./cloudflare": { 33 | "types": "./dist/adapter/cloudflare.d.ts", 34 | "require": "./dist/adapter/cloudflare.cjs", 35 | "import": "./dist/adapter/cloudflare.js" 36 | }, 37 | "./node": { 38 | "types": "./dist/adapter/node.d.ts", 39 | "require": "./dist/adapter/node.cjs", 40 | "import": "./dist/adapter/node.js" 41 | }, 42 | "./bun": { 43 | "types": "./dist/adapter/bun.d.ts", 44 | "require": "./dist/adapter/bun.cjs", 45 | "import": "./dist/adapter/bun.js" 46 | } 47 | }, 48 | "typesVersions": { 49 | "*": { 50 | "types": [ 51 | "./dist/types" 52 | ], 53 | "cloudflare": [ 54 | "./dist/adapter/cloudflare.d.ts" 55 | ], 56 | "node": [ 57 | "./dist/adapter/node.d.ts" 58 | ], 59 | "bun": [ 60 | "./dist/adapter/bun.d.ts" 61 | ] 62 | } 63 | }, 64 | "author": "Yusuke Wada (https://github.com/yusukebe)", 65 | "license": "MIT", 66 | "repository": { 67 | "type": "git", 68 | "url": "https://github.com/honojs/vite-plugins.git" 69 | }, 70 | "publishConfig": { 71 | "registry": "https://registry.npmjs.org", 72 | "access": "public" 73 | }, 74 | "homepage": "https://github.com/honojs/vite-plugins", 75 | "devDependencies": { 76 | "@playwright/test": "^1.37.1", 77 | "glob": "^10.3.10", 78 | "hono": "^4.4.11", 79 | "miniflare": "^3.20240701.0", 80 | "playwright": "^1.39.0", 81 | "publint": "^0.2.5", 82 | "rimraf": "^5.0.1", 83 | "tsup": "^7.2.0", 84 | "vite": "^6.1.1", 85 | "vitest": "^0.34.6", 86 | "wrangler": "^3.63.1" 87 | }, 88 | "peerDependencies": { 89 | "hono": "*", 90 | "miniflare": "*", 91 | "wrangler": "*" 92 | }, 93 | "peerDependenciesMeta": { 94 | "hono": { 95 | "optional": false 96 | }, 97 | "miniflare": { 98 | "optional": true 99 | }, 100 | "wrangler": { 101 | "optional": true 102 | } 103 | }, 104 | "engines": { 105 | "node": ">=18.14.1" 106 | }, 107 | "dependencies": { 108 | "@hono/node-server": "^1.14.2", 109 | "minimatch": "^9.0.3" 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /packages/dev-server/src/adapter/bun.ts: -------------------------------------------------------------------------------- 1 | import type { Adapter } from '../types' 2 | 3 | export const bunAdapter = (): Adapter => { 4 | if (typeof globalThis.navigator === 'undefined') { 5 | // @ts-expect-error not typed well 6 | globalThis.navigator = { 7 | userAgent: 'Bun', 8 | } 9 | } else { 10 | Object.defineProperty(globalThis.navigator, 'userAgent', { 11 | value: 'Bun', 12 | writable: false, 13 | }) 14 | } 15 | return { 16 | env: process.env, 17 | } 18 | } 19 | 20 | export default bunAdapter 21 | -------------------------------------------------------------------------------- /packages/dev-server/src/adapter/cloudflare.ts: -------------------------------------------------------------------------------- 1 | import { WebSocketPair } from 'miniflare' 2 | import { getPlatformProxy } from 'wrangler' 3 | import type { Adapter, Env } from '../types.js' 4 | 5 | Object.assign(globalThis, { WebSocketPair }) 6 | 7 | type CloudflareAdapterOptions = { 8 | proxy: Parameters[0] 9 | } 10 | 11 | let proxy: Awaited>> | undefined = undefined 12 | 13 | export const cloudflareAdapter: (options?: CloudflareAdapterOptions) => Promise = async ( 14 | options 15 | ) => { 16 | proxy ??= await getPlatformProxy(options?.proxy) 17 | // Cache API provided by `getPlatformProxy` currently do nothing. 18 | Object.assign(globalThis, { caches: proxy.caches }) 19 | if (typeof globalThis.navigator === 'undefined') { 20 | // @ts-expect-error not typed well 21 | globalThis.navigator = { 22 | userAgent: 'Cloudflare-Workers', 23 | } 24 | } else { 25 | Object.defineProperty(globalThis.navigator, 'userAgent', { 26 | value: 'Cloudflare-Workers', 27 | writable: false, 28 | }) 29 | } 30 | 31 | Object.defineProperty(Request.prototype, 'cf', { 32 | get: function () { 33 | if (proxy !== undefined) { 34 | return proxy.cf 35 | } 36 | throw new Error('Proxy is not initialized') 37 | }, 38 | configurable: true, 39 | enumerable: true, 40 | }) 41 | 42 | return { 43 | env: proxy.env, 44 | executionContext: proxy.ctx, 45 | onServerClose: async () => { 46 | if (proxy !== undefined) { 47 | try { 48 | await proxy.dispose() 49 | } catch { 50 | /** 51 | * It throws an error if server is not running. 52 | */ 53 | } finally { 54 | proxy = undefined 55 | } 56 | } 57 | }, 58 | } 59 | } 60 | 61 | export default cloudflareAdapter 62 | -------------------------------------------------------------------------------- /packages/dev-server/src/adapter/node.ts: -------------------------------------------------------------------------------- 1 | import type { Adapter } from '../types' 2 | export const nodeAdapter = (): Adapter => { 3 | if (typeof globalThis.navigator === 'undefined') { 4 | // @ts-expect-error not typed well 5 | globalThis.navigator = { 6 | userAgent: 'Node.js', 7 | } 8 | } else { 9 | Object.defineProperty(globalThis.navigator, 'userAgent', { 10 | value: 'Node.js', 11 | writable: false, 12 | }) 13 | } 14 | return { 15 | env: process.env, 16 | } 17 | } 18 | 19 | export default nodeAdapter 20 | -------------------------------------------------------------------------------- /packages/dev-server/src/dev-server.test.ts: -------------------------------------------------------------------------------- 1 | import { devServer, defaultOptions } from './dev-server' 2 | 3 | describe('Config in the dev-server plugin', () => { 4 | it('Should return the default patterns', () => { 5 | const plugin = devServer() 6 | // @ts-expect-error plugin.config() is not typed 7 | const config = plugin.config() 8 | expect(config.server.watch.ignored).toBe(defaultOptions.ignoreWatching) 9 | }) 10 | 11 | it('Should return the user specified patterns', () => { 12 | const plugin = devServer({ 13 | ignoreWatching: [/ignore_dir/], 14 | }) 15 | // @ts-expect-error plugin.config() is not typed 16 | const config = plugin.config() 17 | expect(config.server.watch.ignored).toEqual([/ignore_dir/]) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /packages/dev-server/src/dev-server.ts: -------------------------------------------------------------------------------- 1 | import { getRequestListener } from '@hono/node-server' 2 | import { minimatch } from 'minimatch' 3 | import type { Plugin as VitePlugin, ViteDevServer, Connect } from 'vite' 4 | import fs from 'fs' 5 | import type http from 'http' 6 | import path from 'path' 7 | import type { Env, Fetch, EnvFunc, Adapter, LoadModule } from './types.js' 8 | 9 | export type DevServerOptions = { 10 | entry?: string 11 | export?: string 12 | injectClientScript?: boolean 13 | exclude?: (string | RegExp)[] 14 | ignoreWatching?: (string | RegExp)[] 15 | env?: Env | EnvFunc 16 | loadModule?: LoadModule 17 | /** 18 | * This can be used to inject environment variables into the worker from your wrangler.toml for example, 19 | * by making use of the helper function `getPlatformProxy` from `wrangler`. 20 | * 21 | * @example 22 | * 23 | * ```ts 24 | * import { defineConfig } from 'vite' 25 | * import devServer from '@hono/vite-dev-server' 26 | * import getPlatformProxy from 'wrangler' 27 | * 28 | * export default defineConfig(async () => { 29 | * const { env, dispose } = await getPlatformProxy() 30 | * return { 31 | * plugins: [ 32 | * devServer({ 33 | * adapter: { 34 | * env, 35 | * onServerClose: dispose 36 | * }, 37 | * }), 38 | * ], 39 | * } 40 | * }) 41 | * ``` 42 | * 43 | * 44 | */ 45 | adapter?: Adapter | Promise | (() => Adapter | Promise) 46 | handleHotUpdate?: VitePlugin['handleHotUpdate'] 47 | } 48 | 49 | export const defaultOptions: Required> = { 50 | entry: './src/index.ts', 51 | export: 'default', 52 | injectClientScript: true, 53 | exclude: [ 54 | /.*\.css$/, 55 | /.*\.ts$/, 56 | /.*\.tsx$/, 57 | /^\/@.+$/, 58 | /\?t\=\d+$/, 59 | /^\/favicon\.ico$/, 60 | /^\/static\/.+/, 61 | /^\/node_modules\/.*/, 62 | ], 63 | ignoreWatching: [/\.wrangler/, /\.mf/], 64 | handleHotUpdate: ({ server, modules }) => { 65 | // Force reload the page if any of the modules is SSR 66 | const isSSR = modules.some((mod) => mod._ssrModule) 67 | if (isSSR) { 68 | server.hot.send({ type: 'full-reload' }) 69 | return [] 70 | } 71 | // Apply HMR for the client-side modules 72 | }, 73 | } 74 | 75 | export function devServer(options?: DevServerOptions): VitePlugin { 76 | let publicDirPath = '' 77 | const entry = options?.entry ?? defaultOptions.entry 78 | const plugin: VitePlugin = { 79 | name: '@hono/vite-dev-server', 80 | configResolved(config) { 81 | publicDirPath = config.publicDir 82 | }, 83 | configureServer: async (server) => { 84 | async function createMiddleware(server: ViteDevServer): Promise { 85 | return async function ( 86 | req: http.IncomingMessage, 87 | res: http.ServerResponse, 88 | next: Connect.NextFunction 89 | ): Promise { 90 | if (req.url) { 91 | const filePath = path.join(publicDirPath, req.url) 92 | try { 93 | if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { 94 | return next() 95 | } 96 | } catch { 97 | // do nothing 98 | } 99 | } 100 | 101 | const exclude = options?.exclude ?? defaultOptions.exclude 102 | for (const pattern of exclude) { 103 | if (req.url) { 104 | if (pattern instanceof RegExp) { 105 | if (pattern.test(req.url)) { 106 | return next() 107 | } 108 | } else if (minimatch(req.url?.toString(), pattern)) { 109 | return next() 110 | } 111 | } 112 | } 113 | 114 | let loadModule: LoadModule 115 | 116 | if (options?.loadModule) { 117 | loadModule = options.loadModule 118 | } else { 119 | loadModule = async (server, entry) => { 120 | let appModule 121 | try { 122 | appModule = await server.ssrLoadModule(entry) 123 | } catch (e) { 124 | if (e instanceof Error) { 125 | server.ssrFixStacktrace(e) 126 | } 127 | throw e 128 | } 129 | const exportName = options?.export ?? defaultOptions.export 130 | const app = appModule[exportName] as { fetch: Fetch } 131 | if (!app) { 132 | throw new Error(`Failed to find a named export "${exportName}" from ${entry}`) 133 | } 134 | return app 135 | } 136 | } 137 | 138 | let app: { fetch: Fetch } 139 | 140 | try { 141 | app = await loadModule(server, entry) 142 | } catch (e) { 143 | return next(e) 144 | } 145 | 146 | getRequestListener( 147 | async (request): Promise => { 148 | let env: Env = {} 149 | 150 | if (options?.env) { 151 | if (typeof options.env === 'function') { 152 | env = { ...env, ...(await options.env()) } 153 | } else { 154 | env = { ...env, ...options.env } 155 | } 156 | } 157 | 158 | const adapter = await getAdapterFromOptions(options) 159 | 160 | if (adapter?.env) { 161 | env = { ...env, ...adapter.env } 162 | } 163 | 164 | const executionContext = adapter?.executionContext ?? { 165 | waitUntil: async (fn) => fn, 166 | passThroughOnException: () => { 167 | throw new Error('`passThroughOnException` is not supported') 168 | }, 169 | } 170 | 171 | const response = await app.fetch(request, env, executionContext) 172 | 173 | /** 174 | * If the response is not instance of `Response`, throw it so that it can be handled 175 | * by our custom errorHandler and passed through to Vite 176 | */ 177 | if (!(response instanceof Response)) { 178 | throw response 179 | } 180 | 181 | if ( 182 | options?.injectClientScript !== false && 183 | response.headers.get('content-type')?.match(/^text\/html/) 184 | ) { 185 | const nonce = response.headers 186 | .get('content-security-policy') 187 | ?.match(/'nonce-([^']+)'/)?.[1] 188 | const script = `import("/@vite/client")` 189 | return injectStringToResponse(response, script) ?? response 190 | } 191 | return response 192 | }, 193 | { 194 | overrideGlobalObjects: false, 195 | errorHandler: (e) => { 196 | let err: Error 197 | if (e instanceof Error) { 198 | err = e 199 | server.ssrFixStacktrace(err) 200 | } else if (typeof e === 'string') { 201 | err = new Error(`The response is not an instance of "Response", but: ${e}`) 202 | } else { 203 | err = new Error(`Unknown error: ${e}`) 204 | } 205 | 206 | next(err) 207 | }, 208 | } 209 | )(req, res) 210 | } 211 | } 212 | 213 | server.middlewares.use(await createMiddleware(server)) 214 | server.httpServer?.on('close', async () => { 215 | const adapter = await getAdapterFromOptions(options) 216 | if (adapter?.onServerClose) { 217 | await adapter.onServerClose() 218 | } 219 | }) 220 | }, 221 | handleHotUpdate: options?.handleHotUpdate ?? defaultOptions.handleHotUpdate, 222 | config: () => { 223 | return { 224 | server: { 225 | watch: { 226 | ignored: options?.ignoreWatching ?? defaultOptions.ignoreWatching, 227 | }, 228 | }, 229 | } 230 | }, 231 | } 232 | return plugin 233 | } 234 | 235 | const getAdapterFromOptions = async ( 236 | options: DevServerOptions | undefined 237 | ): Promise => { 238 | let adapter = options?.adapter 239 | if (typeof adapter === 'function') { 240 | adapter = adapter() 241 | } 242 | if (adapter instanceof Promise) { 243 | adapter = await adapter 244 | } 245 | return adapter 246 | } 247 | 248 | function injectStringToResponse(response: Response, content: string) { 249 | const stream = response.body 250 | const newContent = new TextEncoder().encode(content) 251 | 252 | if (!stream) { 253 | return null 254 | } 255 | 256 | const reader = stream.getReader() 257 | const newContentReader = new ReadableStream({ 258 | start(controller) { 259 | controller.enqueue(newContent) 260 | controller.close() 261 | }, 262 | }).getReader() 263 | 264 | const combinedStream = new ReadableStream({ 265 | async start(controller) { 266 | for (;;) { 267 | const [existingResult, newContentResult] = await Promise.all([ 268 | reader.read(), 269 | newContentReader.read(), 270 | ]) 271 | 272 | if (existingResult.done && newContentResult.done) { 273 | controller.close() 274 | break 275 | } 276 | 277 | if (!existingResult.done) { 278 | controller.enqueue(existingResult.value) 279 | } 280 | if (!newContentResult.done) { 281 | controller.enqueue(newContentResult.value) 282 | } 283 | } 284 | }, 285 | }) 286 | 287 | const headers = new Headers(response.headers) 288 | headers.delete('content-length') 289 | 290 | return new Response(combinedStream, { 291 | headers, 292 | status: response.status, 293 | }) 294 | } 295 | -------------------------------------------------------------------------------- /packages/dev-server/src/index.ts: -------------------------------------------------------------------------------- 1 | import { devServer } from './dev-server.js' 2 | export { defaultOptions } from './dev-server.js' 3 | export type { DevServerOptions } from './dev-server.js' 4 | export default devServer 5 | -------------------------------------------------------------------------------- /packages/dev-server/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { ViteDevServer } from 'vite' 2 | 3 | export type Env = Record | Promise> 4 | export type EnvFunc = () => Env | Promise 5 | export type GetEnv = (options: Options) => EnvFunc 6 | export interface ExecutionContext { 7 | waitUntil(promise: Promise): void 8 | passThroughOnException(): void 9 | } 10 | 11 | export type Fetch = ( 12 | request: Request, 13 | env: {}, 14 | executionContext: ExecutionContext 15 | ) => Promise 16 | 17 | export type LoadModule = (server: ViteDevServer, entry: string) => Promise<{ fetch: Fetch }> 18 | 19 | export interface Adapter { 20 | /** 21 | * Environment variables to be injected into the worker 22 | */ 23 | env?: Env 24 | /** 25 | * Function called when the vite dev server is closed 26 | */ 27 | onServerClose?: () => Promise 28 | /** 29 | * Implementation of waitUntil and passThroughOnException 30 | */ 31 | executionContext?: { 32 | waitUntil(promise: Promise): void 33 | passThroughOnException(): void 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/dev-server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src/" 5 | }, 6 | "include": [ 7 | "src/**/*.ts" 8 | ], 9 | "exclude": [ 10 | "src/**/*.test.ts" 11 | ] 12 | } -------------------------------------------------------------------------------- /packages/dev-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": [ 4 | "src", 5 | "test" 6 | ], 7 | "compilerOptions": { 8 | "module": "ES2022", 9 | "target": "ES2022", 10 | "types": [ 11 | "vitest/globals", 12 | "vite/client" 13 | ] 14 | }, 15 | } -------------------------------------------------------------------------------- /packages/dev-server/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { glob } from 'glob' 2 | import { defineConfig } from 'tsup' 3 | 4 | const entryPoints = glob.sync('./src/**/*.+(ts|tsx|json)', { 5 | ignore: ['./src/**/*.test.+(ts|tsx)'], 6 | }) 7 | 8 | export default defineConfig({ 9 | entry: entryPoints, 10 | dts: true, 11 | tsconfig: './tsconfig.build.json', 12 | splitting: false, 13 | minify: false, 14 | format: ['esm'], 15 | bundle: false, 16 | platform: 'node', 17 | }) 18 | -------------------------------------------------------------------------------- /packages/dev-server/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, configDefaults } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | exclude: [...configDefaults.exclude, 'e2e'], 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /packages/ssg/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @hono/vite-ssg 2 | 3 | ## 0.1.2 4 | 5 | ### Patch Changes 6 | 7 | - [#239](https://github.com/honojs/vite-plugins/pull/239) [`b27fca203320e55f74184541d35b4720e9b456c8`](https://github.com/honojs/vite-plugins/commit/b27fca203320e55f74184541d35b4720e9b456c8) Thanks [@ssssota](https://github.com/ssssota)! - Load .env.{mode} to bundle environment variable 8 | 9 | ## 0.1.1 10 | 11 | ### Patch Changes 12 | 13 | - [#209](https://github.com/honojs/vite-plugins/pull/209) [`9a39a51d34e0e6c34212fed15f49aa9248a8b4bc`](https://github.com/honojs/vite-plugins/commit/9a39a51d34e0e6c34212fed15f49aa9248a8b4bc) Thanks [@berlysia](https://github.com/berlysia)! - Preserve the SSR server instance during page rendering 14 | 15 | ## 0.1.0 16 | 17 | ### Minor Changes 18 | 19 | - [#80](https://github.com/honojs/vite-plugins/pull/80) [`b577d9c6bffa3b543a8f0bf94539a61fc139264d`](https://github.com/honojs/vite-plugins/commit/b577d9c6bffa3b543a8f0bf94539a61fc139264d) Thanks [@berlysia](https://github.com/berlysia)! - Simplify the build flow. Just run `toSSG`. Respect Vite's config. 20 | 21 | ## 0.0.1 22 | 23 | ### Patch Changes 24 | 25 | - [#59](https://github.com/honojs/vite-plugins/pull/59) [`92f1e721d4a1e1846c3fdfa121d63fafdaf8f4b3`](https://github.com/honojs/vite-plugins/commit/92f1e721d4a1e1846c3fdfa121d63fafdaf8f4b3) Thanks [@yusukebe](https://github.com/yusukebe)! - Initial release 26 | -------------------------------------------------------------------------------- /packages/ssg/README.md: -------------------------------------------------------------------------------- 1 | # @hono/vite-ssg 2 | 3 | `@hono/vite-ssg` is a Vite plugin to generate a static site from your Hono application. 4 | 5 | ## Usage 6 | 7 | ### Installation 8 | 9 | You can install `vite` and `@hono/vite-ssg` via npm. 10 | 11 | ```plain 12 | npm i -D vite @hono/vite-ssg 13 | ``` 14 | 15 | Or you can install them with Bun. 16 | 17 | ```plain 18 | bun add vite @hono/vite-ssg 19 | ``` 20 | 21 | ### Settings 22 | 23 | Add `"type": "module"` to your `package.json`. Then, create `vite.config.ts` and edit it. 24 | 25 | ```ts 26 | import { defineConfig } from 'vite' 27 | import ssg from '@hono/vite-ssg' 28 | 29 | export default defineConfig({ 30 | plugins: [ssg()], 31 | }) 32 | ``` 33 | 34 | ### Build 35 | 36 | Just run `vite build`. 37 | 38 | ```text 39 | npm exec vite build 40 | ``` 41 | 42 | Or 43 | 44 | ```text 45 | bunx --bun vite build 46 | ``` 47 | 48 | ### Deploy to Cloudflare Pages 49 | 50 | Run the `wrangler` command. 51 | 52 | ```text 53 | wrangler pages deploy ./dist 54 | ``` 55 | 56 | ## Options 57 | 58 | The options are below. 59 | 60 | ```ts 61 | type SSGOptions = { 62 | entry?: string 63 | } 64 | ``` 65 | 66 | Default values: 67 | 68 | ```ts 69 | const defaultOptions = { 70 | entry: './src/index.tsx', 71 | } 72 | ``` 73 | 74 | ## Authors 75 | 76 | - Yusuke Wada 77 | 78 | ## License 79 | 80 | MIT 81 | -------------------------------------------------------------------------------- /packages/ssg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hono/vite-ssg", 3 | "description": "Vite plugin to generate a static site from your Hono application", 4 | "version": "0.1.2", 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 | }, 24 | "typesVersions": { 25 | "*": { 26 | "types": [ 27 | "./dist/types" 28 | ] 29 | } 30 | }, 31 | "author": "Yusuke Wada (https://github.com/yusukebe)", 32 | "license": "MIT", 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/honojs/vite-plugins.git" 36 | }, 37 | "publishConfig": { 38 | "registry": "https://registry.npmjs.org", 39 | "access": "public" 40 | }, 41 | "homepage": "https://github.com/honojs/vite-plugins", 42 | "devDependencies": { 43 | "glob": "^10.3.10", 44 | "hono": "^4.2.7", 45 | "publint": "^0.1.12", 46 | "rimraf": "^5.0.1", 47 | "tsup": "^7.2.0", 48 | "vite": "^6.1.1", 49 | "vitest": "^1.2.1" 50 | }, 51 | "peerDependencies": { 52 | "hono": ">=4.0.0" 53 | }, 54 | "engines": { 55 | "node": ">=18.14.1" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/ssg/src/index.ts: -------------------------------------------------------------------------------- 1 | import { ssgBuild } from './ssg.js' 2 | export default ssgBuild 3 | -------------------------------------------------------------------------------- /packages/ssg/src/ssg.ts: -------------------------------------------------------------------------------- 1 | import type { Hono } from 'hono' 2 | import { toSSG } from 'hono/ssg' 3 | import type { Plugin, ResolvedConfig } from 'vite' 4 | import { createServer } from 'vite' 5 | import { relative } from 'node:path' 6 | 7 | type SSGOptions = { 8 | entry?: string 9 | } 10 | 11 | export const defaultOptions: Required = { 12 | entry: './src/index.tsx', 13 | } 14 | 15 | export const ssgBuild = (options?: SSGOptions): Plugin => { 16 | const virtualId = 'virtual:ssg-void-entry' 17 | const resolvedVirtualId = '\0' + virtualId 18 | 19 | const entry = options?.entry ?? defaultOptions.entry 20 | let config: ResolvedConfig 21 | return { 22 | name: '@hono/vite-ssg', 23 | apply: 'build', 24 | async config() { 25 | return { 26 | build: { 27 | rollupOptions: { 28 | input: [virtualId], 29 | }, 30 | }, 31 | } 32 | }, 33 | configResolved(resolved) { 34 | config = resolved 35 | }, 36 | resolveId(id) { 37 | if (id === virtualId) { 38 | return resolvedVirtualId 39 | } 40 | }, 41 | load(id) { 42 | if (id === resolvedVirtualId) { 43 | return 'console.log("suppress empty chunk message")' 44 | } 45 | }, 46 | async generateBundle(_outputOptions, bundle) { 47 | for (const chunk of Object.values(bundle)) { 48 | if (chunk.type === 'chunk' && chunk.moduleIds.includes(resolvedVirtualId)) { 49 | delete bundle[chunk.fileName] 50 | } 51 | } 52 | 53 | // Create a server to load the module 54 | const server = await createServer({ 55 | plugins: [], 56 | build: { ssr: true }, 57 | mode: config.mode, 58 | }) 59 | const module = await server.ssrLoadModule(entry) 60 | 61 | const app = module['default'] as Hono 62 | 63 | if (!app) { 64 | throw new Error(`Failed to find a named export "default" from ${entry}`) 65 | } 66 | 67 | const outDir = config.build.outDir 68 | 69 | const result = await toSSG( 70 | app, 71 | { 72 | writeFile: async (path, data) => { 73 | // delegate to Vite to emit the file 74 | this.emitFile({ 75 | type: 'asset', 76 | fileName: relative(outDir, path), 77 | source: data, 78 | }) 79 | }, 80 | async mkdir() { 81 | return 82 | }, 83 | }, 84 | { dir: outDir } 85 | ) 86 | 87 | server.close() 88 | 89 | if (!result.success) { 90 | throw result.error 91 | } 92 | }, 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /packages/ssg/test/app.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from 'hono' 2 | 3 | const app = new Hono() 4 | 5 | app.get('/', (c) => { 6 | return c.html('

Hello!

') 7 | }) 8 | 9 | app.get('/dynamic-import', async (c) => { 10 | // @ts-expect-error this is a test 11 | const module = await import('./sample.js') 12 | return c.text('Dynamic import works: ' + module.default) 13 | }) 14 | 15 | export default app 16 | -------------------------------------------------------------------------------- /packages/ssg/test/sample.js: -------------------------------------------------------------------------------- 1 | export default "sample!"; 2 | -------------------------------------------------------------------------------- /packages/ssg/test/ssg.test.ts: -------------------------------------------------------------------------------- 1 | import { build } from 'vite' 2 | import { describe, it, expect, beforeAll, afterAll } from 'vitest' 3 | import fs from 'node:fs' 4 | import path from 'node:path' 5 | import ssgPlugin from '../src/index' 6 | 7 | describe('ssgPlugin', () => { 8 | const testDir = './test-project' 9 | const entryFile = './test/app.ts' 10 | const outDir = path.resolve(testDir, 'dist') 11 | const outputFile = path.resolve(outDir, 'index.html') 12 | const outputFileWithDynamicImport = path.resolve(outDir, 'dynamic-import.txt') 13 | 14 | beforeAll(() => { 15 | fs.mkdirSync(testDir, { recursive: true }) 16 | }) 17 | 18 | afterAll(() => { 19 | fs.rmSync(testDir, { recursive: true, force: true }) 20 | }) 21 | 22 | it('Should generate the content correctly with the plugin', async () => { 23 | expect(fs.existsSync(entryFile)).toBe(true) 24 | 25 | await build({ 26 | plugins: [ 27 | ssgPlugin({ 28 | entry: entryFile, 29 | }), 30 | ], 31 | build: { 32 | outDir, 33 | emptyOutDir: true, 34 | }, 35 | }) 36 | 37 | expect(fs.existsSync(outputFile)).toBe(true) 38 | 39 | const output = fs.readFileSync(outputFile, 'utf-8') 40 | expect(output).toBe('

Hello!

') 41 | 42 | expect(fs.existsSync(outputFileWithDynamicImport)).toBe(true) 43 | 44 | const outputDynamicImport = fs.readFileSync(outputFileWithDynamicImport, 'utf-8') 45 | expect(outputDynamicImport).toBe('Dynamic import works: sample!') 46 | 47 | // Should not output files corresponding to a virtual entry 48 | expect(fs.existsSync(path.resolve(outDir, 'assets'))).toBe(false) 49 | }) 50 | 51 | it('Should keep other inputs politely', async () => { 52 | expect(fs.existsSync(entryFile)).toBe(true) 53 | 54 | await build({ 55 | plugins: [ 56 | ssgPlugin({ 57 | entry: entryFile, 58 | }), 59 | ], 60 | build: { 61 | rollupOptions: { 62 | input: entryFile, 63 | output: { 64 | entryFileNames: 'assets/[name].js', 65 | }, 66 | }, 67 | outDir, 68 | emptyOutDir: true, 69 | }, 70 | }) 71 | 72 | const entryOutputFile = path.resolve(testDir, 'dist', 'assets', 'app.js') 73 | 74 | expect(fs.existsSync(outputFile)).toBe(true) 75 | expect(fs.existsSync(entryOutputFile)).toBe(true) 76 | 77 | const output = fs.readFileSync(outputFile, 'utf-8') 78 | expect(output).toBe('

Hello!

') 79 | 80 | const entryOutput = fs.readFileSync(entryOutputFile, 'utf-8') 81 | expect(entryOutput.length).toBeGreaterThan(0) 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /packages/ssg/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src/" 5 | }, 6 | "include": [ 7 | "src/**/*.ts" 8 | ], 9 | "exclude": [ 10 | "src/**/*.test.ts" 11 | ] 12 | } -------------------------------------------------------------------------------- /packages/ssg/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": [ 4 | "src", 5 | "test" 6 | ], 7 | "compilerOptions": { 8 | "module": "ES2022", 9 | "target": "ES2022", 10 | "types": [ 11 | "vite/client" 12 | ] 13 | }, 14 | } -------------------------------------------------------------------------------- /packages/ssg/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { glob } from 'glob' 2 | import { defineConfig } from 'tsup' 3 | 4 | const entryPoints = glob.sync('./src/**/*.+(ts|tsx|json)', { 5 | ignore: ['./src/**/*.test.+(ts|tsx)'], 6 | }) 7 | 8 | export default defineConfig({ 9 | entry: entryPoints, 10 | dts: true, 11 | tsconfig: './tsconfig.build.json', 12 | splitting: false, 13 | minify: false, 14 | format: ['esm'], 15 | bundle: false, 16 | platform: 'node', 17 | }) 18 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ES2022", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "declaration": true, 9 | "types": [ 10 | "node" 11 | ] 12 | }, 13 | } --------------------------------------------------------------------------------