Skip to content

Commit f4c77a1

Browse files
committed
chore: add doctor tooling, tests, and CI workflows
- Upgrade biome to v2.3.8 - Add CI and release workflows - Add lefthook for git hooks - Add test suite with 7 tests - Update README credits
1 parent fe254c0 commit f4c77a1

File tree

9 files changed

+198
-22
lines changed

9 files changed

+198
-22
lines changed

.github/workflows/ci.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
ci:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- uses: oven-sh/setup-bun@v2
16+
with:
17+
bun-version: latest
18+
19+
- name: Install dependencies
20+
run: bun install
21+
22+
- name: Lint
23+
run: bun run lint
24+
25+
- name: Type check
26+
run: bun run typecheck
27+
28+
- name: Test
29+
run: bun test
30+
31+
- name: Build
32+
run: bun run build

.github/workflows/release.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
branches: [main]
6+
7+
jobs:
8+
release:
9+
uses: SylphxAI/.github/.github/workflows/release.yml@main
10+
secrets: inherit

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,6 @@ Extract entity data type from a StandardEntity type.
9797

9898
## Powered by Sylphx
9999

100+
Built with [@sylphx/tsconfig](https://github.com/SylphxAI/tsconfig), [@sylphx/biome-config](https://github.com/SylphxAI/biome-config), [@sylphx/doctor](https://github.com/SylphxAI/doctor), and [@sylphx/bump](https://github.com/SylphxAI/bump).
101+
100102
https://github.com/SylphxAI

biome.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
2+
"$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
33
"extends": ["@sylphx/biome-config"]
44
}

lefthook.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Managed by @sylphx/doctor
2+
# https://github.com/evilmartians/lefthook
3+
4+
pre-commit:
5+
commands:
6+
doctor:
7+
run: bunx @sylphx/doctor precommit --fix
8+
9+
pre-push:
10+
commands:
11+
doctor:
12+
run: bunx @sylphx/doctor prepush

package.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@
1111
"import": "./dist/index.js"
1212
}
1313
},
14-
"files": ["dist"],
14+
"files": [
15+
"dist"
16+
],
1517
"scripts": {
1618
"build": "tsc",
1719
"typecheck": "tsc --noEmit",
1820
"lint": "biome check .",
1921
"format": "biome format --write .",
20-
"prepublishOnly": "echo 'Use CI to publish' && exit 1"
22+
"prepublishOnly": "bunx @sylphx/doctor prepublish && echo 'Use CI to publish' && exit 1",
23+
"test": "bun test"
2124
},
2225
"keywords": [
2326
"standard-entity",
@@ -38,10 +41,13 @@
3841
"url": "https://github.com/SylphxAI/standard-entity/issues"
3942
},
4043
"devDependencies": {
41-
"@biomejs/biome": "^1.9.4",
44+
"@biomejs/biome": "^2.3.8",
4245
"@sylphx/biome-config": "^0.4.0",
46+
"@sylphx/bump": "^1.4.11",
47+
"@sylphx/doctor": "^1.26.0",
4348
"@sylphx/tsconfig": "^0.3.0",
4449
"@types/bun": "^1.2.17",
50+
"lefthook": "^2.0.4",
4551
"typescript": "^5.8.3"
4652
}
4753
}

progress.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Progress
2+
3+
## Status: 🚧 In Development
4+
5+
## Completed
6+
- [ ] Initial setup
7+
8+
## In Progress
9+
- [ ] Core features
10+
11+
## Planned
12+
- [ ] Documentation
13+
- [ ] Tests

