inline-schema/src/csv-loader/loader.ts

168 lines
4.7 KiB
TypeScript
Raw Normal View History

2026-03-31 13:02:29 +08:00
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';
2026-03-31 13:02:29 +08:00
export interface CsvLoaderOptions {
delimiter?: string;
quote?: string;
escape?: string;
2026-03-31 14:45:02 +08:00
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;
2026-03-31 13:02:29 +08:00
}
interface PropertyConfig {
name: string;
schema: any;
validator: (value: unknown) => boolean;
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[],
relativePath: string
): 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 `declare module "${relativePath}" {
export interface ${interfaceName} {
${properties}
}
export type RowType = ${interfaceName};
const data: ${interfaceName}[];
export default data;
}
`;
}
2026-03-31 13:02:29 +08:00
export default function csvLoader(
this: LoaderContext<CsvLoaderOptions>,
content: string
2026-03-31 14:25:38 +08:00
): string | Buffer {
2026-03-31 14:45:02 +08:00
const options = this.getOptions() as CsvLoaderOptions | undefined;
const delimiter = options?.delimiter ?? ',';
const quote = options?.quote ?? '"';
const escape = options?.escape ?? '\\';
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 ?? '';
2026-03-31 13:02:29 +08:00
const records = parse(content, {
delimiter,
quote,
escape,
2026-03-31 14:45:02 +08:00
bom,
comment,
trim,
2026-03-31 13:02:29 +08:00
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<string, unknown> = {};
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);
// Emit type definition file if enabled
if (emitTypes) {
const relativePath = this.resourcePath.replace(this.context, '').replace(/\\/g, '/');
const dtsFileName = relativePath.replace(/\.csv$/, '.d.ts');
const outputPath = path.join(typesOutputDir, dtsFileName);
const dtsContent = generateTypeDefinition(this.resourcePath, propertyConfigs, relativePath);
this.emitFile?.(outputPath, dtsContent);
}
2026-03-31 13:02:29 +08:00
return `export default ${json};`;
}