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
2 changes: 1 addition & 1 deletion @commitlint/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"@commitlint/utils": "workspace:^",
"@types/node": "^22.0.0",
"@types/yargs": "^17.0.29",
"conventional-commits-parser": "^6.3.0",
"conventional-commits-parser": "^7.0.0",
"es-toolkit": "^1.46.0"
},
"engines": {
Expand Down
2 changes: 1 addition & 1 deletion @commitlint/config-conventional/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
},
"dependencies": {
"@commitlint/types": "workspace:^",
"conventional-changelog-conventionalcommits": "^9.2.0"
"conventional-changelog-conventionalcommits": "^10.0.0"
},
"devDependencies": {
"@commitlint/lint": "workspace:^",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"name": "parser-preset-angular",
"version": "1.0.0",
"devDependencies": {
"conventional-changelog-angular": "^7.0.0"
"conventional-changelog-angular": "^9.0.0"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"name": "parser-preset-conventional-without-factory",
"version": "1.0.0",
"devDependencies": {
"conventional-changelog-conventionalcommits": "^9.2.0"
"conventional-changelog-conventionalcommits": "^10.0.0"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"name": "parser-preset-conventionalcommits",
"version": "1.0.0",
"devDependencies": {
"conventional-changelog-conventionalcommits": "^9.2.0"
"conventional-changelog-conventionalcommits": "^10.0.0"
}
}
4 changes: 2 additions & 2 deletions @commitlint/parse/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
},
"dependencies": {
"@commitlint/types": "workspace:^",
"conventional-changelog-angular": "^8.2.0",
"conventional-commits-parser": "^6.3.0"
"conventional-changelog-angular": "^9.0.0",
"conventional-commits-parser": "^7.0.0"
},
"devDependencies": {
"@commitlint/test": "workspace:^",
Expand Down
26 changes: 14 additions & 12 deletions @commitlint/parse/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { test, expect } from "vitest";
import type { ParserOptions } from "conventional-commits-parser";

import parse from "./index.js";

// conventional-changelog-angular@>=9 exposes a single default `createPreset()`
// whose return is typed as `{}`; the parser options live under `.parser`.
const angularParserOpts = async (): Promise<ParserOptions> => {
const { default: createPreset } = await import("conventional-changelog-angular");
const { parser } = (await createPreset()) as { parser: ParserOptions };
return parser;
};

test("throws when called without params", async () => {
await expect((parse as any)()).rejects.toThrow("Expected a raw commit");
});
Expand Down Expand Up @@ -143,10 +153,8 @@ test("supports scopes with / and empty parserOpts", async () => {

test("ignores comments", async () => {
const message = "type(some/scope): subject\n# some comment";
// @ts-expect-error -- no typings
const changelogOpts = await import("conventional-changelog-angular");
const opts = {
...changelogOpts.parser,
...(await angularParserOpts()),
commentChar: "#",
};
const actual = await parse(message, undefined, opts);
Expand All @@ -158,9 +166,7 @@ test("ignores comments", async () => {

test("parses inline references in subject and body", async () => {
const message = "type(some/scope): subject #reference\n\nthings #reference";
// @ts-expect-error -- no typings
const changelogOpts = await import("conventional-changelog-angular");
const actual = await parse(message, undefined, changelogOpts.parser);
const actual = await parse(message, undefined, await angularParserOpts());

expect(actual.subject).toBe("subject #reference");
expect(actual.body).toBe("");
Expand All @@ -186,10 +192,8 @@ test("parses inline references in subject and body", async () => {

test("filters comment lines when commentChar is set", async () => {
const message = "type(scope): subject\n# this is a comment\nbody content";
// @ts-expect-error -- no typings
const changelogOpts = await import("conventional-changelog-angular");
const opts = {
...changelogOpts.parser,
...(await angularParserOpts()),
commentChar: "#",
};
const actual = await parse(message, undefined, opts);
Expand Down Expand Up @@ -235,11 +239,9 @@ test("allows separating -side nodes- by setting parserOpts.fieldPattern", async

test("parses references leading subject", async () => {
const message = "#1 some subject";
// @ts-expect-error -- no typings
const opts = await import("conventional-changelog-angular");
const {
references: [actual],
} = await parse(message, undefined, opts);
} = await parse(message, undefined, await angularParserOpts());

expect(actual.issue).toBe("1");
});
Expand Down
8 changes: 6 additions & 2 deletions @commitlint/parse/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { Parser } from "@commitlint/types";

import { type Commit, type ParserOptions, CommitParser } from "conventional-commits-parser";
// @ts-expect-error -- no typings
import defaultChangelogOpts from "conventional-changelog-angular";

const defaultParser: Parser = (message, options) => {
Expand All @@ -21,7 +20,12 @@ export async function parse(
parser: Parser = defaultParser,
parserOpts?: ParserOptions,
): Promise<Commit> {
const preset = await defaultChangelogOpts();
// conventional-changelog-angular@>=9 ships typings that declare the preset as
// `{}`; the parser options live under `.parser` at runtime.
const preset = (await defaultChangelogOpts()) as {
parser?: ParserOptions;
parserOpts?: ParserOptions;
};
const defaultOpts = preset.parser || preset.parserOpts;
// Support user-provided parser options passed either flat or nested under a 'parser' key
const userOpts = (parserOpts as any)?.parser || parserOpts || {};
Expand Down
77 changes: 75 additions & 2 deletions @commitlint/resolve-extends/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { test, expect, vi } from "vitest";
import { test, expect, vi, afterEach } from "vitest";
import { createRequire } from "node:module";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { RuleConfigSeverity, UserConfig } from "@commitlint/types";

import resolveExtends, { ResolveExtendsContext } from "./index.js";
import resolveExtends, { ResolveExtendsContext, resolveFrom } from "./index.js";

const require = createRequire(import.meta.url);

Expand Down Expand Up @@ -628,3 +631,73 @@ test("should correctly merge nested configs", async () => {

expect(actual).toEqual(expected);
});

const scaffoldedRoots: string[] = [];

afterEach(() => {
for (const root of scaffoldedRoots.splice(0)) {
fs.rmSync(root, { recursive: true, force: true });
}
});

// Create a throwaway `<root>/node_modules/<pkg>` layout and return <root>.
const scaffoldModule = (
pkg: string,
manifest: Record<string, unknown>,
files: Record<string, string>,
): string => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "resolve-from-"));
scaffoldedRoots.push(root);
const pkgDir = path.join(root, "node_modules", pkg);
fs.mkdirSync(pkgDir, { recursive: true });
fs.writeFileSync(path.join(pkgDir, "package.json"), JSON.stringify(manifest));
for (const [relative, content] of Object.entries(files)) {
const file = path.join(pkgDir, relative);
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, content);
}
return root;
};

// CommonJS resolvers cannot read an `exports` map that only declares the `import`
// condition (no require/default/main), so resolveFrom must fall back to reading the
// manifest itself. See conventional-changelog-angular@>=9 / conventionalcommits@>=10.
test("resolveFrom resolves a pure-ESM package exposing only the import condition", () => {
const root = scaffoldModule(
"esm-only-preset",
{
name: "esm-only-preset",
type: "module",
exports: { types: "./index.d.ts", import: "./index.js" },
},
{ "index.js": "export default {};" },
);

expect(resolveFrom("esm-only-preset", root)).toBe(
path.join(root, "node_modules", "esm-only-preset", "index.js"),
);
});

test("resolveFrom resolves a subpath of a pure-ESM package", () => {
const root = scaffoldModule(
"esm-only-preset",
{ name: "esm-only-preset", type: "module", exports: { import: "./index.js" } },
{ "index.js": "export default {};", "feature.js": "export default {};" },
);

expect(resolveFrom("esm-only-preset/feature", root)).toBe(
path.join(root, "node_modules", "esm-only-preset", "feature.js"),
);
});

test("resolveFrom falls back to main when the exports map has no runtime condition", () => {
const root = scaffoldModule(
"types-only-preset",
{ name: "types-only-preset", exports: { types: "./index.d.ts" }, main: "./main.js" },
{ "main.js": "module.exports = {};" },
);

expect(resolveFrom("types-only-preset", root)).toBe(
path.join(root, "node_modules", "types-only-preset", "main.js"),
);
});
88 changes: 88 additions & 0 deletions @commitlint/resolve-extends/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,90 @@ const pathSuffixes = ["", ".js", ".json", `${path.sep}index.js`, `${path.sep}ind

const specifierSuffixes = ["", ".js", ".json", "/index.js", "/index.json"];

/**
* Recover the entry point from a package manifest, preferring the ESM
* (`import`) condition of the `exports` map and falling back to `module`/`main`.
*/
const resolveExportsEntry = (manifest: Record<string, unknown>): string => {
const fromConditions = (value: unknown): string | undefined => {
if (typeof value === "string") {
return value;
}
if (value && typeof value === "object" && !Array.isArray(value)) {
const conditions = value as Record<string, unknown>;
for (const key of ["import", "module", "node", "default"]) {
if (key in conditions) {
const resolved = fromConditions(conditions[key]);
if (resolved) {
return resolved;
}
}
}
}
return undefined;
};

let exp = manifest.exports;
if (exp && typeof exp === "object" && !Array.isArray(exp) && "." in (exp as object)) {
exp = (exp as Record<string, unknown>)["."];
}

return (
fromConditions(exp) ||
(typeof manifest.module === "string" ? manifest.module : undefined) ||
(typeof manifest.main === "string" ? manifest.main : undefined) ||
"index.js"
);
};

/**
* Pure-ESM presets such as conventional-changelog-angular@>=9 and
* conventional-changelog-conventionalcommits@>=10 declare only the `import`
* condition in their `exports` map (no `require`/`default`/`main`), so the
* CommonJS resolvers above fail with ERR_PACKAGE_PATH_NOT_EXPORTED. Walk
* node_modules from the requesting location and read the manifest ourselves.
*/
const resolveEsmOnly = (lookup: string, fromDir: string): string | undefined => {
if (path.isAbsolute(lookup) || lookup.startsWith(".")) {
return undefined;
}

const segments = lookup.split("/");
const pkgName = lookup.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0];
const subpath = lookup.slice(pkgName.length).replace(/^\//, "");

let dir = fromDir;
for (;;) {
const pkgDir = path.join(dir, "node_modules", pkgName);
const manifestPath = path.join(pkgDir, "package.json");

if (fs.existsSync(manifestPath)) {
if (subpath) {
for (const suffix of pathSuffixes) {
const filename = path.join(pkgDir, subpath) + suffix;
if (fs.existsSync(filename)) {
return filename;
}
}
return undefined;
}

const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
const resolved = path.join(pkgDir, resolveExportsEntry(manifest));
if (fs.existsSync(resolved)) {
return resolved;
}
return undefined;
}

const parentDir = path.dirname(dir);
if (parentDir === dir) {
return undefined;
}
dir = parentDir;
}
};

export const resolveFrom = (lookup: string, parent?: string): string => {
if (path.isAbsolute(lookup)) {
for (const suffix of pathSuffixes) {
Expand Down Expand Up @@ -62,6 +146,10 @@ export const resolveFrom = (lookup: string, parent?: string): string => {
*/
return resolveFrom_(path.dirname(parentPath), lookup);
} catch {
const esmResolved = resolveEsmOnly(lookup, path.dirname(parentPath));
if (esmResolved) {
return esmResolved;
}
throw resolveError;
}
};
Expand Down
4 changes: 2 additions & 2 deletions @commitlint/rules/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@
"@commitlint/test": "workspace:^",
"@commitlint/utils": "workspace:^",
"@types/node": "^22.0.0",
"conventional-changelog-angular": "^8.2.0",
"conventional-commits-parser": "^6.3.0"
"conventional-changelog-angular": "^9.0.0",
"conventional-commits-parser": "^7.0.0"
},
"engines": {
"node": ">=22.12.0"
Expand Down
4 changes: 2 additions & 2 deletions @commitlint/rules/src/references-empty.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { test, expect } from "vitest";
import parse from "@commitlint/parse";
import { referencesEmpty } from "./references-empty.js";

// @ts-expect-error -- no typings
import type { ParserOptions } from "conventional-commits-parser";
import preset from "conventional-changelog-angular";

const messages = {
Expand All @@ -14,7 +14,7 @@ const messages = {
};

const opts = (async () => {
const o = await preset();
const o = (await preset()) as { parser: ParserOptions };
o.parser.commentChar = "#";
return o;
})();
Expand Down
6 changes: 3 additions & 3 deletions @commitlint/rules/src/subject-exclamation-mark.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { test, expect } from "vitest";
import parse from "@commitlint/parse";
// @ts-expect-error -- no typings
import type { ParserOptions } from "conventional-commits-parser";
import preset from "conventional-changelog-angular";

import { subjectExclamationMark } from "./subject-exclamation-mark.js";

const parseMessage = async (str: string) => {
const { parserOpts } = await preset();
return parse(str, undefined, parserOpts);
const { parser } = (await preset()) as { parser: ParserOptions };
return parse(str, undefined, parser);
};

const messages = {
Expand Down
2 changes: 1 addition & 1 deletion @commitlint/types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"pkg": "pkg-check"
},
"dependencies": {
"conventional-commits-parser": "^6.3.0",
"conventional-commits-parser": "^7.0.0",
"picocolors": "^1.1.1"
},
"devDependencies": {
Expand Down
Loading