// src/csv-loader/loader.ts import { parse } from "csv-parse/sync"; // src/parser.ts var ParseError = class extends Error { constructor(message, position) { super(position !== void 0 ? `${message} at position ${position}` : message); this.position = position; this.name = "ParseError"; } }; var Parser = class { constructor(input) { this.pos = 0; this.input = input; } peek() { return this.input[this.pos] || ""; } consume() { return this.input[this.pos++] || ""; } skipWhitespace() { while (this.pos < this.input.length && /\s/.test(this.input[this.pos])) { this.pos++; } } match(str) { return this.input.slice(this.pos, this.pos + str.length) === str; } consumeStr(str) { if (this.match(str)) { this.pos += str.length; return true; } return false; } getPosition() { return this.pos; } getInputLength() { return this.input.length; } parseSchema() { this.skipWhitespace(); if (this.consumeStr("string")) { if (this.consumeStr("[")) { this.skipWhitespace(); if (!this.consumeStr("]")) { throw new ParseError("Expected ]", this.pos); } return { type: "array", element: { type: "string" } }; } return { type: "string" }; } if (this.consumeStr("number")) { if (this.consumeStr("[")) { this.skipWhitespace(); if (!this.consumeStr("]")) { throw new ParseError("Expected ]", this.pos); } return { type: "array", element: { type: "number" } }; } return { type: "number" }; } if (this.consumeStr("boolean")) { if (this.consumeStr("[")) { this.skipWhitespace(); if (!this.consumeStr("]")) { throw new ParseError("Expected ]", this.pos); } return { type: "array", element: { type: "boolean" } }; } return { type: "boolean" }; } if (this.consumeStr("[")) { const elements = []; this.skipWhitespace(); if (this.peek() === "]") { this.consume(); throw new ParseError("Empty array/tuple not allowed", this.pos); } elements.push(this.parseNamedSchema()); this.skipWhitespace(); if (this.consumeStr(";")) { const remainingElements = []; while (true) { this.skipWhitespace(); remainingElements.push(this.parseNamedSchema()); this.skipWhitespace(); if (!this.consumeStr(";")) { break; } } elements.push(...remainingElements); } this.skipWhitespace(); if (!this.consumeStr("]")) { throw new ParseError("Expected ]", this.pos); } if (this.consumeStr("[")) { this.skipWhitespace(); if (!this.consumeStr("]")) { throw new ParseError("Expected ]", this.pos); } if (elements.length === 1 && !elements[0].name) { return { type: "array", element: elements[0].schema }; } return { type: "array", element: { type: "tuple", elements } }; } if (elements.length === 1 && !elements[0].name) { return { type: "array", element: elements[0].schema }; } return { type: "tuple", elements }; } let identifier = ""; while (this.pos < this.input.length && /[a-zA-Z0-9\-_]/.test(this.peek())) { identifier += this.consume(); } if (identifier.length > 0) { if (this.consumeStr("[")) { this.skipWhitespace(); if (!this.consumeStr("]")) { throw new ParseError("Expected ]", this.pos); } return { type: "array", element: { type: "string" } }; } return { type: "string" }; } throw new ParseError(`Unexpected character: ${this.peek()}`, this.pos); } parseNamedSchema() { this.skipWhitespace(); const startpos = this.pos; let identifier = ""; while (this.pos < this.input.length && /[a-zA-Z0-9\-_]/.test(this.peek())) { identifier += this.consume(); } if (identifier.length === 0) { throw new ParseError("Expected schema or named schema", this.pos); } this.skipWhitespace(); if (this.consumeStr(":")) { this.skipWhitespace(); const name = identifier; const schema = this.parseSchema(); return { name, schema }; } else { this.pos = startpos; const schema = this.parseSchema(); return { schema }; } } }; function parseSchema(schemaString) { const parser = new Parser(schemaString.trim()); const schema = parser.parseSchema(); if (parser.getPosition() < parser.getInputLength()) { throw new ParseError("Unexpected input after schema", parser.getPosition()); } return schema; } // src/validator.ts var ValueParser = class { constructor(input) { this.pos = 0; this.input = input; } peek() { return this.input[this.pos] || ""; } consume() { return this.input[this.pos++] || ""; } skipWhitespace() { while (this.pos < this.input.length && /\s/.test(this.input[this.pos])) { this.pos++; } } consumeStr(str) { if (this.input.slice(this.pos, this.pos + str.length) === str) { this.pos += str.length; return true; } return false; } parseValue(schema, allowOmitBrackets = false) { this.skipWhitespace(); switch (schema.type) { case "string": return this.parseStringValue(); case "number": return this.parseNumberValue(); case "boolean": return this.parseBooleanValue(); case "tuple": return this.parseTupleValue(schema, allowOmitBrackets); case "array": return this.parseArrayValue(schema, allowOmitBrackets); default: throw new ParseError(`Unknown schema type: ${schema.type}`, this.pos); } } parseStringValue() { let result = ""; while (this.pos < this.input.length) { const char = this.peek(); if (char === "\\") { this.consume(); const nextChar = this.consume(); if (nextChar === ";" || nextChar === "[" || nextChar === "]" || nextChar === "\\") { result += nextChar; } else { result += "\\" + nextChar; } } else if (char === ";" || char === "]") { break; } else { result += this.consume(); } } return result.trim(); } parseNumberValue() { let numStr = ""; while (this.pos < this.input.length && /[\d.\-+eE]/.test(this.peek())) { numStr += this.consume(); } const num = parseFloat(numStr); if (isNaN(num)) { throw new ParseError("Invalid number", this.pos - numStr.length); } return num; } parseBooleanValue() { if (this.consumeStr("true")) { return true; } if (this.consumeStr("false")) { return false; } throw new ParseError("Expected true or false", this.pos); } parseTupleValue(schema, allowOmitBrackets) { let hasOpenBracket = false; if (this.peek() === "[") { this.consume(); hasOpenBracket = true; } else if (!allowOmitBrackets) { throw new ParseError("Expected [", this.pos); } this.skipWhitespace(); if (this.peek() === "]" && hasOpenBracket) { this.consume(); return []; } const result = []; for (let i = 0; i < schema.elements.length; i++) { this.skipWhitespace(); const elementSchema = schema.elements[i]; if (elementSchema.name) { this.skipWhitespace(); const savedPos = this.pos; if (this.consumeStr(`${elementSchema.name}:`)) { this.skipWhitespace(); } else { this.pos = savedPos; } } result.push(this.parseValue(elementSchema.schema, false)); this.skipWhitespace(); if (i < schema.elements.length - 1) { if (!this.consumeStr(";")) { throw new ParseError("Expected ;", this.pos); } } } this.skipWhitespace(); if (hasOpenBracket) { if (!this.consumeStr("]")) { throw new ParseError("Expected ]", this.pos); } } return result; } parseArrayValue(schema, allowOmitBrackets) { let hasOpenBracket = false; const elementIsTupleOrArray = schema.element.type === "tuple" || schema.element.type === "array"; if (this.peek() === "[") { if (!elementIsTupleOrArray) { this.consume(); hasOpenBracket = true; } else if (this.input[this.pos + 1] === "[") { this.consume(); hasOpenBracket = true; } } if (!hasOpenBracket && !allowOmitBrackets && !elementIsTupleOrArray) { throw new ParseError("Expected [", this.pos); } this.skipWhitespace(); if (this.peek() === "]" && hasOpenBracket) { this.consume(); return []; } const result = []; while (true) { this.skipWhitespace(); result.push(this.parseValue(schema.element, false)); this.skipWhitespace(); if (!this.consumeStr(";")) { break; } } this.skipWhitespace(); if (hasOpenBracket) { if (!this.consumeStr("]")) { throw new ParseError("Expected ]", this.pos); } } return result; } getPosition() { return this.pos; } getInputLength() { return this.input.length; } }; function parseValue(schema, valueString) { const parser = new ValueParser(valueString.trim()); const allowOmitBrackets = schema.type === "tuple" || schema.type === "array"; const value = parser.parseValue(schema, allowOmitBrackets); if (parser.getPosition() < parser.getInputLength()) { throw new ParseError("Unexpected input after value", parser.getPosition()); } return value; } function createValidator(schema) { return function validate(value) { switch (schema.type) { case "string": return typeof value === "string"; case "number": return typeof value === "number" && !isNaN(value); case "boolean": return typeof value === "boolean"; case "tuple": if (!Array.isArray(value)) return false; if (value.length !== schema.elements.length) return false; return schema.elements.every( (elementSchema, index) => createValidator(elementSchema.schema)(value[index]) ); case "array": if (!Array.isArray(value)) return false; return value.every((item) => createValidator(schema.element)(item)); default: return false; } }; } // src/csv-loader/loader.ts import * as path from "path"; import * as fs from "fs"; function schemaToTypeString(schema) { switch (schema.type) { case "string": return "string"; case "number": return "number"; case "boolean": return "boolean"; case "array": if (schema.element.type === "tuple") { const tupleElements2 = schema.element.elements.map((el) => { const typeStr = schemaToTypeString(el.schema); return el.name ? `${el.name}: ${typeStr}` : typeStr; }); return `[${tupleElements2.join(", ")}]`; } return `${schemaToTypeString(schema.element)}[]`; case "tuple": const tupleElements = schema.elements.map((el) => { const typeStr = schemaToTypeString(el.schema); return el.name ? `${el.name}: ${typeStr}` : typeStr; }); return `[${tupleElements.join(", ")}]`; default: return "unknown"; } } function generateTypeDefinition(resourceName, propertyConfigs) { const properties = propertyConfigs.map((config) => ` ${config.name}: ${schemaToTypeString(config.schema)};`).join("\n"); return `type Table = { ${properties} }[]; declare const data: Table; export default data; `; } function csvLoader(content) { const options = this.getOptions(); const delimiter = options?.delimiter ?? ","; const quote = options?.quote ?? '"'; const escape = options?.escape ?? "\\"; const bom = options?.bom ?? true; const comment = options?.comment === false ? void 0 : options?.comment ?? "#"; const trim = options?.trim ?? true; const emitTypes = options?.emitTypes ?? true; const typesOutputDir = options?.typesOutputDir ?? ""; const writeToDisk = options?.writeToDisk ?? false; const records = parse(content, { delimiter, quote, escape, bom, comment, trim, relax_column_count: true }); if (records.length < 2) { throw new Error("CSV must have at least 2 rows: headers and schemas"); } const headers = records[0]; const schemas = records[1]; if (headers.length !== schemas.length) { throw new Error( `Header count (${headers.length}) does not match schema count (${schemas.length})` ); } const propertyConfigs = headers.map((header, index) => { const schemaString = schemas[index]; const schema = parseSchema(schemaString); return { name: header, schema, validator: createValidator(schema), parser: (valueString) => parseValue(schema, valueString) }; }); const dataRows = records.slice(2); const objects = dataRows.map((row, rowIndex) => { const obj = {}; propertyConfigs.forEach((config, colIndex) => { const rawValue = row[colIndex] ?? ""; try { const parsed = config.parser(rawValue); if (!config.validator(parsed)) { throw new Error( `Validation failed for property "${config.name}" at row ${rowIndex + 3}: ${rawValue}` ); } obj[config.name] = parsed; } catch (error) { if (error instanceof Error) { throw new Error( `Failed to parse property "${config.name}" at row ${rowIndex + 3}, column ${colIndex + 1}: ${error.message}` ); } throw error; } }); return obj; }); const json = JSON.stringify(objects, null, 2); if (emitTypes) { const context = this.context || ""; let relativePath = this.resourcePath.replace(context, ""); if (relativePath.startsWith("\\") || relativePath.startsWith("/")) { relativePath = relativePath.substring(1); } relativePath = relativePath.replace(/\\/g, "/"); const dtsFileName = `${relativePath}.d.ts`; const outputPath = typesOutputDir ? path.join(typesOutputDir, dtsFileName) : dtsFileName; const dtsContent = generateTypeDefinition(this.resourcePath, propertyConfigs); if (writeToDisk) { const absolutePath = path.join(this.context || process.cwd(), typesOutputDir || "", dtsFileName); fs.mkdirSync(path.dirname(absolutePath), { recursive: true }); fs.writeFileSync(absolutePath, dtsContent); } else { this.emitFile?.(outputPath, dtsContent); } } return `export default ${json};`; } export { csvLoader as default };