├── .github └── workflows │ ├── ci.yml │ └── jsr.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── demo ├── astro │ ├── .gitignore │ ├── README.md │ ├── astro.config.mjs │ ├── package.json │ ├── public │ │ └── favicon.svg │ ├── src │ │ ├── components │ │ │ └── Card.astro │ │ ├── env.d.ts │ │ ├── layouts │ │ │ └── Layout.astro │ │ └── pages │ │ │ └── index.astro │ └── tsconfig.json ├── basic-vite5 │ ├── .gitignore │ ├── favicon.svg │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── main.ts │ │ ├── style.css │ │ ├── typescript.svg │ │ └── vite-env.d.ts │ ├── tsconfig.json │ └── vite.config.ts └── basic │ ├── .gitignore │ ├── favicon.svg │ ├── index.html │ ├── package.json │ ├── public │ └── vite.svg │ ├── src │ ├── main.ts │ ├── style.css │ ├── typescript.svg │ └── vite-env.d.ts │ ├── tsconfig.json │ └── vite.config.ts ├── eslint.config.js ├── jsr.json ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── screenshot.png ├── src ├── index.ts ├── server.ts └── types.ts ├── test ├── _global.ts ├── _helper.ts ├── browser │ ├── dev.test.ts │ └── preview.test.ts └── unit │ ├── buildWatch.test.ts │ └── error.test.ts ├── tsconfig.json ├── tsup.config.ts ├── vitest.config.ts └── vitest.workspace.ts /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | test: 14 | runs-on: ${{ matrix.os }} 15 | 16 | strategy: 17 | matrix: 18 | node_version: [18, 20] 19 | os: [ubuntu-latest] 20 | include: 21 | # Active LTS + other OS 22 | - os: macos-latest 23 | node_version: 20 24 | - os: windows-latest 25 | node_version: 20 26 | fail-fast: false 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | 31 | - name: Install pnpm 32 | uses: pnpm/action-setup@v4 33 | 34 | - name: Use Node.js ${{ matrix.node_version }} 35 | uses: actions/setup-node@v4 36 | with: 37 | node-version: ${{ matrix.node_version }} 38 | cache: pnpm 39 | 40 | - name: Install 41 | run: pnpm install 42 | 43 | - name: Playwright Install 44 | run: npx playwright install 45 | 46 | - name: Build 47 | run: pnpm build 48 | 49 | - name: Test 50 | run: pnpm test 51 | 52 | - name: Upload artifacts on failure 53 | uses: actions/upload-artifact@v4 54 | if: failure() 55 | with: 56 | name: test-project-${{ matrix.os }}-${{ matrix.node_version }} 57 | path: test/dist/ 58 | 59 | coverage: 60 | runs-on: ubuntu-latest 61 | name: Coverage 62 | 63 | steps: 64 | - uses: actions/checkout@v4 65 | with: 66 | fetch-depth: 0 67 | 68 | - uses: pnpm/action-setup@v4 69 | 70 | - name: Set node version to 20 71 | uses: actions/setup-node@v4 72 | with: 73 | node-version: 20 74 | cache: pnpm 75 | 76 | - name: Install 77 | run: pnpm install 78 | 79 | - name: Playwright Install 80 | run: npx playwright install 81 | 82 | - name: Build 83 | run: pnpm build 84 | 85 | - name: Test 86 | run: pnpm coverage 87 | 88 | - name: Coveralls 89 | uses: coverallsapp/github-action@master 90 | with: 91 | github-token: ${{ secrets.GITHUB_TOKEN }} 92 | 93 | lint: 94 | runs-on: ubuntu-latest 95 | name: 'Lint: node-20, ubuntu-latest' 96 | 97 | steps: 98 | - uses: actions/checkout@v4 99 | with: 100 | fetch-depth: 0 101 | 102 | - uses: pnpm/action-setup@v4 103 | 104 | - name: Set node version to 20 105 | uses: actions/setup-node@v4 106 | with: 107 | node-version: 20 108 | cache: pnpm 109 | 110 | - name: Install 111 | run: pnpm install 112 | 113 | - name: Build 114 | run: pnpm build 115 | 116 | - name: Lint 117 | run: pnpm lint 118 | 119 | - name: Typecheck 120 | run: pnpm typecheck 121 | -------------------------------------------------------------------------------- /.github/workflows/jsr.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | 11 | permissions: 12 | contents: read 13 | id-token: write 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Install pnpm 19 | uses: pnpm/action-setup@v4 20 | 21 | - name: Use Node.js 20 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 20 25 | cache: pnpm 26 | 27 | - name: Install 28 | run: pnpm install 29 | 30 | - name: Publish package 31 | run: npx jsr publish --allow-slow-types 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | .idea 5 | .vscode 6 | coverage 7 | .astro 8 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false 2 | auto-install-peers=true 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024-present Loïs Boubault 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm](https://img.shields.io/npm/v/vite-plugin-browser-sync)](https://www.npmjs.com/package/vite-plugin-browser-sync) [![JSR Package](https://jsr.io/badges/@applelo/vite-plugin-browser-sync)](https://jsr.io/@applelo/vite-plugin-browser-sync) [![node-current](https://img.shields.io/node/v/vite-plugin-browser-sync)](https://nodejs.org/) [![Coverage Status](https://coveralls.io/repos/github/Applelo/vite-plugin-browser-sync/badge.svg?branch=main)](https://coveralls.io/github/Applelo/vite-plugin-browser-sync?branch=main) 2 | 3 | # vite-plugin-browser-sync 4 | 5 | Add [BrowserSync](https://browsersync.io) in your Vite project. 6 | 7 | > This plugin supports Vite 5 and 6. 8 | 9 |

10 | 11 | 12 | 13 |

