Skip to content
Merged
10 changes: 10 additions & 0 deletions .changeset/located-manifest-tsd.md
Original file line number Diff line number Diff line change
@@ -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
34 changes: 34 additions & 0 deletions workspaces/mama/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(mama: ManifestManager<T>): mama is LocatedManifestManager<T>

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<string>(locatedManifest.location);
}
```

### constructor(document: ManifestManagerDocument, options?: ManifestManagerOptions)

document is described by the following type:
Expand Down Expand Up @@ -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`

Expand Down
9 changes: 7 additions & 2 deletions workspaces/mama/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
}
}
13 changes: 13 additions & 0 deletions workspaces/mama/src/ManifestManager.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ export type ManifestManagerDocument =
WorkspacesPackageJSON |
PackumentVersion;

export type LocatedManifestManager<
MetadataDef extends Record<string, any> = Record<string, any>
> = ManifestManager<MetadataDef> & { location: string; };

export class ManifestManager<
MetadataDef extends Record<string, any> = Record<string, any>
> {
Expand All @@ -70,6 +74,15 @@ export class ManifestManager<
gypfile: false
});

/**
* Type guard to check if a ManifestManager instance has a location
*/
static isLocated<T extends Record<string, any>>(
mama: ManifestManager<T>
): mama is LocatedManifestManager<T> {
return typeof mama.location !== "undefined";
}

public metadata: MetadataDef = Object.create(null);
public document: WithRequired<
ManifestManagerDocument,
Expand Down
25 changes: 25 additions & 0 deletions workspaces/mama/test/ManifestManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CustomMetadata>(
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");
Expand Down
33 changes: 33 additions & 0 deletions workspaces/mama/test/types/ManifestManager.test-d.ts
Original file line number Diff line number Diff line change
@@ -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<string>(locatedManifest.location);

// Test generic type preservation
interface CustomMetadata {
customField: string;
}
const customManifest = new ManifestManager<CustomMetadata>(
{ name: "test", version: "1.0.0" },
{ location: "/tmp/path" }
);
customManifest.metadata.customField = "test";

assert.ok(ManifestManager.isLocated(customManifest));
expectType<LocatedManifestManager<CustomMetadata>>(customManifest);