diff --git a/src/csv-loader/esbuild.ts b/src/csv-loader/esbuild.ts new file mode 100644 index 0000000..4080793 --- /dev/null +++ b/src/csv-loader/esbuild.ts @@ -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; + /** Exclude pattern for CSV files */ + exclude?: RegExp | string | Array; +} + +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 { + 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;