diff --git a/csv-loader.md b/csv-loader.md index bd68e90..ab96428 100644 --- a/csv-loader.md +++ b/csv-loader.md @@ -41,6 +41,8 @@ module.exports = { bom: true, // 处理 BOM (默认 true) comment: '#', // 忽略 # 开头的注释行 (默认 '#') trim: true, // 修剪表头和值的前后空格 (默认 true) + // emitTypes: false, // 禁用类型定义生成 (默认 true) + // typesOutputDir: 'types', // 类型文件输出目录 (可选) }, }, }, @@ -49,16 +51,35 @@ module.exports = { }; ``` +### Generated TypeScript Types + +当 `emitTypes: true` 时,loader 会自动生成 `.d.ts` 类型定义文件: + +```typescript +// data.schema.d.ts +export interface data_schema { + name: string; + age: number; + active: boolean; + scores: number[]; +} + +export type RowType = data_schema; + +declare const data: data_schema[]; +export default data; +``` + ### 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] } -// ] -``` +// TypeScript 会自动推断类型: +// data: { name: string; age: number; active: boolean; scores: number[] }[] + +// 如果启用了 emitTypes,也可以显式导入类型: +// import type { RowType } from './data.schema.csv'; ## Options @@ -70,6 +91,8 @@ import data from './data.schema.csv'; | `bom` | boolean | `true` | Handle byte order mark | | `comment` | string \| `false` | `#` | Comment character (set `false` to disable) | | `trim` | boolean | `true` | Trim headers and values | +| `emitTypes` | boolean | `true` | Generate TypeScript declaration file (.d.ts) | +| `typesOutputDir` | string | `''` | Output directory for generated type files (relative to output path) | ## Schema Syntax diff --git a/src/csv-loader/loader.ts b/src/csv-loader/loader.ts index 82b912b..1f871c6 100644 --- a/src/csv-loader/loader.ts +++ b/src/csv-loader/loader.ts @@ -1,6 +1,8 @@ import type { LoaderContext } from '@rspack/core'; import { parse } from 'csv-parse/sync'; import { parseSchema, createValidator, parseValue } from '../index.js'; +import type { Schema } from '../types.js'; +import * as path from 'path'; export interface CsvLoaderOptions { delimiter?: string; @@ -9,6 +11,10 @@ export interface CsvLoaderOptions { bom?: boolean; comment?: string | false; trim?: boolean; + /** Generate TypeScript declaration file (.d.ts) */ + emitTypes?: boolean; + /** Output directory for generated type files (relative to output path) */ + typesOutputDir?: string; } interface PropertyConfig { @@ -18,6 +24,57 @@ interface PropertyConfig { parser: (valueString: string) => unknown; } +/** + * Convert a schema to TypeScript type string + */ +function schemaToTypeString(schema: Schema): string { + switch (schema.type) { + case 'string': + return 'string'; + case 'number': + return 'number'; + case 'boolean': + return 'boolean'; + case 'array': + if (schema.element.type === 'tuple') { + const tupleElements = schema.element.elements.map(schemaToTypeString); + return `[${tupleElements.join(', ')}]`; + } + return `${schemaToTypeString(schema.element)}[]`; + case 'tuple': + const tupleElements = schema.elements.map(schemaToTypeString); + return `[${tupleElements.join(', ')}]`; + default: + return 'unknown'; + } +} + +/** + * Generate TypeScript interface for the CSV data + */ +function generateTypeDefinition( + resourceName: string, + propertyConfigs: PropertyConfig[] +): string { + const interfaceName = path.basename(resourceName, path.extname(resourceName)) + .replace(/[^a-zA-Z0-9_$]/g, '_') + .replace(/^(\d)/, '_$1'); + + const properties = propertyConfigs + .map((config) => ` ${config.name}: ${schemaToTypeString(config.schema)};`) + .join('\n'); + + return `export interface ${interfaceName} { +${properties} +} + +export type RowType = ${interfaceName}; + +declare const data: ${interfaceName}[]; +export default data; +`; +} + export default function csvLoader( this: LoaderContext, content: string @@ -29,6 +86,8 @@ export default function csvLoader( const bom = options?.bom ?? true; const comment = options?.comment === false ? undefined : (options?.comment ?? '#'); const trim = options?.trim ?? true; + const emitTypes = options?.emitTypes ?? true; + const typesOutputDir = options?.typesOutputDir ?? ''; const records = parse(content, { delimiter, @@ -90,5 +149,16 @@ export default function csvLoader( }); const json = JSON.stringify(objects, null, 2); + + // Emit type definition file if enabled + if (emitTypes) { + const dtsContent = generateTypeDefinition(this.resourcePath, propertyConfigs); + const relativePath = this.resourcePath.replace(this.context, ''); + const dtsFileName = relativePath.replace(/\.csv$/, '.d.ts'); + const outputPath = path.join(typesOutputDir, dtsFileName); + + this.emitFile?.(outputPath, dtsContent); + } + return `export default ${json};`; }