From bb1f5affd367c650a16e550c4118775a84e65457 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Tue, 28 May 2024 19:29:47 +0530 Subject: [PATCH] feat: recreate router tree on file creation/deletion in dev --- deno.json | 1 + deno.lock | 128 +++++++++++++++++++++++++++++++++++++++++++++++++- src/router.ts | 32 +++++++++---- 3 files changed, 151 insertions(+), 10 deletions(-) diff --git a/deno.json b/deno.json index 5831c3c..c57c07f 100644 --- a/deno.json +++ b/deno.json @@ -20,6 +20,7 @@ "imports": { "@cliffy/prompt": "jsr:@cliffy/prompt@^1.0.0-rc.4", "@david/dax": "jsr:@david/dax@^0.41.0", + "@parcel/watcher": "npm:@parcel/watcher@^2.4.1", "@std/assert": "jsr:@std/assert@^0.225.3", "@std/fmt": "jsr:@std/fmt@^0.225.1", "@std/fs": "jsr:@std/fs@^0.229.1", diff --git a/deno.lock b/deno.lock index ba9f83c..55a4b38 100644 --- a/deno.lock +++ b/deno.lock @@ -29,7 +29,8 @@ "jsr:@std/regexp@^0.224.1": "jsr:@std/regexp@0.224.1", "jsr:@std/semver@^0.224.0": "jsr:@std/semver@0.224.0", "jsr:@std/streams@0.221.0": "jsr:@std/streams@0.221.0", - "jsr:@std/text@0.221": "jsr:@std/text@0.221.0" + "jsr:@std/text@0.221": "jsr:@std/text@0.221.0", + "npm:@parcel/watcher@^2.4.1": "npm:@parcel/watcher@2.4.1" }, "jsr": { "@cliffy/ansi@1.0.0-rc.4": { @@ -141,6 +142,128 @@ "@std/text@0.221.0": { "integrity": "a2f89ceb0d8851cd33e6774064621a1da9fbc36578cf4f02c5b5bcd7e8c84b67" } + }, + "npm": { + "@parcel/watcher-android-arm64@2.4.1": { + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "dependencies": {} + }, + "@parcel/watcher-darwin-arm64@2.4.1": { + "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==", + "dependencies": {} + }, + "@parcel/watcher-darwin-x64@2.4.1": { + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "dependencies": {} + }, + "@parcel/watcher-freebsd-x64@2.4.1": { + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "dependencies": {} + }, + "@parcel/watcher-linux-arm-glibc@2.4.1": { + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "dependencies": {} + }, + "@parcel/watcher-linux-arm64-glibc@2.4.1": { + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "dependencies": {} + }, + "@parcel/watcher-linux-arm64-musl@2.4.1": { + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "dependencies": {} + }, + "@parcel/watcher-linux-x64-glibc@2.4.1": { + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "dependencies": {} + }, + "@parcel/watcher-linux-x64-musl@2.4.1": { + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "dependencies": {} + }, + "@parcel/watcher-win32-arm64@2.4.1": { + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "dependencies": {} + }, + "@parcel/watcher-win32-ia32@2.4.1": { + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "dependencies": {} + }, + "@parcel/watcher-win32-x64@2.4.1": { + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "dependencies": {} + }, + "@parcel/watcher@2.4.1": { + "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "dependencies": { + "@parcel/watcher-android-arm64": "@parcel/watcher-android-arm64@2.4.1", + "@parcel/watcher-darwin-arm64": "@parcel/watcher-darwin-arm64@2.4.1", + "@parcel/watcher-darwin-x64": "@parcel/watcher-darwin-x64@2.4.1", + "@parcel/watcher-freebsd-x64": "@parcel/watcher-freebsd-x64@2.4.1", + "@parcel/watcher-linux-arm-glibc": "@parcel/watcher-linux-arm-glibc@2.4.1", + "@parcel/watcher-linux-arm64-glibc": "@parcel/watcher-linux-arm64-glibc@2.4.1", + "@parcel/watcher-linux-arm64-musl": "@parcel/watcher-linux-arm64-musl@2.4.1", + "@parcel/watcher-linux-x64-glibc": "@parcel/watcher-linux-x64-glibc@2.4.1", + "@parcel/watcher-linux-x64-musl": "@parcel/watcher-linux-x64-musl@2.4.1", + "@parcel/watcher-win32-arm64": "@parcel/watcher-win32-arm64@2.4.1", + "@parcel/watcher-win32-ia32": "@parcel/watcher-win32-ia32@2.4.1", + "@parcel/watcher-win32-x64": "@parcel/watcher-win32-x64@2.4.1", + "detect-libc": "detect-libc@1.0.3", + "is-glob": "is-glob@4.0.3", + "micromatch": "micromatch@4.0.7", + "node-addon-api": "node-addon-api@7.1.0" + } + }, + "braces@3.0.3": { + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "fill-range@7.1.1" + } + }, + "detect-libc@1.0.3": { + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dependencies": {} + }, + "fill-range@7.1.1": { + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "to-regex-range@5.0.1" + } + }, + "is-extglob@2.1.1": { + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dependencies": {} + }, + "is-glob@4.0.3": { + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "is-extglob@2.1.1" + } + }, + "is-number@7.0.0": { + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dependencies": {} + }, + "micromatch@4.0.7": { + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dependencies": { + "braces": "braces@3.0.3", + "picomatch": "picomatch@2.3.1" + } + }, + "node-addon-api@7.1.0": { + "integrity": "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==", + "dependencies": {} + }, + "picomatch@2.3.1": { + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dependencies": {} + }, + "to-regex-range@5.0.1": { + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "is-number@7.0.0" + } + } } }, "remote": {}, @@ -154,7 +277,8 @@ "jsr:@std/net@^0.224.1", "jsr:@std/path@^0.225.1", "jsr:@std/regexp@^0.224.1", - "jsr:@std/semver@^0.224.0" + "jsr:@std/semver@^0.224.0", + "npm:@parcel/watcher@^2.4.1" ] } } diff --git a/src/router.ts b/src/router.ts index aa2508e..f69ac16 100644 --- a/src/router.ts +++ b/src/router.ts @@ -10,6 +10,7 @@ import { walk } from '@std/fs' import { toFileUrl } from '@std/path' +import watcher from '@parcel/watcher' const methods = new Set(['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH']) @@ -238,7 +239,7 @@ class UrlNode { * ```ts * const { handler } = await createRouter( * fromFileUrl(new URL('./api', import.meta.url)), - * { baseUrl: '/api' } + * { baseUrl: '/api', dev: Deno.env.get('DENO_ENV') === 'development' }, * ) * Deno.serve({ port: 3000, handler }) * ``` @@ -249,7 +250,7 @@ export async function createRouter( ): Promise<{ handler: (req: Request) => Promise }> { - const root = new UrlNode() + let root: UrlNode /** req.url.pathname -> { match, params } */ const lookupCache = new LRUCache(100) @@ -258,11 +259,26 @@ export async function createRouter( /** file:METHOD -> handler */ const handlerCache = new LRUCache(100) - for await (const file of walk(dir, { includeDirs: false, includeSymlinks: false, exts: ['.ts'] })) { - let path = baseUrl + file.path.replace(/\\/g, '/').slice(dir.length) - if (path.endsWith('.d.ts') || path.includes('/_') || path.includes('/.')) continue - path = path.replace(/\.ts$/, '').replace(/\/(index)?$/, '').replace(/^(?!\/)/, '/') - root.insert(path, toFileUrl(file.path).href) + async function createTree() { + root = new UrlNode() + + for await (const file of walk(dir, { includeDirs: false, includeSymlinks: false, exts: ['.ts'] })) { + let path = baseUrl + file.path.replace(/\\/g, '/').slice(dir.length) + if (path.endsWith('.d.ts') || path.includes('/_') || path.includes('/.')) continue + path = path.replace(/\.ts$/, '').replace(/\/(index)?$/, '').replace(/^(?!\/)/, '/') + root.insert(path, toFileUrl(file.path).href) + } + } + + await createTree() + + if (dev) { + watcher.subscribe(dir, async (_, events) => { + if (events.some((event) => event.type === 'create' || event.type === 'delete')) { + console.log('Reloading router...') + await createTree() + } + }) } function getHandler(file: string, method: string): Promise { @@ -318,6 +334,6 @@ export async function createRouter( * - use URLPatternList once it's available (https://github.com/whatwg/urlpattern/pull/166) * - use iterative pattern if there is significant memory/performance improvement * - use more efficient LRU cache implementation - * - reload router in dev mode when files are created/deleted * - use eager loading in production mode + * - don't destroy whole tree on single file change */