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
25 changes: 20 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[![npm version](https://img.shields.io/npm/v/n-readlines.svg)](https://www.npmjs.com/package/n-readlines)
[![npm downloads](https://img.shields.io/npm/dm/n-readlines.svg)](https://www.npmjs.com/package/n-readlines)
[![license](https://img.shields.io/npm/l/n-readlines.svg)](https://github.com/nacholibre/node-readlines/blob/master/LICENSE)
[![TypeScript](https://img.shields.io/badge/TypeScript-ready-blue.svg)](https://www.typescriptlang.org/)

> 📖 Read files line-by-line, synchronously. Zero dependencies.

Expand All @@ -16,6 +17,7 @@ Reading a file line by line may seem trivial, but in Node.js there's no straight
- 🔄 **Synchronous** — no callbacks or promises to manage
- 💾 **Memory efficient** — reads in chunks, doesn't load entire file
- 🔧 **Configurable** — custom chunk sizes and line endings
- 📘 **TypeScript support** — includes type definitions

## 📦 Installation

Expand All @@ -37,6 +39,18 @@ while (line = liner.next()) {
}
```

### TypeScript

```typescript
import LineByLine = require('n-readlines');
const liner = new LineByLine('./textfile.txt');

let line: Buffer | null;
while (line = liner.next()) {
console.log(line.toString());
}
```

## 📖 API Reference

### Constructor
Expand All @@ -55,13 +69,13 @@ new LineByLine(fd, [options])

### Methods

#### `.next()` → `Buffer | false`
#### `.next()` → `Buffer | null`

Returns the next line as a `Buffer` (without the newline character), or `false` when end of file is reached.
Returns the next line as a `Buffer` (without the newline character), or `null` when end of file is reached.

```javascript
const line = liner.next();
if (line) {
if (line !== null) {
console.log(line.toString()); // Convert Buffer to string
}
```
Expand All @@ -79,12 +93,12 @@ liner.next(); // First line again

#### `.close()`

Manually closes the file. Subsequent `next()` calls will return `false`.
Manually closes the file. Subsequent `next()` calls will return `null`.

```javascript
liner.next();
liner.close(); // Done reading early
liner.next(); // Returns false
liner.next(); // Returns null
```

## 📚 Examples
Expand Down Expand Up @@ -158,6 +172,7 @@ while (line = liner.next()) {
- The newline character is **not** included in the returned line
- Files without a trailing newline are handled correctly
- Empty lines are preserved and returned as empty buffers
- Returns `null` (not `false`) when end of file is reached

## 📄 License

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
{
"name": "n-readlines",
"version": "2.0.1",
"version": "3.0.0",
"description": "Read file line by line without buffering the whole file in memory.",
"main": "./readlines.js",
"types": "./readlines.d.ts",
"dependencies": {},
"repository": {
"type": "git",
Expand Down
40 changes: 40 additions & 0 deletions readlines.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
declare class LineByLine {
/**
* File descriptor. Set to `null` after the file is closed.
*/
fd: number | null;

/**
* Creates a new line-by-line file reader.
* @param file - Path to file or file descriptor
* @param options - Configuration options
*/
constructor(
file: string | number,
options?: {
/** Number of bytes to read at once. Default: 1024 */
readChunk?: number;
/** Line ending character. Default: '\n' */
newLineCharacter?: string;
}
);

/**
* Returns the next line as a Buffer, or `null` if end of file is reached.
* The newline character is not included in the returned buffer.
*/
next(): Buffer | null;

/**
* Resets the reader to the beginning of the file.
*/
reset(): void;

/**
* Manually closes the file. Subsequent `next()` calls will return `null`.
*/
close(): void;
}

export = LineByLine;

4 changes: 2 additions & 2 deletions readlines.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,9 @@ class LineByLine {
}

next() {
if (!this.fd) return false;
if (!this.fd) return null;

let line = false;
let line = null;

if (this.eofReached && this.linesCache.length === 0) {
return line;
Expand Down
20 changes: 10 additions & 10 deletions test/readlines.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ test('should get all lines', () => {

assert.strictEqual(liner.next().toString('ascii'), 'hello', 'line 0: hello');
assert.strictEqual(liner.next().toString('ascii'), 'hello2', 'line 1: hello2');
assert.strictEqual(liner.next(), false, 'line 3: false');
assert.strictEqual(liner.next(), false, 'line 4: false');
assert.strictEqual(liner.next(), null, 'line 3: false');
assert.strictEqual(liner.next(), null, 'line 4: false');
assert.strictEqual(liner.fd, null, 'fd null');
});

Expand All @@ -20,22 +20,22 @@ test('should get all lines even if the file doesnt end with new line', () => {

assert.strictEqual(liner.next().toString('ascii'), 'google.com', 'line 0: google.com');
assert.strictEqual(liner.next().toString('ascii'), 'yahoo.com', 'line 1: yahoo.com');
assert.strictEqual(liner.next(), false, 'line 3: false');
assert.strictEqual(liner.next(), null, 'line 3: false');
assert.strictEqual(liner.fd, null, 'fd is null');
});

test('should get all lines if there is no new lines', () => {
const liner = new lineByLine(path.resolve(__dirname, 'fixtures/noNewLinesFile.txt'));

assert.strictEqual(liner.next().toString('ascii'), 'no new line', 'line 0: no new line');
assert.strictEqual(liner.next(), false, 'line 1: false');
assert.strictEqual(liner.next(), null, 'line 1: false');
assert.strictEqual(liner.fd, null, 'fd is null');
});

test('should handle empty files', () => {
const liner = new lineByLine(path.resolve(__dirname, 'fixtures/emptyFile.txt'));

assert.strictEqual(liner.next(), false, 'line 0: false');
assert.strictEqual(liner.next(), null, 'line 0: false');
assert.strictEqual(liner.fd, null, 'line 0: false');
});

Expand All @@ -47,7 +47,7 @@ test('should read right between two chunks', () => {
assert.strictEqual(liner.next().toString('ascii'), 'google.com', 'line 0: google.com');
assert.strictEqual(liner.next().toString('ascii'), 'yahoo.com', 'line 1: yahoo.com');
assert.strictEqual(liner.next().toString('ascii'), 'yandex.ru', 'line 2: yandex.ru');
assert.strictEqual(liner.next(), false, 'line 3: false');
assert.strictEqual(liner.next(), null, 'line 3: false');
assert.strictEqual(liner.fd, null, 'fs is null');
});

Expand All @@ -59,7 +59,7 @@ test('should read empty lines', () => {
assert.strictEqual(liner.next().toString('ascii'), '', 'line 2: ');
assert.strictEqual(liner.next().toString('ascii'), 'hello2', 'line 3: hello2');
assert.strictEqual(liner.next().toString('ascii'), 'hello3', 'line 4: hello3');
assert.strictEqual(liner.next(), false, 'line 5: false');
assert.strictEqual(liner.next(), null, 'line 5: false');
assert.strictEqual(liner.fd, null, 'fs is null');
});

Expand All @@ -76,7 +76,7 @@ test('should reset and start from the beggining', () => {
assert.strictEqual(liner.next().toString('ascii'), 'google.com', 'line 0: google.com');
assert.strictEqual(liner.next().toString('ascii'), 'yahoo.com', 'line 1: yahoo.com');
assert.strictEqual(liner.next().toString('ascii'), 'yandex.ru', 'line 2: yandex.ru');
assert.strictEqual(liner.next(), false, 'line 3: false');
assert.strictEqual(liner.next(), null, 'line 3: false');
assert.strictEqual(liner.fd, null, 'fd is null');
});

Expand All @@ -87,7 +87,7 @@ test('should read big lines', () => {
assert.ok(JSON.parse(liner.next()), 'line 1: valid JSON');
assert.ok(JSON.parse(liner.next()), 'line 2: valid JSON');

assert.strictEqual(liner.next(), false, 'line 3: false');
assert.strictEqual(liner.next(), null, 'line 3: false');
assert.strictEqual(liner.fd, null, 'fd is null');
});

Expand All @@ -107,7 +107,7 @@ test('Manually Close', () => {
liner.close();
assert.strictEqual(liner.fd, null, 'fd is null');

assert.strictEqual(liner.next(), false, 'line after close: false');
assert.strictEqual(liner.next(), null, 'line after close: false');
});

test('should correctly processes NULL character in lines', () => {
Expand Down