feat: esbuild csv-loader support

This commit is contained in:
hypercross 2026-04-07 11:20:12 +08:00
parent 4eb6a17e9f
commit 77ee96d70c
1 changed files with 118 additions and 0 deletions

118
src/csv-loader/esbuild.ts Normal file
View File

@ -0,0 +1,118 @@
import type { CsvLoaderOptions } from './loader.js';
import { csvToModule } from './loader.js';
import * as path from 'path';
import * as fs from 'fs';
import type { Plugin, OnLoadArgs, OnLoadResult } from 'esbuild';
export interface CsvEsbuildOptions extends CsvLoaderOptions {
/** Include pattern for CSV files (default: /\.csv$/) */
include?: RegExp | string | Array<RegExp | string>;
/** Exclude pattern for CSV files */
exclude?: RegExp | string | Array<RegExp | string>;
}
function matchesFilter(
path: string,
filter: RegExp | undefined
): boolean {
if (!filter) return true;
return filter.test(path);
}
/**
* Esbuild plugin for loading CSV files with inline-schema validation.
*/
export function csvLoader(options: CsvEsbuildOptions = {}): Plugin {
const {
include = /\.csv$/,
exclude,
emitTypes = true,
typesOutputDir = '',
writeToDisk = false,
...parseOptions
} = options;
// Convert include/exclude to RegExp for esbuild filter
const includeFilter = includeToRegExp(include);
const excludeFilter = exclude ? includeToRegExp(exclude) : undefined;
return {
name: 'inline-schema-csv',
setup(build) {
build.onLoad({ filter: includeFilter }, async (args: OnLoadArgs) => {
// Check exclude pattern
if (excludeFilter && !matchesFilter(args.path, excludeFilter)) {
return null;
}
// Only process .csv files
if (!args.path.endsWith('.csv')) {
return null;
}
// Read the file content
let content: string;
try {
content = await fs.promises.readFile(args.path, 'utf-8');
} catch (error) {
return {
errors: [{ text: `Failed to read file: ${args.path}` }],
};
}
// Infer resource name from filename
const fileName = path.basename(args.path, '.csv').split('.')[0];
const resourceName = fileName
.replace(/[-_\s]+(.)?/g, (_, char) => char ? char.toUpperCase() : '')
.replace(/^(.)/, (_, char) => char.toUpperCase());
const result = csvToModule(content, {
...parseOptions,
emitTypes,
resourceName,
});
// Emit type definition file if enabled
if (emitTypes && result.dts) {
const dtsPath = typesOutputDir
? path.join(typesOutputDir, path.basename(args.path) + '.d.ts')
: args.path + '.d.ts';
if (writeToDisk) {
// Write directly to disk
const absolutePath = path.isAbsolute(dtsPath)
? dtsPath
: path.join(process.cwd(), dtsPath);
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
fs.writeFileSync(absolutePath, result.dts);
}
// Note: esbuild doesn't have a direct emitFile API like Rollup
// For type generation with writeToDisk: false, you'd need to
// use a separate type generation step or plugin
}
const onLoadResult: OnLoadResult = {
contents: result.js,
loader: 'js',
};
return onLoadResult;
});
},
};
}
function includeToRegExp(include: RegExp | string | Array<RegExp | string>): RegExp {
if (include instanceof RegExp) {
return include;
}
if (Array.isArray(include)) {
// Use the first pattern or create a combined pattern
const first = include[0];
return first instanceof RegExp ? first : new RegExp(first);
}
return new RegExp(include);
}
export default csvLoader;