Compare commits

...

2 Commits

Author SHA1 Message Date
hypercross ad38976e86 refactor: reorg/remove webpack dependency 2026-04-07 11:25:02 +08:00
hypercross 77ee96d70c feat: esbuild csv-loader support 2026-04-07 11:20:12 +08:00
5 changed files with 206 additions and 54 deletions

View File

@ -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
}
}
}

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

@ -0,0 +1,108 @@
import type { CsvLoaderOptions } from './loader.js';
import { csvToModule } from './loader.js';
import * as path from 'path';
import * as fs from 'fs';
import type { Plugin, 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 createFilter(
pattern: RegExp | string | Array<RegExp | string>
): 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<RegExp | string> | undefined
): boolean {
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);
});
}
/**
* 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;
const includeFilter = createFilter(include);
return {
name: 'inline-schema-csv',
setup(build) {
build.onLoad({ filter: includeFilter }, async (args) => {
// Check exclude pattern
if (exclude && !matchesPattern(args.path, exclude)) {
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}` }],
} as OnLoadResult;
}
// 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) {
const absolutePath = path.isAbsolute(dtsPath)
? dtsPath
: path.join(process.cwd(), dtsPath);
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
fs.writeFileSync(absolutePath, result.dts);
}
}
return {
contents: result.js,
loader: 'js' as const,
};
});
},
};
}
export default csvLoader;

View File

@ -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;
@ -203,50 +200,3 @@ export function csvToModule(
dts: result.typeDefinition,
};
}
export default function csvLoader(
this: LoaderContext<CsvLoaderOptions>,
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)};`;
}

59
src/csv-loader/webpack.ts Normal file
View File

@ -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<CsvWebpackLoaderOptions>,
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)};`;
}

View File

@ -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,
},
]);