🔧 fix: repair test, build and CI infrastructure
The vitest suite silently ran zero tests: vitest 4 transpiles with oxc, which does not pick up `experimentalDecorators` from a tsconfig that excludes the files being transformed, so every decorator-using suite failed to parse and was reported as "0 test". A vitest.config.ts enabling legacy decorators brings all 40 existing tests back to life. - Add vitest.config.ts (oxc legacy decorators + v8 coverage config) - Type-check test files: move the test/demo exclusions from the base tsconfig onto the two build configs, and fix the strict-mode errors this surfaced - Stop shipping src/example.ts in dist (it invokes runExample() at import time, a side effect in a package declaring "sideEffects": false) - CI: run lint, tests with coverage, build and entry-point smoke checks; drop EOL Node 18, add Node 24; commit package-lock.json so `npm ci` works - Add `verify`, `test:watch` and `build:docs` scripts, and an engines field; `build:docs` regenerates the previously hand-maintained docs/cereale.js Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
This commit is contained in:
@@ -7,12 +7,14 @@ on:
|
|||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
verify:
|
||||||
|
name: Node ${{ matrix.node-version }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [18.x, 20.x, 22.x]
|
node-version: [20.x, 22.x, 24.x]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -25,5 +27,15 @@ jobs:
|
|||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Type Check
|
- name: Type Check
|
||||||
run: npm run type-check
|
run: npm run type-check
|
||||||
|
- name: Lint
|
||||||
|
run: npm run lint
|
||||||
|
- name: Test
|
||||||
|
run: npm run test:coverage
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
- name: Verify published entry points load
|
||||||
|
run: |
|
||||||
|
node --input-type=module -e "import * as m from './dist/esm/index.js'; if (typeof m.toInstance !== 'function') throw new Error('ESM entry point broken');"
|
||||||
|
node --input-type=commonjs -e "const m = require('./dist/cjs/index.js'); if (typeof m.toInstance !== 'function') throw new Error('CJS entry point broken');"
|
||||||
- name: Run Demo
|
- name: Run Demo
|
||||||
run: npm run demo
|
run: npm run demo
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
# Node modules and dependency files
|
# Node modules and dependency files
|
||||||
|
# NOTE: package-lock.json is intentionally committed — CI installs with `npm ci`,
|
||||||
|
# which requires a lockfile to be present in the repository.
|
||||||
/node_modules/
|
/node_modules/
|
||||||
/package-lock.json
|
|
||||||
|
|
||||||
# Build outputs
|
# Build outputs
|
||||||
/dist/
|
/dist/
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Generated
+3258
File diff suppressed because it is too large
Load Diff
+8
-1
@@ -19,13 +19,19 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rm -rf dist && tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json",
|
"build": "rm -rf dist && tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json",
|
||||||
|
"build:docs": "esbuild src/index.ts --bundle --format=iife --global-name=Cereale --minify --tsconfig=tsconfig.json --outfile=docs/cereale.js",
|
||||||
"demo": "node --no-warnings=ExperimentalWarning --loader ts-node/esm src/example.ts",
|
"demo": "node --no-warnings=ExperimentalWarning --loader ts-node/esm src/example.ts",
|
||||||
"type-check": "tsc --noEmit",
|
"type-check": "tsc --noEmit",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
"test:coverage": "vitest run --coverage",
|
"test:coverage": "vitest run --coverage",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint:fix": "eslint . --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"prepublishOnly": "npm run build"
|
"verify": "npm run type-check && npm run lint && npm run test && npm run build",
|
||||||
|
"prepublishOnly": "npm run verify"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -49,6 +55,7 @@
|
|||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/node": "^25.6.0",
|
"@types/node": "^25.6.0",
|
||||||
"@vitest/coverage-v8": "^4.1.4",
|
"@vitest/coverage-v8": "^4.1.4",
|
||||||
|
"esbuild": "^0.25.0",
|
||||||
"eslint": "^10.2.1",
|
"eslint": "^10.2.1",
|
||||||
"globals": "^17.5.0",
|
"globals": "^17.5.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ describe('Additional Decorators', () => {
|
|||||||
t.val = 'wrong';
|
t.val = 'wrong';
|
||||||
const errors = await JsonMapper.validate(t);
|
const errors = await JsonMapper.validate(t);
|
||||||
expect(errors).toHaveLength(1);
|
expect(errors).toHaveLength(1);
|
||||||
expect(errors[0].constraints['CustomValidator']).toBe('val must be correct');
|
expect(errors[0]!.constraints['CustomValidator']).toBe('val must be correct');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -304,7 +304,7 @@ describe('Additional Decorators', () => {
|
|||||||
t.val = 5;
|
t.val = 5;
|
||||||
const errors = await JsonMapper.validate(t);
|
const errors = await JsonMapper.validate(t);
|
||||||
expect(errors).toHaveLength(1);
|
expect(errors).toHaveLength(1);
|
||||||
expect(errors[0].constraints['custom']).toBe('must be ten');
|
expect(errors[0]!.constraints['custom']).toBe('must be ten');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle options as second argument', async () => {
|
it('should handle options as second argument', async () => {
|
||||||
@@ -318,7 +318,7 @@ describe('Additional Decorators', () => {
|
|||||||
t.val = 2;
|
t.val = 2;
|
||||||
const errors = await JsonMapper.validate(t);
|
const errors = await JsonMapper.validate(t);
|
||||||
expect(errors).toHaveLength(1);
|
expect(errors).toHaveLength(1);
|
||||||
expect(errors[0].constraints['custom']).toBe('must be one');
|
expect(errors[0]!.constraints['custom']).toBe('must be one');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -339,10 +339,10 @@ describe('Additional Decorators', () => {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
const json = '[{"name": "a"}, {"name": "b"}]';
|
const json = '[{"name": "a"}, {"name": "b"}]';
|
||||||
const items = await JsonMapper.fromJson(Item, json);
|
const items = (await JsonMapper.fromJson(Item, json)) as unknown as Item[];
|
||||||
expect(Array.isArray(items)).toBe(true);
|
expect(Array.isArray(items)).toBe(true);
|
||||||
expect(items[0]).toBeInstanceOf(Item);
|
expect(items[0]).toBeInstanceOf(Item);
|
||||||
expect(items[0].name).toBe('a');
|
expect(items[0]!.name).toBe('a');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle single polymorphic object', async () => {
|
it('should handle single polymorphic object', async () => {
|
||||||
@@ -350,7 +350,7 @@ describe('Additional Decorators', () => {
|
|||||||
@IsString() type: string;
|
@IsString() type: string;
|
||||||
}
|
}
|
||||||
class Dog extends Animal {
|
class Dog extends Animal {
|
||||||
type = 'dog';
|
override type = 'dog';
|
||||||
@IsString() breed: string;
|
@IsString() breed: string;
|
||||||
}
|
}
|
||||||
class Test {
|
class Test {
|
||||||
@@ -376,7 +376,7 @@ describe('Additional Decorators', () => {
|
|||||||
t.tags = ['a', 1 as any];
|
t.tags = ['a', 1 as any];
|
||||||
const errors = await JsonMapper.validate(t);
|
const errors = await JsonMapper.validate(t);
|
||||||
expect(errors).toHaveLength(1);
|
expect(errors).toHaveLength(1);
|
||||||
expect(errors[0].constraints['isString']).toContain('each element');
|
expect(errors[0]!.constraints['isString']).toContain('each element');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+9
-9
@@ -202,8 +202,8 @@ describe('JsonMapper', () => {
|
|||||||
|
|
||||||
const errors = await JsonMapper.validate(user);
|
const errors = await JsonMapper.validate(user);
|
||||||
expect(errors).toHaveLength(1);
|
expect(errors).toHaveLength(1);
|
||||||
expect(errors[0].property).toBe('username');
|
expect(errors[0]!.property).toBe('username');
|
||||||
expect(errors[0].constraints).toHaveProperty('minLength');
|
expect(errors[0]!.constraints).toHaveProperty('minLength');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail on invalid email', async () => {
|
it('should fail on invalid email', async () => {
|
||||||
@@ -215,8 +215,8 @@ describe('JsonMapper', () => {
|
|||||||
|
|
||||||
const errors = await JsonMapper.validate(user);
|
const errors = await JsonMapper.validate(user);
|
||||||
expect(errors).toHaveLength(1);
|
expect(errors).toHaveLength(1);
|
||||||
expect(errors[0].property).toBe('email');
|
expect(errors[0]!.property).toBe('email');
|
||||||
expect(errors[0].constraints).toHaveProperty('isEmail');
|
expect(errors[0]!.constraints).toHaveProperty('isEmail');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail on invalid role (IsIn)', async () => {
|
it('should fail on invalid role (IsIn)', async () => {
|
||||||
@@ -228,8 +228,8 @@ describe('JsonMapper', () => {
|
|||||||
|
|
||||||
const errors = await JsonMapper.validate(user);
|
const errors = await JsonMapper.validate(user);
|
||||||
expect(errors).toHaveLength(1);
|
expect(errors).toHaveLength(1);
|
||||||
expect(errors[0].property).toBe('roles');
|
expect(errors[0]!.property).toBe('roles');
|
||||||
expect(errors[0].constraints).toHaveProperty('isIn');
|
expect(errors[0]!.constraints).toHaveProperty('isIn');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip validation for null optional field', async () => {
|
it('should skip validation for null optional field', async () => {
|
||||||
@@ -238,7 +238,7 @@ describe('JsonMapper', () => {
|
|||||||
user.email = 'john@example.com';
|
user.email = 'john@example.com';
|
||||||
user.active = true;
|
user.active = true;
|
||||||
user.roles = ['user'];
|
user.roles = ['user'];
|
||||||
user.age = undefined; // optional
|
delete user.age; // optional
|
||||||
|
|
||||||
const errors = await JsonMapper.validate(user);
|
const errors = await JsonMapper.validate(user);
|
||||||
expect(errors).toHaveLength(0);
|
expect(errors).toHaveLength(0);
|
||||||
@@ -254,8 +254,8 @@ describe('JsonMapper', () => {
|
|||||||
|
|
||||||
const errors = await JsonMapper.validate(user);
|
const errors = await JsonMapper.validate(user);
|
||||||
expect(errors).toHaveLength(1);
|
expect(errors).toHaveLength(1);
|
||||||
expect(errors[0].property).toBe('age');
|
expect(errors[0]!.property).toBe('age');
|
||||||
expect(errors[0].constraints).toHaveProperty('min');
|
expect(errors[0]!.constraints).toHaveProperty('min');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -30,7 +30,7 @@ describe('Standalone Utility Functions', () => {
|
|||||||
user.age = '30' as any;
|
user.age = '30' as any;
|
||||||
const errors2 = await validate(user);
|
const errors2 = await validate(user);
|
||||||
expect(errors2).toHaveLength(1);
|
expect(errors2).toHaveLength(1);
|
||||||
expect(errors2[0].property).toBe('age');
|
expect(errors2[0]!.property).toBe('age');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should transform to plain object directly', async () => {
|
it('should transform to plain object directly', async () => {
|
||||||
|
|||||||
+2
-1
@@ -5,5 +5,6 @@
|
|||||||
"moduleResolution": "Bundler",
|
"moduleResolution": "Bundler",
|
||||||
"outDir": "dist/cjs",
|
"outDir": "dist/cjs",
|
||||||
"declaration": true
|
"declaration": true
|
||||||
}
|
},
|
||||||
|
"exclude": ["node_modules", "dist", "src/**/*.test.ts", "src/example.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -4,5 +4,6 @@
|
|||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"outDir": "dist/esm",
|
"outDir": "dist/esm",
|
||||||
"declaration": true
|
"declaration": true
|
||||||
}
|
},
|
||||||
|
"exclude": ["node_modules", "dist", "src/**/*.test.ts", "src/example.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -35,6 +35,8 @@
|
|||||||
|
|
||||||
"experimentalDecorators": true
|
"experimentalDecorators": true
|
||||||
},
|
},
|
||||||
|
// NOTE: test files are deliberately included here so that `npm run type-check`
|
||||||
|
// covers them. The two build configs exclude them (and the demo) from `dist`.
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "dist", "src/**/*.test.ts"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
// Vitest 4 transpiles with oxc, which does not read `experimentalDecorators`
|
||||||
|
// out of tsconfig.json for files the tsconfig does not `include`. Without this
|
||||||
|
// the decorator syntax in the test files fails to parse and every suite is
|
||||||
|
// silently reported as "0 test".
|
||||||
|
oxc: {
|
||||||
|
decorator: { legacy: true },
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
include: ['src/**/*.test.ts'],
|
||||||
|
coverage: {
|
||||||
|
provider: 'v8',
|
||||||
|
reporter: ['text', 'lcov'],
|
||||||
|
include: ['src/**/*.ts'],
|
||||||
|
exclude: ['src/**/*.test.ts', 'src/example.ts', 'src/index.ts'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user