remix 3.0.0-beta.0 → 3.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/fetch-router.d.ts +7 -0
  2. package/dist/fetch-router.d.ts.map +1 -1
  3. package/dist/node-tsx/load-module.d.ts +2 -0
  4. package/dist/node-tsx/load-module.d.ts.map +1 -0
  5. package/dist/node-tsx/load-module.js +2 -0
  6. package/dist/node-tsx.d.ts +3 -0
  7. package/dist/node-tsx.d.ts.map +1 -0
  8. package/{src/node-serve.ts → dist/node-tsx.js} +2 -1
  9. package/dist/render-middleware.d.ts +2 -0
  10. package/dist/render-middleware.d.ts.map +1 -0
  11. package/dist/render-middleware.js +2 -0
  12. package/dist/route-pattern/href.d.ts +2 -0
  13. package/dist/route-pattern/href.d.ts.map +1 -0
  14. package/dist/route-pattern/href.js +2 -0
  15. package/dist/route-pattern/join.d.ts +2 -0
  16. package/dist/route-pattern/join.d.ts.map +1 -0
  17. package/dist/route-pattern/join.js +2 -0
  18. package/dist/route-pattern/match.d.ts +2 -0
  19. package/dist/route-pattern/match.d.ts.map +1 -0
  20. package/dist/route-pattern/match.js +2 -0
  21. package/package.json +158 -44
  22. package/src/assert/README.md +109 -0
  23. package/src/assets/README.md +539 -0
  24. package/src/async-context-middleware/README.md +100 -0
  25. package/src/auth/README.md +445 -0
  26. package/src/auth-middleware/README.md +246 -0
  27. package/src/cli/README.md +78 -0
  28. package/src/compression-middleware/README.md +176 -0
  29. package/src/cookie/README.md +106 -0
  30. package/src/cop-middleware/README.md +117 -0
  31. package/src/cors-middleware/README.md +174 -0
  32. package/src/csrf-middleware/README.md +99 -0
  33. package/src/data-schema/README.md +422 -0
  34. package/src/data-table/README.md +552 -0
  35. package/src/data-table-mysql/README.md +97 -0
  36. package/src/data-table-postgres/README.md +74 -0
  37. package/src/data-table-sqlite/README.md +84 -0
  38. package/src/fetch-proxy/README.md +46 -0
  39. package/src/fetch-router/README.md +902 -0
  40. package/src/fetch-router.ts +7 -0
  41. package/src/file-storage/README.md +57 -0
  42. package/src/file-storage-s3/README.md +47 -0
  43. package/src/form-data-middleware/README.md +109 -0
  44. package/src/form-data-parser/README.md +160 -0
  45. package/src/fs/README.md +60 -0
  46. package/src/headers/README.md +629 -0
  47. package/src/html-template/README.md +101 -0
  48. package/src/lazy-file/README.md +109 -0
  49. package/src/logger-middleware/README.md +132 -0
  50. package/src/method-override-middleware/README.md +71 -0
  51. package/src/mime/README.md +110 -0
  52. package/src/multipart-parser/README.md +241 -0
  53. package/src/node-fetch-server/README.md +352 -0
  54. package/src/node-tsx/README.md +79 -0
  55. package/src/node-tsx/load-module.ts +2 -0
  56. package/{dist/node-serve.js → src/node-tsx.ts} +2 -1
  57. package/src/render-middleware/README.md +99 -0
  58. package/src/render-middleware.ts +2 -0
  59. package/src/route-pattern/README.md +291 -0
  60. package/src/route-pattern/href.ts +2 -0
  61. package/src/route-pattern/join.ts +2 -0
  62. package/src/route-pattern/match.ts +2 -0
  63. package/src/session/README.md +171 -0
  64. package/src/session-middleware/README.md +109 -0
  65. package/src/session-storage-memcache/README.md +37 -0
  66. package/src/session-storage-redis/README.md +37 -0
  67. package/src/static-middleware/README.md +89 -0
  68. package/src/tar-parser/README.md +74 -0
  69. package/src/terminal/README.md +92 -0
  70. package/src/test/README.md +430 -0
  71. package/src/ui/README.md +219 -0
  72. package/src/ui/accordion/README.md +166 -0
  73. package/src/ui/anchor/README.md +153 -0
  74. package/src/ui/animation/README.md +316 -0
  75. package/src/ui/breadcrumbs/README.md +55 -0
  76. package/src/ui/button/README.md +44 -0
  77. package/src/ui/combobox/README.md +145 -0
  78. package/src/ui/glyph/README.md +72 -0
  79. package/src/ui/listbox/README.md +115 -0
  80. package/src/ui/menu/README.md +96 -0
  81. package/src/ui/popover/README.md +122 -0
  82. package/src/ui/scroll-lock/README.md +33 -0
  83. package/src/ui/select/README.md +107 -0
  84. package/src/ui/server/README.md +90 -0
  85. package/src/ui/test/README.md +107 -0
  86. package/src/ui/theme/README.md +103 -0
  87. package/dist/node-serve.d.ts +0 -2
  88. package/dist/node-serve.d.ts.map +0 -1
