Skip to content

Commit

Permalink
Add code
Browse files Browse the repository at this point in the history
  • Loading branch information
sergiodxa committed May 3, 2021
0 parents commit dcffa3a
Show file tree
Hide file tree
Showing 10 changed files with 9,174 additions and 0 deletions.
32 changes: 32 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: CI
on: [push]
jobs:
build:
name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }}

runs-on: ${{ matrix.os }}
strategy:
matrix:
node: ['10.x', '12.x', '14.x']
os: [ubuntu-latest, windows-latest, macOS-latest]

steps:
- name: Checkout repo
uses: actions/checkout@v2

- name: Use Node ${{ matrix.node }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node }}

- name: Install deps and build (with cache)
uses: bahmutov/npm-install@v1

- name: Lint
run: yarn lint

- name: Test
run: yarn test --ci --coverage --maxWorkers=2

- name: Build
run: yarn build
12 changes: 12 additions & 0 deletions .github/workflows/size.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: size
on: [pull_request]
jobs:
size:
runs-on: ubuntu-latest
env:
CI_JOB_NUMBER: 1
steps:
- uses: actions/checkout@v1
- uses: andresz1/size-limit-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*.log
.DS_Store
node_modules
dist
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2021 Sergio Xalambrí

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
144 changes: 144 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Remix Utils

This package contains simple utility functions and types to use together with [Remix.run](https://remix.run).

**To install it you need to have a valid Remix license**

## Installation

```bash
yarn add remix-utils
npm install remix-utils
```

Remember you need to also install `remix`, `@remix-run/node`, `@remix-run/react` and `react`. For the first three you need a paid Remix license.

## Imports

```ts
import { redirectBack, parseBody, json } from "remix-utils";
import type {
LoaderArgs,
LoaderReturn,
ActionArgs,
ActionReturn,
LinksArgs,
LinksReturn,
MetaArgs,
MetaReturn,
HeadersArgs,
HeadersReturn,
} from "remix-utils";
```

## API

### `redirectBack`

This function is a wrapper of the `redirect` helper from Remix, contrarian to Remix's version this one receives the whole request object as first value and an object with the response init and a fallback URL.

The response created with this function will have the `Location` header pointing to the `Referer` header from the request, or if not available the fallback URL provided in the second argument.

```ts
import { redirectBack } from "remix-utils";
import type { ActionArgs, ActionReturn } from "remix-utils";

export function action({ request }: ActionArgs): ActionReturn {
return redirectBack(request, { fallback: "/" });
}
```

This helper is more useful when used in an action so you can send the user to the same URL it was before.

### `parseBody`

This function receives the whole request and returns a promise with an instance of `URLSearchParams`, and the body of the request already parsed.

```ts
import { parseBody, redirectBack } from "remix-utils";
import type { ActionArgs, ActionReturn } from "remix-utils";

import { updateUser } from "../services/users";

export function action({ request, params }: ActionArgs): ActionReturn {
const body = await parseBody(request);
await updateUser(params.id, { username: body.get("username") });
return redirectBack(request, { fallback: "/" });
}
```

This is a simple wrapper over doing `new URLSearchParams(await request.text());`.

### `json`

This function is a typed version of the `json` helper provided by Remix, it accepts a generic (defaults to `unknown`) and ensure at the compiler lever that the data you are sending from your loader matches the provided type. It's more useful when you create a type or interface for your whole route so you can share it between `json` and `useRouteData` to ensure you are not missing or adding extra parameters to the response.

```tsx
import { useRouteData } from "remix";
import { json } from "remix-utils";
import type { LoaderArgs, LoaderReturn } from "remix-utils";

import { getUser } from "../services/users";
import type { User } from "../types";

interface RouteData {
user: User;
}

export async function loader({ request }: LoaderArgs): LoaderReturn {
const user = await getUser(request);
return json<RouteData>({ user });
}

export default function View() {
const { user } = useRouteData<RouteData>();
return <h1>Hello, {user.name}</h1>;
}
```

### Types

This package exports a list of useful types together with the utility functions, you can import them with

```ts
import type {
LoaderArgs,
LoaderReturn,
ActionArgs,
ActionReturn,
LinksArgs,
LinksReturn,
MetaArgs,
MetaReturn,
HeadersArgs,
HeadersReturn,
} from "remix-utils";
```

This types are generated from the `LoaderFunction`, `ActionFunction`, `LinksFunction`, `MetaFunction` and `HeadersFunction` exported by Remix itself, this ensure they will be up to date with your Remix version.

All the `*Args` types are the first argument of the equivalent `*Funtion` type.
All the `*Return` types are the return type of the equivalent `*Funtion` type.

They are exported in case you don't want to use arrow functions (like me) and still want the types so instead of doing:

```ts
export const loader: LoaderFunction = async (args) => {};
```

You can do:

```ts
export async function loader(args: LoaderArgs): LoaderReturn {}
```

Since all of them are TypeScript types they will not impact your bundle size in case you prefer to use the normal `*Function` types from Remix.

## Author

- [Sergio Xalambrí](https://sergiodxa.com)

## License

- MIT License

64 changes: 64 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"name": "remix-utils",
"version": "0.1.0",
"license": "MIT",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=14"
},
"scripts": {
"start": "tsdx watch",
"build": "tsdx build",
"test": "tsdx test",
"lint": "tsdx lint",
"prepare": "tsdx build",
"size": "size-limit",
"analyze": "size-limit --why"
},
"peerDependencies": {
"@remix-run/node": "^0.17.0",
"@remix-run/react": "^0.17.0",
"react": "^17.0.2",
"remix": "^0.17.0"
},
"husky": {
"hooks": {
"pre-commit": "tsdx lint"
}
},
"prettier": {
"printWidth": 80,
"semi": true,
"singleQuote": true,
"trailingComma": "es5"
},
"author": "Sergio Xalambrí",
"module": "dist/remix-utils.esm.js",
"size-limit": [
{
"path": "dist/remix-utils.cjs.production.min.js",
"limit": "10 KB"
},
{
"path": "dist/remix-utils.esm.js",
"limit": "10 KB"
}
],
"devDependencies": {
"@size-limit/preset-small-lib": "^4.10.2",
"husky": "^6.0.0",
"size-limit": "^4.10.2",
"tsdx": "^0.14.1",
"tslib": "^2.2.0",
"typescript": "^4.2.4",
"@remix-run/node": "^0.17.0",
"@remix-run/react": "^0.17.0",
"react": "^17.0.2",
"remix": "^0.17.0"
}
}
39 changes: 39 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type {
ActionFunction,
HeadersFunction,
LinksFunction,
LoaderFunction,
MetaFunction,
Request,
ResponseInit,
Response,
} from "remix";
import { json as remixJson, redirect } from "remix";

