Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/crazy-drinks-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nodesecure/mama": minor
---

implement Manifest module type detection (with cjs, esm, dual, faux esm and dts)
8 changes: 8 additions & 0 deletions workspaces/mama/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ Default values are injected if they are not present in the document. This behavi
### getEntryFiles(): IterableIterator< string >
Deeply extract entry files from package `main` and Node.js `exports` fields.

### moduleType

Return the type of the module

```ts
Copy link

Copilot AI May 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The documentation for moduleType is brief; consider expanding it to list the possible return values ('dts', 'faux', 'dual', 'esm', 'cjs') and provide a short explanation for each.

Copilot uses AI. Check for mistakes.
type PackageModuleType = "dts" | "faux" | "dual" | "esm" | "cjs";
```

### spec
Return the NPM specification (which is the combinaison of `name@version`).

Expand Down
9 changes: 8 additions & 1 deletion workspaces/mama/src/ManifestManager.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import type {
} from "@nodesecure/npm-types";

// Import Internal Dependencies
import { packageJSONIntegrityHash } from "./utils/index.js";
import {
packageJSONIntegrityHash,
inspectModuleType
} from "./utils/index.js";

type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };

Expand Down Expand Up @@ -88,6 +91,10 @@ export class ManifestManager<
.some((script) => kUnsafeNPMScripts.has(script.toLowerCase()));
}