14 | 15 | ## 🚀 Features 16 | 17 | - ⚡ Fully integrate in your ViteJS environment 18 | - 👌 Zero config available for common use cases 19 | - ✨ All the [BrowserSync features](https://browsersync.io/) 20 | - 🙌 Support for BrowserSync `proxy` and `snippet` mode 21 | - 🔥 Liberty to manage BrowserSync options 22 | - 🎛️ Can run on `dev`, `preview` or `build --watch` 23 | 24 | ## 📦 Install 25 | 26 | ``` 27 | npm i -D vite-plugin-browser-sync 28 | 29 | # yarn 30 | yarn add -D vite-plugin-browser-sync 31 | 32 | # pnpm 33 | pnpm add -D vite-plugin-browser-sync 34 | 35 | # bun 36 | bun add -D vite-plugin-browser-sync 37 | ``` 38 | 39 | ## 👨‍💻 Usage 40 | 41 | By default, BrowserSync will start alongside your Vite Server in `dev`. It uses the `proxy` mode of BrowserSync based on your Vite server options : no need to pass any options to make it works! 42 | 43 | ```js 44 | // vite.config.js / vite.config.ts 45 | import VitePluginBrowserSync from 'vite-plugin-browser-sync' 46 | 47 | export default { 48 | plugins: [VitePluginBrowserSync()] 49 | } 50 | ``` 51 | 52 | If you want to manage BrowserSync or [override default behavior of this plugin](https://github.com/Applelo/vite-plugin-browser-sync#vite-plugin-browser-sync-options-for-browsersync), you can pass a `bs` object with your [BrowserSync options](https://browsersync.io/docs/options) in it : 53 | 54 | ```js 55 | // vite.config.js / vite.config.ts 56 | import VitePluginBrowserSync from 'vite-plugin-browser-sync' 57 | 58 | export default { 59 | plugins: [ 60 | VitePluginBrowserSync({ 61 | dev: { 62 | bs: { 63 | ui: { 64 | port: 8080 65 | }, 66 | notify: false 67 | } 68 | } 69 | }) 70 | ] 71 | } 72 | ``` 73 | 74 | If you need the `snippet` mode of BrowserSync, the plugin supports it by injecting the script automatically. 75 | 76 | ```js 77 | // vite.config.js / vite.config.ts 78 | import VitePluginBrowserSync from 'vite-plugin-browser-sync' 79 | 80 | export default { 81 | plugins: [ 82 | VitePluginBrowserSync({ 83 | dev: { 84 | mode: 'snippet' 85 | } 86 | }) 87 | ] 88 | } 89 | ``` 90 | 91 | You can also enable the plugin on `vite build --watch` mode and `vite preview` mode. 92 | 93 | > [!IMPORTANT] 94 | > - In `buildWatch`, if you use the default `proxy` mode you need to set the `bs` object. 95 | > - `snippet` mode is available in `buildWatch` but it is not recommanded to use since it update your `index.html` file. 96 | > - In `preview`, only the `proxy` mode is supported since it will not inject the `snippet`. 97 | 98 | ```js 99 | // vite.config.js / vite.config.ts 100 | import VitePluginBrowserSync from 'vite-plugin-browser-sync' 101 | 102 | export default { 103 | plugins: [ 104 | VitePluginBrowserSync({ 105 | dev: { 106 | enable: false, 107 | }, 108 | preview: { 109 | enable: true, 110 | }, 111 | buildWatch: { 112 | enable: true, 113 | bs: { 114 | proxy: 'http://localhost:3000', 115 | } 116 | } 117 | }) 118 | ] 119 | } 120 | ``` 121 | 122 | > [!NOTE] 123 | > For Astro user, this plugin is not working in preview mode because of [overrides made by Astro](https://github.com/withastro/astro/blob/a6c4e6754493e7af5c953b644c6a19461f15467b/packages/astro/src/core/preview/static-preview-server.ts#L40). 124 | 125 | ## vite-plugin-browser-sync options for BrowserSync 126 | 127 | This plugin overrides default options from BrowserSync to doesn't duplicate behaviors already handle by ViteJS. Futhermore, your ViteJS config are synced with BrowserSync. 128 | 129 | If you want to change the overrided options you free to do so via the `bs` object. 130 | 131 | | Option | Why | dev | buildWatch | preview | 132 | |-----------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|:---:|:----------:|:-------:| 133 | | [logLevel](https://browsersync.io/docs/options#option-logLevel) | Set to `silent`, use ViteJS [printUrls](https://vitejs.dev/guide/api-javascript.html#createserver) to display BrowserSync info | ✔️ | ✔️ | ✔️ | 134 | | [open](https://browsersync.io/docs/options#option-open) | Apply ViteJS [open option](https://vitejs.dev/config/server-options.html#server-open) | ✔️ | | ✔️ | 135 | | [codeSync](https://browsersync.io/docs/options#option-codeSync) | Disabled because it is already handle by ViteJS | ✔️ | | | 136 | | [online](https://browsersync.io/docs/options#option-online) | Synced with the [server host option](https://vitejs.dev/config/server-options.html#server-host) | ✔️ | | ✔️ | 137 | 138 | ### For `proxy` mode 139 | 140 | | Option | Why | dev | buildWatch | preview | 141 | |------------------------------------------------------------------|-----------------------------------------------|:---:|------------|:-------:| 142 | | [proxy.target](https://browsersync.io/docs/options#option-proxy) | Inject the right url from ViteJS | ✔️ | | ✔️ | 143 | | [proxy.ws](https://browsersync.io/docs/options#option-proxy) | Force websocket proxy to make work ViteJS HMR | ✔️ | | ✔️ | 144 | 145 | ### For `snippet` mode 146 | 147 | | Option | Why | 148 | | ------------------------------------------------------------------- | ------------------------------------------------------ | 149 | | [logSnippet](https://browsersync.io/docs/options#option-logSnippet) | Handle by the plugin so no need to display the snippet | 150 | | [snippet](https://browsersync.io/docs/options#option-snippet) | The snippet injection is handle by the plugin | 151 | 152 | ## 👨‍💼 Licence 153 | 154 | MIT 155 | -------------------------------------------------------------------------------- /demo/astro/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | 4 | # generated types 5 | .astro/ 6 | 7 | # dependencies 8 | node_modules/ 9 | 10 | # logs 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | 23 | # jetbrains setting folder 24 | .idea/ 25 | -------------------------------------------------------------------------------- /demo/astro/README.md: -------------------------------------------------------------------------------- 1 | # Astro Starter Kit: Basics 2 | 3 | ```sh 4 | npm create astro@latest -- --template basics 5 | ``` 6 | 7 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics) 8 | [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics) 9 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json) 10 | 11 | > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! 12 | 13 | ![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554) 14 | 15 | ## 🚀 Project Structure 16 | 17 | Inside of your Astro project, you'll see the following folders and files: 18 | 19 | ```text 20 | / 21 | ├── public/ 22 | │ └── favicon.svg 23 | ├── src/ 24 | │ ├── components/ 25 | │ │ └── Card.astro 26 | │ ├── layouts/ 27 | │ │ └── Layout.astro 28 | │ └── pages/ 29 | │ └── index.astro 30 | └── package.json 31 | ``` 32 | 33 | Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. 34 | 35 | There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. 36 | 37 | Any static assets, like images, can be placed in the `public/` directory. 38 | 39 | ## 🧞 Commands 40 | 41 | All commands are run from the root of the project, from a terminal: 42 | 43 | | Command | Action | 44 | | :------------------------ | :----------------------------------------------- | 45 | | `npm install` | Installs dependencies | 46 | | `npm run dev` | Starts local dev server at `localhost:4321` | 47 | | `npm run build` | Build your production site to `./dist/` | 48 | | `npm run preview` | Preview your build locally, before deploying | 49 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | 50 | | `npm run astro -- --help` | Get help using the Astro CLI | 51 | 52 | ## 👀 Want to learn more? 53 | 54 | Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). 55 | -------------------------------------------------------------------------------- /demo/astro/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config' 2 | import VitePluginBrowserSync from 'vite-plugin-browser-sync' 3 | 4 | // https://astro.build/config 5 | export default defineConfig({ 6 | vite: { 7 | plugins: [ 8 | VitePluginBrowserSync({ 9 | dev: { 10 | enable: true, 11 | bs: { 12 | online: true, 13 | }, 14 | }, 15 | preview: { 16 | enable: true, 17 | bs: { 18 | proxy: 'http://localhost:3000', 19 | }, 20 | }, 21 | buildWatch: { 22 | enable: true, 23 | // mode: 'snippet', 24 | bs: { 25 | // server: 'dist', 26 | proxy: 'http://localhost:3000', 27 | // proxy: { 28 | // target: 'http://localhost:3000', 29 | // }, 30 | }, 31 | }, 32 | }), 33 | ], 34 | }, 35 | }) 36 | -------------------------------------------------------------------------------- /demo/astro/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astro", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "private": true, 6 | "packageManager": "pnpm@9.15.0", 7 | "scripts": { 8 | "dev": "astro dev", 9 | "start": "astro dev", 10 | "build": "astro check && astro build", 11 | "preview": "astro preview", 12 | "astro": "astro" 13 | }, 14 | "dependencies": { 15 | "@astrojs/check": "^0.9.4", 16 | "astro": "^5.0.5", 17 | "typescript": "^5.7.2", 18 | "vite-plugin-browser-sync": "workspace:*" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /demo/astro/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /demo/astro/src/components/Card.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | title: string; 4 | body: string; 5 | href: string; 6 | } 7 | 8 | const { href, title, body } = Astro.props; 9 | --- 10 | 11 | 22 | 62 | -------------------------------------------------------------------------------- /demo/astro/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /demo/astro/src/layouts/Layout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | title: string; 4 | } 5 | 6 | const { title } = Astro.props; 7 | --- 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {title} 18 | 19 | 20 | 21 | 22 | 23 | 52 | -------------------------------------------------------------------------------- /demo/astro/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../layouts/Layout.astro'; 3 | import Card from '../components/Card.astro'; 4 | --- 5 | 6 | 7 |
8 | 36 |

