npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nuport/file-import

v1.1.2

Published

React library for CSV/XLSX/XLS imports: parsing, column mapping, type casting, configurable validation, review-and-edit UI and downloadable error reports.

Readme

@nuport/file-import

React library for importing CSV, XLSX and XLS files: parsing, column matching, type casting, configurable validation, a spreadsheet-style review grid with bulk fixes, and downloadable error reports. Built as a replacement for Flatfile-style import flows, with no third-party service.

  • Headless core (@nuport/file-import/core): pure functions, no React, usable on the server.
  • React UI (@nuport/file-import): the FileImporter component (modal or inline), the useFileImport hook, and the building blocks for custom layouts.
  • Brings its own styling. One stylesheet driven by --nfi-* CSS variables. Interactive parts are headless Radix primitives, so there is no Tailwind or component-library CSS to reconcile with your app. Works with React 18 and 19, Next.js (Pages and App Router), Vite, Tailwind, antd.
  • Flatfile blueprint compatible. Existing fields arrays (key, label, type, constraints: [{ type: 'required' }], config.options) work unchanged.

Install

npm install @nuport/file-import

Peer dependencies: react and react-dom ^18 || ^19. Radix packages (@radix-ui/react-dialog, react-popover, react-select, react-toggle-group) are regular dependencies and install with it.

Quick start

import { FileImporter } from '@nuport/file-import'
import '@nuport/file-import/styles.css'

const config = {
  name: 'Customers',
  fields: [
    { key: 'name', label: 'Name', required: true },
    { key: 'phone', label: 'Phone', required: true, unique: true },
    { key: 'email', label: 'Email', alternativeNames: ['E-mail address'] },
    { key: 'age', label: 'Age', type: 'number', constraints: [{ type: 'min', value: 18 }] },
    { key: 'tier', label: 'Tier', type: 'enum', options: [{ value: 'gold', label: 'Gold' }] },
  ],
}

export function ImportButton() {
  return (
    <FileImporter
      config={config}
      trigger={(open) => <button onClick={open}>Import customers</button>}
      onSubmit={async ({ validRows }) => {
        await api.importCustomers(validRows.map((row) => row.values))
      }}
    />
  )
}

onSubmit receives an ImportResult. If it throws, the error is shown and the user stays on the review step.

What the user sees

The importer is three steps. Steps that need no input are skipped.

  1. Upload file. Drag-and-drop or browse. CSV, TSV, XLSX and XLS; multi-sheet workbooks get a sheet picker.
  2. Match columns. Shown only when something needs attention (configurable, see mappingStep). It leads with the problems: a column picker for every required field that has no column, and a field picker for every file column that matched nothing. Auto-matched columns are listed underneath with a sample value each. Columns matched by fuzzy name similarity carry a "best guess" badge so the user can confirm them. All pickers are searchable.
  3. Review & fix. An issues panel groups every problem by field and value (Warehouse Name from_location is not one of the options, 500 rows), with a one-shot fix next to each group: pick an option, type a replacement, or fill empty cells. One fix applies to every row sharing that value. A "remove rows with issues" button clears the rest. Below it, an All / Ready / Needs-fix filter and a spreadsheet-style grid with every configured field as a column (fields the file did not have start empty), click-to-edit cells, always-visible searchable dropdowns for enum and boolean fields, red cells for errors (message on hover), and per-row removal. Edits re-validate live, including duplicates. The footer offers the error report download, an optional server check, and the import button with the row count.

Back goes to the previous step with the file kept; "Change file" starts over.

How it works

file ─▶ parseFile ─▶ autoMapColumns ─▶ buildRows ─▶ cast ─▶ transform ─▶ validate ─▶ unique checks ─▶ review ─▶ onSubmit
        (csv/xlsx)   (exact + fuzzy)                (type)   (hooks)      (constraints, hooks)
  1. Parse. Format is detected from the file's leading bytes (zip → xlsx, OLE → xls) and then the extension. CSV/TSV is streamed through papaparse with delimiter auto-detection and BOM handling; workbooks are read with SheetJS (numbers raw, dates as ISO strings, first or chosen sheet).
  2. Match. Headers match fields by key, label or alternativeNames, ignoring case, spacing and punctuation. Columns still unmatched get a fuzzy pass: shared words with synonyms (Invoice ID → Invoice No., Mobile → Phone, Qty → Quantity), a near-spelling (Warehose Name), every word of the field name present once filler words and compounds are accounted for (Quantity in a MasterCase/CBC → Master Case Quantity), or the same head word with only abbreviations around it (SO/STO Number → Order Number, while Customer Number stays unmatched). Fuzzy matches are flagged matchedBy: 'fuzzy' and listed in guessedColumns. Each field is assigned to at most one column.
  3. Cast. Cells are coerced to the field type: trimmed strings, numbers with thousands separators removed, booleans (yes/no/true/false/1/0), dates to ISO (YYYY-MM-DD, DD/MM/YYYY, ISO), enums by value or label (case-insensitive) to the canonical value; an enum whose options are still empty (for example loaded from an API later) accepts any value. Empty cells become undefined.
  4. Validate. Constraints (required, unique, pattern, min, max, minLength, maxLength), enum membership, field and row hooks, composite duplicateKeys. Large files are validated in 1000-row slices so the UI stays responsive.
  5. Review. groupIssues builds the issues panel; replaceCellText applies a bulk fix and re-validates the affected rows plus table-level uniqueness. Cell edits go through processRow.
  6. Submit. validOnly (default) submits valid rows and reports how many were skipped; requireAllValid blocks until every row is clean.

