diff --git a/README.md b/README.md index a2118d3..7daf6e2 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,11 @@ session, perhaps adding it to the `meta` data that will be passed to the `GET` handler itself. If there is no session, however, it should return an HTTP `Response` object indicating permission is denied or similar. +#### _pre.ts files + +Any `_pre.ts` files found under the root that export `.load()` and/or `.unload()` methods +will be loaded and those functions will be called at server startup/shutdown, respectively. + ## TODO - [ ] reload typescript if it is modified on disk diff --git a/deno.json b/deno.json index 0473671..87293a5 100644 --- a/deno.json +++ b/deno.json @@ -1,7 +1,7 @@ { "name": "@andyburke/serverus", "description": "A flexible HTTP server for mixed content. Throw static files, markdown, Typescript and (hopefully, eventually) more into a directory and serverus can serve it up a bit more like old-school CGI.", - "version": "0.11.5", + "version": "0.12.0", "license": "MIT", "exports": { ".": "./serverus.ts", diff --git a/handlers/typescript.ts b/handlers/typescript.ts index e68aed8..e48f9e5 100644 --- a/handlers/typescript.ts +++ b/handlers/typescript.ts @@ -34,8 +34,7 @@ type ROUTE_HANDLER_RECORD = { module?: ROUTE_HANDLER; }; const routes: ROUTE_HANDLER_RECORD[] = []; -let loading: boolean = false; -let all_routes_loaded: boolean = false; +const unloaders: (() => Promise | void)[] = []; /** * Handles requests for which there are typescript files in the root tree. @@ -48,57 +47,6 @@ let all_routes_loaded: boolean = false; * @returns Either a response (a handler for the request path and method was found) or undefined if unhandled. */ export default async function handle_typescript(request: Request): Promise { - if (!all_routes_loaded) { - if (!loading) { - loading = true; - - const root_directory = path.resolve(Deno.cwd()); - - const imports: ROUTE_HANDLER_RECORD[] = []; - for await ( - const entry of walk(root_directory, { - exts: ['.ts'], - skip: [/\.test\.ts$/] - }) - ) { - if (entry.isFile) { - const relative_path = entry.path.substring(root_directory.length); - const route_path = relative_path - .replace(/\.ts$/, '') - //.replace(/\[(\w+)\]/g, ':$1') - .replace(/\/index$/, '') - .replace(/___/g, ':') || // required for windows, uncivilized OS that it is - '/'; - - const route_pattern = new URLPattern({ pathname: route_path }); - - const import_path = new URL('file://' + entry.path, import.meta.url).toString(); - - imports.push({ - route_path, - route_pattern, - import_path - }); - } - } - - // try to sort imports such that they're registered like: - // /permissions/test - // /api/echo/hi - // /api/echo/:input - // - // we want paths with parameters to sort later than paths without - routes.push(...imports.sort((lhs, rhs) => rhs.route_path.localeCompare(lhs.route_path))); - - all_routes_loaded = true; - loading = false; - } - - do { - await delay(10); - } while (!all_routes_loaded); - } - for (const route_record of routes) { const match = route_record.route_pattern.exec(request.url.replace(/\/$/, '')); if (match) { @@ -142,8 +90,64 @@ export default async function handle_typescript(request: Request): Promise { + const root_directory = path.resolve(Deno.cwd()); + + const imports: ROUTE_HANDLER_RECORD[] = []; + for await ( + const entry of walk(root_directory, { + exts: ['.ts'], + skip: [/\.test\.ts$/] + }) + ) { + if (entry.isFile) { + const import_path = new URL('file://' + entry.path, import.meta.url).toString(); + + const filename = path.basename(entry.path); + if (filename === '_pre.ts') { + const preloader = await import(import_path); + if (typeof preloader.load === 'function') { + await preloader.load(); + } + + if (typeof preloader.unload === 'function') { + unloaders.push(preloader.unload); + } + continue; + } + + const relative_path = entry.path.substring(root_directory.length); + const route_path = relative_path + .replace(/\.ts$/, '') + //.replace(/\[(\w+)\]/g, ':$1') + .replace(/\/index$/, '') + .replace(/___/g, ':') || // required for windows, uncivilized OS that it is + '/'; + + const route_pattern = new URLPattern({ pathname: route_path }); + + imports.push({ + route_path, + route_pattern, + import_path + }); + } + } + + // try to sort imports such that they're registered like: + // /permissions/test + // /api/echo/hi + // /api/echo/:input + // + // we want paths with parameters to sort later than paths without + routes.push(...imports.sort((lhs, rhs) => rhs.route_path.localeCompare(lhs.route_path))); +} + +export async function unload(): Promise { + for (const unloader of unloaders) { + await unloader(); + } + + routes.splice(0, routes.length); + unloaders.splice(0, unloaders.length); } diff --git a/server.ts b/server.ts index 1e591ec..11dc150 100644 --- a/server.ts +++ b/server.ts @@ -25,6 +25,7 @@ type LOGGER = (request: Request, response: Response, processing_time: number) => */ interface HANDLER_MODULE { default: HANDLER; + load?: () => void | Promise; unload?: () => void | Promise; } @@ -146,6 +147,10 @@ export class SERVER { } this.handlers.push(handler_module); + + if (handler_module.load) { + await handler_module.load(); + } } } catch (error) { if (error instanceof Deno.errors.NotFound) { diff --git a/tests/01_static_files.test.ts b/tests/01_static_files.test.ts index 800aed7..0e0f097 100644 --- a/tests/01_static_files.test.ts +++ b/tests/01_static_files.test.ts @@ -562,10 +562,7 @@ Deno.test({ ); const get_response = await fetch( - `http://${test_server_info.hostname}:${test_server_info.port}/files/test_put_upload_that_should_not_fail.txt`, - { - method: 'GET' - } + `http://${test_server_info.hostname}:${test_server_info.port}/files/test_put_upload_that_should_not_fail.txt` ); asserts.assert(get_response.ok); diff --git a/tests/10_test_preloaders.test.ts b/tests/10_test_preloaders.test.ts new file mode 100644 index 0000000..2782c57 --- /dev/null +++ b/tests/10_test_preloaders.test.ts @@ -0,0 +1,36 @@ +import * as asserts from '@std/assert'; +import { EPHEMERAL_SERVER, get_ephemeral_listen_server } from './helpers.ts'; + +Deno.test({ + name: 'check that _preload.ts files work', + permissions: { + env: true, + read: true, + write: true, + net: true + }, + fn: async () => { + let test_server_info: EPHEMERAL_SERVER | null = null; + const cwd = Deno.cwd(); + + try { + Deno.chdir('./tests/www'); + test_server_info = await get_ephemeral_listen_server(); + + const preloader_env_setting = Deno.env.get('SERVERUS_PRELOADED_TEST'); + asserts.assertEquals(preloader_env_setting, 'true'); + + await test_server_info.server.stop(); + + const preloader_env_setting_after_unload = Deno.env.get('SERVERUS_PRELOADED_TEST'); + asserts.assertEquals(preloader_env_setting_after_unload, undefined); + + test_server_info = null; + } finally { + Deno.chdir(cwd); + if (test_server_info) { + await test_server_info?.server?.stop(); + } + } + } +}); diff --git a/tests/www/_pre.ts b/tests/www/_pre.ts new file mode 100644 index 0000000..3cefd18 --- /dev/null +++ b/tests/www/_pre.ts @@ -0,0 +1,7 @@ +export function load() { + Deno.env.set('SERVERUS_PRELOADED_TEST', 'true'); +} + +export function unload() { + Deno.env.delete('SERVERUS_PRELOADED_TEST'); +}