From 48a0c781ef440c319e87963b70b4093bf5c93a2e Mon Sep 17 00:00:00 2001 From: Patrick McElhaney Date: Fri, 28 Jun 2024 12:41:20 -0400 Subject: [PATCH 1/2] ability at runtime for a context object to access other context objects --- .changeset/tiny-chefs-greet.md | 5 +++++ docs/usage.md | 11 +++++++++++ src/server/module-loader.ts | 4 +++- test/server/module-loader.test.ts | 30 ++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 .changeset/tiny-chefs-greet.md diff --git a/.changeset/tiny-chefs-greet.md b/.changeset/tiny-chefs-greet.md new file mode 100644 index 00000000..8bb5ae63 --- /dev/null +++ b/.changeset/tiny-chefs-greet.md @@ -0,0 +1,5 @@ +--- +"counterfact": minor +--- + +ability at runtime for a context object to access other context objects diff --git a/docs/usage.md b/docs/usage.md index f11b8352..48980a72 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -192,6 +192,17 @@ By default, each `_.context.ts` delegates to its parent directory, so you can de > [!TIP] > You can make the context objects do whatever you want, including things like writing to databases. But remember that Counterfact is meant for testing, so holding on to data between sessions is an anti-pattern. Keeping everything in memory also makes it fast. +> [!TIP] +> An object with loadContext() function is passed to the constructor of a context class. You can use it load the context from another directory at runtime. This is an advanced use case. +> +> ```ts +> class Context { +> constructor({ loadContext }) { +> this.rootContext = loadContext("/"); +> } +> } +> ``` + ### Security: the `$.auth` object If a username and password are sent via basic authentication, the username and password can be found via `$.auth.username` and `$.auth.password` respectively. diff --git a/src/server/module-loader.ts b/src/server/module-loader.ts index 89f242a7..0bc9243e 100644 --- a/src/server/module-loader.ts +++ b/src/server/module-loader.ts @@ -175,7 +175,9 @@ export class ModuleLoader extends EventTarget { // @ts-expect-error TS says Context has no constructable signatures but that's not true? // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - new endpoint.Context(), + new endpoint.Context({ + loadContext: (path: string) => this.contextRegistry.find(path), + }), ); } } else { diff --git a/test/server/module-loader.test.ts b/test/server/module-loader.test.ts index d7ad893c..8c0222a3 100644 --- a/test/server/module-loader.test.ts +++ b/test/server/module-loader.test.ts @@ -190,6 +190,36 @@ describe("a module loader", () => { }); }); + it("provides the parent context if the local _.context.ts doesn't export a default", async () => { + await usingTemporaryFiles(async ($) => { + await $.add( + "_.context.js", + "export class Context { constructor({loadContext}) { this.loadContext = loadContext } }", + ); + await $.add("a/_.context.js", "export class Context { name = 'a' }"); + await $.add("package.json", '{ "type": "module" }'); + + const registry: Registry = new Registry(); + + const contextRegistry: ContextRegistry = new ContextRegistry(); + + const loader: ModuleLoader = new ModuleLoader( + $.path("."), + + registry, + contextRegistry, + ); + + await loader.load(); + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any + const rootContext = contextRegistry.find("/") as any; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + expect(rootContext?.loadContext("/a")?.name).toBe("a"); + }); + }); + // can't test because I can't get Jest to refresh modules it.skip("updates the registry when a dependency is updated", async () => { await usingTemporaryFiles(async ($) => { From ea30fd4d4e3af54f0e5f6781ada0bd0afdd106d9 Mon Sep 17 00:00:00 2001 From: Patrick McElhaney Date: Fri, 28 Jun 2024 13:19:09 -0400 Subject: [PATCH 2/2] structuredClone can't copy functions so use lodash's cloneDeep() instead --- package.json | 2 ++ src/server/context-registry.ts | 11 +++++++---- src/server/module-loader.ts | 6 +++++- yarn.lock | 36 ++++++++-------------------------- 4 files changed, 22 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 3e723552..d02ea665 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@types/koa-bodyparser": "4.3.12", "@types/koa-proxy": "1.0.7", "@types/koa-static": "4.0.4", + "@types/lodash": "^4.17.6", "copyfiles": "2.4.1", "eslint": "8.57.0", "eslint-config-hardcore": "41.3.0", @@ -97,6 +98,7 @@ "koa-bodyparser": "4.4.1", "koa-proxy": "1.0.0-alpha.3", "koa2-swagger-ui": "5.10.0", + "lodash": "^4.17.21", "node-fetch": "3.3.2", "open": "10.1.0", "patch-package": "8.0.0", diff --git a/src/server/context-registry.ts b/src/server/context-registry.ts index 07d4391c..06ed2a23 100644 --- a/src/server/context-registry.ts +++ b/src/server/context-registry.ts @@ -1,4 +1,7 @@ -// eslint-disable-next-line max-classes-per-file +/* eslint-disable max-classes-per-file */ +/* eslint-disable max-statements */ +import cloneDeep from "lodash/cloneDeep.js"; + export class Context { // eslint-disable-next-line @typescript-eslint/no-useless-constructor, @typescript-eslint/no-empty-function public constructor() {} @@ -33,7 +36,8 @@ export class ContextRegistry { public add(path: string, context: Context): void { this.entries.set(path, context); - this.cache.set(path, structuredClone(context)); + + this.cache.set(path, cloneDeep(context)); } public find(path: string): Context { @@ -43,7 +47,6 @@ export class ContextRegistry { ); } - // eslint-disable-next-line max-statements public update(path: string, updatedContext?: Context): void { if (updatedContext === undefined) { return; @@ -66,6 +69,6 @@ export class ContextRegistry { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument Object.setPrototypeOf(context, Object.getPrototypeOf(updatedContext)); - this.cache.set(path, structuredClone(updatedContext)); + this.cache.set(path, cloneDeep(updatedContext)); } } diff --git a/src/server/module-loader.ts b/src/server/module-loader.ts index 0bc9243e..99f717cf 100644 --- a/src/server/module-loader.ts +++ b/src/server/module-loader.ts @@ -170,13 +170,15 @@ export class ModuleLoader extends EventTarget { if (basename(pathName).startsWith("_.context")) { if (isContextModule(endpoint)) { + const loadContext = (path: string) => this.contextRegistry.find(path); + this.contextRegistry.update( directory, // @ts-expect-error TS says Context has no constructable signatures but that's not true? // eslint-disable-next-line @typescript-eslint/no-unsafe-argument new endpoint.Context({ - loadContext: (path: string) => this.contextRegistry.find(path), + loadContext, }), ); } @@ -193,6 +195,8 @@ export class ModuleLoader extends EventTarget { return; } + throw error; + process.stdout.write(`\nError loading ${pathName}:\n${String(error)}\n`); } } diff --git a/yarn.lock b/yarn.lock index c161e019..d4f273c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2718,6 +2718,11 @@ "@types/koa-compose" "*" "@types/node" "*" +"@types/lodash@^4.17.6": + version "4.17.6" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.6.tgz#193ced6a40c8006cfc1ca3f4553444fb38f0e543" + integrity sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA== + "@types/mdast@^3.0.0": version "3.0.12" resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.12.tgz#beeb511b977c875a5b0cc92eab6fcac2f0895514" @@ -10660,7 +10665,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -10677,15 +10682,6 @@ string-width@^2.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -10755,7 +10751,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -10776,13 +10772,6 @@ strip-ansi@^5.1.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -11767,7 +11756,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -11785,15 +11774,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"