diff --git a/.changeset/located-manifest-tsd.md b/.changeset/located-manifest-tsd.md new file mode 100644 index 00000000..5b2188bc --- /dev/null +++ b/.changeset/located-manifest-tsd.md @@ -0,0 +1,10 @@ +--- +"@nodesecure/mama": minor +--- + +feat: add LocatedManifestManager type and isLocated type guard + +- Add new LocatedManifestManager type where location is required +- Add static isLocated method to properly narrow the type +- Update documentation with new type and method usage +- Add type tests for the new functionality \ No newline at end of file diff --git a/workspaces/mama/README.md b/workspaces/mama/README.md index c8f453de..be271026 100644 --- a/workspaces/mama/README.md +++ b/workspaces/mama/README.md @@ -42,6 +42,27 @@ The **location** parameter can either be a full path or the path to the director > [!NOTE] > `location` is automatically dispatched to the ManifestManager constructor options. +### (static) isLocated(mama: ManifestManager): mama is LocatedManifestManager + +A TypeScript type guard to check if a `ManifestManager` instance has a location. This is particularly useful when working with manifests that may or may not have been loaded from the filesystem. + +When the type guard is successful, the `location` property is available on the instance. + +```ts +import { ManifestManager, type LocatedManifestManager } from "@nodesecure/mama"; +import { expectType } from "tsd"; + +const locatedManifest = new ManifestManager( + { name: "test", version: "1.0.0" }, + { location: "/tmp/path" } +); + +if (ManifestManager.isLocated(locatedManifest)) { + // locatedManifest is now of type LocatedManifestManager + expectType(locatedManifest.location); +} +``` + ### constructor(document: ManifestManagerDocument, options?: ManifestManagerOptions) document is described by the following type: @@ -150,6 +171,19 @@ Return true if `workspaces` property is present > [!NOTE] > Workspace are described by the interface `WorkspacesPackageJSON` (from @nodesecure/npm-types) +### location + +A string representing the absolute path to the manifest file's directory, if it was provided in the constructor options. Otherwise, it is `undefined`. + +```ts +const mama = new ManifestManager( + { name: "test", version: "1.0.0" }, + { location: "/home/user/my-project/package.json" } +); + +console.log(mama.location); //-> /home/user/my-project +``` + ### hasZeroSemver Return true if `version` is starting with `0.x` diff --git a/workspaces/mama/package.json b/workspaces/mama/package.json index 9834157b..e0291da0 100644 --- a/workspaces/mama/package.json +++ b/workspaces/mama/package.json @@ -9,7 +9,8 @@ "build": "tsc -b", "prepublishOnly": "npm run build", "test-only": "tsx --test ./test/**/*.spec.ts", - "test": "c8 -r html npm run test-only" + "test:tsd": "npm run build && tsd", + "test": "c8 -r html npm run test-only && npm run test:tsd" }, "files": [ "dist" @@ -35,6 +36,10 @@ "object-hash": "^3.0.0" }, "devDependencies": { - "@types/object-hash": "^3.0.6" + "@types/object-hash": "^3.0.6", + "tsd": "^0.32.0" + }, + "tsd": { + "directory": "test/types" } } diff --git a/workspaces/mama/src/ManifestManager.class.ts b/workspaces/mama/src/ManifestManager.class.ts index ac0dc8df..afd5e001 100644 --- a/workspaces/mama/src/ManifestManager.class.ts +++ b/workspaces/mama/src/ManifestManager.class.ts @@ -60,6 +60,10 @@ export type ManifestManagerDocument = WorkspacesPackageJSON | PackumentVersion; +export type LocatedManifestManager< + MetadataDef extends Record = Record +> = ManifestManager & { location: string; }; + export class ManifestManager< MetadataDef extends Record = Record > { @@ -70,6 +74,15 @@ export class ManifestManager< gypfile: false }); + /** + * Type guard to check if a ManifestManager instance has a location + */ + static isLocated>( + mama: ManifestManager + ): mama is LocatedManifestManager { + return typeof mama.location !== "undefined"; + } + public metadata: MetadataDef = Object.create(null); public document: WithRequired< ManifestManagerDocument, diff --git a/workspaces/mama/test/ManifestManager.spec.ts b/workspaces/mama/test/ManifestManager.spec.ts index a0cb5603..2060ecbb 100644 --- a/workspaces/mama/test/ManifestManager.spec.ts +++ b/workspaces/mama/test/ManifestManager.spec.ts @@ -27,6 +27,31 @@ const kMinimalPackageJSONIntegrity = hash({ }); describe("ManifestManager", () => { + describe("static isLocated()", () => { + it("Should return true for ManifestManager with location", () => { + const mama = new ManifestManager(kMinimalPackageJSON, { location: "/tmp/path" }); + assert.strictEqual(ManifestManager.isLocated(mama), true); + }); + + it("Should properly narrow type with custom metadata", () => { + interface CustomMetadata { + customField: string; + } + + const mama = new ManifestManager( + kMinimalPackageJSON, + { location: "/tmp/path" } + ); + mama.metadata.customField = "test"; + + assert.ok(ManifestManager.isLocated(mama)); + const location: string = mama.location; + const metadata: CustomMetadata = mama.metadata; + assert.strictEqual(location, "/tmp/path"); + assert.strictEqual(metadata.customField, "test"); + }); + }); + describe("static Default", () => { test("Property must be Frozen", () => { const isUpdated = Reflect.set(ManifestManager.Default, "foo", "bar"); diff --git a/workspaces/mama/test/types/ManifestManager.test-d.ts b/workspaces/mama/test/types/ManifestManager.test-d.ts new file mode 100644 index 00000000..e1a27a80 --- /dev/null +++ b/workspaces/mama/test/types/ManifestManager.test-d.ts @@ -0,0 +1,33 @@ +// Import Node.js Dependencies +import assert from "node:assert"; + +// Import Third-party Dependencies +import { expectType } from "tsd"; + +// Import Internal Dependencies +import { + ManifestManager, + type LocatedManifestManager +} from "../../dist/index.js"; + +// Test basic type guard +const locatedManifest = new ManifestManager( + { name: "test", version: "1.0.0" }, + { location: "/tmp/path" } +); +assert.ok(ManifestManager.isLocated(locatedManifest)); +expectType(locatedManifest.location); + +// Test generic type preservation +interface CustomMetadata { + customField: string; +} +const customManifest = new ManifestManager( + { name: "test", version: "1.0.0" }, + { location: "/tmp/path" } +); +customManifest.metadata.customField = "test"; + +assert.ok(ManifestManager.isLocated(customManifest)); +expectType>(customManifest); +