A fluent TypeScript library for parsing and validating product import CSV files. Define rules for each product type, then run your CSV through them — groups are validated, errors are reported per-row with context.
Row groups — Rows are grouped by handle. The first row for a handle is the head row; subsequent rows sharing that handle (or rows with a blank handle immediately following) are continuation rows.
Product definitions — You define what a valid group looks like for each product type: which fields are required on the head row, and what kinds of continuation rows are allowed.
Continuation row types — Each continuation row type has a matchWhen predicate, required fields, and forbidden fields. The first matching type wins.
bun add csv-import-lib # when published# or copy src/ directly into your projectimport{CsvImporter,ProductDefinition,ContinuationRow,}from"csv-import-lib";constimporter=newCsvImporter();// Simple products — no continuation rows allowedimporter.addDefinition(ProductDefinition.create("simple").matchWhen((row)=>row.type==="simple").requireFields(["handle","title","type","sku"]),);// Variable products — each variant is a continuation rowimporter.addDefinition(ProductDefinition.create("variable").matchWhen((row)=>row.type==="variable").requireFields(["handle","title","type","sku","option 1 name","option 1 value",]).allowContinuationRows(ContinuationRow.create().label("variant-row").matchWhen((row)=>!!row.sku).requireFields(["handle","sku","option 1 value"]).forbidFields(["title"]),),);// Serialized products — each IMEI is a continuation rowimporter.addDefinition(ProductDefinition.create("serialized").matchWhen((row)=>row.type==="serialized").requireFields(["handle","title","type","sku","imei"]).allowContinuationRows(ContinuationRow.create().label("imei-row").matchWhen((row)=>!!row.imei&&!row.sku).requireFields(["handle","imei"]).forbidFields(["title","type","sku","price","cost"]),),);constresult=importer.parse(csvString);if(result.ok){for(constgroupofresult.valid){console.log(group.handle,group.definition,group.head,group.continuations,);}}else{for(consterrorofresult.errors){console.error(`Line ${error.line} [${error.handle}]${error.field ? ` field:${error.field}` : ""}: ${error.message}`,);}}The library expects a header row followed by data rows. The handle column groups rows into products.
handle,title,description,type,quantity,price,cost,sku,imei,barcode,category,tags,status,option 1 name,option 1 value,option 2 name,option 2 value,option 3 name,option 3 value,image srcsimple-product,My Product,A description,simple,5,9.99,5.00,SKU-001,,,Electronics,sale,active,,,,,,,variable-product,My Variable,A description,variable,10,9.99,5.00,SKU-002,,,Clothing,,active,Color,Red,,,,,variable-product,,,,8,9.99,5.00,SKU-003,,,,,,,Blue,,,,,serialized-product,My Serialized,A description,serialized,1,199,99,SKU-004,123456789012,,,,,,,,,,,serialized-product,,,,,,,,987654321098,,,,,,,,,,,Continuation rows can either repeat the handle or leave it blank — both are treated as belonging to the previous group.
Creates a new importer instance. addDefinition() returns this so you can chain:
constimporter=newCsvImporter().addDefinition(...).addDefinition(...)Registers a product definition. Definitions are evaluated in registration order — the first whose matchWhen predicate returns true for a group's head row will own that group.
Parses and validates the CSV. Returns:
interfaceParseResult{ok: boolean;// true if zero errorsvalid: ValidGroup[];// groups that passed validationerrors: ValidationError[];}interfaceValidGroup{definition: string;// name of the matched definitionhandle: string;head: RawRow;continuations: RawRow[];}interfaceValidationError{handle: string;line: number;field?: ProductRowField;// set when the error is field-specificmessage: string;}Starts a new product definition builder.
| Method | Description |
|---|---|
.matchWhen(fn) | Predicate against the head row. First match wins. |
.requireFields(fields[]) | Fields that must be non-empty on the head row. |
.allowContinuationRows(builder) | Register a continuation row type. Can be called multiple times for multiple types. |
Starts a new continuation row type builder.
| Method | Description |
|---|---|
.label(name) | Human-readable name used in error messages. |
.matchWhen(fn) | Predicate to identify this row type. First match wins. |
.requireFields(fields[]) | Fields that must be non-empty. |
.forbidFields(fields[]) | Fields that must be empty. |
These are the column names recognized by ProductRow:
| Field | Field | Field |
|---|---|---|
handle | title | description |
type | quantity | price |
cost | sku | imei |
barcode | category | tags |
status | option 1 name | option 1 value |
option 2 name | option 2 value | option 3 name |
option 3 value | image src |
All field names are typed as ProductRowField — passing an invalid field name to requireFields or forbidFields will be caught at compile time.
bun run vitest