@regardio/dev 1.10.3 → 1.11.0

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,74 @@
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 files = args.length > 0 ? args : ['package.json'];
22
+ try {
23
+ execSync(`${bin} ${files.join(' ')}`, { stdio: 'inherit' });
24
+ }
25
+ catch {
26
+ process.exit(1);
27
+ }
28
+ function fixExportsOrder(filePath) {
29
+ const fullPath = resolve(process.cwd(), filePath);
30
+ if (!existsSync(fullPath))
31
+ return;
32
+ const content = readFileSync(fullPath, 'utf-8');
33
+ const pkg = JSON.parse(content);
34
+ if (!pkg.exports || typeof pkg.exports !== 'object')
35
+ return;
36
+ let modified = false;
37
+ function reorderConditions(obj) {
38
+ if (typeof obj !== 'object' || obj === null)
39
+ return obj;
40
+ if ('types' in obj && 'default' in obj) {
41
+ const keys = Object.keys(obj);
42
+ const typesIndex = keys.indexOf('types');
43
+ const defaultIndex = keys.indexOf('default');
44
+ if (defaultIndex < typesIndex) {
45
+ modified = true;
46
+ const reordered = {};
47
+ reordered.types = obj.types;
48
+ for (const key of keys) {
49
+ if (key !== 'types') {
50
+ reordered[key] = obj[key];
51
+ }
52
+ }
53
+ return reordered;
54
+ }
55
+ }
56
+ const result = {};
57
+ for (const [key, value] of Object.entries(obj)) {
58
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
59
+ result[key] = reorderConditions(value);
60
+ }
61
+ else {
62
+ result[key] = value;
63
+ }
64
+ }
65
+ return result;
66
+ }
67
+ pkg.exports = reorderConditions(pkg.exports);
68
+ if (modified) {
69
+ writeFileSync(fullPath, `${JSON.stringify(pkg, null, 2)}\n`);
70
+ }
71
+ }
72
+ for (const file of files) {
73
+ fixExportsOrder(file);
74
+ }
@@ -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.0",
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.1.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,99 @@
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 files = args.length > 0 ? args : ['package.json'];
30
+
31
+ // Run sort-package-json
32
+ try {
33
+ execSync(`${bin} ${files.join(' ')}`, { stdio: 'inherit' });
34
+ } catch {
35
+ process.exit(1);
36
+ }
37
+
38
+ /**
39
+ * Fix exports condition order: types must come before default for TypeScript.
40
+ * See: https://www.typescriptlang.org/docs/handbook/esm-node.html
41
+ */
42
+ function fixExportsOrder(filePath: string): void {
43
+ const fullPath = resolve(process.cwd(), filePath);
44
+ if (!existsSync(fullPath)) return;
45
+
46
+ const content = readFileSync(fullPath, 'utf-8');
47
+ const pkg = JSON.parse(content) as Record<string, unknown>;
48
+
49
+ if (!pkg.exports || typeof pkg.exports !== 'object') return;
50
+
51
+ let modified = false;
52
+
53
+ function reorderConditions(obj: Record<string, unknown>): Record<string, unknown> {
54
+ if (typeof obj !== 'object' || obj === null) return obj;
55
+
56
+ // Check if this object has both 'types' and 'default' keys
57
+ if ('types' in obj && 'default' in obj) {
58
+ const keys = Object.keys(obj);
59
+ const typesIndex = keys.indexOf('types');
60
+ const defaultIndex = keys.indexOf('default');
61
+
62
+ // If default comes before types, reorder
63
+ if (defaultIndex < typesIndex) {
64
+ modified = true;
65
+ const reordered: Record<string, unknown> = {};
66
+ // Put types first, then all other keys in original order
67
+ reordered.types = obj.types;
68
+ for (const key of keys) {
69
+ if (key !== 'types') {
70
+ reordered[key] = obj[key];
71
+ }
72
+ }
73
+ return reordered;
74
+ }
75
+ }
76
+
77
+ // Recursively process nested objects
78
+ const result: Record<string, unknown> = {};
79
+ for (const [key, value] of Object.entries(obj)) {
80
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
81
+ result[key] = reorderConditions(value as Record<string, unknown>);
82
+ } else {
83
+ result[key] = value;
84
+ }
85
+ }
86
+ return result;
87
+ }
88
+
89
+ pkg.exports = reorderConditions(pkg.exports as Record<string, unknown>);
90
+
91
+ if (modified) {
92
+ writeFileSync(fullPath, `${JSON.stringify(pkg, null, 2)}\n`);
93
+ }
94
+ }
95
+
96
+ // Fix exports order in each file
97
+ for (const file of files) {
98
+ fixExportsOrder(file);
99
+ }
@@ -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,