Welcome to Astro

37 |

38 | To get started, open the directory src/pages in your project.
39 | Code Challenge: Tweak the "Welcome to Astro" message above. 40 |

41 | 63 |
64 |
65 | 66 | 124 | -------------------------------------------------------------------------------- /demo/astro/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict" 3 | } 4 | -------------------------------------------------------------------------------- /demo/basic-vite5/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /demo/basic-vite5/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /demo/basic-vite5/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite Plugin Browser Sync 8 | 9 | 10 |

Vite Plugin Browser Sync

11 | 12 |
13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 | 22 |
23 |

24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /demo/basic-vite5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "packageManager": "pnpm@9.15.0", 7 | "scripts": { 8 | "dev": "vite", 9 | "build": "tsc && vite build", 10 | "build:watch": "vite build --watch", 11 | "preview": "vite preview" 12 | }, 13 | "devDependencies": { 14 | "typescript": "^5.7.2", 15 | "vite": "^5.4.11", 16 | "vite-plugin-browser-sync": "workspace:*" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /demo/basic-vite5/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/basic-vite5/src/main.ts: -------------------------------------------------------------------------------- 1 | import './style.css' 2 | 3 | const form = document.getElementById('form') as HTMLFormElement | null 4 | 5 | form?.addEventListener('submit', (e) => { 6 | e.preventDefault() 7 | const data = new FormData(form) 8 | 9 | const firstname = data.get('firstname') 10 | const lastname = data.get('lastname') 11 | 12 | const message = `Hello ${firstname} ${lastname} !` 13 | const messageEl = document.getElementById('message') 14 | if (messageEl) 15 | messageEl.textContent = message 16 | }) 17 | -------------------------------------------------------------------------------- /demo/basic-vite5/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: #dedede; 9 | background-color: #121212; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | body { 19 | margin: 0; 20 | padding: 5vw; 21 | min-height: 200vh; 22 | } 23 | 24 | h1 { 25 | font-size: 3.2em; 26 | line-height: 1.1; 27 | } 28 | -------------------------------------------------------------------------------- /demo/basic-vite5/src/typescript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/basic-vite5/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /demo/basic-vite5/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["ESNext", "DOM"], 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "moduleResolution": "Node", 8 | "resolveJsonModule": true, 9 | "strict": true, 10 | "noImplicitReturns": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "noEmit": true, 14 | "sourceMap": true, 15 | "esModuleInterop": true, 16 | "isolatedModules": true, 17 | "skipLibCheck": true 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /demo/basic-vite5/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import VitePluginBrowserSync from 'vite-plugin-browser-sync' 3 | 4 | export default defineConfig({ 5 | // build: { 6 | // watch: {}, 7 | // }, 8 | plugins: [ 9 | VitePluginBrowserSync({ 10 | dev: { 11 | enable: true, 12 | bs: { 13 | online: true, 14 | }, 15 | }, 16 | preview: { 17 | enable: true, 18 | bs: { 19 | proxy: 'http://localhost:3000', 20 | }, 21 | }, 22 | buildWatch: { 23 | enable: true, 24 | // mode: 'snippet', 25 | bs: { 26 | // server: 'dist', 27 | proxy: 'http://localhost:3000', 28 | // proxy: { 29 | // target: 'http://localhost:3000', 30 | // }, 31 | }, 32 | }, 33 | }), 34 | ], 35 | }) 36 | -------------------------------------------------------------------------------- /demo/basic/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /demo/basic/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /demo/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite Plugin Browser Sync 8 | 9 | 10 |

Vite Plugin Browser Sync

11 | 12 |
13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 | 22 |
23 |

24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /demo/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "packageManager": "pnpm@9.15.0", 7 | "scripts": { 8 | "dev": "vite", 9 | "build": "tsc && vite build", 10 | "build:watch": "vite build --watch", 11 | "preview": "vite preview" 12 | }, 13 | "devDependencies": { 14 | "typescript": "^5.7.2", 15 | "vite": "^6.0.3", 16 | "vite-plugin-browser-sync": "workspace:*" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /demo/basic/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/basic/src/main.ts: -------------------------------------------------------------------------------- 1 | import './style.css' 2 | 3 | const form = document.getElementById('form') as HTMLFormElement | null 4 | 5 | form?.addEventListener('submit', (e) => { 6 | e.preventDefault() 7 | const data = new FormData(form) 8 | 9 | const firstname = data.get('firstname') 10 | const lastname = data.get('lastname') 11 | 12 | const message = `Hello ${firstname} ${lastname} !` 13 | const messageEl = document.getElementById('message') 14 | if (messageEl) 15 | messageEl.textContent = message 16 | }) 17 | -------------------------------------------------------------------------------- /demo/basic/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: #dedede; 9 | background-color: #121212; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | body { 19 | margin: 0; 20 | padding: 5vw; 21 | min-height: 200vh; 22 | } 23 | 24 | h1 { 25 | font-size: 3.2em; 26 | line-height: 1.1; 27 | } 28 | -------------------------------------------------------------------------------- /demo/basic/src/typescript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/basic/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /demo/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["ESNext", "DOM"], 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "moduleResolution": "Node", 8 | "resolveJsonModule": true, 9 | "strict": true, 10 | "noImplicitReturns": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "noEmit": true, 14 | "sourceMap": true, 15 | "esModuleInterop": true, 16 | "isolatedModules": true, 17 | "skipLibCheck": true 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /demo/basic/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import VitePluginBrowserSync from 'vite-plugin-browser-sync' 3 | 4 | export default defineConfig({ 5 | // build: { 6 | // watch: {}, 7 | // }, 8 | plugins: [ 9 | VitePluginBrowserSync({ 10 | dev: { 11 | enable: true, 12 | bs: { 13 | online: true, 14 | }, 15 | }, 16 | preview: { 17 | enable: true, 18 | bs: { 19 | proxy: 'http://localhost:3000', 20 | }, 21 | }, 22 | buildWatch: { 23 | enable: true, 24 | // mode: 'snippet', 25 | bs: { 26 | // server: 'dist', 27 | proxy: 'http://localhost:3000', 28 | // proxy: { 29 | // target: 'http://localhost:3000', 30 | // }, 31 | }, 32 | }, 33 | }), 34 | ], 35 | }) 36 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // eslint.config.js 2 | import antfu from '@antfu/eslint-config' 3 | 4 | export default antfu() 5 | -------------------------------------------------------------------------------- /jsr.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@applelo/vite-plugin-browser-sync", 3 | "version": "4.0.0", 4 | "exports": "./src/index.ts", 5 | "exclude": [ 6 | "test", 7 | "demo", 8 | "pnpm-lock.yaml", 9 | "pnpm-workspace.yaml", 10 | ".npmrc", 11 | "eslint.config.js", 12 | "tsup.config.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-plugin-browser-sync", 3 | "type": "module", 4 | "version": "4.0.0", 5 | "packageManager": "pnpm@9.15.0", 6 | "description": "Add BrowserSync in your Vite project", 7 | "author": "Applelo", 8 | "license": "MIT", 9 | "homepage": "https://github.com/Applelo/vite-plugin-browser-sync", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/Applelo/vite-plugin-browser-sync" 13 | }, 14 | "bugs": "https://github.com/Applelo/vite-plugin-browser-sync/issues", 15 | "keywords": [ 16 | "browsersync", 17 | "browser-sync", 18 | "livereload", 19 | "serve", 20 | "vite", 21 | "vite-plugin" 22 | ], 23 | "exports": { 24 | ".": { 25 | "import": "./dist/index.js", 26 | "require": "./dist/index.cjs" 27 | } 28 | }, 29 | "main": "./dist/index.js", 30 | "module": "./dist/index.js", 31 | "types": "./dist/index.d.ts", 32 | "files": [ 33 | "dist" 34 | ], 35 | "engines": { 36 | "node": "^18.0.0 || >=20.0.0" 37 | }, 38 | "scripts": { 39 | "lint": "eslint .", 40 | "lint:fix": "eslint . --fix", 41 | "build": "tsup src/index.ts", 42 | "build:watch": "tsup src/index.ts --watch", 43 | "typecheck": "tsc --noEmit --skipLibCheck", 44 | "test": "vitest", 45 | "coverage": "vitest run --coverage", 46 | "prepublishOnly": "npm run build" 47 | }, 48 | "peerDependencies": { 49 | "vite": "^5.0.0 || ^6.0.0" 50 | }, 51 | "dependencies": { 52 | "@types/browser-sync": "^2.29.0", 53 | "browser-sync": "^3.0.3", 54 | "kolorist": "^1.8.0" 55 | }, 56 | "devDependencies": { 57 | "@antfu/eslint-config": "^3.12.0", 58 | "@vitest/coverage-v8": "^2.1.8", 59 | "eslint": "^9.17.0", 60 | "playwright": "^1.49.1", 61 | "rollup": "^4.28.1", 62 | "tsup": "^8.3.5", 63 | "typescript": "^5.7.2", 64 | "vite": "^6.0.3", 65 | "vitest": "^2.1.8" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - demo/** 3 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applelo/vite-plugin-browser-sync/f687afc97f5de4d4ffdf4f35d86745570aba93ca/screenshot.png -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This entry file is for the plugin. 3 | * @module 4 | */ 5 | 6 | import type { HtmlTagDescriptor, Plugin, ResolvedConfig } from 'vite' 7 | import type { Env, Options } from './types' 8 | import { italic, red } from 'kolorist' 9 | import { Server } from './server' 10 | 11 | /** 12 | * Vite plugin 13 | * 14 | * @example Basic Usage 15 | * ```ts 16 | * // vite.config.js / vite.config.ts 17 | * import VitePluginBrowserSync from 'vite-plugin-browser-sync' 18 | * 19 | * export default { 20 | * plugins: [VitePluginBrowserSync()] 21 | * } 22 | * ``` 23 | * @example With options 24 | * ```ts 25 | * // vite.config.js / vite.config.ts 26 | * import VitePluginBrowserSync from 'vite-plugin-browser-sync' 27 | * 28 | * export default { 29 | * plugins: [ 30 | * VitePluginBrowserSync({ 31 | * dev: { 32 | * bs: { 33 | * ui: { 34 | * port: 8080 35 | * }, 36 | * notify: false 37 | * } 38 | * } 39 | * }) 40 | * ] 41 | * } 42 | * ``` 43 | */ 44 | export default function VitePluginBrowserSync(options?: Options): Plugin { 45 | const name = 'vite-plugin-browser-sync' 46 | // eslint-disable-next-line node/prefer-global/process 47 | const bsClientVersion = process.env.BS_VERSION 48 | let config: ResolvedConfig 49 | let env: Env = 'dev' 50 | let bsServer: Server | null = null 51 | let started = false 52 | let applyOnDev = false 53 | let applyOnPreview = false 54 | let applyOnBuildWatch = false 55 | 56 | return { 57 | name, 58 | apply(_config, env) { 59 | if (options?.bs) { 60 | console.error( 61 | red( 62 | `[vite-plugin-browser-sync] Since 3.0, you should wrap your ${italic('bs')} option inside a ${italic('dev')} object.`, 63 | ), 64 | ) 65 | return false 66 | } 67 | 68 | applyOnDev = env.command === 'serve' 69 | && (typeof env.isPreview === 'undefined' || env.isPreview === false) 70 | && options?.dev?.enable !== false 71 | 72 | applyOnPreview = env.command === 'serve' 73 | && env.isPreview === true 74 | && options?.preview?.enable === true 75 | 76 | applyOnBuildWatch = env.command === 'build' 77 | // @ts-expect-error true exist on config object with CLI 78 | && (_config.build?.watch === true || typeof _config.build?.watch === 'object') 79 | && options?.buildWatch?.enable === true 80 | 81 | if ( 82 | applyOnBuildWatch 83 | && options?.buildWatch?.mode !== 'snippet' 84 | && typeof options?.buildWatch?.bs?.proxy !== 'string' 85 | && typeof options?.buildWatch?.bs?.proxy?.target !== 'string' 86 | ) { 87 | console.error( 88 | red( 89 | '[vite-plugin-browser-sync] You need to set a browsersync target.', 90 | ), 91 | ) 92 | return false 93 | } 94 | 95 | return applyOnDev || applyOnPreview || applyOnBuildWatch 96 | }, 97 | configResolved(_config) { 98 | config = _config 99 | }, 100 | buildStart() { 101 | if (started || !applyOnBuildWatch) 102 | return 103 | env = 'buildWatch' 104 | bsServer = new Server({ 105 | env, 106 | name, 107 | options, 108 | config, 109 | }) 110 | started = true 111 | }, 112 | async configureServer(server) { 113 | env = 'dev' 114 | bsServer = new Server({ 115 | env, 116 | name, 117 | server, 118 | options, 119 | config, 120 | }) 121 | }, 122 | async configurePreviewServer(server) { 123 | env = 'preview' 124 | bsServer = new Server({ 125 | env, 126 | name, 127 | server, 128 | options, 129 | config, 130 | }) 131 | }, 132 | transformIndexHtml: { 133 | order: 'post', 134 | handler: (html) => { 135 | const applySnippet = applyOnDev || applyOnBuildWatch 136 | if (!bsServer || bsServer.mode !== 'snippet' || !applySnippet) 137 | return html 138 | const urls: Record = bsServer.bs.getOption('urls').toJS() 139 | 140 | const bsScript: HtmlTagDescriptor = { 141 | tag: 'script', 142 | attrs: { 143 | async: '', 144 | src: `${urls.local}/browser-sync/browser-sync-client.js?v=${bsClientVersion}`, 145 | }, 146 | injectTo: 'body', 147 | } 148 | 149 | return [bsScript] 150 | }, 151 | }, 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import type { BrowserSyncInstance, Options as BrowserSyncOptions } from 'browser-sync' 2 | import type { OutputPlugin } from 'rollup' 3 | import type { ResolvedConfig } from 'vite' 4 | import type { BsMode, Env, Options, OptionsBuildWatch, OptionsDev, OptionsPreview, ViteServer } from './types' 5 | import process from 'node:process' 6 | import { create } from 'browser-sync' 7 | import { bold, lightYellow, red } from 'kolorist' 8 | 9 | const defaultPorts: Record = { 10 | dev: 5173, 11 | preview: 4173, 12 | buildWatch: null, 13 | } 14 | 15 | /** 16 | * Hook browsersync server on vite 17 | */ 18 | export class Server { 19 | private name: string 20 | private server?: ViteServer 21 | private options?: Options 22 | private config: ResolvedConfig 23 | private env: Env 24 | private bsServer: BrowserSyncInstance 25 | private logged: boolean = false 26 | 27 | constructor(obj: { 28 | name: string 29 | server?: ViteServer 30 | options?: Options 31 | config: ResolvedConfig 32 | env: Env 33 | }) { 34 | const { name, server, config, options, env } = obj 35 | this.name = name 36 | this.server = server 37 | this.config = config 38 | this.options = options 39 | this.env = env 40 | 41 | this.bsServer = create(this.name) 42 | 43 | if (typeof this.userBsOptions.logLevel === 'undefined') 44 | this.logged = true 45 | 46 | this.registerInit() 47 | this.registerClose() 48 | } 49 | 50 | /** 51 | * Get browser sync mode 52 | * @readonly 53 | */ 54 | public get mode(): BsMode { 55 | if (this.env === 'preview') 56 | return 'proxy' 57 | let mode: BsMode = this.userOptions 58 | && 'mode' in this.userOptions 59 | && this.userOptions.mode 60 | ? this.userOptions.mode 61 | : 'proxy' 62 | 63 | if (this.userBsOptions.proxy) 64 | mode = 'proxy' 65 | 66 | return mode 67 | } 68 | 69 | /** 70 | * Get browser sync instance 71 | * @readonly 72 | */ 73 | public get bs(): BrowserSyncInstance { 74 | return this.bsServer 75 | } 76 | 77 | /** 78 | * Get vite server port 79 | * @readonly 80 | */ 81 | private get port(): number | null { 82 | if (this.env === 'buildWatch' || !this.server) 83 | return null 84 | const defaultPort = defaultPorts[this.env] 85 | const configPort = this.env === 'dev' 86 | ? this.config.server.port 87 | : this.config.preview.port 88 | return configPort || defaultPort 89 | } 90 | 91 | /** 92 | * Get user options 93 | * @readonly 94 | */ 95 | private get userOptions(): OptionsPreview | OptionsBuildWatch | OptionsDev | undefined { 96 | return this.options && this.env in this.options 97 | ? this.options[this.env] 98 | : {} 99 | } 100 | 101 | /** 102 | * Get user browsersync options 103 | * @readonly 104 | */ 105 | private get userBsOptions(): BrowserSyncOptions { 106 | return this.userOptions && this.userOptions.bs ? this.userOptions.bs : {} 107 | } 108 | 109 | /** 110 | * Get Final browsersync options 111 | */ 112 | private get bsOptions(): BrowserSyncOptions { 113 | const bsOptions = this.userBsOptions 114 | 115 | if (typeof bsOptions.logLevel === 'undefined') 116 | bsOptions.logLevel = 'silent' 117 | 118 | if (this.server && typeof bsOptions.open === 'undefined') 119 | bsOptions.open = typeof this.config.server.open !== 'undefined' 120 | 121 | // Handle by vite so we disable it 122 | if (this.env === 'dev' && typeof bsOptions.codeSync === 'undefined') 123 | bsOptions.codeSync = false 124 | 125 | if (this.mode === 'snippet') { 126 | // disable log snippet because it is handle by the plugin 127 | bsOptions.logSnippet = false 128 | bsOptions.snippet = false 129 | } 130 | 131 | bsOptions.online 132 | = bsOptions.online === true 133 | || (this.server && typeof this.config.server.host !== 'undefined') 134 | || false 135 | 136 | if (this.env === 'buildWatch') 137 | return bsOptions 138 | 139 | if (this.mode === 'proxy') { 140 | let target 141 | 142 | if (this.server?.resolvedUrls?.local[0]) { 143 | target = this.server?.resolvedUrls?.local[0] 144 | } 145 | else if (this.port) { 146 | const protocol = this.config.server.https ? 'https' : 'http' 147 | target = `${protocol}://localhost:${this.port}/` 148 | } 149 | 150 | if (!bsOptions.proxy) { 151 | bsOptions.proxy = { 152 | target, 153 | ws: true, 154 | } 155 | } 156 | else if (typeof bsOptions.proxy === 'string') { 157 | bsOptions.proxy = { 158 | target: bsOptions.proxy, 159 | ws: true, 160 | } 161 | } 162 | else if ( 163 | typeof bsOptions.proxy === 'object' 164 | && !bsOptions.proxy.ws 165 | ) { 166 | bsOptions.proxy.ws = true 167 | } 168 | } 169 | 170 | return bsOptions 171 | } 172 | 173 | /** 174 | * Init browsersync server 175 | */ 176 | private init(): Promise { 177 | return new Promise((resolve, reject) => { 178 | this.bsServer.init(this.bsOptions, (error, bs) => { 179 | if (error) { 180 | this.config.logger.error( 181 | red(`[vite-plugin-browser-sync] ${error.name} ${error.message}`), 182 | { error }, 183 | ) 184 | reject(error) 185 | } 186 | resolve(bs) 187 | }) 188 | }) 189 | } 190 | 191 | /* c8 ignore start */ 192 | /** 193 | * Log browsersync infos 194 | */ 195 | private log() { 196 | const colorUrl = (url: string) => 197 | lightYellow(url.replace(/:(\d+)$/, (_, port) => `:${bold(port)}/`)) 198 | 199 | const urls: Record = this.bsServer.getOption('urls').toJS() 200 | const consoleTexts: Record = { 201 | 'local': 'Local', 202 | 'external': 'External', 203 | 'ui': 'UI', 204 | 'ui-external': 'UI External', 205 | 'tunnel': 'Tunnel', 206 | } 207 | for (const key in consoleTexts) { 208 | if (Object.prototype.hasOwnProperty.call(consoleTexts, key)) { 209 | const text = consoleTexts[key] 210 | if (Object.prototype.hasOwnProperty.call(urls, key)) { 211 | this.config.logger.info( 212 | ` ${lightYellow('➜')} ${bold( 213 | `BrowserSync - ${text}`, 214 | )}: ${colorUrl(urls[key])}`, 215 | ) 216 | } 217 | } 218 | } 219 | } 220 | 221 | /** 222 | * Register log function on vite 223 | */ 224 | private registerLog() { 225 | if (!this.logged) 226 | return 227 | 228 | if (this.server && this.env === 'dev') { 229 | // Fix for Astro 230 | let astroServer = false 231 | try { 232 | // Vite 6 233 | astroServer = 'pluginContainer' in this.server 234 | && this.server.environments.client.plugins.findIndex( 235 | plugin => plugin.name === 'astro:server', 236 | ) > -1 237 | } 238 | catch { 239 | // Vite 5 240 | astroServer = 'pluginContainer' in this.server 241 | // @ts-expect-error Vite 5 support 242 | && this.server.pluginContainer.plugins.findIndex( 243 | (plugin: OutputPlugin) => plugin.name === 'astro:server', 244 | ) > -1 245 | } 246 | 247 | if (astroServer) { 248 | setTimeout(() => this.log(), 1000) 249 | } 250 | else { 251 | const _print = this.server.printUrls 252 | this.server.printUrls = () => { 253 | _print() 254 | this.log() 255 | } 256 | } 257 | } 258 | else { 259 | this.log() 260 | } 261 | } 262 | /* c8 ignore stop */ 263 | 264 | /** 265 | * Register init 266 | */ 267 | private async registerInit() { 268 | if (this.server && 'listen' in this.server) { 269 | const _listen = this.server.listen 270 | this.server.listen = async () => { 271 | const out = await _listen() 272 | await this.init() 273 | return out 274 | } 275 | } 276 | else if (this.server) { 277 | await new Promise((resolve) => { 278 | this.server?.httpServer?.once('listening', () => { 279 | resolve(true) 280 | }) 281 | }) 282 | await this.init() 283 | } 284 | else { 285 | await this.init() 286 | } 287 | this.registerLog() 288 | } 289 | 290 | /** 291 | * Register close 292 | */ 293 | private registerClose() { 294 | if (this.server) { 295 | const _close = this.server.close 296 | this.server.close = async () => { 297 | this.bsServer.exit() 298 | await _close() 299 | } 300 | 301 | this.server.httpServer?.on('close', () => { 302 | this.bsServer.exit() 303 | }) 304 | } 305 | 306 | process.once('SIGINT', () => { 307 | this.bsServer.exit() 308 | process.exit() 309 | }) 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Options as BrowserSyncOptions } from 'browser-sync' 2 | import type { PreviewServer, ViteDevServer } from 'vite' 3 | 4 | export type BsMode = 'snippet' | 'proxy' 5 | export type BsOptions = BrowserSyncOptions 6 | export type ViteServer = ViteDevServer | PreviewServer 7 | 8 | interface PartOptions { 9 | /** 10 | * Activate BrowserSync 11 | * @default false 12 | */ 13 | enable?: boolean 14 | /** 15 | * BrowserSync options 16 | * @see https://browsersync.io/docs/options 17 | */ 18 | bs?: BsOptions 19 | } 20 | 21 | interface PartOptionsMode { 22 | /** 23 | * proxy (default): Browsersync will wrap your vhost with a proxy URL to view your site. 24 | * snippet: Inject Browsersync inside your html page 25 | */ 26 | mode?: BsMode 27 | } 28 | 29 | export interface OptionsBuildWatch extends PartOptions, PartOptionsMode { 30 | /** 31 | * Activate BrowserSync 32 | * @default true 33 | */ 34 | enable?: boolean 35 | } 36 | 37 | export interface OptionsDev extends PartOptions, PartOptionsMode { 38 | /** 39 | * Activate BrowserSync 40 | * @default true 41 | */ 42 | enable?: boolean 43 | } 44 | 45 | export interface OptionsPreview extends PartOptions {} 46 | 47 | export interface Options { 48 | dev?: OptionsDev 49 | buildWatch?: OptionsBuildWatch 50 | preview?: OptionsPreview 51 | /** 52 | * @deprecated since version 3.0 53 | */ 54 | bs?: BsOptions 55 | } 56 | 57 | export type Env = 'dev' | 'buildWatch' | 'preview' 58 | -------------------------------------------------------------------------------- /test/_global.ts: -------------------------------------------------------------------------------- 1 | import events from 'node:events' 2 | import process from 'node:process' 3 | 4 | export default function setup() { 5 | events.defaultMaxListeners = 20 6 | process.setMaxListeners(20) 7 | } 8 | -------------------------------------------------------------------------------- /test/_helper.ts: -------------------------------------------------------------------------------- 1 | import type { RollupWatcher } from 'rollup' 2 | import type { UserConfig } from 'vite' 3 | import type { Options } from '../src/types' 4 | import path from 'node:path' 5 | import { build, createServer, preview } from 'vite' 6 | import VitePluginBrowserSync from '../src' 7 | 8 | export async function devServer( 9 | plugin: Options = {}, 10 | vite: UserConfig = {}, 11 | demo: 'basic' | 'astro' = 'basic', 12 | ) { 13 | const server = await createServer({ 14 | configFile: false, 15 | root: path.resolve(__dirname, `./../demo/${demo}`), 16 | plugins: [VitePluginBrowserSync(plugin)], 17 | ...vite, 18 | }) 19 | await server.listen() 20 | 21 | return { 22 | printUrls: server.printUrls, 23 | close: server.close, 24 | } 25 | } 26 | 27 | export async function previewServer( 28 | plugin: Options['preview'] = {}, 29 | vite: UserConfig = {}, 30 | ) { 31 | const previewServer = await preview({ 32 | configFile: false, 33 | root: path.resolve(__dirname, './../demo/basic'), 34 | plugins: [ 35 | VitePluginBrowserSync({ 36 | preview: { 37 | enable: true, 38 | ...plugin, 39 | }, 40 | }), 41 | ], 42 | ...vite, 43 | }) 44 | 45 | const closePromise = new Promise( 46 | resolve => previewServer.httpServer.on('close', () => { 47 | resolve(true) 48 | }), 49 | ) 50 | 51 | const close = async () => { 52 | previewServer.httpServer.close() 53 | await closePromise 54 | } 55 | 56 | return { 57 | printUrls: previewServer.printUrls, 58 | close, 59 | } 60 | } 61 | 62 | export async function buildWatchServer( 63 | name: string, 64 | plugin: Options['buildWatch'] = {}, 65 | vite: UserConfig = {}, 66 | ) { 67 | const watcher = await new Promise((resolve) => { 68 | const watcher = build({ 69 | configFile: false, 70 | root: path.resolve(__dirname, './../demo/basic'), 71 | build: { 72 | watch: {}, 73 | emptyOutDir: true, 74 | outDir: path.resolve(__dirname, `./unit/dist/buildWatch_${name}`), 75 | }, 76 | plugins: [ 77 | VitePluginBrowserSync({ 78 | buildWatch: { 79 | enable: true, 80 | ...plugin, 81 | }, 82 | }), 83 | { 84 | enforce: 'post', 85 | name: 'test', 86 | closeBundle() { 87 | resolve(watcher as any as RollupWatcher) 88 | }, 89 | }, 90 | ], 91 | ...vite, 92 | }) 93 | }) 94 | return { 95 | close: watcher.close, 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /test/browser/dev.test.ts: -------------------------------------------------------------------------------- 1 | import type { Browser, Page } from 'playwright' 2 | import type { UserConfig } from 'vite' 3 | import type { Options } from '../../src/types' 4 | import { chromium } from 'playwright' 5 | import { 6 | afterAll, 7 | afterEach, 8 | beforeAll, 9 | beforeEach, 10 | describe, 11 | expect, 12 | it, 13 | } from 'vitest' 14 | import { devServer } from './../_helper' 15 | 16 | let browser: Browser 17 | let page: Page 18 | 19 | beforeAll(async () => { 20 | browser = await chromium.launch() 21 | }) 22 | beforeEach(async () => { 23 | page = await browser.newPage() 24 | }) 25 | afterEach(async () => { 26 | await page.close() 27 | }) 28 | afterAll(async () => { 29 | await browser.close() 30 | }) 31 | 32 | interface TestConfig { 33 | vite: UserConfig 34 | plugin: Options['dev'] 35 | url: string 36 | } 37 | 38 | const configProxy: Record = { 39 | 'default': { 40 | vite: {}, 41 | plugin: {}, 42 | url: 'http://localhost:3000', 43 | }, 44 | 'custom vitejs port': { 45 | vite: { 46 | server: { 47 | port: 3000, 48 | }, 49 | }, 50 | plugin: {}, 51 | url: 'http://localhost:3001', 52 | }, 53 | 'custom browsersync proxy': { 54 | vite: {}, 55 | plugin: { 56 | bs: { 57 | proxy: 'http://localhost:5173', 58 | }, 59 | }, 60 | url: 'http://localhost:3000', 61 | }, 62 | 'custom browsersync proxy object': { 63 | vite: {}, 64 | plugin: { 65 | bs: { 66 | proxy: { target: 'http://localhost:5173' }, 67 | }, 68 | }, 69 | url: 'http://localhost:3000', 70 | }, 71 | 'custom browsersync proxy and vitejs port': { 72 | vite: { 73 | server: { 74 | port: 3000, 75 | }, 76 | }, 77 | plugin: { 78 | bs: { 79 | proxy: 'http://localhost:3000', 80 | port: 5174, 81 | }, 82 | }, 83 | url: 'http://localhost:5174', 84 | }, 85 | } 86 | 87 | describe('proxy option', () => { 88 | const demos = ['basic', 'astro'] 89 | demos.forEach((demo) => { 90 | for (const [name, { vite, plugin, url }] of Object.entries(configProxy)) { 91 | it(`${demo} - ${name}`, async () => { 92 | const { close } = await devServer({ dev: plugin }, vite, demo as 'basic' | 'astro') 93 | await page.waitForTimeout(100) 94 | // need to use playwright to test the proxy 95 | await page.goto(url) 96 | const script = page.locator( 97 | 'script[src^="/browser-sync/browser-sync-client.js?v="]', 98 | ) 99 | 100 | await close() 101 | expect(script).not.toBeNull() 102 | }) 103 | } 104 | }) 105 | }) 106 | 107 | it('snippet option', async () => { 108 | const { close } = await devServer({ dev: { mode: 'snippet' } }) 109 | 110 | await page.goto('http://localhost:5173') 111 | const script = page.locator( 112 | 'script[src^="http://localhost:3000/browser-sync/browser-sync-client.js?v="]', 113 | ) 114 | 115 | await close() 116 | expect(script).not.toBeNull() 117 | }) 118 | -------------------------------------------------------------------------------- /test/browser/preview.test.ts: -------------------------------------------------------------------------------- 1 | import type { Browser, Page } from 'playwright' 2 | import type { UserConfig } from 'vite' 3 | import type { Options } from '../../src/types' 4 | import { chromium } from 'playwright' 5 | import { 6 | afterAll, 7 | afterEach, 8 | beforeAll, 9 | beforeEach, 10 | describe, 11 | expect, 12 | it, 13 | } from 'vitest' 14 | import { previewServer } from './../_helper' 15 | 16 | let browser: Browser 17 | let page: Page 18 | 19 | beforeAll(async () => { 20 | browser = await chromium.launch() 21 | }) 22 | beforeEach(async () => { 23 | page = await browser.newPage() 24 | }) 25 | afterEach(async () => { 26 | await page.close() 27 | }) 28 | afterAll(async () => { 29 | await browser.close() 30 | }) 31 | 32 | interface TestConfig { 33 | vite: UserConfig 34 | plugin: Options['preview'] 35 | url: string 36 | } 37 | 38 | const configProxy: Record = { 39 | 'default': { 40 | vite: {}, 41 | plugin: {}, 42 | url: 'http://localhost:3000', 43 | }, 44 | 'custom vitejs port': { 45 | vite: { 46 | preview: { 47 | port: 3000, 48 | }, 49 | }, 50 | plugin: {}, 51 | url: 'http://localhost:3001', 52 | }, 53 | 'custom browsersync proxy': { 54 | vite: {}, 55 | plugin: { 56 | bs: { 57 | proxy: 'http://localhost:4173', 58 | }, 59 | }, 60 | url: 'http://localhost:3000', 61 | }, 62 | 'custom browsersync proxy object': { 63 | vite: {}, 64 | plugin: { 65 | bs: { 66 | proxy: { target: 'http://localhost:4173' }, 67 | }, 68 | }, 69 | url: 'http://localhost:3000', 70 | }, 71 | 'custom browsersync proxy and vitejs port': { 72 | vite: { 73 | preview: { 74 | port: 3000, 75 | }, 76 | }, 77 | plugin: { 78 | bs: { 79 | proxy: 'http://localhost:3000', 80 | port: 4173, 81 | }, 82 | 83 | }, 84 | url: 'http://localhost:4173', 85 | }, 86 | } 87 | 88 | describe('proxy option', () => { 89 | for (const [name, { vite, plugin, url }] of Object.entries(configProxy)) { 90 | it(name, async () => { 91 | const { printUrls, close } = await previewServer(plugin, vite) 92 | printUrls() 93 | await page.waitForTimeout(100) 94 | 95 | await page.goto(url) 96 | const script = page.locator( 97 | 'script[src^="/browser-sync/browser-sync-client.js?v="]', 98 | ) 99 | 100 | expect(script).not.toBeNull() 101 | await close() 102 | }) 103 | } 104 | }) 105 | -------------------------------------------------------------------------------- /test/unit/buildWatch.test.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'node:fs' 2 | import path from 'node:path' 3 | import { 4 | expect, 5 | it, 6 | } from 'vitest' 7 | import { buildWatchServer } from './../_helper' 8 | 9 | it('snippet option', async () => { 10 | const { close } = await buildWatchServer('snippet', { mode: 'snippet' }) 11 | const html = await fs.readFile(path.resolve(__dirname, './dist/buildWatch_snippet/index.html')) 12 | expect(html).not.toBeNull() 13 | expect(html.toString()).toMatch(/