Configuration

interface ImportConfig {
  name: string // shown in the title; also used for the slug
  slug?: string // used in the error report file name
  fields: FieldConfig[]
  allowUnmappedColumns?: boolean // keep unknown columns under their header (default false)
  acceptedFormats?: ('csv' | 'xlsx' | 'xls')[] // default all three
  maxFileSize?: number // bytes, default 50 MB
  maxRows?: number
  duplicateKeys?: Array<string | string[]> // composite uniqueness
  transformRow?: (values, ctx) => values | void
  validateRow?: (values, ctx) => string | CellError | CellError[] | null
  validateOnServer?: (rows: ImportRow[]) => Promise<ServerRowError[]>
  submitMode?: 'validOnly' | 'requireAllValid'
  messages?: Partial<Record<ErrorCode, string | ((ctx) => string)>> // '{label}', '{key}', '{value}'
}

interface FieldConfig {
  key: string
  label?: string
  type?: 'string' | 'number' | 'boolean' | 'date' | 'enum' // default 'string'
  description?: string // shown as the column header tooltip
  alternativeNames?: string[]
  required?: boolean
  unique?: boolean
  options?: { value: string; label?: string }[] // enum
  constraints?: Constraint[] // { type, value?, regex?, flags?, message? }
  config?: { options?: EnumOption[] } // Flatfile-compatible alias
  transform?: (value, ctx) => unknown
  validate?: (value, ctx) => string | CellError | CellError[] | null
}

Hooks run in this order per row: cast → field.transform → transformRow → constraints → field.validate → validateRow. Uniqueness is computed over the whole table afterwards. A cell with a cast error does not also get constraint errors.

Rows and results

interface ImportRow {
  index: number // 1-based data row; the number shown in the grid, error report and duplicate messages
  sourceRow: number // spreadsheet row (header is 1)
  raw: Record<string, string> // text as uploaded (or edited), keyed by field key
  values: Record<string, unknown> // cast + transformed
  errors: CellError[] // { field: string | null, code, message, meta? }
  valid: boolean
  edited: boolean
  editedFields: string[] // keys the user changed in the review step
}

interface ColumnMappingEntry {
  column: number // zero-based column in the file
  header: string
  fieldKey: string | null
  matchedBy?: 'exact' | 'fuzzy' // absent once the user picks a field by hand
}

interface MappingResult {
  mapping: ColumnMappingEntry[]
  unmappedColumns: number[]
  guessedColumns: number[] // fuzzy matches the user should confirm
  missingRequired: string[]
  unmatchedFields: string[]
}

interface IssueGroup {
  field: string | null // null for row-level errors
  code: ErrorCode
  message: string
  text: string | null // the raw cell text every row in the group shares ('' for empty)
  rowIndexes: number[]
}

interface ImportResult {
  fileName: string
  sheetName?: string
  mapping: ColumnMappingEntry[]
  rows: ImportRow[]
  validRows: ImportRow[]
  invalidRows: ImportRow[]
  summary: { total; valid; invalid; duplicates; errorsByField }
}

FileImporter props