@@ -1,2 +1,9 @@
1
1
  // IMPORTANT: This file is auto-generated, please do not edit manually.
2
2
  export * from '@remix-run/fetch-router'
3
+
4
+ export interface RouterTypes {}
5
+ type RemixRouterTypes = RouterTypes
6
+
7
+ declare module '@remix-run/fetch-router' {
8
+ interface RouterTypes extends RemixRouterTypes {}
9
+ }
@@ -0,0 +1,57 @@
1
+ # file-storage
2
+
3
+ Key/value storage interfaces for server-side [`File` objects](https://developer.mozilla.org/en-US/docs/Web/API/File). `file-storage` gives Remix apps one consistent API across local disk and memory backends.
4
+
5
+ ## Features
6
+
7
+ - **Simple API** - Intuitive key/value API (like [Web Storage](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API), but for `File`s instead of strings)
8
+ - **Multiple Backends** - Built-in filesystem and memory backends
9
+ - **Streaming Support** - Stream file content to and from storage
10
+ - **Metadata Preservation** - Preserves all `File` metadata including `file.name`, `file.type`, `file.size`, and `file.lastModified`
11
+
12
+ ## Installation
13
+
14
+ ```sh
15
+ npm i remix
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ### File System
21
+
22
+ ```ts
23
+ import { createFsFileStorage } from 'remix/file-storage/fs'
24
+
25
+ let storage = createFsFileStorage('./user/files')
26
+
27
+ let file = new File(['hello world'], 'hello.txt', { type: 'text/plain' })
28
+ let key = 'hello-key'
29
+
30
+ // Put the file in storage.
31
+ await storage.set(key, file)
32
+
33
+ // Then, sometime later...
34
+ let fileFromStorage = await storage.get(key)
35
+
36
+ if (fileFromStorage != null) {
37
+ // All of the original file's metadata is intact
38
+ fileFromStorage.name // 'hello.txt'
39
+ fileFromStorage.type // 'text/plain'
40
+
41
+ // The filesystem backend returns a LazyFile, so you can stream it directly.
42
+ let response = new Response(fileFromStorage.stream())
43
+ }
44
+
45
+ // To remove from storage
46
+ await storage.remove(key)
47
+ ```
48
+
49
+ ## Related Packages
50
+
51
+ - [`file-storage-s3`](https://github.com/remix-run/remix/tree/main/packages/file-storage-s3) - S3 backend for `file-storage`
52
+ - [`form-data-parser`](https://github.com/remix-run/remix/tree/main/packages/form-data-parser) - Pairs well with this library for storing `FileUpload` objects received in `multipart/form-data` requests
53
+ - [`lazy-file`](https://github.com/remix-run/remix/tree/main/packages/lazy-file) - The streaming `File` implementation used internally to stream files from storage
54
+
55
+ ## License
56
+
57
+ See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
@@ -0,0 +1,47 @@
1
+ # file-storage-s3
2
+
3
+ S3 backend for [`remix/file-storage`](https://github.com/remix-run/remix/tree/main/packages/file-storage).
4
+ Use this package when you want the `FileStorage` API backed by AWS S3 or an S3-compatible provider.
5
+
6
+ ## Features
7
+
8
+ - **S3-Compatible API** - Works with AWS S3 and S3-compatible APIs (e.g. MinIO, LocalStack)
9
+ - **Metadata Preservation** - Preserves `File` metadata (`name`, `type`, `size`, `lastModified`)
10
+ - **Runtime-Agnostic Signing** - Uses [`aws4fetch`](https://github.com/mhart/aws4fetch) for SigV4 signing
11
+
12
+ ## Installation
13
+
14
+ ```sh
15
+ npm i remix
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```ts
21
+ import { createS3FileStorage } from 'remix/file-storage/s3'
22
+
23
+ let storage = createS3FileStorage({
24
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
25
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
26
+ bucket: 'my-app-uploads',
27
+ region: 'us-east-1',
28
+ })
29
+
30
+ await storage.set(
31
+ 'uploads/hello.txt',
32
+ new File(['hello world'], 'hello.txt', { type: 'text/plain' }),
33
+ )
34
+ let file = await storage.get('uploads/hello.txt')
35
+ await storage.remove('uploads/hello.txt')
36
+ ```
37
+
38
+ For S3-compatible providers such as MinIO and LocalStack, set `endpoint` and `forcePathStyle: true`.
39
+
40
+ ## Related Packages
41
+
42
+ - [`file-storage`](https://github.com/remix-run/remix/tree/main/packages/file-storage) - Core `FileStorage` interface and filesystem/memory backends
43
+ - [`form-data-parser`](https://github.com/remix-run/remix/tree/main/packages/form-data-parser) - Parses `multipart/form-data` uploads into `FileUpload` objects
44
+
45
+ ## License
46
+
47
+ See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
@@ -0,0 +1,109 @@
1
+ # form-data-middleware
2
+
3
+ Form body parsing middleware for Remix. It parses incoming [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) and exposes it via `context.formData` (or `context.get(FormData)`).
4
+
5
+ ## Features
6
+
7
+ - **Request Form Parsing** - Parses request body form data once per request
8
+ - **File Access** - Uploaded files are available from `context.formData` (or `context.get(FormData)`)
9
+ - **Custom Upload Handling** - Supports pluggable upload handlers for file processing
10
+ - **Error Control** - Optional suppression for malformed form data
11
+
12
+ ## Installation
13
+
14
+ ```sh
15
+ npm i remix
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ Use the `formData()` middleware at the router level to parse `FormData` from the request body and make it available as `context.formData` (or `context.get(FormData)`).
21
+
22
+ When `formData()` runs successfully it always provides a `FormData` value. Requests that do not contain a form body, including `GET` and `HEAD` requests, receive an empty `FormData`.
23
+
24
+ Uploaded files are available in the parsed `FormData` object. For a single file field, use `formData.get(name)`. For repeated file fields, use `formData.getAll(name)`.
25
+
26
+ ```ts
27
+ import { createRouter } from 'remix/router'
28
+ import { formData } from 'remix/middleware/form-data'
29
+
30
+ let router = createRouter({
31
+ middleware: [formData()],
32
+ })
33
+
34
+ router.post('/users', async (context) => {
35
+ let formData = context.formData
36
+ let name = formData.get('name')
37
+ let email = formData.get('email')
38
+
39
+ // Handle file uploads
40
+ let avatar = formData.get('avatar')
41
+
42
+ return Response.json({ name, email, hasAvatar: avatar instanceof File })
43
+ })
44
+ ```
45
+
46
+ Use `context.formData` (or `context.get(FormData)`).
47
+
48
+ ### Custom File Upload Handler
49
+
50
+ You can use a custom upload handler to customize how file uploads are handled. The return value of the upload handler will be used as the value of the form field in the `FormData` object.
51
+
52
+ ```ts
53
+ import { formData } from 'remix/middleware/form-data'
54
+ import { writeFile } from 'node:fs/promises'
55
+
56
+ let router = createRouter({
57
+ middleware: [
58
+ formData({
59
+ async uploadHandler(upload) {
60
+ // Save to disk and return path
61
+ let path = `./uploads/${upload.name}`
62
+ await writeFile(path, Buffer.from(await upload.arrayBuffer()))
63
+ return path
64
+ },
65
+ }),
66
+ ],
67
+ })
68
+ ```
69
+
70
+ ### Limit Multipart Growth
71
+
72
+ `formData()` forwards multipart limit options to `parseFormData()`, so you can cap uploads with
73
+ `maxHeaderSize`, `maxFiles`, `maxFileSize`, `maxParts`, and `maxTotalSize`.
74
+
75
+ ```ts
76
+ let router = createRouter({
77
+ middleware: [
78
+ formData({
79
+ maxFiles: 5,
80
+ maxFileSize: 10 * 1024 * 1024,
81
+ maxParts: 25,
82
+ maxTotalSize: 12 * 1024 * 1024,
83
+ }),
84
+ ],
85
+ })
86
+ ```
87
+
88
+ ### Suppress Parse Errors
89
+
90
+ Some requests may contain invalid form data that cannot be parsed. You can suppress those malformed-body parse errors by setting `suppressErrors` to `true`. In these cases, `context.formData` will be an empty `FormData` object. Multipart limit violations from `maxHeaderSize`, `maxFiles`, `maxFileSize`, `maxParts`, or `maxTotalSize` are never suppressed.
91
+
92
+ ```ts
93
+ let router = createRouter({
94
+ middleware: [
95
+ formData({
96
+ suppressErrors: true, // Invalid form data won't throw
97
+ }),
98
+ ],
99
+ })
100
+ ```
101
+
102
+ ## Related Packages
103
+
104
+ - [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Router for the web Fetch API
105
+ - [`form-data-parser`](https://github.com/remix-run/remix/tree/main/packages/form-data-parser) - The underlying form data parser
106
+
107
+ ## License
108
+
109
+ See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
@@ -0,0 +1,160 @@
1
+ # form-data-parser
2
+
3
+ A streaming `multipart/form-data` parser that solves memory issues with file uploads in server environments. Built as an enhanced replacement for the native `request.formData()` API, it enables efficient handling of large file uploads by streaming directly to disk or cloud storage services like [AWS S3](https://aws.amazon.com/s3/) or [Cloudflare R2](https://www.cloudflare.com/developer-platform/r2/), preventing server crashes from memory exhaustion.
4
+
5
+ ## Features
6
+
7
+ - **Drop-in replacement** for `request.formData()` with streaming file upload support
8
+ - **Minimal buffering** - processes file upload streams with minimal memory footprint
9
+ - **Standards-based** - built on the [web Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) and [File API](https://developer.mozilla.org/en-US/docs/Web/API/File)
10
+ - **Smart fallback** - automatically uses native `request.formData()` for non-`multipart/form-data` requests
11
+ - **Storage agnostic** - works with any storage backend (local disk, S3, R2, etc.)
12
+
13
+ ## Why You Need This
14
+
15
+ The native [`request.formData()` method](https://developer.mozilla.org/en-US/docs/Web/API/Request/formData) has a few major flaws in server environments:
16
+
17
+ - It buffers all file uploads in memory
18
+ - It does not provide fine-grained control over file upload handling
19
+ - It does not prevent DoS attacks from malicious requests
20
+
21
+ In normal usage, this makes it difficult to process requests with large file uploads because they can exhaust your server's RAM and crash the application.
22
+
23
+ For attackers, this creates an attack vector where malicious actors can overwhelm your server's memory by sending large payloads with many files.
24
+
25
+ `form-data-parser` solves this by handling file uploads as they arrive in the request body stream, allowing you to safely store files and use either a) the `File` directly or b) a unique identifier for that file in the returned `FormData` object.
26
+
27
+ ## Installation
28
+
29
+ ```sh
30
+ npm i remix
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ The `parseFormData` interface allows you to define an "upload handler" function for fine-grained control of handling file uploads.
36
+
37
+ ```ts
38
+ import * as fsp from 'node:fs/promises'
39
+ import type { FileUpload } from 'remix/form-data-parser'
40
+ import { parseFormData } from 'remix/form-data-parser'
41
+
42
+ // Define how to handle incoming file uploads
43
+ async function uploadHandler(fileUpload: FileUpload) {
44
+ // Is this file upload from the <input type="file" name="user-avatar"> field?
45
+ if (fileUpload.fieldName === 'user-avatar') {
46
+ let filename = `/uploads/user-${user.id}-avatar.bin`
47
+
48
+ // Store the file safely on disk
49
+ await fsp.writeFile(filename, fileUpload.bytes)
50
+
51
+ // Return the file name to use in the FormData object so we don't
52
+ // keep the file contents around in memory.
53
+ return filename
54
+ }
55
+
56
+ // Ignore unrecognized fields
57
+ }
58
+
59
+ // Handle form submissions with file uploads
60
+ async function requestHandler(request: Request) {
61
+ // Parse the form data from the request.body stream, passing any files
62
+ // through your upload handler as they are parsed from the stream
63
+ let formData = await parseFormData(request, uploadHandler)
64
+
65
+ let avatarFilename = formData.get('user-avatar')
66
+
67
+ if (avatarFilename != null) {
68
+ console.log(`User avatar uploaded to ${avatarFilename}`)
69
+ } else {
70
+ console.log(`No user avatar file was uploaded`)
71
+ }
72
+ }
73
+ ```
74
+
75
+ To validate the resulting `FormData` object with `remix/data-schema`, use the
76
+ `remix/data-schema/form-data` helpers.
77
+
78
+ To limit the overall shape of multipart requests, use the `maxHeaderSize`, `maxFileSize`, `maxFiles`, `maxParts`, and `maxTotalSize` options. By default, `parseFormData()` uses `maxFiles = 20`, `maxParts = 1000`, and `maxTotalSize = maxFiles * maxFileSize + 1 MiB`.
79
+
80
+ Known limit errors are thrown directly so you can handle them with `instanceof` checks. Other failures while parsing the request body are wrapped in `FormDataParseError`, with the original error available as `error.cause`. Errors thrown or rejected by your `uploadHandler` are not wrapped.
81
+
82
+ ```ts
83
+ import {
84
+ FormDataParseError,
85
+ MaxFilesExceededError,
86
+ MaxFileSizeExceededError,
87
+ MaxHeaderSizeExceededError,
88
+ MaxPartsExceededError,
89
+ MaxTotalSizeExceededError,
90
+ } from 'remix/form-data-parser'
91
+
92
+ const oneKb = 1024
93
+ const oneMb = 1024 * oneKb
94
+
95
+ try {
96
+ let formData = await parseFormData(request, {
97
+ maxFiles: 5,
98
+ maxFileSize: 10 * oneMb,
99
+ maxParts: 25,
100
+ maxTotalSize: 12 * oneMb,
101
+ })
102
+ } catch (error) {
103
+ if (error instanceof MaxFilesExceededError) {
104
+ console.error(`Request may not contain more than 5 files`)
105
+ } else if (error instanceof MaxHeaderSizeExceededError) {
106
+ console.error(`Multipart headers may not exceed the configured size limit`)
107
+ } else if (error instanceof MaxFileSizeExceededError) {
108
+ console.error(`Files may not be larger than 10 MiB`)
109
+ } else if (error instanceof MaxPartsExceededError) {
110
+ console.error(`Request may not contain more than 25 multipart parts`)
111
+ } else if (error instanceof MaxTotalSizeExceededError) {
112
+ console.error(`Multipart request may not exceed 12 MiB of total content`)
113
+ } else if (error instanceof FormDataParseError) {
114
+ console.error(`Could not parse form data:`, error.cause ?? error)
115
+ } else {
116
+ throw error
117
+ }
118
+ }
119
+ ```
120
+
121
+ If you're looking for a more flexible storage solution for `FileUpload` objects, this library pairs really well with [the `file-storage` library](https://github.com/remix-run/remix/tree/main/packages/file-storage) for keeping files in various storage backends.
122
+
123
+ ```ts
124
+ import { createFsFileStorage } from 'remix/file-storage/fs'
125
+ import type { FileUpload } from 'remix/form-data-parser'
126
+ import { parseFormData } from 'remix/form-data-parser'
127
+
128
+ // Set up storage for uploaded files
129
+ const fileStorage = createFsFileStorage('/uploads/user-avatars')
130
+
131
+ // Define how to handle incoming file uploads
132
+ async function uploadHandler(fileUpload: FileUpload) {
133
+ // Is this file upload from the <input type="file" name="user-avatar"> field?
134
+ if (fileUpload.fieldName === 'user-avatar') {
135
+ let storageKey = `user-${user.id}-avatar`
136
+
137
+ // Put the file in storage and return the stored LazyFile
138
+ return fileStorage.put(storageKey, fileUpload)
139
+ }
140
+
141
+ // Ignore unrecognized fields
142
+ }
143
+ ```
144
+
145
+ ## Demos
146
+
147
+ The [`demos` directory](https://github.com/remix-run/remix/tree/main/packages/form-data-parser/demos) contains working demos:
148
+
149
+ - [`demos/node`](https://github.com/remix-run/remix/tree/main/packages/form-data-parser/demos/node) - using form-data-parser with file-storage in Node.js
150
+
151
+ ## Related Packages
152
+
153
+ - [`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema) - Tiny,
154
+ standards-aligned validation with a `form-data` export for `FormData` and `URLSearchParams`
155
+ - [`file-storage`](https://github.com/remix-run/remix/tree/main/packages/file-storage) - A simple key/value interface for storing `FileUpload` objects you get from the parser
156
+ - [`multipart-parser`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser) - The parser used internally for parsing `multipart/form-data` HTTP messages
157
+
158
+ ## License
159
+
160
+ See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
@@ -0,0 +1,60 @@
1
+ # fs
2
+
3
+ Lazy, streaming filesystem utilities for JavaScript. This package provides utilities for working with files on the local filesystem using the [`LazyFile`](https://github.com/remix-run/remix/tree/main/packages/lazy-file)/ native [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) API.
4
+
5
+ ## Features
6
+
7
+ - **Web Standards** - Uses [`LazyFile`](https://github.com/remix-run/remix/tree/main/packages/lazy-file) which matches the native [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) API and provides `.stream()`, `.toFile()`, and `.toBlob()` for converting to native types.
8
+ - **Seamless Node.js Compat** - Works seamlessly with Node.js file descriptors and handles
9
+
10
+ ## Installation
11
+
12
+ ```sh
13
+ npm i remix
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ### Opening Lazy Files
19
+
20
+ ```ts
21
+ import { openLazyFile } from 'remix/fs'
22
+
23
+ // Open a file from the filesystem
24
+ let lazyFile = openLazyFile('./path/to/file.json')
25
+
26
+ // The file is lazy - no data is read until you call lazyFile.text(), lazyFile.bytes(), etc.
27
+ let json = JSON.parse(await lazyFile.text())
28
+
29
+ // You can override file metadata
30
+ let customLazyFile = openLazyFile('./image.jpg', {
31
+ name: 'custom-name.jpg',
32
+ type: 'image/jpeg',
33
+ lastModified: Date.now(),
34
+ })
35
+ ```
36
+
37
+ ### Writing Files
38
+
39
+ ```ts
40
+ import { openLazyFile, writeFile } from 'remix/fs'
41
+
42
+ // Read a file and write it elsewhere
43
+ let lazyFile = openLazyFile('./source.txt')
44
+ await writeFile('./destination.txt', lazyFile)
45
+
46
+ // Write to an open file handle
47
+ import * as fsp from 'node:fs/promises'
48
+ let handle = await fsp.open('./destination.txt', 'w')
49
+ await writeFile(handle, lazyFile)
50
+ await handle.close()
51
+ ```
52
+
53
+ ## Related Packages
54
+
55
+ - [`lazy-file`](https://github.com/remix-run/remix/tree/main/packages/lazy-file) - Lazy, streaming `Blob`/`File` implementation
56
+ - [`file-storage`](https://github.com/remix-run/remix/tree/main/packages/file-storage) - Storage abstraction for files
57
+
58
+ ## License
59
+
60
+ See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)