🔧 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 ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
verify:
|
||||
name: Node ${{ matrix.node-version }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [18.x, 20.x, 22.x]
|
||||
node-version: [20.x, 22.x, 24.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -25,5 +27,15 @@ jobs:
|
||||
run: npm ci
|
||||
- name: 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
|
||||
run: npm run demo
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
# 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/
|
||||
/package-lock.json
|
||||
|
||||
# Build outputs
|
||||
/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": {
|
||||
"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",
|
||||
"type-check": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"lint": "eslint .",
|
||||
"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": {
|
||||
"type": "git",
|
||||
@@ -49,6 +55,7 @@
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/node": "^25.6.0",
|
||||
"@vitest/coverage-v8": "^4.1.4",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^10.2.1",
|
||||
"globals": "^17.5.0",
|
||||
"ts-node": "^10.9.2",
|
||||
|
||||
@@ -238,7 +238,7 @@ describe('Additional Decorators', () => {
|
||||
t.val = 'wrong';
|
||||
const errors = await JsonMapper.validate(t);
|
||||
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;
|
||||
const errors = await JsonMapper.validate(t);
|
||||
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 () => {
|
||||
@@ -318,7 +318,7 @@ describe('Additional Decorators', () => {
|
||||
t.val = 2;
|
||||
const errors = await JsonMapper.validate(t);
|
||||
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;
|
||||
}
|
||||
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(items[0]).toBeInstanceOf(Item);
|
||||
expect(items[0].name).toBe('a');
|
||||
expect(items[0]!.name).toBe('a');
|
||||
});
|
||||
|
||||
it('should handle single polymorphic object', async () => {
|
||||
@@ -350,7 +350,7 @@ describe('Additional Decorators', () => {
|
||||
@IsString() type: string;
|
||||
}
|
||||
class Dog extends Animal {
|
||||
type = 'dog';
|
||||
override type = 'dog';
|
||||
@IsString() breed: string;
|
||||
}
|
||||
class Test {
|
||||
@@ -376,7 +376,7 @@ describe('Additional Decorators', () => {
|
||||
t.tags = ['a', 1 as any];
|
||||
const errors = await JsonMapper.validate(t);
|
||||
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);
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors[0].property).toBe('username');
|
||||
expect(errors[0].constraints).toHaveProperty('minLength');
|
||||
expect(errors[0]!.property).toBe('username');
|
||||
expect(errors[0]!.constraints).toHaveProperty('minLength');
|
||||
});
|
||||
|
||||
it('should fail on invalid email', async () => {
|
||||
@@ -215,8 +215,8 @@ describe('JsonMapper', () => {
|
||||
|
||||
const errors = await JsonMapper.validate(user);
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors[0].property).toBe('email');
|
||||
expect(errors[0].constraints).toHaveProperty('isEmail');
|
||||
expect(errors[0]!.property).toBe('email');
|
||||
expect(errors[0]!.constraints).toHaveProperty('isEmail');
|
||||
});
|
||||
|
||||
it('should fail on invalid role (IsIn)', async () => {
|
||||
@@ -228,8 +228,8 @@ describe('JsonMapper', () => {
|
||||
|
||||
const errors = await JsonMapper.validate(user);
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors[0].property).toBe('roles');
|
||||
expect(errors[0].constraints).toHaveProperty('isIn');
|
||||
expect(errors[0]!.property).toBe('roles');
|
||||
expect(errors[0]!.constraints).toHaveProperty('isIn');
|
||||
});
|
||||
|
||||
it('should skip validation for null optional field', async () => {
|
||||
@@ -238,7 +238,7 @@ describe('JsonMapper', () => {
|
||||
user.email = 'john@example.com';
|
||||
user.active = true;
|
||||
user.roles = ['user'];
|
||||
user.age = undefined; // optional
|
||||
delete user.age; // optional
|
||||
|
||||
const errors = await JsonMapper.validate(user);
|
||||
expect(errors).toHaveLength(0);
|
||||
@@ -254,8 +254,8 @@ describe('JsonMapper', () => {
|
||||
|
||||
const errors = await JsonMapper.validate(user);
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors[0].property).toBe('age');
|
||||
expect(errors[0].constraints).toHaveProperty('min');
|
||||
expect(errors[0]!.property).toBe('age');
|
||||
expect(errors[0]!.constraints).toHaveProperty('min');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ describe('Standalone Utility Functions', () => {
|
||||
user.age = '30' as any;
|
||||
const errors2 = await validate(user);
|
||||
expect(errors2).toHaveLength(1);
|
||||
expect(errors2[0].property).toBe('age');
|
||||
expect(errors2[0]!.property).toBe('age');
|
||||
});
|
||||
|
||||
it('should transform to plain object directly', async () => {
|
||||
|
||||
+2
-1
@@ -5,5 +5,6 @@
|
||||
"moduleResolution": "Bundler",
|
||||
"outDir": "dist/cjs",
|
||||
"declaration": true
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "src/**/*.test.ts", "src/example.ts"]
|
||||
}
|
||||
|
||||
+2
-1
@@ -4,5 +4,6 @@
|
||||
"module": "NodeNext",
|
||||
"outDir": "dist/esm",
|
||||
"declaration": true
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "src/**/*.test.ts", "src/example.ts"]
|
||||
}
|
||||
|
||||
+3
-1
@@ -35,6 +35,8 @@
|
||||
|
||||
"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/**/*"],
|
||||
"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