diff --git a/package.json b/package.json index 1be56bf..3a4e634 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,20 @@ "import": "./dist/csv-loader/loader.mjs", "require": "./dist/csv-loader/loader.js" }, + "./csv-loader/webpack": { + "types": "./dist/csv-loader/webpack.d.ts", + "import": "./dist/csv-loader/webpack.mjs", + "require": "./dist/csv-loader/webpack.js" + }, "./csv-loader/rollup": { "types": "./dist/csv-loader/rollup.d.ts", "import": "./dist/csv-loader/rollup.mjs", "require": "./dist/csv-loader/rollup.js" + }, + "./csv-loader/esbuild": { + "types": "./dist/csv-loader/esbuild.d.ts", + "import": "./dist/csv-loader/esbuild.mjs", + "require": "./dist/csv-loader/esbuild.js" } }, "scripts": { @@ -49,6 +59,15 @@ "typescript": "^5.9.3" }, "peerDependencies": { - "@rspack/core": "^1.x" + "@rspack/core": "^1.x", + "esbuild": "*" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "esbuild": { + "optional": true + } } } diff --git a/src/csv-loader/esbuild.ts b/src/csv-loader/esbuild.ts index 4080793..9c6fc88 100644 --- a/src/csv-loader/esbuild.ts +++ b/src/csv-loader/esbuild.ts @@ -2,7 +2,7 @@ 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'; +import type { Plugin, OnLoadResult } from 'esbuild'; export interface CsvEsbuildOptions extends CsvLoaderOptions { /** Include pattern for CSV files (default: /\.csv$/) */ @@ -11,12 +11,27 @@ export interface CsvEsbuildOptions extends CsvLoaderOptions { exclude?: RegExp | string | Array; } -function matchesFilter( - path: string, - filter: RegExp | undefined +function createFilter( + pattern: RegExp | string | Array +): RegExp { + if (pattern instanceof RegExp) return pattern; + if (Array.isArray(pattern)) { + const first = pattern[0]; + return first instanceof RegExp ? first : new RegExp(first); + } + return new RegExp(pattern); +} + +function matchesPattern( + id: string, + pattern: RegExp | string | Array | undefined ): boolean { - if (!filter) return true; - return filter.test(path); + if (!pattern) return true; + const patterns = Array.isArray(pattern) ? pattern : [pattern]; + return patterns.some((p) => { + if (p instanceof RegExp) return p.test(id); + return id.includes(p); + }); } /** @@ -32,22 +47,15 @@ export function csvLoader(options: CsvEsbuildOptions = {}): Plugin { ...parseOptions } = options; - // Convert include/exclude to RegExp for esbuild filter - const includeFilter = includeToRegExp(include); - const excludeFilter = exclude ? includeToRegExp(exclude) : undefined; + const includeFilter = createFilter(include); return { name: 'inline-schema-csv', setup(build) { - build.onLoad({ filter: includeFilter }, async (args: OnLoadArgs) => { + build.onLoad({ filter: includeFilter }, async (args) => { // Check exclude pattern - if (excludeFilter && !matchesFilter(args.path, excludeFilter)) { - return null; - } - - // Only process .csv files - if (!args.path.endsWith('.csv')) { + if (exclude && !matchesPattern(args.path, exclude)) { return null; } @@ -58,7 +66,7 @@ export function csvLoader(options: CsvEsbuildOptions = {}): Plugin { } catch (error) { return { errors: [{ text: `Failed to read file: ${args.path}` }], - }; + } as OnLoadResult; } // Infer resource name from filename @@ -80,39 +88,21 @@ export function csvLoader(options: CsvEsbuildOptions = {}): Plugin { : 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 = { + return { contents: result.js, - loader: 'js', + loader: 'js' as const, }; - - 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; diff --git a/src/csv-loader/loader.ts b/src/csv-loader/loader.ts index 100538e..a624556 100644 --- a/src/csv-loader/loader.ts +++ b/src/csv-loader/loader.ts @@ -1,9 +1,6 @@ -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'; -import * as fs from 'fs'; export interface CsvLoaderOptions { delimiter?: string; @@ -184,7 +181,7 @@ export function parseCsv( /** * Generate JavaScript module code from CSV content. * Returns a string that can be used as a module export. - * + * * @param content - CSV content string * @param options - Parsing options * @returns JavaScript module code string @@ -194,59 +191,12 @@ export function csvToModule( options: CsvLoaderOptions & { resourceName?: string } = {} ): { js: string; dts?: string } { const result = parseCsv(content, options); - + const json = JSON.stringify(result.data, null, 2); const js = `export default ${json};`; - + return { js, dts: result.typeDefinition, }; } - -export default function csvLoader( - this: LoaderContext, - content: string -): string | Buffer { - const options = this.getOptions() as CsvLoaderOptions | undefined; - const emitTypes = options?.emitTypes ?? true; - const typesOutputDir = options?.typesOutputDir ?? ''; - const writeToDisk = options?.writeToDisk ?? false; - - // Infer resource name from filename - const fileName = path.basename(this.resourcePath, '.csv').split('.')[0]; - const resourceName = fileName - .replace(/[-_\s]+(.)?/g, (_, char) => char ? char.toUpperCase() : '') - .replace(/^(.)/, (_, char) => char.toUpperCase()); - - const result = parseCsv(content, { ...options, resourceName }); - - // Emit type definition file if enabled - if (emitTypes && result.typeDefinition) { - const context = this.context || ''; - // Get relative path from context, normalize to forward slashes - let relativePath = this.resourcePath.replace(context, ''); - if (relativePath.startsWith('\\') || relativePath.startsWith('/')) { - relativePath = relativePath.substring(1); - } - relativePath = relativePath.replace(/\\/g, '/'); - - // Replace .csv with .csv.d.ts for the output filename - const dtsFileName = `${relativePath}.d.ts`; - const outputPath = typesOutputDir - ? path.join(typesOutputDir, dtsFileName) - : dtsFileName; - - if (writeToDisk) { - // Write directly to disk (useful for dev server) - const absolutePath = path.join(this.context || process.cwd(), typesOutputDir || '', dtsFileName); - fs.mkdirSync(path.dirname(absolutePath), { recursive: true }); - fs.writeFileSync(absolutePath, result.typeDefinition); - } else { - // Emit to in-memory filesystem (for production build) - this.emitFile?.(outputPath, result.typeDefinition); - } - } - - return `export default ${JSON.stringify(result.data, null, 2)};`; -} diff --git a/src/csv-loader/webpack.ts b/src/csv-loader/webpack.ts new file mode 100644 index 0000000..56a212a --- /dev/null +++ b/src/csv-loader/webpack.ts @@ -0,0 +1,59 @@ +import type { LoaderContext } from '@rspack/core'; +import type { CsvLoaderOptions } from './loader.js'; +import { parseCsv } from './loader.js'; +import * as path from 'path'; +import * as fs from 'fs'; + +export interface CsvWebpackLoaderOptions extends CsvLoaderOptions { + /** Output directory for generated type files (relative to output path) */ + typesOutputDir?: string; + /** Write .d.ts files to disk (useful for dev server) */ + writeToDisk?: boolean; +} + +export default function csvLoader( + this: LoaderContext, + content: string +): string | Buffer { + const options = this.getOptions() as CsvWebpackLoaderOptions | undefined; + const emitTypes = options?.emitTypes ?? true; + const typesOutputDir = options?.typesOutputDir ?? ''; + const writeToDisk = options?.writeToDisk ?? false; + + // Infer resource name from filename + const fileName = path.basename(this.resourcePath, '.csv').split('.')[0]; + const resourceName = fileName + .replace(/[-_\s]+(.)?/g, (_, char) => char ? char.toUpperCase() : '') + .replace(/^(.)/, (_, char) => char.toUpperCase()); + + const result = parseCsv(content, { ...options, resourceName }); + + // Emit type definition file if enabled + if (emitTypes && result.typeDefinition) { + const context = this.context || ''; + // Get relative path from context, normalize to forward slashes + let relativePath = this.resourcePath.replace(context, ''); + if (relativePath.startsWith('\\') || relativePath.startsWith('/')) { + relativePath = relativePath.substring(1); + } + relativePath = relativePath.replace(/\\/g, '/'); + + // Replace .csv with .csv.d.ts for the output filename + const dtsFileName = `${relativePath}.d.ts`; + const outputPath = typesOutputDir + ? path.join(typesOutputDir, dtsFileName) + : dtsFileName; + + if (writeToDisk) { + // Write directly to disk (useful for dev server) + const absolutePath = path.join(this.context || process.cwd(), typesOutputDir || '', dtsFileName); + fs.mkdirSync(path.dirname(absolutePath), { recursive: true }); + fs.writeFileSync(absolutePath, result.typeDefinition); + } else { + // Emit to in-memory filesystem (for production build) + this.emitFile?.(outputPath, result.typeDefinition); + } + } + + return `export default ${JSON.stringify(result.data, null, 2)};`; +} diff --git a/tsup.config.ts b/tsup.config.ts index 5fb6041..f47f449 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -16,6 +16,14 @@ export default defineConfig([ external: ['@rspack/core', 'csv-parse'], clean: false, }, + { + entry: ['src/csv-loader/webpack.ts'], + format: ['cjs', 'esm'], + dts: true, + outDir: 'dist/csv-loader', + external: ['@rspack/core', 'csv-parse'], + clean: false, + }, { entry: ['src/csv-loader/rollup.ts'], format: ['cjs', 'esm'], @@ -24,4 +32,12 @@ export default defineConfig([ external: ['rollup', 'csv-parse'], clean: false, }, + { + entry: ['src/csv-loader/esbuild.ts'], + format: ['cjs', 'esm'], + dts: true, + outDir: 'dist/csv-loader', + external: ['esbuild', 'csv-parse'], + clean: false, + }, ]);