Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: replace node-fetch with @remix-run/web-fetch #2736

Merged
merged 54 commits into from
May 14, 2022
Merged
Show file tree
Hide file tree
Changes from 53 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
d11a6ed
feat: replace node-fetch with @web-std/fetch
jacob-ebey Apr 11, 2022
86c63d5
added stream to express peer deps
jacob-ebey Apr 11, 2022
7610062
chore: lint issues
jacob-ebey Apr 11, 2022
529b1b7
Merge branch 'dev' of https://github.com/remix-run/remix into jacob/w…
jacob-ebey Apr 11, 2022
fd3c351
updated dep
jacob-ebey Apr 11, 2022
9c86ffd
Merge branch 'dev' of https://github.com/remix-run/remix into jacob/w…
jacob-ebey Apr 11, 2022
9b598f5
updated deps to use our fork
jacob-ebey Apr 28, 2022
8ede722
Merge branch 'dev' into jacob/web-std-fetch
jacob-ebey Apr 28, 2022
1abd9bf
remove content type check as it was merged in the fetch fork
jacob-ebey Apr 28, 2022
7da9ec7
updated architect and express adapters
jacob-ebey Apr 28, 2022
fa6cd4c
updated architect and express adapters
jacob-ebey Apr 28, 2022
008ec1f
reverted cookie changes
jacob-ebey Apr 28, 2022
fb8851e
bind in test
jacob-ebey Apr 28, 2022
1b0606c
remove console.log
jacob-ebey Apr 28, 2022
0a7493c
updated vercel adapter
jacob-ebey Apr 28, 2022
269fea5
feat(remix-node): replaced busboy
jacob-ebey May 4, 2022
ff89d63
chore: make types proper for node fetch
jacob-ebey May 5, 2022
567a11b
remove form-data dep
jacob-ebey May 5, 2022
2c8b53e
chore: fix lint issues
jacob-ebey May 5, 2022
a581f8e
Merge branch 'dev' into jacob/web-std-fetch
jacob-ebey May 5, 2022
3fc5f04
updated types
jacob-ebey May 5, 2022
4b56405
update @remix-run/web-stream to v1.0.3
jacob-ebey May 6, 2022
5c910f2
chore: speed up build with ts config change
jacob-ebey May 6, 2022
0ef2b13
Clean up some types
mjackson May 6, 2022
ab3491b
feat: updated node file upload handler to support slice
jacob-ebey May 7, 2022
07b84a6
updated test to check error type
jacob-ebey May 7, 2022
c935a41
fix: allow multiple slice of file
jacob-ebey May 7, 2022
af1b64a
do typecheck
jacob-ebey May 7, 2022
8cf505c
Merge branch 'dev' of https://github.com/remix-run/remix into jacob/w…
jacob-ebey May 8, 2022
09006c2
feat: add Readable and WritableStream to globals
jacob-ebey May 8, 2022
dc70911
updated fetch dep
jacob-ebey May 10, 2022
4a58644
Merge branch 'dev' of https://github.com/remix-run/remix into jacob/w…
jacob-ebey May 10, 2022
dc8ee67
wait for file to finish writing before finishing
jacob-ebey May 10, 2022
c7bc907
use byteLength in memory upload handler
jacob-ebey May 10, 2022
9e549a4
feat: updated parseMultipartFormData API
jacob-ebey May 10, 2022
45d072e
fix build
jacob-ebey May 10, 2022
1f259c4
feat: renamed MeterError to MaxPartSizeExceededError
jacob-ebey May 11, 2022
f2f6f00
only pass basename to handlers
jacob-ebey May 11, 2022
f2e3214
Merge branch 'dev' of https://github.com/remix-run/remix into jacob/w…
jacob-ebey May 11, 2022
37576e4
added another test for upload handlers
jacob-ebey May 11, 2022
5792bb1
added more tests
jacob-ebey May 11, 2022
44f10e6
updated docs and example
jacob-ebey May 11, 2022
3f06542
fix: add arch to the playwright cache key
jacob-ebey May 11, 2022
cc0497e
updated file upload tests to work on windows
jacob-ebey May 11, 2022
c173433
added link to spec
jacob-ebey May 12, 2022
36fd7b0
updated docs
jacob-ebey May 12, 2022
c23b3ac
renamed stream util and added error handling
jacob-ebey May 12, 2022
593e166
update references from pipeReadableStreamToWritable -> writeReadableS…
jacob-ebey May 12, 2022
b8797f7
feat: added writeAsyncIterableToWritable
jacob-ebey May 12, 2022
83b9e16
rename readableStreamFromStream -> createReadableStreamFromReadable a…
jacob-ebey May 12, 2022
518f8b5
revert tsconfig lib change
jacob-ebey May 13, 2022
48538d0
fix: pass through request without clone
jacob-ebey May 13, 2022
28a7481
updated doc
jacob-ebey May 13, 2022
a57d90b
updated docs
jacob-ebey May 13, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -151,15 +151,15 @@ jobs:
id: playwright-cache
with:
path: ${{ matrix.playwright_binary_path }}
key: ${{ runner.os }}-cache-playwright-${{ steps.playwright-version.outputs.version }}
key: ${{ runner.os }}-${{ runner.arch }}-cache-playwright-${{ steps.playwright-version.outputs.version }}

