@regardio/dev 1.10.3 → 1.11.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.
package/README.md CHANGED
@@ -27,7 +27,7 @@ The goal is code that's correct, consistent, and a pleasure to work with.
27
27
  | **Testing** | Vitest, Playwright, Testing Library |
28
28
  | **Build** | TypeScript, tsx, Vite |
29
29
  | **Workflow** | Husky, Changesets |
30
- | **CLI utilities** | exec-clean, exec-p, exec-s, exec-ts, flow-release, lint-biome, lint-md |
30
+ | **CLI utilities** | exec-clean, exec-p, exec-s, exec-ts, flow-release, lint-biome, lint-md, lint-package |
31
31
 
32
32
  ## Quick Start
33
33
 
@@ -30,6 +30,17 @@ if (!packageName) {
30
30
  process.exit(1);
31
31
  }
32
32
  console.log(`Releasing ${packageName} with ${bumpType} bump...`);
33
+ console.log('Running quality checks...');
34
+ try {
35
+ run('pnpm build');
36
+ run('pnpm typecheck');
37
+ run('pnpm report');
38
+ }
39
+ catch {
40
+ console.error('Quality checks failed. Fix issues before releasing.');
41
+ process.exit(1);
42
+ }
43
+ console.log('✅ Quality checks passed');
33
44
  try {
34
45
  const status = runQuiet('git status --porcelain');
35
46
  if (status) {
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=lint-package.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lint-package.d.ts","sourceRoot":"","sources":["../../src/bin/lint-package.ts"],"names":[],"mappings":""}
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ import { execSync } from 'node:child_process';
3
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const devRoot = resolve(__dirname, '../..');
8
+ const sortPkgBin = join(devRoot, 'node_modules/.bin/sort-package-json');
9
+ const sortPkgBinAlt = join(devRoot, 'node_modules/sort-package-json/cli.js');
10
+ let bin = '';
11
+ if (existsSync(sortPkgBin)) {
12
+ bin = sortPkgBin;
13
+ }
14
+ else if (existsSync(sortPkgBinAlt)) {
15
+ bin = `node ${sortPkgBinAlt}`;
16
+ }
17
+ else {
18
+ bin = 'npx sort-package-json';
19
+ }
20
+ const args = process.argv.slice(2);
21
+ const fixMode = args.includes('--fix');
22
+ const files = args.filter((arg) => arg !== '--fix');
23
+ const targets = files.length > 0 ? files : ['package.json'];
24
+ try {
25
+ const checkFlag = fixMode ? '' : '--check';
26
+ execSync(`${bin} ${checkFlag} ${targets.join(' ')}`.trim(), { stdio: 'inherit' });
27
+ }
28
+ catch {
29
+ process.exit(1);
30
+ }
31
+ function fixExportsOrder(filePath, fix) {
32
+ const fullPath = resolve(process.cwd(), filePath);
33
+ if (!existsSync(fullPath))
34
+ return false;
35
+ const content = readFileSync(fullPath, 'utf-8');
36
+ const pkg = JSON.parse(content);
37
+ if (!pkg.exports || typeof pkg.exports !== 'object')
38
+ return false;
39
+ let modified = false;
40
+ function reorderConditions(obj) {
41
+ if (typeof obj !== 'object' || obj === null)
42
+ return obj;
43
+ if ('types' in obj && 'default' in obj) {
44
+ const keys = Object.keys(obj);
45
+ const typesIndex = keys.indexOf('types');
46
+ const defaultIndex = keys.indexOf('default');
47
+ if (defaultIndex < typesIndex) {
48
+ modified = true;
49
+ const reordered = {};
50
+ reordered.types = obj.types;
51
+ for (const key of keys) {
52
+ if (key !== 'types') {
53
+ reordered[key] = obj[key];
54
+ }
55
+ }
56
+ return reordered;
57
+ }
58
+ }
59
+ const result = {};
60
+ for (const [key, value] of Object.entries(obj)) {
61
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
62
+ result[key] = reorderConditions(value);
63
+ }
64
+ else {
65
+ result[key] = value;
66
+ }
67
+ }
68
+ return result;
69
+ }
70
+ pkg.exports = reorderConditions(pkg.exports);
71
+ if (modified && fix) {
72
+ writeFileSync(fullPath, `${JSON.stringify(pkg, null, 2)}\n`);
73
+ }
74
+ return modified;
75
+ }
76
+ let hasExportsIssues = false;
77
+ for (const file of targets) {
78
+ const needsFix = fixExportsOrder(file, fixMode);
79
+ if (needsFix && !fixMode) {
80
+ console.error(`${file}: exports condition order is incorrect (types must come before default)`);
81
+ hasExportsIssues = true;
82
+ }
83
+ }
84
+ if (hasExportsIssues) {
85
+ process.exit(1);
86
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=lint-package.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lint-package.test.d.ts","sourceRoot":"","sources":["../../src/bin/lint-package.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,111 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ function reorderConditions(obj) {
3
+ function processObject(o) {
4
+ if (typeof o !== 'object' || o === null)
5
+ return o;
6
+ const processed = {};
7
+ for (const [key, value] of Object.entries(o)) {
8
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
9
+ processed[key] = processObject(value);
10
+ }
11
+ else {
12
+ processed[key] = value;
13
+ }
14
+ }
15
+ if ('types' in processed && 'default' in processed) {
16
+ const keys = Object.keys(processed);
17
+ const typesIndex = keys.indexOf('types');
18
+ const defaultIndex = keys.indexOf('default');
19
+ if (defaultIndex < typesIndex) {
20
+ const reordered = {};
21
+ reordered.types = processed.types;
22
+ for (const key of keys) {
23
+ if (key !== 'types') {
24
+ reordered[key] = processed[key];
25
+ }
26
+ }
27
+ return reordered;
28
+ }
29
+ }
30
+ return processed;
31
+ }
32
+ return processObject(obj);
33
+ }
34
+ describe('lint-package', () => {
35
+ describe('reorderConditions', () => {
36
+ it('should reorder types before default when default comes first', () => {
37
+ const input = {
38
+ './foo': {
39
+ default: './dist/foo.js',
40
+ types: './dist/foo.d.ts',
41
+ },
42
+ };
43
+ const result = reorderConditions(input);
44
+ expect(Object.keys(result['./foo'])).toEqual(['types', 'default']);
45
+ });
46
+ it('should not modify when types already comes before default', () => {
47
+ const input = {
48
+ './foo': {
49
+ default: './dist/foo.js',
50
+ types: './dist/foo.d.ts',
51
+ },
52
+ };
53
+ const result = reorderConditions(input);
54
+ expect(Object.keys(result['./foo'])).toEqual(['types', 'default']);
55
+ });
56
+ it('should handle multiple exports with mixed order', () => {
57
+ const input = {
58
+ './a': {
59
+ default: './dist/a.js',
60
+ types: './dist/a.d.ts',
61
+ },
62
+ './b': {
63
+ default: './dist/b.js',
64
+ types: './dist/b.d.ts',
65
+ },
66
+ };
67
+ const result = reorderConditions(input);
68
+ expect(Object.keys(result['./a'])[0]).toBe('types');
69
+ expect(Object.keys(result['./b'])[0]).toBe('types');
70
+ });
71
+ it('should preserve other keys after types', () => {
72
+ const input = {
73
+ './foo': {
74
+ default: './dist/foo.js',
75
+ import: './dist/foo.mjs',
76
+ require: './dist/foo.cjs',
77
+ types: './dist/foo.d.ts',
78
+ },
79
+ };
80
+ const result = reorderConditions(input);
81
+ const keys = Object.keys(result['./foo']);
82
+ expect(keys[0]).toBe('types');
83
+ expect(keys.slice(1)).toEqual(['default', 'import', 'require']);
84
+ });
85
+ it('should handle exports without types or default', () => {
86
+ const input = {
87
+ './styles.css': './dist/styles.css',
88
+ };
89
+ const result = reorderConditions(input);
90
+ expect(result).toEqual(input);
91
+ });
92
+ it('should handle deeply nested condition objects', () => {
93
+ const input = {
94
+ './foo': {
95
+ browser: {
96
+ default: './dist/foo.browser.js',
97
+ types: './dist/foo.browser.d.ts',
98
+ },
99
+ node: {
100
+ default: './dist/foo.node.js',
101
+ types: './dist/foo.node.d.ts',
102
+ },
103
+ },
104
+ };
105
+ const result = reorderConditions(input);
106
+ const foo = result['./foo'];
107
+ expect(Object.keys(foo.node)[0]).toBe('types');
108
+ expect(Object.keys(foo.browser)[0]).toBe('types');
109
+ });
110
+ });
111
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=post-build-exports.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"post-build-exports.test.d.ts","sourceRoot":"","sources":["../../src/bin/post-build-exports.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,119 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ function parseArgs(args) {
3
+ const result = {
4
+ dist: 'dist',
5
+ prefix: './',
6
+ preserve: [],
7
+ strip: '',
8
+ };
9
+ for (let i = 0; i < args.length; i++) {
10
+ const arg = args[i];
11
+ const next = args[i + 1];
12
+ if (arg === '--dist' && next) {
13
+ result.dist = next;
14
+ i++;
15
+ }
16
+ else if (arg === '--preserve' && next) {
17
+ result.preserve = next.split(',').map((p) => p.trim());
18
+ i++;
19
+ }
20
+ else if (arg === '--prefix' && next) {
21
+ result.prefix = next;
22
+ i++;
23
+ }
24
+ else if (arg === '--strip' && next) {
25
+ result.strip = next;
26
+ i++;
27
+ }
28
+ }
29
+ return result;
30
+ }
31
+ function generateExportName(jsPath, strip) {
32
+ let exportPath = jsPath.replace(/\.js$/, '');
33
+ if (strip && exportPath.startsWith(strip)) {
34
+ exportPath = exportPath.slice(strip.length);
35
+ if (exportPath.startsWith('/')) {
36
+ exportPath = exportPath.slice(1);
37
+ }
38
+ }
39
+ if (exportPath.endsWith('/index')) {
40
+ exportPath = exportPath.slice(0, -6);
41
+ }
42
+ return `./${exportPath}`;
43
+ }
44
+ describe('post-build-exports', () => {
45
+ describe('parseArgs', () => {
46
+ it('should return defaults when no args provided', () => {
47
+ const result = parseArgs([]);
48
+ expect(result).toEqual({
49
+ dist: 'dist',
50
+ prefix: './',
51
+ preserve: [],
52
+ strip: '',
53
+ });
54
+ });
55
+ it('should parse --dist option', () => {
56
+ const result = parseArgs(['--dist', 'build']);
57
+ expect(result.dist).toBe('build');
58
+ });
59
+ it('should parse --preserve option with single value', () => {
60
+ const result = parseArgs(['--preserve', './tailwind.css']);
61
+ expect(result.preserve).toEqual(['./tailwind.css']);
62
+ });
63
+ it('should parse --preserve option with multiple comma-separated values', () => {
64
+ const result = parseArgs(['--preserve', './a.css, ./b.css, ./c.css']);
65
+ expect(result.preserve).toEqual(['./a.css', './b.css', './c.css']);
66
+ });
67
+ it('should parse --prefix option', () => {
68
+ const result = parseArgs(['--prefix', './lib/']);
69
+ expect(result.prefix).toBe('./lib/');
70
+ });
71
+ it('should parse --strip option', () => {
72
+ const result = parseArgs(['--strip', 'generated']);
73
+ expect(result.strip).toBe('generated');
74
+ });
75
+ it('should parse multiple options together', () => {
76
+ const result = parseArgs([
77
+ '--dist',
78
+ 'output',
79
+ '--preserve',
80
+ './styles.css',
81
+ '--strip',
82
+ 'src',
83
+ ]);
84
+ expect(result).toEqual({
85
+ dist: 'output',
86
+ prefix: './',
87
+ preserve: ['./styles.css'],
88
+ strip: 'src',
89
+ });
90
+ });
91
+ it('should ignore unknown options', () => {
92
+ const result = parseArgs(['--unknown', 'value', '--dist', 'build']);
93
+ expect(result.dist).toBe('build');
94
+ });
95
+ });
96
+ describe('generateExportName', () => {
97
+ it('should convert .js path to export name', () => {
98
+ expect(generateExportName('foo.js', '')).toBe('./foo');
99
+ });
100
+ it('should handle nested paths', () => {
101
+ expect(generateExportName('utils/helpers.js', '')).toBe('./utils/helpers');
102
+ });
103
+ it('should strip /index suffix', () => {
104
+ expect(generateExportName('components/button/index.js', '')).toBe('./components/button');
105
+ });
106
+ it('should apply strip prefix', () => {
107
+ expect(generateExportName('generated/icons/arrow.js', 'generated')).toBe('./icons/arrow');
108
+ });
109
+ it('should handle strip with leading slash', () => {
110
+ expect(generateExportName('generated/icons/arrow.js', 'generated/')).toBe('./icons/arrow');
111
+ });
112
+ it('should handle index.js at root', () => {
113
+ expect(generateExportName('index.js', '')).toBe('./index');
114
+ });
115
+ it('should handle deeply nested index files', () => {
116
+ expect(generateExportName('a/b/c/index.js', '')).toBe('./a/b/c');
117
+ });
118
+ });
119
+ });
@@ -1,3 +1,9 @@
1
1
  import type { TestUserConfig } from 'vitest/node';
2
+ export declare const coverageThresholds: {
3
+ branches: number;
4
+ functions: number;
5
+ lines: number;
6
+ statements: number;
7
+ };
2
8
  export declare const vitestNodeConfig: TestUserConfig;
3
9
  //# sourceMappingURL=node.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/vitest/node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAMlD,eAAO,MAAM,gBAAgB,EAAE,cAK9B,CAAC"}
1
+ {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/vitest/node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAMlD,eAAO,MAAM,kBAAkB;;;;;CAK9B,CAAC;AAMF,eAAO,MAAM,gBAAgB,EAAE,cAS9B,CAAC"}
@@ -1,4 +1,14 @@
1
+ export const coverageThresholds = {
2
+ branches: 80,
3
+ functions: 80,
4
+ lines: 80,
5
+ statements: 80,
6
+ };
1
7
  export const vitestNodeConfig = {
8
+ coverage: {
9
+ provider: 'v8',
10
+ thresholds: coverageThresholds,
11
+ },
2
12
  environment: 'node',
3
13
  exclude: ['node_modules', 'dist', 'build', '.turbo', '.react-router'],
4
14
  globals: true,
@@ -1 +1 @@
1
- {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../../src/vitest/react.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAQlD,eAAO,MAAM,iBAAiB,EAAE,cAM/B,CAAC"}
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../../src/vitest/react.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AASlD,eAAO,MAAM,iBAAiB,EAAE,cAU/B,CAAC"}
@@ -1,4 +1,9 @@
1
+ import { coverageThresholds } from './node';
1
2
  export const vitestReactConfig = {
3
+ coverage: {
4
+ provider: 'v8',
5
+ thresholds: coverageThresholds,
6
+ },
2
7
  environment: 'jsdom',
3
8
  exclude: ['node_modules', 'dist', 'build', '.turbo', '.react-router'],
4
9
  globals: true,
package/package.json CHANGED
@@ -1,6 +1,60 @@
1
1
  {
2
2
  "$schema": "https://www.schemastore.org/package.json",
3
+ "name": "@regardio/dev",
4
+ "version": "1.11.1",
5
+ "private": false,
6
+ "description": "Regardio developer tooling for testing, linting, and build workflows",
7
+ "keywords": [
8
+ "biome",
9
+ "changesets",
10
+ "commitlint",
11
+ "dev",
12
+ "husky",
13
+ "linting",
14
+ "markdownlint",
15
+ "playwright",
16
+ "testing",
17
+ "tooling",
18
+ "typescript",
19
+ "vitest"
20
+ ],
21
+ "homepage": "https://github.com/regardio/dev/blob/main/README.md",
22
+ "bugs": {
23
+ "url": "https://github.com/regardio/dev/issues"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/regardio/dev.git"
28
+ },
29
+ "license": "MIT",
3
30
  "author": "Bernd Matzner <bernd.matzner@regard.io>",
31
+ "sideEffects": false,
32
+ "type": "module",
33
+ "exports": {
34
+ "./biome": "./src/biome/preset.json",
35
+ "./commitlint": "./src/commitlint/commitlint.cjs",
36
+ "./markdownlint": "./src/markdownlint/markdownlint.json",
37
+ "./markdownlint-cli2": "./src/markdownlint/markdownlint-cli2.jsonc",
38
+ "./playwright": {
39
+ "types": "./dist/playwright/index.d.ts",
40
+ "default": "./dist/playwright/index.js"
41
+ },
42
+ "./testing/setup-react": {
43
+ "types": "./dist/testing/setup-react.d.ts",
44
+ "default": "./dist/testing/setup-react.js"
45
+ },
46
+ "./typescript/base.json": "./src/typescript/base.json",
47
+ "./typescript/build.json": "./src/typescript/build.json",
48
+ "./typescript/react.json": "./src/typescript/react.json",
49
+ "./vitest/node": {
50
+ "types": "./dist/vitest/node.d.ts",
51
+ "default": "./dist/vitest/node.js"
52
+ },
53
+ "./vitest/react": {
54
+ "types": "./dist/vitest/react.d.ts",
55
+ "default": "./dist/vitest/react.js"
56
+ }
57
+ },
4
58
  "bin": {
5
59
  "exec-clean": "dist/bin/exec-clean.js",
6
60
  "exec-husky": "dist/bin/exec-husky.js",
@@ -13,10 +67,30 @@
13
67
  "lint-biome": "dist/bin/lint-biome.js",
14
68
  "lint-commit": "dist/bin/lint-commit.js",
15
69
  "lint-md": "dist/bin/lint-md.js",
70
+ "lint-package": "dist/bin/lint-package.js",
16
71
  "post-build-exports": "dist/bin/post-build-exports.js"
17
72
  },
18
- "bugs": {
19
- "url": "https://github.com/regardio/dev/issues"
73
+ "files": [
74
+ "dist",
75
+ "src"
76
+ ],
77
+ "scripts": {
78
+ "build": "tsc -p tsconfig.build.json",
79
+ "clean": "tsx src/bin/exec-clean.ts .turbo dist",
80
+ "fix": "run-p fix:*",
81
+ "fix:biome": "biome check --write --unsafe .",
82
+ "fix:md": "markdownlint-cli2 --fix \"**/*.md\" \"**/*.mdx\" \"!**/node_modules/**\" \"!**/dist/**\"",
83
+ "fix:pkg": "tsx src/bin/lint-package.ts",
84
+ "lint": "run-p lint:*",
85
+ "lint:biome": "biome check .",
86
+ "lint:md": "markdownlint-cli2 \"**/*.md\" \"**/*.mdx\" \"!**/node_modules/**\" \"!**/dist/**\"",
87
+ "prepare": "husky",
88
+ "release": "tsx src/bin/flow-release.ts",
89
+ "report": "vitest run --coverage",
90
+ "test": "run-p test:*",
91
+ "test:unit": "vitest run",
92
+ "typecheck": "tsc --noEmit",
93
+ "version": "tsx src/bin/flow-changeset.ts version"
20
94
  },
21
95
  "dependencies": {
22
96
  "@biomejs/biome": "2.3.11",
@@ -29,6 +103,7 @@
29
103
  "@testing-library/react": "16.3.1",
30
104
  "@total-typescript/ts-reset": "0.6.1",
31
105
  "@types/node": "25.0.3",
106
+ "@vitest/coverage-v8": "4.0.16",
32
107
  "@vitest/ui": "4.0.16",
33
108
  "husky": "9.1.7",
34
109
  "jsdom": "27.4.0",
@@ -36,58 +111,12 @@
36
111
  "npm-run-all": "4.1.5",
37
112
  "postcss": "8.5.6",
38
113
  "rimraf": "6.1.2",
114
+ "sort-package-json": "3.6.0",
39
115
  "tsx": "4.21.0",
40
116
  "typescript": "5.9.3",
41
117
  "vite": "7.3.1",
42
118
  "vitest": "4.0.16"
43
119
  },
44
- "description": "Regardio developer tooling for testing, linting, and build workflows",
45
- "engines": {
46
- "node": ">=18"
47
- },
48
- "exports": {
49
- "./biome": "./src/biome/preset.json",
50
- "./commitlint": "./src/commitlint/commitlint.cjs",
51
- "./markdownlint": "./src/markdownlint/markdownlint.json",
52
- "./markdownlint-cli2": "./src/markdownlint/markdownlint-cli2.jsonc",
53
- "./playwright": {
54
- "default": "./dist/playwright/index.js",
55
- "types": "./dist/playwright/index.d.ts"
56
- },
57
- "./testing/setup-react": {
58
- "default": "./dist/testing/setup-react.js",
59
- "types": "./dist/testing/setup-react.d.ts"
60
- },
61
- "./typescript/base.json": "./src/typescript/base.json",
62
- "./typescript/build.json": "./src/typescript/build.json",
63
- "./typescript/react.json": "./src/typescript/react.json",
64
- "./vitest/node": {
65
- "default": "./dist/vitest/node.js",
66
- "types": "./dist/vitest/node.d.ts"
67
- },
68
- "./vitest/react": {
69
- "default": "./dist/vitest/react.js",
70
- "types": "./dist/vitest/react.d.ts"
71
- }
72
- },
73
- "files": ["dist", "src"],
74
- "homepage": "https://github.com/regardio/dev/blob/main/README.md",
75
- "keywords": [
76
- "biome",
77
- "changesets",
78
- "commitlint",
79
- "dev",
80
- "husky",
81
- "linting",
82
- "markdownlint",
83
- "playwright",
84
- "testing",
85
- "tooling",
86
- "typescript",
87
- "vitest"
88
- ],
89
- "license": "MIT",
90
- "name": "@regardio/dev",
91
120
  "peerDependencies": {
92
121
  "postcss": "8.4"
93
122
  },
@@ -96,30 +125,10 @@
96
125
  "optional": true
97
126
  }
98
127
  },
99
- "private": false,
128
+ "engines": {
129
+ "node": ">=18"
130
+ },
100
131
  "publishConfig": {
101
132
  "access": "public"
102
- },
103
- "repository": {
104
- "type": "git",
105
- "url": "git+https://github.com/regardio/dev.git"
106
- },
107
- "scripts": {
108
- "build": "pnpm exec tsc -p tsconfig.build.json",
109
- "clean": "pnpm exec tsx src/bin/exec-clean.ts .turbo dist",
110
- "fix": "exec-p fix:*",
111
- "fix:biome": "biome check --write --unsafe .",
112
- "fix:md": "markdownlint-cli2 --fix \"**/*.md\" \"**/*.mdx\" \"!**/node_modules/**\" \"!**/dist/**\"",
113
- "lint": "run-p lint:*",
114
- "lint:biome": "biome check .",
115
- "lint:md": "markdownlint-cli2 \"**/*.md\" \"**/*.mdx\" \"!**/node_modules/**\" \"!**/dist/**\"",
116
- "prepare": "husky",
117
- "release": "pnpm exec tsx src/bin/flow-release.ts",
118
- "test": "run-p test:*",
119
- "test:unit": "vitest run",
120
- "version": "flow-changeset version"
121
- },
122
- "sideEffects": false,
123
- "type": "module",
124
- "version": "1.10.3"
133
+ }
125
134
  }
@@ -70,6 +70,18 @@ if (!packageName) {
70
70
 
71
71
  console.log(`Releasing ${packageName} with ${bumpType} bump...`);
72
72
 
73
+ // Run quality checks before release
74
+ console.log('Running quality checks...');
75
+ try {
76
+ run('pnpm build');
77
+ run('pnpm typecheck');
78
+ run('pnpm report');
79
+ } catch {
80
+ console.error('Quality checks failed. Fix issues before releasing.');
81
+ process.exit(1);
82
+ }
83
+ console.log('✅ Quality checks passed');
84
+
73
85
  // Ensure we're in a clean git state
74
86
  try {
75
87
  const status = runQuiet('git status --porcelain');
@@ -0,0 +1,140 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ /**
4
+ * Reorder exports conditions: types must come before default for TypeScript.
5
+ * Extracted from lint-package.ts for testing.
6
+ */
7
+ function reorderConditions(obj: Record<string, unknown>): Record<string, unknown> {
8
+ function processObject(o: Record<string, unknown>): Record<string, unknown> {
9
+ if (typeof o !== 'object' || o === null) return o;
10
+
11
+ // First, recursively process all nested objects
12
+ const processed: Record<string, unknown> = {};
13
+ for (const [key, value] of Object.entries(o)) {
14
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
15
+ processed[key] = processObject(value as Record<string, unknown>);
16
+ } else {
17
+ processed[key] = value;
18
+ }
19
+ }
20
+
21
+ // Then check if this object has both 'types' and 'default' keys
22
+ if ('types' in processed && 'default' in processed) {
23
+ const keys = Object.keys(processed);
24
+ const typesIndex = keys.indexOf('types');
25
+ const defaultIndex = keys.indexOf('default');
26
+
27
+ // If default comes before types, reorder
28
+ if (defaultIndex < typesIndex) {
29
+ const reordered: Record<string, unknown> = {};
30
+ reordered.types = processed.types;
31
+ for (const key of keys) {
32
+ if (key !== 'types') {
33
+ reordered[key] = processed[key];
34
+ }
35
+ }
36
+ return reordered;
37
+ }
38
+ }
39
+
40
+ return processed;
41
+ }
42
+
43
+ return processObject(obj);
44
+ }
45
+
46
+ describe('lint-package', () => {
47
+ describe('reorderConditions', () => {
48
+ it('should reorder types before default when default comes first', () => {
49
+ const input = {
50
+ './foo': {
51
+ default: './dist/foo.js',
52
+ types: './dist/foo.d.ts',
53
+ },
54
+ };
55
+
56
+ const result = reorderConditions(input);
57
+
58
+ expect(Object.keys(result['./foo'] as Record<string, unknown>)).toEqual(['types', 'default']);
59
+ });
60
+
61
+ it('should not modify when types already comes before default', () => {
62
+ const input = {
63
+ './foo': {
64
+ default: './dist/foo.js',
65
+ types: './dist/foo.d.ts',
66
+ },
67
+ };
68
+
69
+ const result = reorderConditions(input);
70
+
71
+ expect(Object.keys(result['./foo'] as Record<string, unknown>)).toEqual(['types', 'default']);
72
+ });
73
+
74
+ it('should handle multiple exports with mixed order', () => {
75
+ const input = {
76
+ './a': {
77
+ default: './dist/a.js',
78
+ types: './dist/a.d.ts',
79
+ },
80
+ './b': {
81
+ default: './dist/b.js',
82
+ types: './dist/b.d.ts',
83
+ },
84
+ };
85
+
86
+ const result = reorderConditions(input);
87
+
88
+ expect(Object.keys(result['./a'] as Record<string, unknown>)[0]).toBe('types');
89
+ expect(Object.keys(result['./b'] as Record<string, unknown>)[0]).toBe('types');
90
+ });
91
+
92
+ it('should preserve other keys after types', () => {
93
+ const input = {
94
+ './foo': {
95
+ default: './dist/foo.js',
96
+ import: './dist/foo.mjs',
97
+ require: './dist/foo.cjs',
98
+ types: './dist/foo.d.ts',
99
+ },
100
+ };
101
+
102
+ const result = reorderConditions(input);
103
+ const keys = Object.keys(result['./foo'] as Record<string, unknown>);
104
+
105
+ expect(keys[0]).toBe('types');
106
+ expect(keys.slice(1)).toEqual(['default', 'import', 'require']);
107
+ });
108
+
109
+ it('should handle exports without types or default', () => {
110
+ const input = {
111
+ './styles.css': './dist/styles.css',
112
+ };
113
+
114
+ const result = reorderConditions(input);
115
+
116
+ expect(result).toEqual(input);
117
+ });
118
+
119
+ it('should handle deeply nested condition objects', () => {
120
+ const input = {
121
+ './foo': {
122
+ browser: {
123
+ default: './dist/foo.browser.js',
124
+ types: './dist/foo.browser.d.ts',
125
+ },
126
+ node: {
127
+ default: './dist/foo.node.js',
128
+ types: './dist/foo.node.d.ts',
129
+ },
130
+ },
131
+ };
132
+
133
+ const result = reorderConditions(input);
134
+ const foo = result['./foo'] as Record<string, Record<string, unknown>>;
135
+
136
+ expect(Object.keys(foo.node as object)[0]).toBe('types');
137
+ expect(Object.keys(foo.browser as object)[0]).toBe('types');
138
+ });
139
+ });
140
+ });
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Sorts package.json files using sort-package-json and fixes
4
+ * exports condition order (types must come before default for TypeScript).
5
+ */
6
+ import { execSync } from 'node:child_process';
7
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { dirname, join, resolve } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ const devRoot = resolve(__dirname, '../..');
13
+
14
+ // Find sort-package-json binary
15
+ const sortPkgBin = join(devRoot, 'node_modules/.bin/sort-package-json');
16
+ const sortPkgBinAlt = join(devRoot, 'node_modules/sort-package-json/cli.js');
17
+
18
+ let bin = '';
19
+ if (existsSync(sortPkgBin)) {
20
+ bin = sortPkgBin;
21
+ } else if (existsSync(sortPkgBinAlt)) {
22
+ bin = `node ${sortPkgBinAlt}`;
23
+ } else {
24
+ bin = 'npx sort-package-json';
25
+ }
26
+
27
+ // Get args passed to this script
28
+ const args = process.argv.slice(2);
29
+ const fixMode = args.includes('--fix');
30
+ const files = args.filter((arg) => arg !== '--fix');
31
+ const targets = files.length > 0 ? files : ['package.json'];
32
+
33
+ // Run sort-package-json
34
+ try {
35
+ const checkFlag = fixMode ? '' : '--check';
36
+ execSync(`${bin} ${checkFlag} ${targets.join(' ')}`.trim(), { stdio: 'inherit' });
37
+ } catch {
38
+ process.exit(1);
39
+ }
40
+
41
+ /**
42
+ * Fix exports condition order: types must come before default for TypeScript.
43
+ * See: https://www.typescriptlang.org/docs/handbook/esm-node.html
44
+ * Returns true if the file needs changes.
45
+ */
46
+ function fixExportsOrder(filePath: string, fix: boolean): boolean {
47
+ const fullPath = resolve(process.cwd(), filePath);
48
+ if (!existsSync(fullPath)) return false;
49
+
50
+ const content = readFileSync(fullPath, 'utf-8');
51
+ const pkg = JSON.parse(content) as Record<string, unknown>;
52
+
53
+ if (!pkg.exports || typeof pkg.exports !== 'object') return false;
54
+
55
+ let modified = false;
56
+
57
+ function reorderConditions(obj: Record<string, unknown>): Record<string, unknown> {
58
+ if (typeof obj !== 'object' || obj === null) return obj;
59
+
60
+ // Check if this object has both 'types' and 'default' keys
61
+ if ('types' in obj && 'default' in obj) {
62
+ const keys = Object.keys(obj);
63
+ const typesIndex = keys.indexOf('types');
64
+ const defaultIndex = keys.indexOf('default');
65
+
66
+ // If default comes before types, reorder
67
+ if (defaultIndex < typesIndex) {
68
+ modified = true;
69
+ const reordered: Record<string, unknown> = {};
70
+ // Put types first, then all other keys in original order
71
+ reordered.types = obj.types;
72
+ for (const key of keys) {
73
+ if (key !== 'types') {
74
+ reordered[key] = obj[key];
75
+ }
76
+ }
77
+ return reordered;
78
+ }
79
+ }
80
+
81
+ // Recursively process nested objects
82
+ const result: Record<string, unknown> = {};
83
+ for (const [key, value] of Object.entries(obj)) {
84
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
85
+ result[key] = reorderConditions(value as Record<string, unknown>);
86
+ } else {
87
+ result[key] = value;
88
+ }
89
+ }
90
+ return result;
91
+ }
92
+
93
+ pkg.exports = reorderConditions(pkg.exports as Record<string, unknown>);
94
+
95
+ if (modified && fix) {
96
+ writeFileSync(fullPath, `${JSON.stringify(pkg, null, 2)}\n`);
97
+ }
98
+
99
+ return modified;
100
+ }
101
+
102
+ // Fix exports order in each file
103
+ let hasExportsIssues = false;
104
+ for (const file of targets) {
105
+ const needsFix = fixExportsOrder(file, fixMode);
106
+ if (needsFix && !fixMode) {
107
+ console.error(`${file}: exports condition order is incorrect (types must come before default)`);
108
+ hasExportsIssues = true;
109
+ }
110
+ }
111
+
112
+ if (hasExportsIssues) {
113
+ process.exit(1);
114
+ }
@@ -0,0 +1,161 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ /**
4
+ * Parse command line arguments for post-build-exports.
5
+ * Extracted from post-build-exports.ts for testing.
6
+ */
7
+ function parseArgs(args: string[]): {
8
+ dist: string;
9
+ preserve: string[];
10
+ prefix: string;
11
+ strip: string;
12
+ } {
13
+ const result = {
14
+ dist: 'dist',
15
+ prefix: './',
16
+ preserve: [] as string[],
17
+ strip: '',
18
+ };
19
+
20
+ for (let i = 0; i < args.length; i++) {
21
+ const arg = args[i];
22
+ const next = args[i + 1];
23
+
24
+ if (arg === '--dist' && next) {
25
+ result.dist = next;
26
+ i++;
27
+ } else if (arg === '--preserve' && next) {
28
+ result.preserve = next.split(',').map((p) => p.trim());
29
+ i++;
30
+ } else if (arg === '--prefix' && next) {
31
+ result.prefix = next;
32
+ i++;
33
+ } else if (arg === '--strip' && next) {
34
+ result.strip = next;
35
+ i++;
36
+ }
37
+ }
38
+
39
+ return result;
40
+ }
41
+
42
+ /**
43
+ * Generate export name from JS file path.
44
+ * Extracted from post-build-exports.ts for testing.
45
+ */
46
+ function generateExportName(jsPath: string, strip: string): string {
47
+ let exportPath = jsPath.replace(/\.js$/, '');
48
+
49
+ if (strip && exportPath.startsWith(strip)) {
50
+ exportPath = exportPath.slice(strip.length);
51
+ if (exportPath.startsWith('/')) {
52
+ exportPath = exportPath.slice(1);
53
+ }
54
+ }
55
+
56
+ // Strip /index suffix for cleaner exports
57
+ if (exportPath.endsWith('/index')) {
58
+ exportPath = exportPath.slice(0, -6);
59
+ }
60
+
61
+ return `./${exportPath}`;
62
+ }
63
+
64
+ describe('post-build-exports', () => {
65
+ describe('parseArgs', () => {
66
+ it('should return defaults when no args provided', () => {
67
+ const result = parseArgs([]);
68
+
69
+ expect(result).toEqual({
70
+ dist: 'dist',
71
+ prefix: './',
72
+ preserve: [],
73
+ strip: '',
74
+ });
75
+ });
76
+
77
+ it('should parse --dist option', () => {
78
+ const result = parseArgs(['--dist', 'build']);
79
+
80
+ expect(result.dist).toBe('build');
81
+ });
82
+
83
+ it('should parse --preserve option with single value', () => {
84
+ const result = parseArgs(['--preserve', './tailwind.css']);
85
+
86
+ expect(result.preserve).toEqual(['./tailwind.css']);
87
+ });
88
+
89
+ it('should parse --preserve option with multiple comma-separated values', () => {
90
+ const result = parseArgs(['--preserve', './a.css, ./b.css, ./c.css']);
91
+
92
+ expect(result.preserve).toEqual(['./a.css', './b.css', './c.css']);
93
+ });
94
+
95
+ it('should parse --prefix option', () => {
96
+ const result = parseArgs(['--prefix', './lib/']);
97
+
98
+ expect(result.prefix).toBe('./lib/');
99
+ });
100
+
101
+ it('should parse --strip option', () => {
102
+ const result = parseArgs(['--strip', 'generated']);
103
+
104
+ expect(result.strip).toBe('generated');
105
+ });
106
+
107
+ it('should parse multiple options together', () => {
108
+ const result = parseArgs([
109
+ '--dist',
110
+ 'output',
111
+ '--preserve',
112
+ './styles.css',
113
+ '--strip',
114
+ 'src',
115
+ ]);
116
+
117
+ expect(result).toEqual({
118
+ dist: 'output',
119
+ prefix: './',
120
+ preserve: ['./styles.css'],
121
+ strip: 'src',
122
+ });
123
+ });
124
+
125
+ it('should ignore unknown options', () => {
126
+ const result = parseArgs(['--unknown', 'value', '--dist', 'build']);
127
+
128
+ expect(result.dist).toBe('build');
129
+ });
130
+ });
131
+
132
+ describe('generateExportName', () => {
133
+ it('should convert .js path to export name', () => {
134
+ expect(generateExportName('foo.js', '')).toBe('./foo');
135
+ });
136
+
137
+ it('should handle nested paths', () => {
138
+ expect(generateExportName('utils/helpers.js', '')).toBe('./utils/helpers');
139
+ });
140
+
141
+ it('should strip /index suffix', () => {
142
+ expect(generateExportName('components/button/index.js', '')).toBe('./components/button');
143
+ });
144
+
145
+ it('should apply strip prefix', () => {
146
+ expect(generateExportName('generated/icons/arrow.js', 'generated')).toBe('./icons/arrow');
147
+ });
148
+
149
+ it('should handle strip with leading slash', () => {
150
+ expect(generateExportName('generated/icons/arrow.js', 'generated/')).toBe('./icons/arrow');
151
+ });
152
+
153
+ it('should handle index.js at root', () => {
154
+ expect(generateExportName('index.js', '')).toBe('./index');
155
+ });
156
+
157
+ it('should handle deeply nested index files', () => {
158
+ expect(generateExportName('a/b/c/index.js', '')).toBe('./a/b/c');
159
+ });
160
+ });
161
+ });
@@ -32,6 +32,7 @@
32
32
  },
33
33
  "files": {
34
34
  "ignoreUnknown": false,
35
+ "includes": ["**", "!**/package.json"],
35
36
  "maxSize": 1048576
36
37
  },
37
38
  "formatter": {
@@ -52,6 +52,12 @@ jobs:
52
52
  - name: Build
53
53
  run: pnpm build
54
54
 
55
+ - name: Type check
56
+ run: pnpm typecheck
57
+
58
+ - name: Test with coverage
59
+ run: pnpm report
60
+
55
61
  - name: Publish to npm
56
62
  id: publish
57
63
  run: |
@@ -1,10 +1,25 @@
1
1
  import type { TestUserConfig } from 'vitest/node';
2
2
 
3
+ /**
4
+ * Coverage thresholds for library packages.
5
+ * These ensure adequate test coverage before publishing.
6
+ */
7
+ export const coverageThresholds = {
8
+ branches: 80,
9
+ functions: 80,
10
+ lines: 80,
11
+ statements: 80,
12
+ };
13
+
3
14
  /**
4
15
  * Base Vitest configuration for Node.js packages.
5
16
  * Use with defineConfig() in your vitest.config.ts
6
17
  */
7
18
  export const vitestNodeConfig: TestUserConfig = {
19
+ coverage: {
20
+ provider: 'v8',
21
+ thresholds: coverageThresholds,
22
+ },
8
23
  environment: 'node',
9
24
  exclude: ['node_modules', 'dist', 'build', '.turbo', '.react-router'],
10
25
  globals: true,
@@ -1,4 +1,5 @@
1
1
  import type { TestUserConfig } from 'vitest/node';
2
+ import { coverageThresholds } from './node';
2
3
 
3
4
  /**
4
5
  * Vitest configuration for React packages with jsdom environment.
@@ -7,6 +8,10 @@ import type { TestUserConfig } from 'vitest/node';
7
8
  * Requires a setup file that imports '@testing-library/jest-dom/vitest'
8
9
  */
9
10
  export const vitestReactConfig: TestUserConfig = {
11
+ coverage: {
12
+ provider: 'v8',
13
+ thresholds: coverageThresholds,
14
+ },
10
15
  environment: 'jsdom',
11
16
  exclude: ['node_modules', 'dist', 'build', '.turbo', '.react-router'],
12
17
  globals: true,