From dccf8f086fa285418fae6e34456f422b097b5b85 Mon Sep 17 00:00:00 2001 From: aabccd021 Date: Thu, 5 Dec 2024 11:15:29 +0700 Subject: [PATCH 1/3] v1.0 --- .envrc | 3 - .github/workflows/test.yml | 27 +++++ README.md | 145 +++++++++++++++++--------- bun.lockb | Bin 48537 -> 4582 bytes example.ts | 21 ++-- flake.lock | 6 +- flake.nix | 16 ++- index.ts | 203 ++++++++++++++++--------------------- package.json | 23 ++--- test/reloads.ts | 41 ++++++++ tsconfig.json | 25 ++--- 11 files changed, 292 insertions(+), 218 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 test/reloads.ts diff --git a/.envrc b/.envrc index 9307b21..d4b93ce 100644 --- a/.envrc +++ b/.envrc @@ -1,5 +1,2 @@ -if ! has nix_direnv_version || ! nix_direnv_version 2.1.1; then - source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.1.1/direnvrc" "sha256-b6qJ4r34rbE23yWjMqbmu3ia2z4b2wIlZUksBke/ol0=" -fi use flake layout node diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..f3eb187 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,27 @@ +on: + workflow_dispatch: + push: + branches-ignore: + - main + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: DeterminateSystems/nix-installer-action@main + + - uses: HatsuneMiku3939/direnv-action@v1 + + - run: bun install + + - run: bun run prettier-check + + - run: bun run typescript-check + + - run: bun test/reloads.ts + + - uses: JS-DevTools/npm-publish@v3 + with: + token: ${{ secrets.NPM_TOKEN }} diff --git a/README.md b/README.md index 8870d73..d48ae42 100644 --- a/README.md +++ b/README.md @@ -2,86 +2,135 @@ HTML live reload for Bun -## Installation +## Getting Started ```sh bun add -d bun-html-live-reload ``` -## Getting Started - ```ts // example.ts import { withHtmlLiveReload } from "bun-html-live-reload"; -export default withHtmlLiveReload({ - fetch: () => { +Bun.serve({ + fetch: withHtmlLiveReload(async (request) => { return new Response("
hello world
", { headers: { "Content-Type": "text/html" }, }); - }, + }), }); ``` -- Wrap your regular [hot reloading Bun server](https://bun.sh/docs/runtime/hot#http-servers) with `withHtmlLiveReload`. - Run the server with `bun --hot example.ts`, open browser, and try to edit the `hello world` part. - The page should live reload as you edit! - -## Response Header - -This plugin relies on response header to identify html file, -so don't forget to add `{ headers: { "Content-Type": "text/html" }, }` to your `Response`. +- This plugin relies on response header to identify html response, + so don't forget to add `"Content-Type": "text/html"` header to your `Response`. ## Options -### `wsPath` +### `eventPath` and `scriptPath` + +You can specify URL paths used for server-sent events and live reloader script. -URL path used for websocket connection. +```ts +Bun.serve({ + fetch: withHtmlLiveReload( + async (request) => { + /* ... */ + }, + { + // SSE Path + // default: "/__dev__/reload" + eventPath: "/__reload", + + // Live reload script path + // default: "/__dev__/reload.js" + scriptPath: "/__reload.js", + }, + ), +}); +``` -This library relies on websocket to live reload an HTML. -The path `wsPath` will be used to upgrade HTTP connection to websocket one. +## Manually reload clients -For example, the default `wsPath` value `__bun_live_reload_websocket__`, -will upgrade `http://localhost:3000/__bun_live_reload_websocket__` -to `ws://localhost:3000/__bun_live_reload_websocket__`. +You can manually reload clients (refresh tabs) by calling `reloadClients` function. ```ts -export default withHtmlLiveReload( - { - fetch: () => { - return new Response("
hello world
", { - headers: { "Content-Type": "text/html" }, - }); - }, - }, - { - wsPath: "your_ws_path", - } -); +import { withHtmlLiveReload, reloadClients } from "bun-html-live-reload"; + +Bun.serve({ + fetch: withHtmlLiveReload(async (request) => { + /* ... */ + }), +}); + +// reload clients every second +setInterval(() => { + reloadClients(); +}, 1000); ``` -### React HMR: `watchPath`, `buildConfig`, and `onChange` +# Changes from v0.1 -The `watchPath` is the file or folder path that should be watched to trigger the reloads. This could be used to reload html files on changing files in other folders like `src` for react projects. +- Messages are sent through SSE (HTTP streaming) instead of Websocket. +- Wraps only `fetch` function instead of the whole server. +- Exposes `reloadClients` function to manually reload clients. +- Uses separate javascript file instead of inline script to comply with strict CSP. +- Supports multiple clients (tabs). +- Added tests -The `buildConfig` is used for running the `Bun.build()` command when the files in the `watchPath` change. The `Bun.build()` command will always be run once before starting the server. +# Migration from v0.1 -The `onChange` is a function which runs before `Bun.build()` when using `buildConfig` when the files in `watchPath` change. This command does not run at start. +## v0.1 ```ts -export default withHtmlLiveReload( - { - ... - }, - { - watchPath: path.resolve(import.meta.dir, "src"), - buildConfig: { - entrypoints: ["./src/index.tsx"], - outdir: "./build" +import { withHtmlLiveReload } from "bun-html-live-reload"; +import { $ } from "bun"; + +export default Bun.serve( + withHtmlLiveReload( + { + fetch: (request) => { + /* ... */ + }, + }, + { + watchPath: path.resolve(import.meta.dir, "src"), + buildConfig: { + entrypoints: ["./src/index.tsx"], + outdir: "./build", + }, + onChange: async () => { + await $`rm -r ./dist`; + }, }, - onChange: async () => { - await $`rm -r ./dist` - } - } + ), ); ``` + +## v1.0 + +```ts +import { withHtmlLiveReload, reloadClients } from "bun-html-live-reload"; +import { FSWatcher, watch } from "fs"; +import { $ } from "bun"; + +const buildConfig = { + entrypoints: ["./src/index.tsx"], + outdir: "./build", +}; + +Bun.build(buildConfig); + +watch(path.resolve(import.meta.url, "src")).on("change", async () => { + await $`rm -r ./dist`; + await Bun.build(buildConfig); + reloadClients(); +}); + +Bun.serve({ + fetch: withHtmlLiveReload(async (request) => { + /* ... */ + }), +}); +``` diff --git a/bun.lockb b/bun.lockb index db8389e7088d64446922fb36667e519744ce6b22..2611ab59c7238d0eafaf68f2c32c06edffe5c965 100755 GIT binary patch literal 4582 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 diff --git a/example.ts b/example.ts index df796ab..28851e2 100644 --- a/example.ts +++ b/example.ts @@ -1,14 +1,9 @@ -import { withHtmlLiveReload } from "./index.js"; +import { withHtmlLiveReload } from "./index.ts"; -export default withHtmlLiveReload( - { - fetch: () => { - return new Response("
hello world
", { - headers: { "Content-Type": "text/html" }, - }); - }, - }, - { - wsPath: "your_ws_path", - } -); +Bun.serve({ + fetch: withHtmlLiveReload(async () => { + return new Response("
Init
", { + headers: { "Content-Type": "text/html" }, + }); + }), +}); diff --git a/flake.lock b/flake.lock index fefca3a..ff2e65c 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1720368505, - "narHash": "sha256-5r0pInVo5d6Enti0YwUSQK4TebITypB42bWy5su3MrQ=", + "lastModified": 1733229606, + "narHash": "sha256-FLYY5M0rpa5C2QAE3CKLYAM6TwbKicdRK6qNrSHlNrE=", "owner": "nixos", "repo": "nixpkgs", - "rev": "ab82a9612aa45284d4adf69ee81871a389669a9e", + "rev": "566e53c2ad750c84f6d31f9ccb9d00f823165550", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 83a44c6..b2b6efb 100644 --- a/flake.nix +++ b/flake.nix @@ -3,11 +3,21 @@ nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; }; - outputs = { self, nixpkgs }: with nixpkgs.legacyPackages.x86_64-linux; { - devShell.x86_64-linux = mkShellNoCC { + outputs = { nixpkgs, ... }: + let + pkgs = nixpkgs.legacyPackages.x86_64-linux; + + in + { + + devShells.x86_64-linux.default = pkgs.mkShellNoCC { + shellHook = '' + export PLAYWRIGHT_BROWSERS_PATH=${pkgs.playwright-driver.browsers-chromium} + ''; buildInputs = [ - bun + pkgs.bun ]; }; + }; } diff --git a/index.ts b/index.ts index 7fbd5e1..e370f9a 100644 --- a/index.ts +++ b/index.ts @@ -1,138 +1,109 @@ -import type { - Server, - ServerWebSocket, - WebSocketHandler, - WebSocketServeOptions, - BuildConfig, -} from "bun"; -import { FSWatcher, watch } from "fs"; - declare global { - var ws: ServerWebSocket | undefined; + var clients: Set | undefined; } -const reloadCommand = "reload"; +globalThis.clients ??= new Set(); -globalThis.ws?.send(reloadCommand); +export function reloadClients(): void { + if (globalThis.clients !== undefined) { + for (const client of globalThis.clients) { + client.enqueue(`data: RELOAD\n\n`); + } + } +} -const makeLiveReloadScript = (wsUrl: string) => ` - - - -`; - -export type PureWebSocketServeOptions = Omit< - WebSocketServeOptions, - "fetch" | "websocket" -> & { - fetch(request: Request, server: Server): Promise | Response; - websocket?: WebSocketHandler; -}; +reloadClients(); export type LiveReloadOptions = { /** - * URL path used for websocket connection - * @default "__bun_live_reload_websocket__" + * URL path used for server-sent events + * @default "__dev__/reload" */ - readonly wsPath?: string; - readonly buildConfig?: BuildConfig; - readonly watchPath?: string; - readonly onChange?: () => Promise | void; + readonly eventPath?: string; + + /** + * URL path used for live reload script + * @default "__dev__/ws" + */ + readonly scriptPath?: string; }; +type Fetch = (req: Request) => Promise; + /** * Automatically reload html when Bun server hot reloads * - * @param serverOptions Bun's server options + * @param fetch Bun server's fetch function * @param options Live reload options * - * @returns Bun's server with provided options that live reloads HTML - * - * @example - *```ts - *import { withHtmlLiveReload } from "bun-html-live-reload"; - * - *export default withHtmlLiveReload({ - * fetch: () => { - * return new Response("
hello world
", { - * headers: { "Content-Type": "text/html" }, - * }); - * }, - *}); + * @returns fetch function with live reload */ -export const withHtmlLiveReload = < - WebSocketDataType, - T extends PureWebSocketServeOptions ->( - serveOptions: T, - options?: LiveReloadOptions -): WebSocketServeOptions => { - const wsPath = options?.wsPath ?? "__bun_live_reload_websocket__"; - - const { buildConfig, watchPath, onChange } = options ?? {}; - if (buildConfig) Bun.build(buildConfig); - let watcher: FSWatcher; - if (watchPath) watcher = watch(watchPath); - - return { - ...serveOptions, - fetch: async (req, server) => { - const reqUrl = new URL(req.url); - if (reqUrl.pathname === '/' + wsPath) { - const upgraded = server.upgrade(req); - - if (!upgraded) { - return new Response( - "Failed to upgrade websocket connection for live reload", - { status: 400 } - ); - } - return; - } - - const response = await serveOptions.fetch(req, server); - - if (!response.headers.get("Content-Type")?.startsWith("text/html")) { - return response; - } - - const liveReloadScript = makeLiveReloadScript(`${reqUrl.host}/${wsPath}`); - - const rewriter = new HTMLRewriter(); - rewriter.onDocument({ - end(end) { - end.append(liveReloadScript, { - html: true +export function withHtmlLiveReload( + handler: Fetch, + options?: { + eventPath?: string; + scriptPath?: string; + }, +): Fetch { + return async (req) => { + if (req.method !== "GET") { + return handler(req); + } + + const requestUrl = new URL(req.url); + + const { eventPath, scriptPath } = { + eventPath: "/__dev__/reload", + scriptPath: "/__dev__/reload.js", + ...options, + }; + + if (requestUrl.pathname === eventPath) { + const stream = new ReadableStream({ + start(controller): void { + globalThis.clients?.add(controller); + req.signal.addEventListener("abort", () => { + controller.close(); + globalThis.clients?.delete(controller); }); }, + }); + + return new Response(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + }, + }); + } + + if (requestUrl.pathname === scriptPath) { + return new Response( + `new EventSource("${eventPath}").onmessage = function(msg) { + if(msg.data === 'RELOAD') { location.reload(); } + };`, + { headers: { "Content-Type": "text/javascript" } }, + ); + } + + const response = await handler(req); + + const contentType = response.headers.get("Content-Type"); + const isResponseHtml = contentType?.startsWith("text/html") ?? false; + if (!isResponseHtml) { + return response; + } + + const liveReloadScript = ``; + + const output = new HTMLRewriter() + .onDocument({ + end: (el) => { + el.append(liveReloadScript, { html: true }); + }, }) + .transform(response); - const output = rewriter.transform(response); - return new Response(await output.blob(), output); - }, - websocket: { - ...serveOptions.websocket, - open: async (ws) => { - globalThis.ws = ws; - await serveOptions.websocket?.open?.(ws); - - if (watcher) - watcher.on("change", async () => { - if (onChange) await onChange(); - if (buildConfig) await Bun.build(buildConfig); - ws.send(reloadCommand); - }); - }, - }, + return new Response(await output.blob(), output); }; -}; +} diff --git a/package.json b/package.json index bda3fa2..2758db5 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,11 @@ { "name": "bun-html-live-reload", "description": "HTML live reload for Bun", - "version": "0.1.4", + "version": "1.0.0", "module": "index.ts", "type": "module", "author": "aabccd021", "license": "MIT", - "eslintConfig": { - "extends": [ - "plugin:prettier/recommended" - ], - "parser": "@typescript-eslint/parser" - }, "repository": { "type": "git", "url": "git+https://github.com/aabccd021/bun-html-live-reload.git" @@ -29,16 +23,13 @@ "index.ts" ], "scripts": { - "start": "bun --hot example.ts", - "lint": "eslint . --ext ts" + "prettier-check": "prettier --check .", + "typescript-check": "tsc" }, "devDependencies": { - "@typescript-eslint/parser": "^5.54.0", - "bun-types": "^0.7.3", - "eslint": "^8.35.0", - "eslint-config-prettier": "^8.6.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.4", - "typescript": "^4.9.5" + "@types/bun": "^1.1.14", + "playwright": "1.47.2", + "prettier": "^3.4.2", + "typescript": "^5.7.2" } } diff --git a/test/reloads.ts b/test/reloads.ts new file mode 100644 index 0000000..30232ab --- /dev/null +++ b/test/reloads.ts @@ -0,0 +1,41 @@ +import { chromium } from "playwright"; +import * as fs from "fs"; + +const systemTmp = process.env["TMPDIR"] ?? "/tmp"; +const tmpdir = fs.mkdtempSync(`${systemTmp}/bun-`); + +fs.copyFileSync(`${import.meta.dir}/../example.ts`, `${tmpdir}/example.ts`); +fs.copyFileSync(`${import.meta.dir}/../index.ts`, `${tmpdir}/index.ts`); + +const child = Bun.spawn(["bun", "--hot", `${tmpdir}/example.ts`], { + stderr: "ignore", +}); + +process.on("exit", async () => { + child?.kill(); + await browser.close(); +}); + +const browser = await chromium.launch(); + +const context = await browser.newContext(); +const page = await context.newPage(); +await page.goto("http://localhost:3000"); + +const name = await page.innerText("div"); +if (name !== "Init") { + throw new Error(`Unexpected content ${name}`); +} + +const text = await Bun.file(`${tmpdir}/example.ts`).text(); +const newText = text.replace("Init", "Changed"); +await Bun.write(`${tmpdir}/example.ts`, newText); + +await page.waitForEvent("framenavigated"); + +const newName = await page.innerText("div"); +if (newName !== "Changed") { + throw new Error(`Unexpected content: ${newName}`); +} + +process.exit(0); diff --git a/tsconfig.json b/tsconfig.json index ac35178..7e19c82 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,19 +1,13 @@ { "compilerOptions": { - "allowJs": true, - "allowSyntheticDefaultImports": true, + "allowImportingTsExtensions": true, "allowUnreachableCode": false, "allowUnusedLabels": false, - "checkJs": true, - "downlevelIteration": true, - "esModuleInterop": true, - "exactOptionalPropertyTypes": true, - "forceConsistentCasingInFileNames": true, - "lib": [ - "esNext" - ], - "module": "esnext", - "moduleResolution": "nodenext", + "lib": ["ESNext"], + "module": "ESNext", + "moduleDetection": "force", + "moduleResolution": "bundler", + "noEmit": true, "noFallthroughCasesInSwitch": true, "noImplicitOverride": true, "noImplicitReturns": true, @@ -23,9 +17,8 @@ "noUnusedParameters": true, "skipLibCheck": true, "strict": true, - "target": "esnext", - "types": [ - "bun-types" - ] + "target": "ESNext", + "types": ["bun-types"], + "verbatimModuleSyntax": true } } From e641b8c05f3482e3b32e466263f27bd92eb4ba2d Mon Sep 17 00:00:00 2001 From: aabccd021 Date: Thu, 5 Dec 2024 14:15:33 +0700 Subject: [PATCH 2/3] docs: Update deployment and testing workflows --- .github/workflows/deploy.yml | 13 +++++++++++++ .github/workflows/test.yml | 4 ---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index fb0703b..7deeb11 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -7,6 +7,19 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + + - uses: DeterminateSystems/nix-installer-action@main + + - uses: HatsuneMiku3939/direnv-action@v1 + + - run: bun install + + - run: bun run prettier-check + + - run: bun run typescript-check + + - run: bun test/reloads.ts + - uses: JS-DevTools/npm-publish@v3 with: token: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f3eb187..e41b14f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,3 @@ jobs: - run: bun run typescript-check - run: bun test/reloads.ts - - - uses: JS-DevTools/npm-publish@v3 - with: - token: ${{ secrets.NPM_TOKEN }} From 52b660351986246c809f4c6eff5310f27038fd63 Mon Sep 17 00:00:00 2001 From: aabccd021 Date: Thu, 5 Dec 2024 14:16:20 +0700 Subject: [PATCH 3/3] chore: Bump up version to 1.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2758db5..a124646 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bun-html-live-reload", "description": "HTML live reload for Bun", - "version": "1.0.0", + "version": "1.0.1", "module": "index.ts", "type": "module", "author": "aabccd021",