-
Notifications
You must be signed in to change notification settings - Fork 23
feat(mama): implement moduleType getter along with inspectModuleType fn #397
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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] }; | ||||||
|
|
||||||
|
|
@@ -88,6 +91,10 @@ export class ManifestManager< | |||||
| .some((script) => kUnsafeNPMScripts.has(script.toLowerCase())); | ||||||
| } | ||||||
|
|
||||||
| get moduleType() { | ||||||
|
||||||
| get moduleType() { | |
| get moduleType(): ReturnType<typeof inspectModuleType> { |
| 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"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| export * from "./integrity-hash.js"; | ||
| export * from "./inspectModuleType.js"; |
| 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") { | ||
|
||
| 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; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| 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" | ||
| ); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
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.