export function redirectBack(
request: Request,
{ fallback, ...init }: ResponseInit & { fallback: string }
): Response {
return redirect(request.headers.get("Referer") ?? fallback, init);
}

export function parseBody(request: Request): Promise<URLSearchParams> {
return request.text().then(body => new URLSearchParams(body));
}

export function json<Data = unknown>(data: Data, init?: number | ResponseInit) {
return remixJson(data, init);
}

export type LoaderArgs = Parameters<LoaderFunction>[0];
export type ActionArgs = Parameters<ActionFunction>[0];
export type LinksArgs = Parameters<LinksFunction>[0];
export type MetaArgs = Parameters<MetaFunction>[0];
export type HeadersArgs = Parameters<HeadersFunction>[0];

export type LoaderReturn = ReturnType<LoaderFunction>;
export type ActionReturn = ReturnType<ActionFunction>;
export type LinksReturn = ReturnType<LinksFunction>;
export type MetaReturn = ReturnType<MetaFunction>;
export type HeadersReturn = ReturnType<HeadersFunction>;

41 changes: 41 additions & 0 deletions test/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Request } from 'remix';
import { redirectBack, parseBody, json } from '../src';

describe('redirectBack', () => {
it('uses the referer if available', () => {
const request = new Request('/', {
headers: { Referer: '/referer' },
});
const response = redirectBack(request, { fallback: '/fallback' });
expect(response.headers.get('Location')).toBe('/referer');
});

it('uses the fallback if referer is not available', () => {
const request = new Request('/');
const response = redirectBack(request, { fallback: '/fallback' });
expect(response.headers.get('Location')).toBe('/fallback');
});
});

describe('parseBody', () => {
it('reads the body as a URLSearchParams instance', async () => {
const request = new Request('/', {
method: 'POST',
body: new URLSearchParams({ framework: 'Remix' }).toString(),
});
const body = await parseBody(request);
expect(body).toBeInstanceOf(URLSearchParams);
expect(body.get('framework')).toBe('Remix');
});
});

describe('json', () => {
it('returns a response with the JSON data', async () => {
interface RouteData {
framework: 'Remix';
}
const response = json<RouteData>({ framework: 'Remix' });
const body = await response.json();
expect(body.framework).toBe('Remix');
});
});
Loading

0 comments on commit dcffa3a

Please sign in to comment.