Skip to content
LogoLogo

@portable/lines

Splits a stream of bytes or text into lines. Runs in browsers, Cloudflare Workers, Deno, Bun and Node 24+.

Replaces split2 and byline.

npm install @portable/lines
import * as Lines from '@portable/lines';
import * as Streams from '@portable/streams';
 
const response = await fetch('https://example.com/access.log');
const lines = response.body!.pipeThrough(new Lines.DecoderStream());
for await (const line of Streams.toAsyncIterable(lines)) {
  console.log(line);
}

Lines.DecoderStream is a TransformStream<Uint8Array | string, string>. Byte chunks are decoded with TextDecoder, so a multi-byte character split across chunks is decoded correctly, and a CRLF split across chunks counts as one line break.

Options

OptionDefaultDescription
separator/\r?\n/A string, a RegExp, or any object with [Symbol.split]. With the default or a string, a line that spans many chunks is split in linear time. Any other separator splits the unfinished line again for each chunk, so set maxLength when lines can be long.
encoding'utf-8'TextDecoder label for byte chunks.
maxLengthInfinityLongest allowed line, in UTF-16 code units.
overflow'error'When a line exceeds maxLength: 'error' fails the stream with MaxLengthError, 'skip' drops the line.
skipEmptyfalseDrop empty lines.
signalnoneAborting errors the stream with signal.reason and cancels the source piped into it.

Types

TypeDescription
Lines.SeparatorThe separator option: a string to match, or anything with [Symbol.split], such as a RegExp.

Why use this over split2 and byline

split2 and byline are Node Transform streams. Lines.DecoderStream is a TransformStream, so it splits a fetch response body in any JavaScript runtime, and it has no dependencies outside @portable/*.

  • It split 1.7 MB of text at the same speed as split2 in Node. node:readline was about 2× faster than both, but it exists only in Node.
  • maxLength is checked on every line. split2 checks only the unfinished line at the end of each chunk, so whether it catches a long line depends on how the input was chunked.
  • A multi-byte character or a CRLF split across two chunks is read as one character or one line break.
  • Empty lines are kept by default, as in split2. byline drops them unless you pass keepEmptyLines, and skipEmpty: true drops them here.
  • split2 takes a mapper function, such as split2(JSON.parse). Lines.DecoderStream has no mapper, so pipe its output through another TransformStream.
  • byline splits on every Unicode line break by default. Pass separator: /\r\n|[\n\v\f\r\x85\u2028\u2029]/ for the same result.
@portable/linessplit2bylinenode:readline
Split 1.7 MB into lines82.884.640.7165

Operations per second from pnpm bench, higher is better, in Node 24.22.0, on Intel(R) Xeon(R) Processor @ 2.10GHz (4 cores). A dash means the library has no equivalent.

@portable/linessplit2bylinenode:readline
Minified and gzipped1.3 kB1.1 kB1.2 kBbuilt in
Runtime dependencies300–
Node built-ins usednonestream, string_decoderBuffer, stream, timers, utilNode only
TypeScript typesincluded@types/split2@types/byline–
Version compared–4.2.05.0.0–

Sizes cover the imports in the usage example, or the equivalent imports of each library, bundled for the browser by scripts/bundle-size.ts. Runtime dependencies count every package installed with the published package. A library that uses Node built-ins needs polyfills in a browser and the nodejs_compat flag in Cloudflare Workers.