src/index.test.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { describe, expect, test } from 'bun:test'
2+
import {
3+
type EntityMarker,
4+
type InferEntityName,
5+
type InferEntityType,
6+
isStandardEntity,
7+
type SimpleEntity,
8+
type StandardEntity,
9+
} from './index'
10+
11+
describe('StandardEntity', () => {
12+
test('isStandardEntity returns true for valid entity', () => {
13+
const entity: StandardEntity<'User', { id: string }> = {
14+
'~entity': {
15+
name: 'User',
16+
type: undefined as unknown as { id: string },
17+
},
18+
}
19+
20+
expect(isStandardEntity(entity)).toBe(true)
21+
})
22+
23+
test('isStandardEntity returns false for non-entity', () => {
24+
expect(isStandardEntity(null)).toBe(false)
25+
expect(isStandardEntity(undefined)).toBe(false)
26+
expect(isStandardEntity({})).toBe(false)
27+
expect(isStandardEntity({ name: 'User' })).toBe(false)
28+
expect(isStandardEntity({ '~entity': null })).toBe(false)
29+
expect(isStandardEntity({ '~entity': {} })).toBe(false)
30+
})
31+
32+
test('isStandardEntity returns true for entity with fields', () => {
33+
const entity = {
34+
'~entity': {
35+
name: 'Post',
36+
type: undefined,
37+
},
38+
fields: {
39+
title: { type: 'string' },
40+
},
41+
}
42+
43+
expect(isStandardEntity(entity)).toBe(true)
44+
})
45+
})
46+
47+
describe('Type inference', () => {
48+
test('InferEntityName extracts name', () => {
49+
type UserEntity = StandardEntity<'User', { id: string }>
50+
type Name = InferEntityName<UserEntity>
51+
52+
// Type-level test - if this compiles, it works
53+
const name: Name = 'User'
54+
expect(name).toBe('User')
55+
})
56+
57+
test('InferEntityType extracts data type', () => {
58+
type UserData = { id: string; name: string }
59+
type UserEntity = StandardEntity<'User', UserData>
60+
type Data = InferEntityType<UserEntity>
61+
62+
// Type-level test - if this compiles, it works
63+
const data: Data = { id: '1', name: 'Alice' }
64+
expect(data.id).toBe('1')
65+
expect(data.name).toBe('Alice')
66+
})
67+
})
68+
69+
describe('EntityMarker', () => {
70+
test('can create entity with EntityMarker', () => {
71+
interface MyEntity<N extends string, D> extends EntityMarker<N, D> {
72+
readonly customProp: string
73+
}
74+
75+
const entity: MyEntity<'Custom', { value: number }> = {
76+
'~entity': {
77+
name: 'Custom',
78+
type: undefined as unknown as { value: number },
79+
},
80+
customProp: 'test',
81+
}
82+
83+
expect(isStandardEntity(entity)).toBe(true)
84+
expect(entity['~entity'].name).toBe('Custom')
85+
expect(entity.customProp).toBe('test')
86+
})
87+
})
88+
89+
describe('SimpleEntity', () => {
90+
test('SimpleEntity is a valid StandardEntity', () => {
91+
const entity: SimpleEntity<'Simple', { foo: string }> = {
92+
'~entity': {
93+
name: 'Simple',
94+
type: undefined as unknown as { foo: string },
95+
},
96+
}
97+
98+
expect(isStandardEntity(entity)).toBe(true)
99+
expect(entity['~entity'].name).toBe('Simple')
100+
})
101+
})

src/index.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -75,20 +75,20 @@ export interface StandardEntity<TName extends string = string, TData = unknown>
7575
* Standard Entity marker with phantom types.
7676
* The `~` prefix is a convention indicating this is a protocol marker.
7777
*/
78-
readonly "~entity": {
78+
readonly '~entity': {
7979
/** Entity name as a string literal type */
80-
readonly name: TName;
80+
readonly name: TName
8181
/**
8282
* Entity data type.
8383
* Can be `unknown` if type is inferred from `fields` property.
8484
*/
85-
readonly type: TData;
86-
};
85+
readonly type: TData
86+
}
8787
/**
8888
* Optional fields for type inference.
8989
* Libraries like Lens use this for field-based type inference.
9090
*/
91-
readonly fields?: unknown;
91+
readonly fields?: unknown
9292
}
9393

9494
// =============================================================================
@@ -107,13 +107,13 @@ export interface StandardEntity<TName extends string = string, TData = unknown>
107107
*/
108108
export function isStandardEntity(value: unknown): value is StandardEntity {
109109
return (
110-
typeof value === "object" &&
110+
typeof value === 'object' &&
111111
value !== null &&
112-
"~entity" in value &&
113-
typeof (value as StandardEntity)["~entity"] === "object" &&
114-
(value as StandardEntity)["~entity"] !== null &&
115-
"name" in (value as StandardEntity)["~entity"]
116-
);
112+
'~entity' in value &&
113+
typeof (value as StandardEntity)['~entity'] === 'object' &&
114+
(value as StandardEntity)['~entity'] !== null &&
115+
'name' in (value as StandardEntity)['~entity']
116+
)
117117
}
118118

119119
// =============================================================================
@@ -128,7 +128,7 @@ export function isStandardEntity(value: unknown): value is StandardEntity {
128128
* type Name = InferEntityName<typeof User>; // "User"
129129
* ```
130130
*/
131-
export type InferEntityName<T> = T extends StandardEntity<infer N, unknown> ? N : never;
131+
export type InferEntityName<T> = T extends StandardEntity<infer N, unknown> ? N : never
132132

133133
/**
134134
* Extract entity data type from StandardEntity.
@@ -142,7 +142,7 @@ export type InferEntityName<T> = T extends StandardEntity<infer N, unknown> ? N
142142
* type Data = InferEntityType<typeof User>; // { id: string; name: string; }
143143
* ```
144144
*/
145-
export type InferEntityType<T> = T extends StandardEntity<string, infer D> ? D : never;
145+
export type InferEntityType<T> = T extends StandardEntity<string, infer D> ? D : never
146146

147147
// =============================================================================
148148
// Utility Types for Implementers
@@ -160,10 +160,10 @@ export type InferEntityType<T> = T extends StandardEntity<string, infer D> ? D :
160160
* ```
161161
*/
162162
export interface EntityMarker<TName extends string, TData = unknown> {
163-
readonly "~entity": {
164-
readonly name: TName;
165-
readonly type: TData;
166-
};
163+
readonly '~entity': {
164+
readonly name: TName
165+
readonly type: TData
166+
}
167167
}
168168

169169
/**
@@ -177,4 +177,4 @@ export interface EntityMarker<TName extends string, TData = unknown> {
177177
* };
178178
* ```
179179
*/
180-
export type SimpleEntity<TName extends string, TData> = EntityMarker<TName, TData>;
180+
export type SimpleEntity<TName extends string, TData> = EntityMarker<TName, TData>

0 commit comments

Comments
 (0)