From fe2e323d1956316aa7bf2f3909b3536e03b42523 Mon Sep 17 00:00:00 2001 From: hypercross Date: Tue, 31 Mar 2026 13:02:29 +0800 Subject: [PATCH] feat: csv-loader? --- README.md | 18 +++++++++ csv-loader.md | 82 ++++++++++++++++++++++++++++++++++++++ package.json | 21 ++++++++-- src/csv-loader/loader.ts | 85 ++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 15 ++++--- tsup.config.ts | 19 +++++++++ 6 files changed, 229 insertions(+), 11 deletions(-) create mode 100644 csv-loader.md create mode 100644 src/csv-loader/loader.ts create mode 100644 tsup.config.ts diff --git a/README.md b/README.md index 42aba08..8ec707b 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,24 @@ Creates a validation function for the given schema. - Special characters can be escaped with backslash: `\;`, `\[`, `\]`, `\\` - Empty arrays/tuples are not allowed +## CSV Loader + +For loading CSV files with schema validation in rspack, see [csv-loader.md](./csv-loader.md). + +```javascript +// rspack.config.js +module.exports = { + module: { + rules: [ + { + test: /\.schema\.csv$/, + use: 'inline-schema/csv-loader', + }, + ], + }, +}; +``` + ## License ISC diff --git a/csv-loader.md b/csv-loader.md new file mode 100644 index 0000000..3bdf5a9 --- /dev/null +++ b/csv-loader.md @@ -0,0 +1,82 @@ +# inline-schema/csv-loader + +A rspack loader for CSV files that uses inline-schema for type validation. + +## Installation + +```bash +npm install inline-schema +``` + +## Usage + +The loader expects: +- **First row**: Property names (headers) +- **Second row**: Inline-schema definitions for each property +- **Remaining rows**: Data values + +### Example CSV + +```csv +name,age,active,scores +string,number,boolean,number[] +Alice,30,true,[90; 85; 95] +Bob,25,false,[75; 80; 70] +``` + +### rspack.config.js + +```javascript +module.exports = { + module: { + rules: [ + { + test: /\.schema\.csv$/, + use: { + loader: 'inline-schema/csv-loader', + options: { + delimiter: ',', + quote: '"', + escape: '\\', + }, + }, + }, + ], + }, +}; +``` + +### Importing in TypeScript + +```typescript +import data from './data.schema.csv'; + +// data = [ +// { name: "Alice", age: 30, active: true, scores: [90, 85, 95] }, +// { name: "Bob", age: 25, active: false, scores: [75, 80, 70] } +// ] +``` + +## Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `delimiter` | string | `,` | Column delimiter | +| `quote` | string | `"` | Quote character | +| `escape` | string | `\` | Escape character | + +## Schema Syntax + +Uses [inline-schema](https://github.com/your-repo/inline-schema) syntax: + +| Type | Schema | Example | +|------|--------|---------| +| String | `string` | `hello` | +| Number | `number` | `42` | +| Boolean | `boolean` | `true` | +| Array | `string[]` or `[string][]` | `[a; b; c]` | +| Tuple | `[string; number]` | `[hello; 42]` | + +## License + +ISC diff --git a/package.json b/package.json index 92745a4..51a8e19 100644 --- a/package.json +++ b/package.json @@ -9,26 +9,41 @@ "types": "./dist/index.d.ts", "import": "./dist/index.mjs", "require": "./dist/index.js" + }, + "./csv-loader": { + "types": "./dist/csv-loader/loader.d.ts", + "import": "./dist/csv-loader/loader.mjs", + "require": "./dist/csv-loader/loader.js" } }, "scripts": { - "build": "tsup src/index.ts --format cjs,esm --dts", - "dev": "tsup src/index.ts --format cjs,esm --dts --watch", + "build": "tsup", + "dev": "tsup --watch", "test": "tsx src/test.ts" }, "keywords": [ "schema", "parser", "validator", - "typescript" + "typescript", + "rspack", + "loader", + "csv" ], "author": "", "license": "ISC", "description": "A TypeScript library for parsing and validating inline schemas", + "dependencies": { + "csv-parse": "^5.5.6" + }, "devDependencies": { + "@rspack/core": "^1.1.6", "@types/node": "^25.5.0", "tsup": "^8.5.1", "tsx": "^4.21.0", "typescript": "^6.0.2" + }, + "peerDependencies": { + "@rspack/core": "^1.x" } } diff --git a/src/csv-loader/loader.ts b/src/csv-loader/loader.ts new file mode 100644 index 0000000..cfb225d --- /dev/null +++ b/src/csv-loader/loader.ts @@ -0,0 +1,85 @@ +import type { LoaderContext } from '@rspack/core'; +import { parse } from 'csv-parse/sync'; +import { parseSchema, createValidator, parseValue } from '../index.js'; + +export interface CsvLoaderOptions { + delimiter?: string; + quote?: string; + escape?: string; +} + +interface PropertyConfig { + name: string; + schema: any; + validator: (value: unknown) => boolean; + parser: (valueString: string) => unknown; +} + +export default function csvLoader( + this: LoaderContext, + content: string +): string { + const options = this.getOptions() || {}; + const delimiter = options.delimiter ?? ','; + const quote = options.quote ?? '"'; + const escape = options.escape ?? '\\'; + + const records = parse(content, { + delimiter, + quote, + escape, + relax_column_count: true, + }); + + if (records.length < 2) { + throw new Error('CSV must have at least 2 rows: headers and schemas'); + } + + const headers = records[0]; + const schemas = records[1]; + + if (headers.length !== schemas.length) { + throw new Error( + `Header count (${headers.length}) does not match schema count (${schemas.length})` + ); + } + + const propertyConfigs: PropertyConfig[] = headers.map((header: string, index: number) => { + const schemaString = schemas[index]; + const schema = parseSchema(schemaString); + return { + name: header, + schema, + validator: createValidator(schema), + parser: (valueString: string) => parseValue(schema, valueString), + }; + }); + + const dataRows = records.slice(2); + const objects = dataRows.map((row: string[], rowIndex: number) => { + const obj: Record = {}; + propertyConfigs.forEach((config, colIndex) => { + const rawValue = row[colIndex] ?? ''; + try { + const parsed = config.parser(rawValue); + if (!config.validator(parsed)) { + throw new Error( + `Validation failed for property "${config.name}" at row ${rowIndex + 3}: ${rawValue}` + ); + } + obj[config.name] = parsed; + } catch (error) { + if (error instanceof Error) { + throw new Error( + `Failed to parse property "${config.name}" at row ${rowIndex + 3}, column ${colIndex + 1}: ${error.message}` + ); + } + throw error; + } + }); + return obj; + }); + + const json = JSON.stringify(objects, null, 2); + return `export default ${json};`; +} diff --git a/tsconfig.json b/tsconfig.json index 3348294..c9b40c1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,18 +3,17 @@ "target": "ES2020", "module": "ESNext", "lib": ["ES2020"], - "types": ["node"], + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "strict": true, "declaration": true, "declarationMap": true, "sourceMap": true, "outDir": "./dist", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "bundler", - "ignoreDeprecations": "6.0" + "rootDir": "./src" }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "src/csv-loader"] } diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..874d6fe --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig([ + { + entry: ['src/index.ts'], + format: ['cjs', 'esm'], + dts: true, + outDir: 'dist', + clean: true, + }, + { + entry: ['src/csv-loader/loader.ts'], + format: ['cjs', 'esm'], + dts: true, + outDir: 'dist/csv-loader', + external: ['@rspack/core', 'csv-parse'], + clean: false, + }, +]);