get moduleType() {
Copy link

Copilot AI May 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider adding an explicit return type annotation for the moduleType getter to improve readability and type clarity.

Suggested change
get moduleType() {
get moduleType(): ReturnType<typeof inspectModuleType> {

Copilot uses AI. Check for mistakes.
return inspectModuleType(this.document);
}

get hasZeroSemver() {
if (typeof this.document.version === "string") {
return /^0(\.\d+)*$/
Expand Down
4 changes: 3 additions & 1 deletion workspaces/mama/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export * from "./ManifestManager.class.js";
export {
packageJSONIntegrityHash
packageJSONIntegrityHash,
inspectModuleType,
type PackageModuleType
} from "./utils/index.js";
1 change: 1 addition & 0 deletions workspaces/mama/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./integrity-hash.js";
export * from "./inspectModuleType.js";
136 changes: 136 additions & 0 deletions workspaces/mama/src/utils/inspectModuleType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Ported and modified from: https://github.com/wooorm/npm-esm-vs-cjs/blob/main/script/crawl.js
// AND https://github.com/antfu/node-modules-inspector/blob/main/packages/node-modules-tools/src/analyze-esm.ts#L8
// Copyright (c) Titus Wormer <tituswormer@gmail.com>
// MIT Licensed

// Import Third-party Dependencies
import type {
PackageJSON,
WorkspacesPackageJSON,
PackumentVersion
} from "@nodesecure/npm-types";

export type PackageModuleType = "dts" | "faux" | "dual" | "esm" | "cjs";

export function inspectModuleType(
packageJson: PackageJSON | WorkspacesPackageJSON | PackumentVersion
): PackageModuleType {
// We aggressively assume `@types/` are all type-only packages.
if (packageJson.name?.startsWith("@types/")) {
return "dts";
}

const { exports, main, type } = packageJson;
let cjs: boolean | undefined;
let esm: boolean | undefined;
const fauxEsm = Boolean(packageJson.module);

// Check exports map.
if (exports && typeof exports === "object") {
for (const exportId in exports) {
if (Object.hasOwn(exports, exportId) && typeof exportId === "string") {
const value = exports[exportId];
analyzeThing(value, `${packageJson.name}#exports`);
}
}
}

// Explicit `commonjs` set, with a explicit `import` or `.mjs` too.
if (esm && type === "commonjs") {
Copy link

Copilot AI May 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conditions on lines 39-46 check flags (esm and cjs) that may not be initialized yet, which could lead to unexpected behavior. Consider reviewing or reordering these conditions to ensure that they correctly reflect the intended logic.

Copilot uses AI. Check for mistakes.
cjs = true;
}

// Explicit `module` set, with explicit `require` or `.cjs` too.
if (cjs && type === "module") {
esm = true;
}

// If there are no explicit exports:
if (cjs === undefined && esm === undefined) {
if (type === "module" || (main && /\.mjs$/.test(main))) {
esm = true;
}
else if (main) {
cjs = true;
}
// If main is not yet, it might be a type only/cli only package.
}

if (esm && cjs) {
return "dual";
}
if (esm) {
return "esm";
}
if (fauxEsm) {
return "faux";
}
if (!esm && !cjs && !packageJson.main && !packageJson.exports && packageJson.types) {
return "dts";
}

return "cjs";

function analyzeThing(value: any, path: string): void {
if (value && typeof value === "object") {
if (Array.isArray(value)) {
const values = value;
let index = -1;
while (++index < values.length) {
analyzeThing(values[index], `${path}[${index}]`);
}
}
else {
let dots = false;
const record = value;
for (const [key, subvalue] of Object.entries(value)) {
if (key.charAt(0) !== ".") {
break;
}
analyzeThing(subvalue, `${path}["${key}"]`);
dots = true;
}

if (dots) {
return;
}

let explicit = false;
const conditionImport = Boolean("import" in record && record.import);
const conditionRequire = Boolean("require" in record && record.require);
const conditionDefault = Boolean("default" in record && record.default);

if (conditionImport || conditionRequire) {
explicit = true;
}

if (conditionImport || (conditionRequire && conditionDefault)) {
esm = true;
}

if (conditionRequire || (conditionImport && conditionDefault)) {
cjs = true;
}

const defaults = record.node || record.default;

if (typeof defaults === "string" && !explicit) {
if (/\.mjs$/.test(defaults)) {
esm = true;
}
if (/\.cjs$/.test(defaults)) {
cjs = true;
}
}
}
}
else if (typeof value === "string") {
if (/\.mjs$/.test(value)) {
esm = true;
}
if (/\.cjs$/.test(value)) {
cjs = true;
}
}
}
}
9 changes: 9 additions & 0 deletions workspaces/mama/test/ManifestManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,15 @@ describe("ManifestManager", () => {
});
});

describe("get moduleType", () => {
test("return cjs for a minimal PackageJSON", () => {
const mama = new ManifestManager({
...kMinimalPackageJSON
});
assert.deepEqual(mama.moduleType, "cjs");
});
});

describe("get dependencies", () => {
test("Given a PackageJSON with no dependencies, it must return an empty Array", () => {
const packageJSON: PackageJSON = {
Expand Down
127 changes: 127 additions & 0 deletions workspaces/mama/test/inspectModuleType.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Import Node.js Dependencies
import assert from "node:assert";
import { describe, test } from "node:test";

// Import Third-party Dependencies
import type {
PackageJSON
} from "@nodesecure/npm-types";

// Import Internal Dependencies
import { inspectModuleType } from "../src/utils/index.js";

// CONSTANTS
const kMinimalPackageJSON = {
name: "foobar",
version: "1.0.0"
} as const;

describe("inspectModuleType", () => {
test("package with the absolute minimal properties must return 'cjs' by default", () => {
assert.strictEqual(
inspectModuleType(kMinimalPackageJSON),
"cjs"
);
});

test("package name starting with @types should be detected as dts", () => {
const packageJSON: PackageJSON = {
...kMinimalPackageJSON,
name: "@types/foobar"
};

assert.strictEqual(
inspectModuleType(packageJSON),
"dts"
);
});

test("package with absolutely no exports defined but types is should return 'dts'", () => {
const packageJSON: PackageJSON = {
...kMinimalPackageJSON,
types: "./index.d.ts"
};

assert.strictEqual(
inspectModuleType(packageJSON),
"dts"
);
});

test("package with type equal 'commonjs' should return 'cjs'", () => {
const packageJSON: PackageJSON = {
...kMinimalPackageJSON,
type: "commonjs"
};

assert.strictEqual(
inspectModuleType(packageJSON),
"cjs"
);
});

test("package with type equal 'module' should return 'esm'", () => {
const packageJSON: PackageJSON = {
...kMinimalPackageJSON,
type: "module"
};

assert.strictEqual(
inspectModuleType(packageJSON),
"esm"
);
});

test("package with a main containing a .mjs file should return 'esm'", () => {
const packageJSON: PackageJSON = {
...kMinimalPackageJSON,
main: "./index.mjs"
};

assert.strictEqual(
inspectModuleType(packageJSON),
"esm"
);
});

test("package with a main not containing .mjs file should return cjs", () => {
const packageJSON: PackageJSON = {
...kMinimalPackageJSON,
main: "./index.js"
};

assert.strictEqual(
inspectModuleType(packageJSON),
"cjs"
);
});

test("package with legacy module property set should return 'faux'", () => {
const packageJSON: PackageJSON = {
...kMinimalPackageJSON,
module: true
};

assert.strictEqual(
inspectModuleType(packageJSON),
"faux"
);
});

test("package with dual CJS & ESM exports must return 'dual'", () => {
const packageJSON: PackageJSON = {
...kMinimalPackageJSON,
exports: {
".": {
require: "./dist/index.cjs",
import: "./dist/index.js"
}
}
};

assert.strictEqual(
inspectModuleType(packageJSON),
"dual"
);
});
});