commitlint-config-spellbookx 1.1.1 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/__tests__/index.test.ts +35 -18
- package/__tests__/lint.test.ts +26 -54
- package/__tests__/scopes.test.ts +19 -32
- package/__tests__/workspaces.test.ts +111 -50
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
package/__tests__/index.test.ts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* @file index.test.ts
|
|
3
|
-
* Tests verifying the main configuration export and metadata of commitlint-config-spellbookx.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, expect, test } from 'vitest';
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
7
2
|
|
|
8
3
|
import defaultExport, {
|
|
9
4
|
COMMIT_TYPES,
|
|
@@ -17,12 +12,11 @@ import defaultExport, {
|
|
|
17
12
|
} from '../src/index.js';
|
|
18
13
|
import { packageJson } from '../src/lib/package-json.js';
|
|
19
14
|
|
|
20
|
-
describe('commitlint-config-spellbookx entrypoint', () => {
|
|
21
|
-
|
|
15
|
+
describe('commitlint-config-spellbookx (entrypoint)', () => {
|
|
16
|
+
it('exports a valid commitlint UserConfig object as default and named export', () => {
|
|
22
17
|
expect(defaultExport).toBe(config);
|
|
23
|
-
expect(
|
|
24
|
-
expect(
|
|
25
|
-
expect(config.extends).toContain('@commitlint/config-conventional');
|
|
18
|
+
expect(config).toBeTypeOf('object');
|
|
19
|
+
expect(config.extends).toEqual(['@commitlint/config-conventional']);
|
|
26
20
|
expect(config.parserPreset).toBe(
|
|
27
21
|
'conventional-changelog-conventionalcommits'
|
|
28
22
|
);
|
|
@@ -30,18 +24,41 @@ describe('commitlint-config-spellbookx entrypoint', () => {
|
|
|
30
24
|
expect(config.prompt).toBe(prompt);
|
|
31
25
|
});
|
|
32
26
|
|
|
33
|
-
|
|
27
|
+
it('exposes accurate package metadata', () => {
|
|
34
28
|
expect(packageJson.name).toBe('commitlint-config-spellbookx');
|
|
35
|
-
expect(packageJson.version).
|
|
29
|
+
expect(packageJson.version).toMatch(/^\d+\.\d+\.\d+/);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('exports all 11 standard conventional commit types', () => {
|
|
33
|
+
expect(COMMIT_TYPES).toEqual([
|
|
34
|
+
'feat',
|
|
35
|
+
'fix',
|
|
36
|
+
'docs',
|
|
37
|
+
'style',
|
|
38
|
+
'refactor',
|
|
39
|
+
'perf',
|
|
40
|
+
'test',
|
|
41
|
+
'chore',
|
|
42
|
+
'ci',
|
|
43
|
+
'build',
|
|
44
|
+
'revert',
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('configures cz-git interactive prompt questions and settings', () => {
|
|
49
|
+
expect(prompt?.settings?.enableMultipleScopes).toBe(true);
|
|
50
|
+
expect(prompt?.settings?.scopeEnumSeparator).toBe(',');
|
|
51
|
+
expect(prompt?.questions?.scope?.enum).toBe(scopesEnum);
|
|
52
|
+
|
|
53
|
+
for (const type of COMMIT_TYPES) {
|
|
54
|
+
expect(prompt?.questions?.type?.enum?.[type]).toBeDefined();
|
|
55
|
+
}
|
|
36
56
|
});
|
|
37
57
|
|
|
38
|
-
|
|
39
|
-
expect(Array.isArray(COMMIT_TYPES)).toBe(true);
|
|
40
|
-
expect(COMMIT_TYPES).toContain('feat');
|
|
41
|
-
expect(COMMIT_TYPES).toContain('fix');
|
|
58
|
+
it('exports special scope constants and dynamically discovered scopes', () => {
|
|
42
59
|
expect(SCOPE_WORKSPACE).toBe('workspace');
|
|
43
60
|
expect(SCOPE_RELEASE).toBe('release');
|
|
44
61
|
expect(Array.isArray(scopesArray)).toBe(true);
|
|
45
|
-
expect(
|
|
62
|
+
expect(scopesEnum).toBeTypeOf('object');
|
|
46
63
|
});
|
|
47
64
|
});
|
package/__tests__/lint.test.ts
CHANGED
|
@@ -1,65 +1,37 @@
|
|
|
1
|
-
|
|
2
|
-
* @file lint.test.ts
|
|
3
|
-
* Tests verifying conventional commit validation rules using `@commitlint/lint`.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, expect, test } from 'vitest';
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
7
2
|
|
|
8
3
|
import lint from '@commitlint/lint';
|
|
9
4
|
|
|
10
5
|
import config from '../src/index.js';
|
|
11
6
|
|
|
12
|
-
describe('commitlint
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
test('should validate valid workspace scope commits', async () => {
|
|
23
|
-
const result = await lint(
|
|
24
|
-
'chore(workspace): update root configs',
|
|
25
|
-
config.rules
|
|
26
|
-
);
|
|
27
|
-
expect(result.valid).toBe(true);
|
|
28
|
-
expect(result.errors).toHaveLength(0);
|
|
29
|
-
});
|
|
7
|
+
describe('commitlint-config-spellbookx (lint rules)', () => {
|
|
8
|
+
it.each([
|
|
9
|
+
'feat(eslint-plugin-spellbookx): add new flat config preset',
|
|
10
|
+
'fix(prettier-config-spellbookx): resolve plugin sort order',
|
|
11
|
+
'chore(workspace): update root dependencies',
|
|
12
|
+
'chore(release): bump to v1.1.1',
|
|
13
|
+
'docs(@spellbookx/recipes): document ignore globs',
|
|
14
|
+
'test(spellbookx): rewrite test suite with vitest',
|
|
15
|
+
])('accepts valid conventional commit: "%s"', async (message) => {
|
|
16
|
+
const result = await lint(message, config.rules);
|
|
30
17
|
|
|
31
|
-
test('should validate valid release scope commits', async () => {
|
|
32
|
-
const result = await lint('feat(release): publish v1.0.0', config.rules);
|
|
33
18
|
expect(result.valid).toBe(true);
|
|
34
19
|
expect(result.errors).toHaveLength(0);
|
|
35
20
|
});
|
|
36
21
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
config.rules
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
test('should fail when subject has a trailing period', async () => {
|
|
53
|
-
const result = await lint('fix(workspace): fix config typo.', config.rules);
|
|
54
|
-
expect(result.valid).toBe(false);
|
|
55
|
-
expect(result.errors.some((e) => e.name === 'subject-full-stop')).toBe(
|
|
56
|
-
true
|
|
57
|
-
);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test('should fail when subject is empty', async () => {
|
|
61
|
-
const result = await lint('fix(workspace): ', config.rules);
|
|
62
|
-
expect(result.valid).toBe(false);
|
|
63
|
-
expect(result.errors.some((e) => e.name === 'subject-empty')).toBe(true);
|
|
64
|
-
});
|
|
22
|
+
it.each([
|
|
23
|
+
['unknown(workspace): invalid commit type', 'type-enum'],
|
|
24
|
+
['feat(nonexistent-scope): unknown package scope', 'scope-enum'],
|
|
25
|
+
['fix(workspace): trailing period in subject.', 'subject-full-stop'],
|
|
26
|
+
['fix(workspace): ', 'subject-empty'],
|
|
27
|
+
[`feat(workspace): ${'a'.repeat(80)}`, 'header-max-length'],
|
|
28
|
+
])(
|
|
29
|
+
'rejects invalid commit "%s" with rule "%s"',
|
|
30
|
+
async (message, expectedRule) => {
|
|
31
|
+
const result = await lint(message, config.rules);
|
|
32
|
+
|
|
33
|
+
expect(result.valid).toBe(false);
|
|
34
|
+
expect(result.errors.some((err) => err.name === expectedRule)).toBe(true);
|
|
35
|
+
}
|
|
36
|
+
);
|
|
65
37
|
});
|
package/__tests__/scopes.test.ts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* @file scopes.test.ts
|
|
3
|
-
* Tests for scope identifiers, scope descriptions, and scopes enumeration.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, expect, test } from 'vitest';
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
7
2
|
|
|
8
3
|
import {
|
|
9
4
|
getScopeDescription,
|
|
@@ -13,43 +8,35 @@ import {
|
|
|
13
8
|
scopesEnum,
|
|
14
9
|
} from '../src/index.js';
|
|
15
10
|
|
|
16
|
-
describe('commitlint scopes', () => {
|
|
17
|
-
|
|
11
|
+
describe('commitlint-config-spellbookx (scopes)', () => {
|
|
12
|
+
it('defines workspace and release scope constants', () => {
|
|
18
13
|
expect(SCOPE_WORKSPACE).toBe('workspace');
|
|
19
14
|
expect(SCOPE_RELEASE).toBe('release');
|
|
20
15
|
});
|
|
21
16
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
17
|
+
it.each([
|
|
18
|
+
SCOPE_WORKSPACE,
|
|
19
|
+
SCOPE_RELEASE,
|
|
20
|
+
'@spellbookx/recipes',
|
|
21
|
+
'commitlint-config-spellbookx',
|
|
22
|
+
'cspell-config-spellbookx',
|
|
23
|
+
'eslint-plugin-spellbookx',
|
|
24
|
+
'prettier-config-spellbookx',
|
|
25
|
+
'spellbookx',
|
|
26
|
+
])('includes "%s" in scopesArray and scopesEnum', (scope) => {
|
|
27
|
+
expect(scopesArray).toContain(scope);
|
|
28
|
+
expect(scopesEnum[scope]).toEqual({
|
|
29
|
+
description: getScopeDescription(scope),
|
|
30
|
+
});
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
it('returns descriptive text for special and package scopes', () => {
|
|
34
34
|
expect(getScopeDescription(SCOPE_WORKSPACE)).toBe(
|
|
35
35
|
'workspace scope - root files such as config files, root package.json, etc'
|
|
36
36
|
);
|
|
37
37
|
expect(getScopeDescription(SCOPE_RELEASE)).toBe(
|
|
38
38
|
'Special scope for release commits'
|
|
39
39
|
);
|
|
40
|
-
expect(getScopeDescription('custom-pkg')).toBe('custom-pkg scope');
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test('scopesEnum should populate descriptions for all scopes', () => {
|
|
44
|
-
expect(scopesEnum[SCOPE_WORKSPACE]?.description).toBe(
|
|
45
|
-
getScopeDescription(SCOPE_WORKSPACE)
|
|
46
|
-
);
|
|
47
|
-
expect(scopesEnum[SCOPE_RELEASE]?.description).toBe(
|
|
48
|
-
getScopeDescription(SCOPE_RELEASE)
|
|
49
|
-
);
|
|
50
|
-
for (const scope of scopesArray) {
|
|
51
|
-
expect(scopesEnum[scope]).toBeDefined();
|
|
52
|
-
expect(scopesEnum[scope]?.description).toBe(getScopeDescription(scope));
|
|
53
|
-
}
|
|
40
|
+
expect(getScopeDescription('my-custom-pkg')).toBe('my-custom-pkg scope');
|
|
54
41
|
});
|
|
55
42
|
});
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from 'node:fs';
|
|
8
|
+
import os from 'node:os';
|
|
7
9
|
import path from 'node:path';
|
|
8
10
|
import { fileURLToPath } from 'node:url';
|
|
9
11
|
|
|
10
|
-
import {
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
11
13
|
|
|
12
14
|
import {
|
|
13
15
|
findGitRoot,
|
|
@@ -16,61 +18,120 @@ import {
|
|
|
16
18
|
} from '../src/lib/index.js';
|
|
17
19
|
|
|
18
20
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
-
const testDir = path.resolve(__dirname, '../temp-test-commitlint');
|
|
20
21
|
|
|
21
|
-
describe('workspace discovery
|
|
22
|
-
|
|
23
|
-
if (existsSync(testDir)) rmSync(testDir, { recursive: true, force: true });
|
|
24
|
-
mkdirSync(testDir, { recursive: true });
|
|
25
|
-
});
|
|
22
|
+
describe('commitlint-config-spellbookx (workspace discovery)', () => {
|
|
23
|
+
let tempDir: string;
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
tempDir = mkdtempSync(path.join(os.tmpdir(), 'sbx-commitlint-'));
|
|
29
27
|
});
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
expect(root).not.toBeNull();
|
|
34
|
-
expect(existsSync(path.join(root!, '.git'))).toBe(true);
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
35
31
|
});
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
writeFileSync(
|
|
41
|
-
jsonPath,
|
|
42
|
-
JSON.stringify({ workspaces: ['packages/*', 'apps/*'] })
|
|
43
|
-
);
|
|
33
|
+
describe('findGitRoot', () => {
|
|
34
|
+
it('locates the git root directory from a nested package path', () => {
|
|
35
|
+
const root = findGitRoot(__dirname);
|
|
44
36
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
37
|
+
expect(root).not.toBeNull();
|
|
38
|
+
expect(existsSync(path.join(root!, '.git'))).toBe(true);
|
|
39
|
+
});
|
|
48
40
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
mkdirSync(testDir, { recursive: true });
|
|
52
|
-
writeFileSync(yamlPath, 'packages:\n - "packages/*"\n - "plugins/*"\n');
|
|
41
|
+
it('returns null when no .git directory exists up to filesystem root', () => {
|
|
42
|
+
const root = findGitRoot(tempDir);
|
|
53
43
|
|
|
54
|
-
|
|
55
|
-
|
|
44
|
+
expect(root).toBeNull();
|
|
45
|
+
});
|
|
56
46
|
});
|
|
57
47
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
48
|
+
describe('getWorkspacesRawPath', () => {
|
|
49
|
+
it('parses workspace globs from a JSON file', () => {
|
|
50
|
+
const jsonPath = path.join(tempDir, 'package.json');
|
|
51
|
+
writeFileSync(
|
|
52
|
+
jsonPath,
|
|
53
|
+
JSON.stringify({ workspaces: ['packages/*', 'apps/*'] })
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
expect(getWorkspacesRawPath(jsonPath, 'json', 'workspaces')).toEqual([
|
|
57
|
+
'packages/*',
|
|
58
|
+
'apps/*',
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('parses package globs from a YAML file', () => {
|
|
63
|
+
const yamlPath = path.join(tempDir, 'pnpm-workspace.yaml');
|
|
64
|
+
writeFileSync(yamlPath, 'packages:\n - "packages/*"\n - "plugins/*"\n');
|
|
65
|
+
|
|
66
|
+
expect(getWorkspacesRawPath(yamlPath, 'yaml', 'packages')).toEqual([
|
|
67
|
+
'packages/*',
|
|
68
|
+
'plugins/*',
|
|
69
|
+
]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('returns an empty array when file does not exist or property is missing', () => {
|
|
73
|
+
expect(
|
|
74
|
+
getWorkspacesRawPath(path.join(tempDir, 'missing.json'), 'json')
|
|
75
|
+
).toEqual([]);
|
|
76
|
+
|
|
77
|
+
const jsonPath = path.join(tempDir, 'empty.json');
|
|
78
|
+
writeFileSync(jsonPath, JSON.stringify({ name: 'no-workspaces' }));
|
|
79
|
+
expect(getWorkspacesRawPath(jsonPath, 'json', 'workspaces')).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('throws an error when given an unsupported parser type', () => {
|
|
83
|
+
const filePath = path.join(tempDir, 'config.txt');
|
|
84
|
+
writeFileSync(filePath, 'content');
|
|
85
|
+
|
|
86
|
+
expect(() => {
|
|
87
|
+
// @ts-expect-error Testing invalid parser runtime check
|
|
88
|
+
getWorkspacesRawPath(filePath, 'xml');
|
|
89
|
+
}).toThrow(/Unsupported parser type: xml/);
|
|
90
|
+
});
|
|
65
91
|
});
|
|
66
92
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
93
|
+
describe('getScopes', () => {
|
|
94
|
+
it('discovers all package scopes in the current monorepo', () => {
|
|
95
|
+
const scopes = getScopes(__dirname);
|
|
96
|
+
|
|
97
|
+
expect(scopes).toEqual(
|
|
98
|
+
expect.arrayContaining([
|
|
99
|
+
'@spellbookx/recipes',
|
|
100
|
+
'commitlint-config-spellbookx',
|
|
101
|
+
'cspell-config-spellbookx',
|
|
102
|
+
'eslint-plugin-spellbookx',
|
|
103
|
+
'prettier-config-spellbookx',
|
|
104
|
+
'spellbookx',
|
|
105
|
+
])
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('discovers scopes from a simulated git repository with workspaces and ignores malformed package.json', () => {
|
|
110
|
+
mkdirSync(path.join(tempDir, '.git'));
|
|
111
|
+
writeFileSync(
|
|
112
|
+
path.join(tempDir, 'package.json'),
|
|
113
|
+
JSON.stringify({ name: 'root-pkg', workspaces: ['packages/*'] })
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const pkgA = path.join(tempDir, 'packages/pkg-a');
|
|
117
|
+
const pkgInvalid = path.join(tempDir, 'packages/pkg-invalid');
|
|
118
|
+
mkdirSync(pkgA, { recursive: true });
|
|
119
|
+
mkdirSync(pkgInvalid, { recursive: true });
|
|
120
|
+
|
|
121
|
+
writeFileSync(
|
|
122
|
+
path.join(pkgA, 'package.json'),
|
|
123
|
+
JSON.stringify({ name: '@acme/pkg-a' })
|
|
124
|
+
);
|
|
125
|
+
writeFileSync(path.join(pkgInvalid, 'package.json'), '{ invalid json');
|
|
126
|
+
|
|
127
|
+
const scopes = getScopes(tempDir);
|
|
128
|
+
|
|
129
|
+
expect(scopes).toContain('root-pkg');
|
|
130
|
+
expect(scopes).toContain('@acme/pkg-a');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('throws a fatal error when invoked outside a git repository', () => {
|
|
134
|
+
expect(() => getScopes(tempDir)).toThrow('[FATAL] Not a repository.');
|
|
135
|
+
});
|
|
75
136
|
});
|
|
76
137
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "commitlint-config-spellbookx",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A shareable Commitlint configuration for enforcing conventional commits.",
|
|
6
6
|
"keywords": [
|
|
@@ -90,6 +90,6 @@
|
|
|
90
90
|
"scripts": {
|
|
91
91
|
"build": "rolldown -c && tsc",
|
|
92
92
|
"release": "pnpm publish --access public --no-git-checks",
|
|
93
|
-
"test": "vitest run"
|
|
93
|
+
"test": "vitest run --no-fsModuleCache"
|
|
94
94
|
}
|
|
95
95
|
}
|