- name: 🖨️ Playwright info
shell: bash
run: |
echo "OS: ${{ matrix.os }}"
echo "Playwright version: ${{ steps.playwright-version.outputs.version }}"
echo "Playwright install dir: ${{ matrix.playwright_binary_path }}"
echo "Cache key: ${{ runner.os }}-cache-playwright-${{ steps.playwright-version.outputs.version }}"
echo "Cache key: ${{ runner.os }}-${{ runner.arch }}-cache-playwright-${{ steps.playwright-version.outputs.version }}"
echo "Cache hit: ${{ steps.playwright-cache.outputs.cache-hit == 'true' }}"

- name: 📥 Install Playwright
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ yarn-error.log
/fixtures/deno-app
/playwright-report
/test-results
/uploads

.eslintcache
.tmp
Expand Down
2 changes: 1 addition & 1 deletion docs/api/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ export default function SomeRouteComponent() {

<docs-success>Watch the <a href="https://www.youtube.com/playlist?list=PLXoynULbYuEDG2wBFSZ66b85EIspy3fy6">📼 Remix Single</a>: <a href="https://www.youtube.com/watch?v=NXqEP_PsPNc&list=PLXoynULbYuEDG2wBFSZ66b85EIspy3fy6">Loading data into components</a></docs-success>

Each route can define a "loader" function that will be called on the server before rendering to provide data to the route.
Each route can define a "loader" function that will be called on the server before rendering to provide data to the route. You may think of this as a "GET" request handler in that you should not be reading the body of the request; that is the job of an [`action`](#action).

```js
import { json } from "@remix-run/{runtime}";
Expand Down
28 changes: 17 additions & 11 deletions docs/api/remix.md
Original file line number Diff line number Diff line change
Expand Up @@ -1523,7 +1523,7 @@ return new Response(null, {
});
```

## `unstable_parseMultipartFormData` (node)
## `unstable_parseMultipartFormData`

Allows you to handle multipart forms (file uploads) for your app.

Expand Down Expand Up @@ -1572,7 +1572,7 @@ export default function AvatarUploadRoute() {

### `uploadHandler`

The `uploadHandler` is the key to the whole thing. It's responsible for what happens to the file as it's being streamed from the client. You can save it to disk, store it in memory, or act as a proxy to send it somewhere else (like a file storage provider).
The `uploadHandler` is the key to the whole thing. It's responsible for what happens to the multipart/form-data parts as they are being streamed from the client. You can save it to disk, store it in memory, or act as a proxy to send it somewhere else (like a file storage provider).

Remix has two utilities to create `uploadHandler`s for you:

Expand All @@ -1581,26 +1581,32 @@ Remix has two utilities to create `uploadHandler`s for you:

These are fully featured utilities for handling fairly simple use cases. It's not recommended to load anything but quite small files into memory. Saving files to disk is a reasonable solution for many use cases. But if you want to upload the file to a file hosting provider, then you'll need to write your own.

#### `unstable_createFileUploadHandler`
#### `unstable_createFileUploadHandler (node)`

An upload handler that will write parts with a filename to disk to keep them out of memory, parts without a filename will not be parsed. Should be composed with another upload handler.

**Example:**

```tsx
export const action: ActionFunction = async ({
request,
}) => {
const uploadHandler = unstable_createFileUploadHandler({
maxFileSize: 5_000_000,
file: ({ filename }) => filename,
});
const uploadHandler = unstable_composeUploadHandlers(
unstable_createFileUploadHandler({
maxPartSize: 5_000_000,
file: ({ filename }) => filename,
}),
// parse everything else into memory
unstable_createMemoryUploadHandler()
);
const formData = await unstable_parseMultipartFormData(
request,
uploadHandler
);

const file = formData.get("avatar");

// file is a "NodeFile" which has a similar API to "File"
// file is a "NodeOnDiskFile" which implements the "File" API
// ... etc
};
```
Expand All @@ -1612,7 +1618,7 @@ export const action: ActionFunction = async ({
| avoidFileConflicts | boolean | true | Avoid file conflicts by appending a timestamp on the end of the filename if it already exists on disk |
| directory | string \| Function | os.tmpdir() | The directory to write the upload. |
| file | Function | () => `upload_${random}.${ext}` | The name of the file in the directory. Can be a relative path, the directory structure will be created if it does not exist. |
| maxFileSize | number | 3000000 | The maximum upload size allowed (in bytes). If the size is exceeded an error will be thrown. |
| maxPartSize | number | 3000000 | The maximum upload size allowed (in bytes). If the size is exceeded a MaxPartSizeExceededError will be thrown. |
| filter | Function | OPTIONAL | A function you can write to prevent a file upload from being saved based on filename, mimetype, or encoding. Return `false` and the file will be ignored. |

The function API for `file` and `directory` are the same. They accept an `object` and return a `string`. The object it accepts has `filename`, `encoding`, and `mimetype` (all strings).The `string` returned is the path.
Expand All @@ -1628,7 +1634,7 @@ export const action: ActionFunction = async ({
request,
}) => {
const uploadHandler = unstable_createMemoryUploadHandler({
maxFileSize: 500_000,
maxPartSize: 500_000,
});
const formData = await unstable_parseMultipartFormData(
request,
Expand All @@ -1642,7 +1648,7 @@ export const action: ActionFunction = async ({
};
```

**Options:** The only options supported are `maxFileSize` and `filter` which work the same as in `unstable_createFileUploadHandler` above. This API is not recommended for anything at scale, but is a convenient utility for simple use cases.
**Options:** The only options supported are `maxPartSize` and `filter` which work the same as in `unstable_createFileUploadHandler` above. This API is not recommended for anything at scale, but is a convenient utility for simple use cases and as a fallback for another handler.

### Custom `uploadHandler`

Expand Down
19 changes: 19 additions & 0 deletions docs/decisions/0002-do-not-clone-request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Do not clone request

Date: 2022-05-13

Status: accepted

## Context

To allow multiple loaders / actions to read the body of a request, we have been cloning the request before forwarding it to user-code. This is not the best thing to do as some runtimes will begin buffering the body to allow for multiple consumers. It is also goes against "the platform" that states a request body should only be consumed once.

## Decision

Do not clone requests before they are passed to user-code (loaders, actions, handleDocumentRequest, handleDataRequest, etc.).

## Consequences

If you are reading the request body in both an action and a loader this will now fail. Loaders should be thought of as a "GET" / "HEAD" request handler. These request methods are not allowed to have a body, therefore you should not be reading it in your Remix loader function.

If you wish to continue reading the request body in multiple places for a single request against recommendations, consider using `.clone()` before reading it; just know this comes with tradeoffs.
11 changes: 11 additions & 0 deletions docs/decisions/template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Title

Date: YYYY-MM-DD

Status: proposed | rejected | accepted | deprecated | … | superseded by [0005](0005-example.md)

## Context

## Decision

## Consequences
4 changes: 2 additions & 2 deletions docs/pages/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ So instead of doing:
import { unstable_createFileUploadHandler } from "@remix-run/{runtime}";

const uploadHandler = unstable_createFileUploadHandler({
maxFileSize: 5_000_000,
maxPartSize: 5_000_000,
file: ({ filename }) => filename,
});

Expand All @@ -81,7 +81,7 @@ import { unstable_createFileUploadHandler } from "@remix-run/{runtime}";

export async function action() {
const uploadHandler = unstable_createFileUploadHandler({
maxFileSize: 5_000_000,
maxPartSize: 5_000_000,
file: ({ filename }) => filename,
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { ActionFunction, UploadHandler } from "@remix-run/node";
import { json, unstable_parseMultipartFormData } from "@remix-run/node";
import {
json,
unstable_composeUploadHandlers as composeUploadHandlers,
unstable_createMemoryUploadHandler as createMemoryUploadHandler,
unstable_parseMultipartFormData as parseMultipartFormData
} from "@remix-run/node";
import { Form, useActionData } from "@remix-run/react";

import { uploadImage } from "~/utils/utils.server";
Expand All @@ -11,16 +16,18 @@ type ActionData = {
};

export const action: ActionFunction = async ({ request }) => {
const uploadHandler: UploadHandler = async ({ name, stream }) => {
if (name !== "img") {
stream.resume();
return;
}
const uploadedImage = await uploadImage(stream);
return uploadedImage.secure_url;
};
const uploadHandler: UploadHandler = composeUploadHandlers(
async ({ name, contentType, data, filename }) => {
if (name !== "img") {
return undefined;
}
const uploadedImage = await uploadImage(data);
return uploadedImage.secure_url;
},
createMemoryUploadHandler()
);

const formData = await unstable_parseMultipartFormData(
const formData = await parseMultipartFormData(
request,
uploadHandler
);
Expand Down
22 changes: 12 additions & 10 deletions examples/file-and-cloudinary-upload/app/routes/local-upload.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { ActionFunction } from "@remix-run/node";
import {
json,
unstable_createFileUploadHandler,
unstable_parseMultipartFormData,
unstable_composeUploadHandlers as composeUploadHandlers,
unstable_createFileUploadHandler as createFileUploadHandler,
unstable_createMemoryUploadHandler as createMemoryUploadHandler,
unstable_parseMultipartFormData as parseMultipartFormData,
} from "@remix-run/node";
import { Form, useActionData } from "@remix-run/react";

Expand All @@ -12,16 +14,16 @@ type ActionData = {
};

export const action: ActionFunction = async ({ request }) => {
const uploadHandler = unstable_createFileUploadHandler({
directory: "public",
maxFileSize: 30000,
});
const formData = await unstable_parseMultipartFormData(
request,
uploadHandler
const uploadHandler = composeUploadHandlers(
createFileUploadHandler({
directory: "public/uploads",
maxPartSize: 30000,
}),
createMemoryUploadHandler()
);
const formData = await parseMultipartFormData(request, uploadHandler);
const image = formData.get("img");
if (!image) {
if (!image || typeof image === "string") {
return json({
error: "something wrong",
});
Expand Down
11 changes: 7 additions & 4 deletions examples/file-and-cloudinary-upload/app/utils/utils.server.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
import cloudinary from "cloudinary";
import type { Stream } from "stream";
import { writeAsyncIterableToWritable } from "@remix-run/node";

cloudinary.v2.config({
cloud_name: process.env.CLOUD_NAME,
api_key: process.env.API_KEY,
api_secret: process.env.API_SECRET,
});

async function uploadImage(fileStream: Stream) {
return new Promise((resolve, reject) => {
async function uploadImage(data: AsyncIterable<Uint8Array>) {
const uploadPromise = new Promise(async (resolve, reject) => {
const uploadStream = cloudinary.v2.uploader.upload_stream(
{
folder: "remix",
},
(error, result) => {
if (error) {
reject(error);
return;
}
resolve(result);
}
);
fileStream.pipe(uploadStream);
await writeAsyncIterableToWritable(data, uploadStream);
});

return uploadPromise;
}

console.log("configs", cloudinary.v2.config());
Expand Down
Loading