Skip to content

Commit ab75d2d

Browse files
committed
feat: release the library
0 parents  commit ab75d2d

File tree

9 files changed

+1019
-0
lines changed

9 files changed

+1019
-0
lines changed

.github/workflows/release.yml

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
name: release
2+
on: [push, pull_request]
3+
concurrency:
4+
group: ${{ github.workflow }}-${{ github.ref }}
5+
cancel-in-progress: true
6+
7+
jobs:
8+
test:
9+
name: 🧪 Test
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: ⬇️ Checkout repo
13+
uses: actions/checkout@v4
14+
15+
- name: ⎔ Setup node
16+
uses: actions/setup-node@v4
17+
with:
18+
node-version: 20
19+
20+
- name: 📥 Download deps
21+
uses: bahmutov/npm-install@v1
22+
with:
23+
useLockFile: false
24+
25+
- name: 🧪 Test
26+
run: npm run test
27+
28+
release:
29+
name: 🚀 Release
30+
needs: [test]
31+
runs-on: ubuntu-latest
32+
if:
33+
${{ github.repository == 'epicweb-dev/invariant' &&
34+
contains('refs/heads/main,refs/heads/beta,refs/heads/next,refs/heads/alpha',
35+
github.ref) && github.event_name == 'push' }}
36+
steps:
37+
- name: ⬇️ Checkout repo
38+
uses: actions/checkout@v4
39+
40+
- name: ⎔ Setup node
41+
uses: actions/setup-node@v4
42+
with:
43+
node-version: 20
44+
45+
- name: 📥 Download deps
46+
uses: bahmutov/npm-install@v1
47+
with:
48+
useLockFile: false
49+
50+
- name: 📦 Run Build
51+
run: npm run build
52+
53+
- name: 🚀 Release
54+
uses: cycjimmy/semantic-release-action@v4
55+
with:
56+
semantic_version: 17
57+
branches: |
58+
[
59+
'+([0-9])?(.{+([0-9]),x}).x',
60+
'main',
61+
'next',
62+
'next-major',
63+
{name: 'beta', prerelease: true},
64+
{name: 'alpha', prerelease: true}
65+
]
66+
env:
67+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
68+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
dist
2+
node_modules
3+
tsconfig.tsbuildinfo

