@naturalcycles/dev-lib 20.17.0 → 20.18.1

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.
@@ -277,6 +277,7 @@
277
277
  "vitest/prefer-spy-on": 2,
278
278
  "vitest/prefer-to-be": 2,
279
279
  "vitest/prefer-to-contain": 2,
280
+ "vitest/consistent-test-filename": 2,
280
281
  "jest/consistent-test-it": [
281
282
  2,
282
283
  {
@@ -5,7 +5,8 @@ import { _assert } from '@naturalcycles/js-lib/error/assert.js';
5
5
  import { fs2 } from '@naturalcycles/nodejs-lib/fs2';
6
6
  import { runScript } from '@naturalcycles/nodejs-lib/runScript';
7
7
  import { buildCopy, buildProd, runTSCInFolders } from '../build.util.js';
8
- import { eslintAll, lintAllCommand, lintStagedCommand, requireOxlintConfig, runBiome, runCommitlintCommand, runOxlint, runPrettier, stylelintAll, } from '../lint.util.js';
8
+ import { runCommitlint } from '../commitlint.js';
9
+ import { eslintAll, lintAllCommand, lintStagedCommand, requireOxlintConfig, runBiome, runOxlint, runPrettier, stylelintAll, } from '../lint.util.js';
9
10
  import { runTest } from '../test.util.js';
10
11
  const commands = [
11
12
  new Separator(), // build
@@ -105,7 +106,7 @@ const commands = [
105
106
  desc: 'Run stylelint with auto-fix disabled.',
106
107
  },
107
108
  { name: 'stylelint-no-fix', cliOnly: true, fn: () => stylelintAll(false) },
108
- { name: 'commitlint', fn: runCommitlintCommand, desc: 'Run commitlint.', cliOnly: true },
109
+ { name: 'commitlint', fn: runCommitlint, desc: 'Run commitlint.', cliOnly: true },
109
110
  new Separator(), // interactive-only
110
111
  {
111
112
  name: 'exit',
@@ -0,0 +1,15 @@
1
+ import { type DevLibCommitlintConfig } from './config.js';
2
+ /**
3
+ * Validates the commit message,
4
+ * which is read from a file, passed as process.argv.at(-1)
5
+ */
6
+ export declare function runCommitlint(): Promise<void>;
7
+ /**
8
+ * Commit message validator following Conventional Commits specification.
9
+ * https://www.conventionalcommits.org/
10
+ */
11
+ export declare function validateCommitMessage(input: string, cfg?: DevLibCommitlintConfig): CommitMessageValidationResponse;
12
+ export interface CommitMessageValidationResponse {
13
+ valid: boolean;
14
+ errors: string[];
15
+ }
@@ -0,0 +1,106 @@
1
+ import { _assert } from '@naturalcycles/js-lib/error';
2
+ import { fs2 } from '@naturalcycles/nodejs-lib/fs2';
3
+ import { readDevLibConfigIfPresent } from './config.js';
4
+ const ALLOWED_TYPES = new Set([
5
+ 'feat',
6
+ 'fix',
7
+ 'chore',
8
+ 'refactor',
9
+ 'docs',
10
+ 'style',
11
+ 'test',
12
+ 'perf',
13
+ 'ci',
14
+ 'build',
15
+ 'revert',
16
+ ]);
17
+ const SUBJECT_MAX_LENGTH = 120; // Only applies to subject line (first line)
18
+ /**
19
+ * Validates the commit message,
20
+ * which is read from a file, passed as process.argv.at(-1)
21
+ */
22
+ export async function runCommitlint() {
23
+ // || '.git/COMMIT_EDITMSG' // fallback is unnecessary, first argument should be always present
24
+ const arg1 = process.argv.at(-1);
25
+ _assert(arg1, 'dev-lib commitlint2 is called with $1 (first argument) missing');
26
+ console.log({ arg1 });
27
+ fs2.requireFileToExist(arg1);
28
+ const msg = fs2.readText(arg1);
29
+ console.log({ msg });
30
+ const devLibCfg = await readDevLibConfigIfPresent();
31
+ console.log({ devLibCfg });
32
+ const { valid, errors } = validateCommitMessage(msg, devLibCfg.commitlint);
33
+ if (valid) {
34
+ console.log('✓ Valid commit message');
35
+ return;
36
+ }
37
+ console.error('✗ Invalid commit message:');
38
+ for (const err of errors) {
39
+ console.error(` - ${err}`);
40
+ }
41
+ process.exit(1);
42
+ }
43
+ /**
44
+ * Commit message validator following Conventional Commits specification.
45
+ * https://www.conventionalcommits.org/
46
+ */
47
+ export function validateCommitMessage(input, cfg = {}) {
48
+ const errors = [];
49
+ const msg = input.trim();
50
+ if (!msg) {
51
+ return { valid: false, errors: ['Commit message is empty'] };
52
+ }
53
+ const lines = msg.split('\n');
54
+ const subjectLine = lines[0];
55
+ // Step 1: Validate subject line format
56
+ // Pattern: type(scope)!: description OR type!: description OR type(scope): description OR type: description
57
+ const subjectPattern = /^(\w+)(?:\(([^)]+)\))?(!)?\s*:\s*(.+)$/;
58
+ const match = subjectLine.match(subjectPattern);
59
+ if (!match) {
60
+ errors.push(`Subject line must match format: type(scope): description\n` +
61
+ ` Got: "${subjectLine}"\n` +
62
+ ` Examples: "feat(auth): add login", "fix: resolve crash"`);
63
+ return { valid: false, errors };
64
+ }
65
+ const [, type, scope, _breaking, description] = match;
66
+ // Step 2: Validate type
67
+ if (!ALLOWED_TYPES.has(type)) {
68
+ errors.push(`Invalid type "${type}". Allowed types: ${[...ALLOWED_TYPES].join(', ')}`);
69
+ }
70
+ // Step 3: Validate subject line length
71
+ if (subjectLine.length > SUBJECT_MAX_LENGTH) {
72
+ errors.push(`Subject line too long: ${subjectLine.length} chars (max ${SUBJECT_MAX_LENGTH})`);
73
+ }
74
+ // Step 4: Validate description is not empty
75
+ if (!description?.trim()) {
76
+ errors.push('Description after colon cannot be empty');
77
+ }
78
+ // Step 5: Validate description doesn't start with capital letter (conventional style)
79
+ // Disabled: many existing commits use capitals
80
+ // if (description && /^[A-Z]/.test(description.trim())) {
81
+ // errors.push('Description should start with lowercase letter')
82
+ // }
83
+ // Step 6: Validate blank line between subject and body (if body exists)
84
+ if (lines.length > 1 && lines[1].trim() !== '') {
85
+ errors.push('There must be a blank line between subject and body');
86
+ }
87
+ // Note: No line length validation for body lines - they can be any length
88
+ // Step 7: scope validation
89
+ if (cfg.requireScope && !scope) {
90
+ errors.push('Scope is required');
91
+ }
92
+ if (scope && cfg.allowedScopes && !cfg.allowedScopes.includes(scope)) {
93
+ errors.push(`Scope must be one of the allowed scopes:\n${cfg.allowedScopes.join('\n')}`);
94
+ }
95
+ return {
96
+ valid: errors.length === 0,
97
+ errors,
98
+ // parsed: {
99
+ // type,
100
+ // scope: scope || null,
101
+ // breaking: !!breaking,
102
+ // description: description?.trim(),
103
+ // body: lines.slice(2).join('\n').trim() || null,
104
+ // },
105
+ };
106
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Returns an empty config if the file is absent.
3
+ */
4
+ export declare function readDevLibConfigIfPresent(cwd?: string): Promise<DevLibConfig>;
5
+ export interface DevLibConfig {
6
+ commitlint?: DevLibCommitlintConfig;
7
+ }
8
+ export interface DevLibCommitlintConfig {
9
+ /**
10
+ * Defaults to false.
11
+ * If set to true - commit scope becomes required.
12
+ */
13
+ requireScope?: boolean;
14
+ /**
15
+ * If defined - commitlint (which is run on git precommit hook) will validate that
16
+ * the scope is one of the allowedScopes.
17
+ * Empty (not present) scope will pass this rule, as it depends on the `requireScope` option instead.
18
+ */
19
+ allowedScopes?: string[];
20
+ }
package/dist/config.js ADDED
@@ -0,0 +1,24 @@
1
+ import { AjvSchema, j } from '@naturalcycles/nodejs-lib/ajv';
2
+ import { fs2 } from '@naturalcycles/nodejs-lib/fs2';
3
+ /**
4
+ * Returns an empty config if the file is absent.
5
+ */
6
+ export async function readDevLibConfigIfPresent(cwd = process.cwd()) {
7
+ const devLibConfigPath = `${cwd}/dev-lib.config.js`;
8
+ let cfg = {};
9
+ if (fs2.pathExists(devLibConfigPath)) {
10
+ cfg = (await import(devLibConfigPath)).default;
11
+ console.log(`read ${devLibConfigPath}`);
12
+ }
13
+ return devLibConfigSchema.validate(cfg);
14
+ }
15
+ const devLibConfigSchema = AjvSchema.create(j.object({
16
+ commitlint: j.object
17
+ .infer({
18
+ requireScope: j.boolean().optional(),
19
+ allowedScopes: j.array(j.string()).optional(),
20
+ })
21
+ .optional(),
22
+ }), {
23
+ inputName: 'dev-lib.config.js',
24
+ });
package/dist/index.d.ts CHANGED
@@ -1 +1 @@
1
- export type {};
1
+ export * from './config.js';
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export {};
1
+ export * from './config.js';
@@ -21,7 +21,6 @@ interface RunPrettierOptions {
21
21
  export declare function runPrettier(opt?: RunPrettierOptions): void;
22
22
  export declare function stylelintAll(fix?: boolean): void;
23
23
  export declare function lintStagedCommand(): Promise<void>;
24
- export declare function runCommitlintCommand(): void;
25
24
  export declare function requireActionlintVersion(): void;
26
25
  export declare function getActionLintVersion(): SemVerString | undefined;
27
26
  export declare function runBiome(fix?: boolean): void;
package/dist/lint.util.js CHANGED
@@ -7,7 +7,6 @@ import { _since } from '@naturalcycles/js-lib/datetime/time.util.js';
7
7
  import { _assert } from '@naturalcycles/js-lib/error/assert.js';
8
8
  import { _filterFalsyValues } from '@naturalcycles/js-lib/object/object.util.js';
9
9
  import { semver2 } from '@naturalcycles/js-lib/semver';
10
- import { git2 } from '@naturalcycles/nodejs-lib';
11
10
  import { dimGrey, white } from '@naturalcycles/nodejs-lib/colors';
12
11
  import { exec2 } from '@naturalcycles/nodejs-lib/exec2';
13
12
  import { fs2 } from '@naturalcycles/nodejs-lib/fs2';
@@ -231,24 +230,6 @@ export async function lintStagedCommand() {
231
230
  if (!success)
232
231
  process.exit(3);
233
232
  }
234
- export function runCommitlintCommand() {
235
- const editMsg = process.argv.at(-1) || '.git/COMMIT_EDITMSG';
236
- // console.log(editMsg)
237
- const cwd = process.cwd();
238
- const localConfig = `${cwd}/commitlint.config.js`;
239
- const sharedConfig = `${cfgDir}/commitlint.config.js`;
240
- const config = existsSync(localConfig) ? localConfig : sharedConfig;
241
- const env = {
242
- GIT_BRANCH: git2.getCurrentBranchName(),
243
- };
244
- const commitlintPath = findPackageBinPath('@commitlint/cli', 'commitlint');
245
- exec2.spawn(`${commitlintPath} --edit ${editMsg} --config ${config}`, {
246
- env,
247
- passProcessEnv: true, // important to pass it through, to preserve $PATH
248
- forceColor: false,
249
- log: false,
250
- });
251
- }
252
233
  async function runKTLint(fix = true) {
253
234
  if (!existsSync(`node_modules/@naturalcycles/ktlint`))
254
235
  return;
package/dist/paths.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export declare const projectDir: string;
2
+ export declare const repoDir: string;
2
3
  export declare const srcDir: string;
3
4
  export declare const testDir: string;
4
5
  export declare const cfgDir: string;
package/dist/paths.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { join } from 'node:path';
2
2
  export const projectDir = join(import.meta.dirname, '..');
3
+ export const repoDir = join(projectDir, '../..');
3
4
  export const srcDir = `${projectDir}/src`;
4
5
  export const testDir = `${srcDir}/test`;
5
6
  export const cfgDir = `${projectDir}/cfg`;
package/package.json CHANGED
@@ -1,11 +1,9 @@
1
1
  {
2
2
  "name": "@naturalcycles/dev-lib",
3
3
  "type": "module",
4
- "version": "20.17.0",
4
+ "version": "20.18.1",
5
5
  "dependencies": {
6
6
  "@biomejs/biome": "^2",
7
- "@commitlint/cli": "^20",
8
- "@commitlint/config-conventional": "^20",
9
7
  "@eslint/js": "^9",
10
8
  "@inquirer/prompts": "^8",
11
9
  "@naturalcycles/js-lib": "^15",