{"id":24083397,"url":"https://github.com/mitranim/afr","last_synced_at":"2025-04-30T18:23:23.909Z","repository":{"id":48590478,"uuid":"243192147","full_name":"mitranim/afr","owner":"mitranim","description":"Always FResh: Deno library for serving files, with optional client integration for CSS injection and page reload. Simpler, better alternative to tools like Browsersync and Livereload","archived":false,"fork":false,"pushed_at":"2022-02-08T13:14:36.000Z","size":226,"stargazers_count":9,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-26T06:56:55.510Z","etag":null,"topics":["browsersync","deno","devserver","file-server","http","livereload","reload","watch"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"unlicense","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mitranim.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-02-26T07:07:14.000Z","updated_at":"2024-09-02T13:40:47.000Z","dependencies_parsed_at":"2022-08-27T11:12:33.080Z","dependency_job_id":null,"html_url":"https://github.com/mitranim/afr","commit_stats":null,"previous_names":[],"tags_count":18,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mitranim%2Fafr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mitranim%2Fafr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mitranim%2Fafr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mitranim%2Fafr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mitranim","download_url":"https://codeload.github.com/mitranim/afr/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251758780,"owners_count":21639108,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["browsersync","deno","devserver","file-server","http","livereload","reload","watch"],"created_at":"2025-01-09T23:56:34.060Z","updated_at":"2025-04-30T18:23:23.885Z","avatar_url":"https://github.com/mitranim.png","language":"TypeScript","readme":"## Overview\n\n`afr`: **A**lways **Fr**esh. Tiny library for [Deno](https://deno.land). Simple and flexible file server, with optional \"live reload\" integration for development.\n\n* Serve from multiple directories.\n* Restrict paths via regexps or functions.\n* Optional: automatic `.html` or `index.html` fallbacks, just like GitHub Pages.\n* Optional: on file changes, reinject CSS or reload page.\n* Optional: partial [Go](https://golang.org) implementation.\n  * Only broadcaster, no file server.\n  * API doc: https://pkg.go.dev/github.com/mitranim/afr.\n\nComponents:\n\n* Collection of file-serving functions; see [API](#api).\n* Optional broadcaster:\n  * Used inside your server, or via optional CLI.\n  * Notifies clients.\n  * Notification can be triggered by HTTP request from another process.\n    * Allows page reload _immediately_ after server restart. See [`examples`](examples).\n* Client component:\n  * Tiny [script](client.mjs).\n  * Listens for server notifications.\n  * Reinjects CSS without reloading. Reloads on other changes.\n\nOther features:\n\n* Tiny and dependency-free. Being small is a big feature!\n* Doesn't force a separate server. Runs from _within_ your Deno server, without complicating your environment. Optionally, run a broadcaster separately via CLI.\n* Can signal page reload _after server restart_. Extremely useful when developing a server-rendered app.\n  * Implemented by running in a separate process, sending notifications from your main server process.\n  * Accepts signals over HTTP, which can be sent from any process, from any language.\n\nSuper-lightweight alternative to other file-serving libraries, and also to tools like Browsersync, Livereload, etc.\n\n**This readme is for Deno.** For Node support, see [`afr@0.3.2`](https://github.com/mitranim/afr/blob/ef96d7daa0e6d1540e54e43c5e295521e95ab020/readme.md). For Go, see https://pkg.go.dev/github.com/mitranim/afr.\n\n## TOC\n\n* [Usage](#usage)\n  * [As Library](#as-library)\n  * [As CLI](#as-cli)\n* [Examples](#examples)\n* [API](#api)\n  * [`class Dir`](#class-dirpath-filter)\n  * [`function resFile`](#function-resfilereq-dirs-opts)\n  * [`function resSite`](#function-ressitereq-dirs-opts)\n  * [`function resSiteWithNotFound`](#function-ressitewithnotfoundreq-dirs-opts)\n  * [`function resExactFile`](#function-resexactfilepath-opts)\n  * [`class Broad`](#class-broadopts)\n  * [`function send`](#function-sendmsg-opts)\n  * [`function maybeSend`](#function-maybesendmsg-opts)\n  * [`function watch`](#function-watchpath-dirs-opts)\n  * [Undocumented](#undocumented)\n* [Known Limitations](#known-limitations)\n* [Misc](#misc)\n\n## Usage\n\n### As Library\n\nFor Deno (see API below):\n\n```js\nimport * as a from 'https://deno.land/x/afr@v0.6.3/afr.ts'\n```\n\nFor Go (see https://pkg.go.dev/github.com/mitranim/afr):\n\n```golang\nimport \"github.com/mitranim/afr\"\n```\n\n### As CLI\n\nThe CLI doesn't serve files. It's a development tool that runs a [broadcaster](#class-broadopts) in a separate process, allowing clients to remain connected, so that your server can signal page reload on restart.\n\nThe following command runs the Deno CLI. This should be put in a makefile, and ran concurrently with your server. See [examples](#examples). Requires Deno \u003e= 1.13.0 or `--unstable`.\n\n```sh\ndeno run --allow-net --allow-read --no-check https://deno.land/x/afr@v0.6.3/afr.ts --port 23456 --verbose true\n```\n\nAlternatively, use the equivalent Go CLI:\n\n```sh\ngo install github.com/mitranim/afr/afr@latest\n\nafr --help\n\nafr -v -p 23456\n```\n\n## Examples\n\nRunnable example: clone this repo, `cd` to [`examples`](examples), and run `make`.\n\nExample server code has functionally-identical TS and JS versions (`srv.ts` and `srv.mjs`).\n\n## API\n\n### `class Dir(path, filter)`\n\n`ƒ(string|URL, RegExp|(string)=\u003ebool)`\n\nFundamental tool for serving files and handling FS events. Takes an FS path and an optional filter. For example:\n\n```js\nconst dir = a.dir('target', /[.](?:html|css|mjs)$/)\n```\n\nMany Afr functions require an array of dirs:\n\n```js\nconst dirs = [\n  a.dir('target'),\n  a.dir('.', /[.](?:html|css|mjs)$/),\n]\n```\n\nThe filter may be either a regexp or a function. Afr applies it to a path that is Posix-style (`/`-separated), relative to the dir, and _not_ URL-encoded. Dirs without a filter are permissive and \"allow\" any sub-path when asked.\n\n```js\nconst dirs = [\n  a.dir('target'),\n  a.dir('.', /^(?:static|images|scripts)[/]/),\n]\n```\n\n### `function resFile(req, dirs, opts)`\n\n`ƒ(Request, []Dir, ResponseInit) -\u003e Promise\u003cResponse | undefined\u003e`\n\nTries to serve a file specified by `req.url` from `dirs`.\n\n`dirs` must be an array of [`Dir`](#class-dirpath-filter). They're used as mount points _and_ filters. For each dir, `req.url` is resolved relative to that directory, and only the paths \"allowed\" by its filter may be served. Unlike most file-serving libraries, this allows you to easily and _safely_ serve files out of `.`. In addition, this will automatically reject paths containing `..`.\n\nHas limited `content-type` detection; see [`resExactFile`](#function-resexactfilepath-opts) for details.\n\nFile closing should be automatic; see [`resExactFile`](#function-resexactfilepath-opts) for details.\n\n```js\nconst dirs = [a.dir('target'), a.dir('.', /[.]html$/)]\n\nasync function response(req) {\n  return (\n    (await a.resFile(req, dirs)) ||\n    new Response('not found', {status: 404})\n  )\n}\n```\n\n### `function resSite(req, dirs, opts)`\n\n`ƒ(Request, []Dir, ResponseInit) -\u003e Promise\u003cResponse | undefined\u003e`\n\nSame as [`resSiteWithNotFound`](#function-ressitewithnotfoundreq-dirs-opts), but without the `404.html` fallback.\n\n### `function resSiteWithNotFound(req, dirs, opts)`\n\n`ƒ(Request, []Dir, ResponseInit) -\u003e Promise\u003cResponse | undefined\u003e`\n\nVariant of [`resFile`](#function-resfilereq-dirs-opts) that mimics GitHub Pages, Netlify, and other static-site hosting providers, by trying additional fallbacks when no exact match is found:\n\n  * Try appending `.html`, unless the URL already looks like a file request or ends with `/`.\n  * Try appending `/index.html`, unless the URL already looks like a file request.\n  * Try serving `404.html` with status code 404.\n\nExtremely handy for developing a static site to be served by providers such as GitHub. Check [`examples`](examples) for runnable examples.\n\n```js\nconst dirs = [a.dir('target'), a.dir('.', /[.]html$/)]\n\nasync function response(req) {\n  return (\n    (await a.resSiteWithNotFound(req, dirs)) ||\n    new Response('not found', {status: 404})\n  )\n}\n```\n\n### `function resExactFile(path, opts)`\n\n`ƒ(string|URL, ResponseInit) -\u003e Promise\u003cResponse\u003e`\n\nLower-level tool used by other file-serving functions. Serves a specific file, which _must_ exist in the FS. `path` is anything accepted by `Deno.open`; it may be a relative FS path, absolute FS path, or file URL.\n\nHas limited `content-type` detection. If `opts.headers` doesn't already include `content-type`, tries to guess it by file extension. Known content types are stored in the `contentTypes` dictionary (exported but undocumented), which you can import and mutate.\n\n**Warning**: this keeps the file open until the stream is fully read, or until `res.body.cancel()`. Both are handled automatically by Deno when serving the response, but it's _your_ responsibility to immediately start serving this response. Otherwise the file descriptor may leak.\n\n**Warning**: this may blindly serve **any** file from the filesystem. _Never_ pass externally-provided paths such as `req.url` to this function. This must be used _only_ for paths that are safe to publicly expose. For serving arbitrary files from a folder, use [`resFile`](#function-resfilereq-dirs-opts) or [`resSite`](#function-ressitereq-dirs-opts).\n\n```js\nasync function response() {\n  return (\n    (await a.resExactFile('index.html')) ||\n    (await a.resExactFile('404.html', {status: 404})) ||\n    new Response('not found', {status: 404})\n  )\n}\n```\n\n### `class Broad(opts)`\n\n```ts\ninterface BroadOpts {\n  // URL pathname prefix for all Afr endpoints, including the client script.\n  namespace?: string = '/afr/'\n}\n```\n\nShort for \"broadcaster\". Handles Afr clients:\n\n  * Serves `client.mjs`.\n  * Maintains persistent connections from clients waiting for notifications.\n  * Broadcasts notifications to those clients.\n\n```ts\nconst bro = new a.Broad()\n\n// Broadcasts a reload signal to all clients.\nawait bro.send({type: 'change'})\n\nfunction serveFetchEvent(event) {\n  return event.respondWith(response(event.request))\n}\n\nfunction response(req) {\n  return bro.res(req) || new Response('fallback')\n}\n\n// Broadcasts a reload signal to all clients.\nasync function change() {\n  await bro.send({type: 'change'})\n}\n```\n\nRunning Afr [as a CLI](#as-cli) starts an HTTP server that handles all requests using a [`Broad`](#class-broadopts) instance and responds with 404 to everything unknown.\n\n`Broad` is also available in the Go port. See `afr.go`.\n\n### `function send(msg, opts)`\n\n`ƒ(any, SendOpts) -\u003e Promise`\n\n```ts\ninterface SendOpts {\n  url?: URL\n  port?: number\n  hostname?: string\n  namespace?: string\n}\n```\n\nBroadcasts `msg` to Afr clients. Assumes that on `opts.url` or `opts.hostname + opts.port` there is a reachable server that handles requests using [`Broad`](#class-broadopts) instance, and makes an HTTP request that causes that broadcaster to relay `msg`, as JSON, to every connected client.\n\nThis is useful when running Afr and your own server in separate processes. This allows clients to stay connected when your server restarts, and immediately reload when it's ready.\n\nSee the [`examples`](examples) folder for a runnable example using this pattern.\n\n```js\nconst afrOpts = {port: 23456}\nconst dirs = [a.dir('target')]\n\n// Call this when your server starts.\nasync function watch() {\n  // May cause connected clients to immediately reload.\n  a.maybeSend(a.change, afrOpts)\n\n  // Watch files and notify clients about changes that don't involve restarting\n  // the server, for example in CSS files.\n  for await (const msg of a.watch('target', dirs, {recursive: true})) {\n    await a.maybeSend(msg, afrOpts)\n  }\n}\n```\n\n### `function maybeSend(msg, opts)`\n\n`ƒ(any, SendOpts) -\u003e Promise`\n\nSame as [`send`](#function-sendmsg-opts), but ignores any connection errors.\n\n### `function watch(path, dirs, opts)`\n\n`ƒ(string|URL, []Dir, WatchOpts) -\u003e AsyncIterator\u003cFsEvent\u003e`\n\n```ts\ninterface WatchOpts {\n  recursive?: bool\n  signal?: AbortSignal\n}\n\ninterface FsEvent {\n  type: string\n  path: string\n}\n```\n\nWraps `Deno.watchFs`, converting FS events into messages understood by `client.mjs`.\n\n`path` and `opts` are passed directly to the underlying FS watch API. `dirs` must be an array of [`Dir`](#class-dirpath-filter); they're used to convert absolute FS paths to relative URL paths, and to filter events via `dir.allow`.\n\nTo ignore certain paths, use dir filters; see [`Dir`](#class-dirpath-filter).\n\nThe resulting messages can be broadcast to connected clients via `bro.send` (when using a [broadcaster](#class-broadopts) in the same process) or [`send`](#function-sendmsg-opts) (when using an external process).\n\nFor cancelation, just break out of the loop or call `.return()` on the iterator. You can also pass `opts.signal`, which must be an `AbortSignal`, and later abort it.\n\nExample:\n\n```js\nconst dirs = [a.dir('target'), a.dir('.', /[.]mjs$/)]\n\nfor await (const msg of a.watch('.', dirs, {recursive: true})) {\n  await a.maybeSend(msg, afrOpts)\n}\n```\n\n### Undocumented\n\nSome APIs are exported but undocumented to avoid bloating the docs. Check the source files and look for `export`.\n\n## Known Limitations\n\nSupports only the built-in Deno HTTP server. For stdlib support, use [`afr@0.3.2`](https://github.com/mitranim/afr/blob/ef96d7daa0e6d1540e54e43c5e295521e95ab020/readme.md).\n\nNo compression support. Put your Deno server behind a reverse proxy, such as Nginx, configured for compression.\n\nNo default HTTP cache headers. Caching strategies may vary. You should add your own cache headers. Afr makes it easy:\n\n```js\nfunction respond(req) {return a.resFile(req, dirs, fileInit)}\n\nconst fileInit = {headers: {'cache-control': 'max-age=31536000'}}\n```\n\nFor etag support, use slightly lower-level tools. Use the undocumented function `resolveFile` to get FS stats, generate an etag from that, then serve via `resExactFile`.\n\n## Changelog\n\n### `v0.6.3`\n\nRevert `v0.6.2`.\n\n### `v0.6.2`\n\nClient stops auto-reconnecting after N attempts.\n\n### `v0.6.1`\n\nAdd unlicense file for pkg.go.dev.\n\n### `v0.6.0`\n\nAdded a partial Go implementation. There are no changes in JS.\n\n### `0.5.1`\n\nMinor TS tweak to satisfy some incorrect environmental lib definitions.\n\n### `0.5.0`\n\nNow written in TS, courtesy of @pleshevskiy.\n\n### `0.4.2`\n\n\"File response\" functions now return responses only for GET.\n\n### `0.4.1`\n\nCorrected file extension parsing.\n\n### `0.4.0`\n\n* Support only Deno.\n* Support only built-in HTTP server.\n* Revamped many signatures to `ƒ(Request) -\u003e Response`.\n\n### `0.3.2`\n\nImproved the timing of the first response over a new HTTP connection when running via CLI in Node on Windows.\n\n### `0.3.1`\n\nIn Deno, when loading/running Afr by URL, `Broad` should now be able to serve the client script.\n\n### `0.3.0`\n\n* Support both Node and Deno.\n* Removed daemon features. Run Afr in foreground, in parallel with your server. Use Make to orchestrate build tasks and sub-processes.\n* Removed `Watcher` class; use `watch` to iterate over FS messages.\n* Removed `Aio`.\n* Removed `Dirs`.\n* Moved IO methods from `Dirs` and `Dir` into plain functions, with some minor renaming.\n\n### `0.2.3`\n\nFile server corrections for Windows compatibility (for real this time).\n\n### `0.2.2`\n\nFile server corrections for Windows compatibility.\n\n### `0.2.1`\n\nCorrected minor race condition in CSS replacement.\n\n### `0.2.0`\n\nNow an extra-powerful all-in-one.\n\n## License\n\nhttps://unlicense.org\n\n## Misc\n\nI'm receptive to suggestions. If this library _almost_ satisfies you but needs changes, open an issue or chat me up. Contacts: https://mitranim.com/#contacts\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmitranim%2Fafr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmitranim%2Fafr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmitranim%2Fafr/lists"}