README.md

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
<div>
2+
<h1 align="center"><a href="https://npm.im/@epic-web/invariant">💥 @epic-web/invariant</a></h1>
3+
<strong>
4+
Throw errors when thing's aren't right.
5+
</strong>
6+
<p>
7+
Type safe utilities for throwing errors (and responses) in exceptional
8+
situations in a declarative way.
9+
</p>
10+
</div>
11+
12+
```
13+
npm install @epic-web/invariant
14+
```
15+
16+
<div align="center">
17+
<a
18+
alt="Epic Web logo"
19+
href="https://www.epicweb.dev"
20+
>
21+
<img
22+
width="300px"
23+
src="https://github-production-user-asset-6210df.s3.amazonaws.com/1500684/257881576-fd66040b-679f-4f25-b0d0-ab886a14909a.png"
24+
/>
25+
</a>
26+
</div>
27+
28+
<hr />
29+
30+
<!-- prettier-ignore-start -->
31+
[![Build Status][build-badge]][build]
32+
[![MIT License][license-badge]][license]
33+
[![Code of Conduct][coc-badge]][coc]
34+
<!-- prettier-ignore-end -->
35+
36+
## The Problem
37+
38+
Your application has boundaries. Network requests/responses, file system reads,
39+
etc. When you're working with these boundaries, you need to be able to handle
40+
errors that may occur, even in TypeScript.
41+
42+
TypeScript will typically make these boundaries much more obvious because it
43+
doesn't like not knowing what the type of something is. For example:
44+
45+
```ts
46+
const formData = new FormData(formElement)
47+
const name = await formData.get('name')
48+
// name is `File | string | null`
49+
```
50+
51+
Often it's a good idea to use a proper parsing library for situations like this,
52+
but for simple cases that can often feel like overkill. But you don't want to
53+
just ignore TypeScript because:
54+
55+
> TypeScript is that brutally honest friend you put up with because they save
56+
> you from making terrible mistakes. –
57+
> [@kentcdodds](https://twitter.com/kentcdodds/status/1715562350835855396)
58+
59+
So you check it:
60+
61+
```ts
62+
const formData = new FormData(formElement)
63+
const name = await formData.get('name')
64+
// name is `File | string | null`
65+
if (typeof name !== 'string') {
66+
throw new Error('Name must be a string')
67+
}
68+
// now name is `string` (and TypeScript knows it too)
69+
```
70+
71+
You're fine throwing a descriptive error here because it's just _very_ unlikely
72+
this will ever happen and even if it does you wouldn't really know what to do
73+
about it anyway.
74+
75+
It's not a big deal, but there's a tiny bit of boilerplate that would be nice to
76+
avoid. Especially when you find yourself doing this all over the codebase. This
77+
is the problem `@epic-web/invariant` solves.
78+
79+
## The Solution
80+
81+
Here's the diff from what we had above:
82+
83+
```diff
84+
const formData = new FormData(formElement)
85+
const name = await formData.get('name')
86+
// name is `File | string | null`
87+
- if (typeof name !== 'string') {
88+
- throw new Error('Name must be a string')
89+
- }
90+
+ invariant(typeof name === 'string', 'Name must be a string')
91+
// now name is `string` (and TypeScript knows it too)
92+
```
93+
94+
It's pretty simple. But honestly, it's nicer to read, it throws a special
95+
`InvariantError` object to distinguish it from other types of errors, and we
96+
have another useful utility for throwing `Response` objects instead of `Error`
97+
objects which is handy
98+
[in Remix](https://remix.run/docs/en/main/route/loader#throwing-responses-in-loaders).
99+
100+
## Usage
101+
102+
### `invariant`
103+
104+
The `invariant` function is used to assert that a condition is true. If the
105+
condition is false, it throws an error with the provided message.
106+
107+
**Basic Usage**
108+
109+
```ts
110+
import { invariant } from '@epic-web/invariant'
111+
112+
const creature = { name: 'Dragon', type: 'Fire' }
113+
invariant(creature.name === 'Dragon', 'Creature must be a Dragon')
114+
```
115+
116+
**Throwing a Response on False Condition**
117+
118+
```ts
119+
import { invariant } from '@epic-web/invariant'
120+
121+
const creature = { name: 'Unicorn', type: 'Magic' }
122+
invariant(creature.type === 'Fire', 'Creature must be of type Fire')
123+
// Throws: InvariantError: Creature must be of type Fire
124+
```
125+
126+
**Using Callback for Error Message**
127+
128+
```ts
129+
import { invariantResponse } from '@epic-web/invariant'
130+
131+
const creature = { name: 'Elf', type: 'Forest' }
132+
invariant(creature.type === 'Water', () => 'Creature must be of type Water')
133+
// Throws: InvariantError: Creature must be of type Water
134+
```
135+
136+
### `invariantResponse`
137+
138+
The `invariantResponse` function works similarly to `invariant`, but instead of
139+
throwing an `InvariantError`, it throws a Response object.
140+
141+
**Basic Usage**
142+
143+
```ts
144+
import { invariantResponse } from '@epic-web/invariant'
145+
146+
const creature = { name: 'Phoenix', type: 'Fire' }
147+
invariantResponse(creature.type === 'Fire', 'Creature must be of type Fire')
148+
```
149+
150+
**Throwing a Response on False Condition**
151+
152+
```ts
153+
import { invariantResponse } from '@epic-web/invariant'
154+
155+
const creature = { name: 'Griffin', type: 'Air' }
156+
invariantResponse(creature.type === 'Water', 'Creature must be of type Water')
157+
// Throws: Response { status: 400, body: 'Creature must be of type Water' }
158+
```
159+
160+
The response status default if 400 (Bad Request), but you'll find how to change
161+
that below.
162+
163+
**Using Callback for Response Message**
164+
165+
```ts
166+
import { invariantResponse } from '@epic-web/invariant'
167+
168+
const creature = { name: 'Mermaid', type: 'Water' }
169+
invariantResponse(
170+
creature.type === 'Land',
171+
() => `Expected a Land creature, but got a ${creature.type} creature`,
172+
)
173+
```
174+
175+
**Throwing a Response with Additional Options**
176+
177+
```ts
178+
import { invariantResponse } from '@epic-web/invariant'
179+
180+
const creature = { name: 'Cerberus', type: 'Underworld' }
181+
invariantResponse(
182+
creature.type === 'Sky',
183+
JSON.stringify({ error: 'Creature must be of type Sky' }),
184+
{ status: 500, headers: { 'Content-Type': 'text/json' } },
185+
)
186+
```
187+
188+
## Differences from [invariant](https://www.npmjs.com/package/invariant)
189+
190+
There are three main differences. With `@epic-web/invariant`:
191+
192+
1. Error messages are the same in dev and prod
193+
2. It's typesafe
194+
3. We support the common case (for Remix anyway) of throwing Responses as well
195+
with `invariantResponse`.
196+
197+
## License
198+
199+
MIT
200+
201+
<!-- prettier-ignore-start -->
202+
[build-badge]: https://img.shields.io/github/actions/workflow/status/epicweb-dev/invariant/release.yml?branch=main&logo=github&style=flat-square
203+
[build]: https://github.com/epicweb-dev/invariant/actions?query=workflow%3Arelease
204+
[license-badge]: https://img.shields.io/badge/license-MIT%20License-blue.svg?style=flat-square
205+
[license]: https://github.com/epicweb-dev/invariant/blob/main/LICENSE
206+
[coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square
207+
[coc]: https://kentcdodds.com/conduct
208+
<!-- prettier-ignore-end -->
209+
210+
```
211+
212+
```

0 commit comments

Comments
 (0)