fix(csv-loader): escape quotes in schema rows

Implement loader-side escaping for the schema row to prevent
`csv-parse` from misinterpreting double-quoted string literals (e.g.,
`"active" | "inactive"`) as field delimiters.
This commit is contained in:
2026-08-06 10:25:24 +08:00
parent cdff31c126
commit 37e3514c0c
4 changed files with 82 additions and 8 deletions
+56
View File
@@ -33,6 +33,53 @@ import {
parseReferenceValue,
} from "./reference-resolver.js";
import { generateTypeDefinition } from "./type-gen.js";
/**
* Escape double-quote characters in the schema row so csv-parse doesn't
* misinterpret them as CSV field delimiters.
*
* The schema row is the 2nd non-empty line. Cells that contain the quote
* character (e.g. `"active" | "inactive"`) have their inner quotes escaped
* and are wrapped in quotes, so csv-parse treats each as a single field.
*/
function escapeSchemaRowQuotes(
content: string,
delimiter: string,
quote: string,
escape: string,
): string {
if (!quote || !content.includes(quote)) {
return content;
}
const lines = content.split(/\r?\n/);
let schemaRowIndex = -1;
let nonEmptyCount = 0;
for (let i = 0; i < lines.length; i++) {
if (lines[i].trim() === "") continue;
nonEmptyCount++;
if (nonEmptyCount === 2) {
schemaRowIndex = i;
break;
}
}
if (schemaRowIndex === -1) {
return content;
}
const cells = lines[schemaRowIndex].split(delimiter);
const transformed = cells
.map((cell) => {
if (!cell.includes(quote)) {
return cell;
}
const escaped = cell.split(quote).join(escape + quote);
return quote + escaped + quote;
})
.join(delimiter);
lines[schemaRowIndex] = transformed;
return lines.join("\n");
}
import { csvToModule } from "./module-gen.js";
import {
parseTypeDeclaration,
@@ -96,6 +143,15 @@ export function parseCsv(
filteredContent = nonCommentLines.join("\n");
}
// Escape double-quotes in the schema row so csv-parse doesn't treat them
// as CSV field delimiters (e.g. `"active" | "inactive"` in a schema cell).
filteredContent = escapeSchemaRowQuotes(
filteredContent,
delimiter,
quote,
escape,
);
const records = parse(filteredContent, {
delimiter,
quote,
@@ -65,6 +65,21 @@ describe("parseCsv - basic parsing", () => {
expect(result.data[1]).toEqual({ name: "Bob", status: "off" });
});
it("should parse CSV with double-quoted string literal columns", () => {
const csv = [
"name,status",
'string,"active" | "inactive"',
"Alice,active",
"Bob,inactive",
].join("\n");
const result = parseCsv(csv, { emitTypes: false });
expect(result.data).toHaveLength(2);
expect(result.data[0]).toEqual({ name: "Alice", status: "active" });
expect(result.data[1]).toEqual({ name: "Bob", status: "inactive" });
});
it("should parse CSV with array columns", () => {
const csv = [
"name,tags",