Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Update `@remix-run/fs` peer dependency to use new `openLazyFile()` API
6 changes: 3 additions & 3 deletions packages/file-storage/src/lib/backends/fs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as fs from 'node:fs'
import * as fsp from 'node:fs/promises'
import * as path from 'node:path'
import { openFile, writeFile } from '@remix-run/fs'
import { openLazyFile, writeFile } from '@remix-run/fs'

import type { FileStorage, FileMetadata, ListOptions, ListResult } from '../file-storage.ts'

Expand Down Expand Up @@ -69,7 +69,7 @@ export function createFsFileStorage(directory: string): FileStorage {

let metaData = await readMetadata(metaPath)

return openFile(filePath, {
return openLazyFile(filePath, {
lastModified: metaData.lastModified,
name: metaData.name,
type: metaData.type,
Expand All @@ -83,7 +83,7 @@ export function createFsFileStorage(directory: string): FileStorage {
try {
let meta = await readMetadata(metaPath)

return openFile(filePath, {
return openLazyFile(filePath, {
lastModified: meta.lastModified,
name: meta.name,
type: meta.type,
Expand Down
19 changes: 19 additions & 0 deletions packages/fs/.changes/minor.rename-openfile-to-openlazyfile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
BREAKING CHANGE: Renamed `openFile()` to `openLazyFile()`, removed `getFile()`

Since `LazyFile` no longer extends `File`, the function name now explicitly reflects the return type. The `getFile()` alias has also been removed—use `openLazyFile()` instead.

**Migration:**

```ts
import { openLazyFile } from '@remix-run/fs'

let lazyFile = openLazyFile('./document.pdf')

// Streaming
let response = new Response(lazyFile.stream())

// For non-streaming APIs that require a complete File (e.g. FormData)
formData.append('file', await lazyFile.toFile())
```

**Note:** `.toFile()` and `.toBlob()` read the entire file into memory. Only use these for non-streaming APIs that require a complete `File` or `Blob` (e.g. `FormData`). Always prefer `.stream()` if possible.
26 changes: 13 additions & 13 deletions packages/fs/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# fs

Filesystem utilities using the Web File API.
Lazy, streaming filesystem utilities for JavaScript.

This package provides utilities for working with files on the local filesystem using the Web [File API](https://developer.mozilla.org/en-US/docs/Web/API/File).
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.

## Features

- **Web Standards** - Use the Web File API for maximum portability
- **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.
- **Seamless Node.js Compat** - Works seamlessly with Node.js file descriptors and handles

## Installation
Expand All @@ -19,19 +19,19 @@ npm install @remix-run/fs

## Usage

### Opening Files
### Opening Lazy Files

```ts
import { openFile } from '@remix-run/fs'
import { openLazyFile } from '@remix-run/fs'

// Open a file from the filesystem
let file = openFile('./path/to/file.json')
let lazyFile = openLazyFile('./path/to/file.json')

// The file is lazy - no data is read until you call file.text(), file.bytes(), etc.
let json = JSON.parse(await file.text())
// The file is lazy - no data is read until you call lazyFile.text(), lazyFile.bytes(), etc.
let json = JSON.parse(await lazyFile.text())

// You can override file metadata
let customFile = openFile('./image.jpg', {
let customLazyFile = openLazyFile('./image.jpg', {
name: 'custom-name.jpg',
type: 'image/jpeg',
lastModified: Date.now(),
Expand All @@ -41,16 +41,16 @@ let customFile = openFile('./image.jpg', {
### Writing Files

```ts
import { openFile, writeFile } from '@remix-run/fs'
import { openLazyFile, writeFile } from '@remix-run/fs'

// Read a file and write it elsewhere
let file = openFile('./source.txt')
await writeFile('./destination.txt', file)
let lazyFile = openLazyFile('./source.txt')
await writeFile('./destination.txt', lazyFile)

// Write to an open file handle
import * as fsp from 'node:fs/promises'
let handle = await fsp.open('./destination.txt', 'w')
await writeFile(handle, file)
await writeFile(handle, lazyFile)
await handle.close()
```

Expand Down
4 changes: 2 additions & 2 deletions packages/fs/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export type { GetFileOptions, OpenFileOptions } from './lib/fs.ts'
export { getFile, openFile, writeFile } from './lib/fs.ts'
export type { OpenLazyFileOptions } from './lib/fs.ts'
export { openLazyFile, writeFile } from './lib/fs.ts'
86 changes: 43 additions & 43 deletions packages/fs/src/lib/fs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import * as os from 'node:os'
import * as path from 'node:path'
import { beforeEach, afterEach, describe, it } from 'node:test'

import { openFile, writeFile } from './fs.ts'
import { openLazyFile, writeFile } from './fs.ts'

describe('openFile', () => {
describe('openLazyFile', () => {
let tmpDir: string

beforeEach(() => {
Expand All @@ -33,62 +33,62 @@ describe('openFile', () => {
it('opens a file and reads content', async () => {
let filePath = createTestFile('test.txt', 'hello world')

let file = openFile(filePath)
let lazyFile = openLazyFile(filePath)

assert.equal(file.name, filePath)
assert.equal(file.size, 11)
assert.equal(await file.text(), 'hello world')
assert.equal(lazyFile.name, filePath)
assert.equal(lazyFile.size, 11)
assert.equal(await lazyFile.text(), 'hello world')
})

it('sets MIME type based on file extension', () => {
let htmlPath = createTestFile('test.html', '<html></html>')
let jsonPath = createTestFile('test.json', '{}')
let txtPath = createTestFile('test.txt', 'text')

assert.equal(openFile(htmlPath).type, 'text/html')
assert.equal(openFile(jsonPath).type, 'application/json')
assert.equal(openFile(txtPath).type, 'text/plain')
assert.equal(openLazyFile(htmlPath).type, 'text/html')
assert.equal(openLazyFile(jsonPath).type, 'application/json')
assert.equal(openLazyFile(txtPath).type, 'text/plain')
})

it('sets lastModified from file stats', () => {
let filePath = createTestFile('test.txt', 'content')
let stats = fs.statSync(filePath)

let file = openFile(filePath)
let lazyFile = openLazyFile(filePath)

assert.equal(file.lastModified, stats.mtimeMs)
assert.equal(lazyFile.lastModified, stats.mtimeMs)
})

it('overrides file name with options.name', () => {
let filePath = createTestFile('test.txt', 'content')

let file = openFile(filePath, { name: 'custom.txt' })
let lazyFile = openLazyFile(filePath, { name: 'custom.txt' })

assert.equal(file.name, 'custom.txt')
assert.equal(lazyFile.name, 'custom.txt')
})

it('overrides MIME type with options.type', () => {
let filePath = createTestFile('test.txt', 'content')

let file = openFile(filePath, { type: 'application/custom' })
let lazyFile = openLazyFile(filePath, { type: 'application/custom' })

assert.equal(file.type, 'application/custom')
assert.equal(lazyFile.type, 'application/custom')
})

it('overrides lastModified with options.lastModified', () => {
let filePath = createTestFile('test.txt', 'content')
let customTime = Date.now() - 1000000

let file = openFile(filePath, { lastModified: customTime })
let lazyFile = openLazyFile(filePath, { lastModified: customTime })

assert.equal(file.lastModified, customTime)
assert.equal(lazyFile.lastModified, customTime)
})

it('reads file as ArrayBuffer', async () => {
let filePath = createTestFile('test.txt', 'hello')

let file = openFile(filePath)
let buffer = await file.arrayBuffer()
let lazyFile = openLazyFile(filePath)
let buffer = await lazyFile.arrayBuffer()

assert.equal(buffer.byteLength, 5)
assert.equal(new TextDecoder().decode(buffer), 'hello')
Expand All @@ -97,10 +97,10 @@ describe('openFile', () => {
it('streams file content', async () => {
let filePath = createTestFile('test.txt', 'streaming content')

let file = openFile(filePath)
let lazyFile = openLazyFile(filePath)
let chunks: Uint8Array[] = []

for await (let chunk of file.stream()) {
for await (let chunk of lazyFile.stream()) {
chunks.push(chunk)
}

Expand All @@ -117,27 +117,27 @@ describe('openFile', () => {
it('handles empty files', async () => {
let filePath = createTestFile('empty.txt', '')

let file = openFile(filePath)
let lazyFile = openLazyFile(filePath)

assert.equal(file.size, 0)
assert.equal(await file.text(), '')
assert.equal(lazyFile.size, 0)
assert.equal(await lazyFile.text(), '')
})

it('handles large files', async () => {
let largeContent = 'x'.repeat(10000)
let filePath = createTestFile('large.txt', largeContent)

let file = openFile(filePath)
let lazyFile = openLazyFile(filePath)

assert.equal(file.size, 10000)
assert.equal(await file.text(), largeContent)
assert.equal(lazyFile.size, 10000)
assert.equal(await lazyFile.text(), largeContent)
})

it('throws error for non-existent files', () => {
let nonExistentPath = path.join(tmpDir, 'nonexistent.txt')

assert.throws(
() => openFile(nonExistentPath),
() => openLazyFile(nonExistentPath),
(error: Error) => error.message.includes('ENOENT'),
)
})
Expand All @@ -147,7 +147,7 @@ describe('openFile', () => {
fs.mkdirSync(dirPath)

assert.throws(
() => openFile(dirPath),
() => openLazyFile(dirPath),
(error: Error) => error.message.includes('is not a file'),
)
})
Expand All @@ -171,8 +171,8 @@ describe('writeFile', () => {
let destPath = path.join(tmpDir, 'dest.txt')
fs.writeFileSync(sourcePath, 'test content')

let file = openFile(sourcePath)
await writeFile(destPath, file)
let lazyFile = openLazyFile(sourcePath)
await writeFile(destPath, lazyFile)

assert.equal(fs.readFileSync(destPath, 'utf-8'), 'test content')
})
Expand All @@ -182,10 +182,10 @@ describe('writeFile', () => {
let destPath = path.join(tmpDir, 'dest.txt')
fs.writeFileSync(sourcePath, 'test content')

let file = openFile(sourcePath)
let lazyFile = openLazyFile(sourcePath)
let fd = fs.openSync(destPath, 'w')

await writeFile(fd, file)
await writeFile(fd, lazyFile)
// Note: fd is automatically closed by the write stream

assert.equal(fs.readFileSync(destPath, 'utf-8'), 'test content')
Expand All @@ -196,10 +196,10 @@ describe('writeFile', () => {
let destPath = path.join(tmpDir, 'dest.txt')
fs.writeFileSync(sourcePath, 'test content')

let file = openFile(sourcePath)
let lazyFile = openLazyFile(sourcePath)
let handle = await fsp.open(destPath, 'w')

await writeFile(handle, file)
await writeFile(handle, lazyFile)
await handle.close()

assert.equal(fs.readFileSync(destPath, 'utf-8'), 'test content')
Expand All @@ -210,8 +210,8 @@ describe('writeFile', () => {
let destPath = path.join(tmpDir, 'dest.txt')
fs.writeFileSync(sourcePath, '')

let file = openFile(sourcePath)
await writeFile(destPath, file)
let lazyFile = openLazyFile(sourcePath)
await writeFile(destPath, lazyFile)

assert.equal(fs.readFileSync(destPath, 'utf-8'), '')
})
Expand All @@ -222,8 +222,8 @@ describe('writeFile', () => {
let destPath = path.join(tmpDir, 'dest.txt')
fs.writeFileSync(sourcePath, largeContent)

let file = openFile(sourcePath)
await writeFile(destPath, file)
let lazyFile = openLazyFile(sourcePath)
await writeFile(destPath, lazyFile)

assert.equal(fs.readFileSync(destPath, 'utf-8'), largeContent)
})
Expand All @@ -234,8 +234,8 @@ describe('writeFile', () => {
fs.writeFileSync(sourcePath, 'content')
fs.mkdirSync(path.dirname(destPath), { recursive: true })

let file = openFile(sourcePath)
await writeFile(destPath, file)
let lazyFile = openLazyFile(sourcePath)
await writeFile(destPath, lazyFile)

assert.ok(fs.existsSync(destPath))
assert.equal(fs.readFileSync(destPath, 'utf-8'), 'content')
Expand All @@ -247,8 +247,8 @@ describe('writeFile', () => {
let destPath = path.join(tmpDir, 'dest.dat')
fs.writeFileSync(sourcePath, binaryData)

let file = openFile(sourcePath)
await writeFile(destPath, file)
let lazyFile = openLazyFile(sourcePath)
await writeFile(destPath, lazyFile)

let written = fs.readFileSync(destPath)
assert.deepEqual(written, binaryData)
Expand Down
Loading