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 de3fb809..ffe9fee9 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/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 89f242a7..99f717cf 100644 --- a/src/server/module-loader.ts +++ b/src/server/module-loader.ts @@ -170,12 +170,16 @@ 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(), + new endpoint.Context({ + loadContext, + }), ); } } else { @@ -191,6 +195,8 @@ export class ModuleLoader extends EventTarget { return; } + throw error; + process.stdout.write(`\nError loading ${pathName}:\n${String(error)}\n`); } } 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 ($) => { 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"