| Prop | Type | Notes | | ------------------ | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | config | ImportConfig | Required. | | onSubmit | (result) => void \| Promise<void> | Required. Throwing keeps the user on the review step. | | onError | (error) => void | Parse, mapping, server and submit errors. | | onCancel | () => void | | | mode | 'modal' \| 'inline' | Default modal. | | trigger | (open) => ReactNode | Uncontrolled modal launcher. | | open, onClose | | Controlled modal visibility. | | title | ReactNode | Default Import {name}. | | labels | Partial<Labels> | Override any UI string (see DEFAULT_LABELS). | | theme | 'light' \| 'dark' \| 'auto' | Default light; auto follows prefers-color-scheme. | | mappingStep | 'auto' \| 'always' \| 'never' | auto (default) shows the step when a required field is unmatched, a match was a guess, or columns would be skipped. never fails with MISSING_REQUIRED_COLUMNS instead of asking. | | pageSize | number | Review grid page size, default 50. | | closeOnSubmit | boolean | Default true. | | showFieldPreview | boolean | Default true: the upload step draws the expected columns as an empty spreadsheet behind the drop target. | | previewRows | number | Empty rows drawn in that preview, default 8. | | className | string | Added to the modal or inline root. |

All user-facing text lives in DEFAULT_LABELS and can be overridden per string through labels, including the step names, the issues-panel wording and the search placeholder.

Theming

Import the stylesheet once and override tokens on :root (or on .nfi-root for a scoped theme). The library declares its defaults with zero specificity, so your :root rule wins regardless of import order:

:root {
  --nfi-primary: #278ea5;
  --nfi-radius: 0;
  --nfi-font: 'Inter', sans-serif;
}

Available tokens: --nfi-primary, --nfi-primary-contrast, --nfi-danger, --nfi-danger-bg, --nfi-warning, --nfi-warning-bg, --nfi-success, --nfi-success-bg, --nfi-bg, --nfi-bg-muted, --nfi-bg-hover, --nfi-text, --nfi-text-muted, --nfi-border, --nfi-border-strong, --nfi-radius, --nfi-font, --nfi-font-mono, --nfi-font-size, --nfi-z-index, --nfi-mask, --nfi-shadow, --nfi-shadow-sm. All classes are prefixed nfi-.

The modal, dropdown menus and the filter toggle are Radix primitives styled by the same stylesheet. Menus render in a portal and carry the importer's data-nfi-theme, so dark mode applies to them too. The modal is portaled to document.body and inherits its font unless --nfi-font is set.

Headless usage

import { runImportPipeline, buildErrorReport, rowsToCsvFile } from '@nuport/file-import/core'

const result = await runImportPipeline(file, config, { onProgress: console.log })
const csvForBackend = rowsToCsvFile(
  result.validRows.map((r) => r.values),
  { headers: 'key' },
)
const report = buildErrorReport(result.rows, defineImportConfig(config).fields, {
  headers: 'label',
})

Lower-level pieces are exported too: parseFile, detectFormat, autoMapColumns, matchColumn, headerTokens, assignColumn, buildRows, castValue, processRow, applyUniqueChecks, validateRows, applyServerErrors, groupIssues, replaceCellText, summarize, toCsv, recordsToCsv, downloadBlob, downloadCsv.

Your own UI

useFileImport exposes the state machine the component uses: status, parsed, mappingResult, rows, visibleRows, summary, issues (grouped problems), filter (all / valid / invalid), plus selectFile, selectSheet, setMapping, confirmMapping, updateCell, replaceValue, removeRow, removeInvalidRows, setFilter, runServerValidation, downloadErrorReport, submit, goBack, reset.

The building blocks are exported for custom layouts: Dropzone, MappingStep, ReviewStep, IssuesPanel, Modal, Stepper, SegmentedControl, Select (Radix select for short lists) and Combobox (searchable dropdown used for cells and pickers). Wrap them in ThemeContext.Provider to pass the theme to portaled menus.

Error codes

REQUIRED, INVALID_NUMBER, INVALID_BOOLEAN, INVALID_DATE, INVALID_ENUM, DUPLICATE, PATTERN, MIN, MAX, MIN_LENGTH, MAX_LENGTH, CUSTOM, SERVER on cells; ImportError with FILE_TOO_LARGE, UNSUPPORTED_FORMAT, EMPTY_FILE, PARSE_ERROR, TOO_MANY_ROWS, MISSING_REQUIRED_COLUMNS, INVALID_CONFIG for whole-file problems.

Integration guides

Development

npm install
npm test            # vitest (jsdom; Radix needs the polyfills in tests/setup.ts)
npm run lint        # eslint + prettier
npm run typecheck
npm run build       # tsup → dist/ (esm, cjs, d.ts, styles.css)

CI runs lint, typecheck, tests and build on every push and pull request.

Releases: bump version in package.json, update CHANGELOG.md, tag vX.Y.Z and push the tag. The release workflow checks the tag against the package version and publishes to npm. It needs an NPM_TOKEN repository secret from an npm account that owns the @nuport scope. Provenance attestation is off because npm only supports it for public source repositories.

License

MIT © Nuport