Compare commits
No commits in common. "d9a91ae8beb2f53c46ab4040bc58f419c992d38a" and "16d88d610827b5df7f77d7bc22f66b0a8a802dd7" have entirely different histories.
d9a91ae8be
...
16d88d6108
|
|
@ -4,8 +4,8 @@ package-lock.json
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
|
||||||
# Build output
|
# Build output (kept in git for npm distribution)
|
||||||
dist/
|
# dist/
|
||||||
|
|
||||||
# TypeScript Cache
|
# TypeScript Cache
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { LoaderContext } from '@rspack/core';
|
||||||
|
|
||||||
|
interface CsvLoaderOptions {
|
||||||
|
delimiter?: string;
|
||||||
|
quote?: string;
|
||||||
|
escape?: string;
|
||||||
|
bom?: boolean;
|
||||||
|
comment?: string | false;
|
||||||
|
trim?: boolean;
|
||||||
|
/** Generate TypeScript declaration file (.d.ts) */
|
||||||
|
emitTypes?: boolean;
|
||||||
|
/** Output directory for generated type files (relative to output path) */
|
||||||
|
typesOutputDir?: string;
|
||||||
|
/** Write .d.ts files to disk (useful for dev server) */
|
||||||
|
writeToDisk?: boolean;
|
||||||
|
}
|
||||||
|
interface CsvParseResult {
|
||||||
|
/** Parsed CSV data as array of objects */
|
||||||
|
data: Record<string, unknown>[];
|
||||||
|
/** Generated TypeScript type definition string (if emitTypes is true) */
|
||||||
|
typeDefinition?: string;
|
||||||
|
/** Property configurations for the CSV columns */
|
||||||
|
propertyConfigs: PropertyConfig[];
|
||||||
|
}
|
||||||
|
interface PropertyConfig {
|
||||||
|
name: string;
|
||||||
|
schema: any;
|
||||||
|
validator: (value: unknown) => boolean;
|
||||||
|
parser: (valueString: string) => unknown;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Parse CSV content string into structured data with schema validation.
|
||||||
|
* This is a standalone function that doesn't depend on webpack/rspack LoaderContext.
|
||||||
|
*
|
||||||
|
* @param content - CSV content string (must have at least headers + schema row + 1 data row)
|
||||||
|
* @param options - Parsing options
|
||||||
|
* @returns CsvParseResult containing parsed data and optional type definitions
|
||||||
|
*/
|
||||||
|
declare function parseCsv(content: string, options?: CsvLoaderOptions): CsvParseResult;
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
declare function csvToModule(content: string, options?: CsvLoaderOptions): {
|
||||||
|
js: string;
|
||||||
|
dts?: string;
|
||||||
|
};
|
||||||
|
declare function csvLoader(this: LoaderContext<CsvLoaderOptions>, content: string): string | Buffer;
|
||||||
|
|
||||||
|
export { type CsvLoaderOptions, type CsvParseResult, csvToModule, csvLoader as default, parseCsv };
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { LoaderContext } from '@rspack/core';
|
||||||
|
|
||||||
|
interface CsvLoaderOptions {
|
||||||
|
delimiter?: string;
|
||||||
|
quote?: string;
|
||||||
|
escape?: string;
|
||||||
|
bom?: boolean;
|
||||||
|
comment?: string | false;
|
||||||
|
trim?: boolean;
|
||||||
|
/** Generate TypeScript declaration file (.d.ts) */
|
||||||
|
emitTypes?: boolean;
|
||||||
|
/** Output directory for generated type files (relative to output path) */
|
||||||
|
typesOutputDir?: string;
|
||||||
|
/** Write .d.ts files to disk (useful for dev server) */
|
||||||
|
writeToDisk?: boolean;
|
||||||
|
}
|
||||||
|
interface CsvParseResult {
|
||||||
|
/** Parsed CSV data as array of objects */
|
||||||
|
data: Record<string, unknown>[];
|
||||||
|
/** Generated TypeScript type definition string (if emitTypes is true) */
|
||||||
|
typeDefinition?: string;
|
||||||
|
/** Property configurations for the CSV columns */
|
||||||
|
propertyConfigs: PropertyConfig[];
|
||||||
|
}
|
||||||
|
interface PropertyConfig {
|
||||||
|
name: string;
|
||||||
|
schema: any;
|
||||||
|
validator: (value: unknown) => boolean;
|
||||||
|
parser: (valueString: string) => unknown;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Parse CSV content string into structured data with schema validation.
|
||||||
|
* This is a standalone function that doesn't depend on webpack/rspack LoaderContext.
|
||||||
|
*
|
||||||
|
* @param content - CSV content string (must have at least headers + schema row + 1 data row)
|
||||||
|
* @param options - Parsing options
|
||||||
|
* @returns CsvParseResult containing parsed data and optional type definitions
|
||||||
|
*/
|
||||||
|
declare function parseCsv(content: string, options?: CsvLoaderOptions): CsvParseResult;
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
declare function csvToModule(content: string, options?: CsvLoaderOptions): {
|
||||||
|
js: string;
|
||||||
|
dts?: string;
|
||||||
|
};
|
||||||
|
declare function csvLoader(this: LoaderContext<CsvLoaderOptions>, content: string): string | Buffer;
|
||||||
|
|
||||||
|
export { type CsvLoaderOptions, type CsvParseResult, csvToModule, csvLoader as default, parseCsv };
|
||||||
|
|
@ -0,0 +1,548 @@
|
||||||
|
"use strict";
|
||||||
|
var __create = Object.create;
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __getProtoOf = Object.getPrototypeOf;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||||
|
// If the importer is in node compatibility mode or this is not an ESM
|
||||||
|
// file that has been converted to a CommonJS file using a Babel-
|
||||||
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||||
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||||
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||||
|
mod
|
||||||
|
));
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
|
||||||
|
// src/csv-loader/loader.ts
|
||||||
|
var loader_exports = {};
|
||||||
|
__export(loader_exports, {
|
||||||
|
csvToModule: () => csvToModule,
|
||||||
|
default: () => csvLoader,
|
||||||
|
parseCsv: () => parseCsv
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(loader_exports);
|
||||||
|
var import_sync = require("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
|
||||||
|
var path = __toESM(require("path"));
|
||||||
|
var fs = __toESM(require("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 parseCsv(content, options = {}) {
|
||||||
|
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 records = (0, import_sync.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 result = {
|
||||||
|
data: objects,
|
||||||
|
propertyConfigs
|
||||||
|
};
|
||||||
|
if (emitTypes) {
|
||||||
|
result.typeDefinition = generateTypeDefinition("", propertyConfigs);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
function csvToModule(content, options = {}) {
|
||||||
|
const result = parseCsv(content, options);
|
||||||
|
const json = JSON.stringify(result.data, null, 2);
|
||||||
|
const js = `export default ${json};`;
|
||||||
|
return {
|
||||||
|
js,
|
||||||
|
dts: result.typeDefinition
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function csvLoader(content) {
|
||||||
|
const options = this.getOptions();
|
||||||
|
const emitTypes = options?.emitTypes ?? true;
|
||||||
|
const typesOutputDir = options?.typesOutputDir ?? "";
|
||||||
|
const writeToDisk = options?.writeToDisk ?? false;
|
||||||
|
const result = parseCsv(content, options);
|
||||||
|
if (emitTypes && result.typeDefinition) {
|
||||||
|
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;
|
||||||
|
if (writeToDisk) {
|
||||||
|
const absolutePath = path.join(this.context || process.cwd(), typesOutputDir || "", dtsFileName);
|
||||||
|
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
||||||
|
fs.writeFileSync(absolutePath, result.typeDefinition);
|
||||||
|
} else {
|
||||||
|
this.emitFile?.(outputPath, result.typeDefinition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `export default ${JSON.stringify(result.data, null, 2)};`;
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
csvToModule,
|
||||||
|
parseCsv
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,512 @@
|
||||||
|
// 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 parseCsv(content, options = {}) {
|
||||||
|
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 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 result = {
|
||||||
|
data: objects,
|
||||||
|
propertyConfigs
|
||||||
|
};
|
||||||
|
if (emitTypes) {
|
||||||
|
result.typeDefinition = generateTypeDefinition("", propertyConfigs);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
function csvToModule(content, options = {}) {
|
||||||
|
const result = parseCsv(content, options);
|
||||||
|
const json = JSON.stringify(result.data, null, 2);
|
||||||
|
const js = `export default ${json};`;
|
||||||
|
return {
|
||||||
|
js,
|
||||||
|
dts: result.typeDefinition
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function csvLoader(content) {
|
||||||
|
const options = this.getOptions();
|
||||||
|
const emitTypes = options?.emitTypes ?? true;
|
||||||
|
const typesOutputDir = options?.typesOutputDir ?? "";
|
||||||
|
const writeToDisk = options?.writeToDisk ?? false;
|
||||||
|
const result = parseCsv(content, options);
|
||||||
|
if (emitTypes && result.typeDefinition) {
|
||||||
|
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;
|
||||||
|
if (writeToDisk) {
|
||||||
|
const absolutePath = path.join(this.context || process.cwd(), typesOutputDir || "", dtsFileName);
|
||||||
|
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
||||||
|
fs.writeFileSync(absolutePath, result.typeDefinition);
|
||||||
|
} else {
|
||||||
|
this.emitFile?.(outputPath, result.typeDefinition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `export default ${JSON.stringify(result.data, null, 2)};`;
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
csvToModule,
|
||||||
|
csvLoader as default,
|
||||||
|
parseCsv
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
type SchemaType = 'string' | 'number' | 'boolean';
|
||||||
|
interface PrimitiveSchema {
|
||||||
|
type: SchemaType;
|
||||||
|
}
|
||||||
|
interface NamedSchema {
|
||||||
|
name?: string;
|
||||||
|
schema: Schema;
|
||||||
|
}
|
||||||
|
interface TupleSchema {
|
||||||
|
type: 'tuple';
|
||||||
|
elements: NamedSchema[];
|
||||||
|
}
|
||||||
|
interface ArraySchema {
|
||||||
|
type: 'array';
|
||||||
|
element: Schema;
|
||||||
|
}
|
||||||
|
type Schema = PrimitiveSchema | TupleSchema | ArraySchema;
|
||||||
|
interface ParsedSchema {
|
||||||
|
schema: Schema;
|
||||||
|
validator: (value: unknown) => boolean;
|
||||||
|
parse: (valueString: string) => unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class ParseError extends Error {
|
||||||
|
position?: number | undefined;
|
||||||
|
constructor(message: string, position?: number | undefined);
|
||||||
|
}
|
||||||
|
declare function parseSchema(schemaString: string): Schema;
|
||||||
|
|
||||||
|
declare function parseValue(schema: Schema, valueString: string): unknown;
|
||||||
|
declare function createValidator(schema: Schema): (value: unknown) => boolean;
|
||||||
|
|
||||||
|
declare function defineSchema(schemaString: string): ParsedSchema;
|
||||||
|
|
||||||
|
export { type ArraySchema, ParseError, type ParsedSchema, type PrimitiveSchema, type Schema, type TupleSchema, createValidator, defineSchema, parseSchema, parseValue };
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
type SchemaType = 'string' | 'number' | 'boolean';
|
||||||
|
interface PrimitiveSchema {
|
||||||
|
type: SchemaType;
|
||||||
|
}
|
||||||
|
interface NamedSchema {
|
||||||
|
name?: string;
|
||||||
|
schema: Schema;
|
||||||
|
}
|
||||||
|
interface TupleSchema {
|
||||||
|
type: 'tuple';
|
||||||
|
elements: NamedSchema[];
|
||||||
|
}
|
||||||
|
interface ArraySchema {
|
||||||
|
type: 'array';
|
||||||
|
element: Schema;
|
||||||
|
}
|
||||||
|
type Schema = PrimitiveSchema | TupleSchema | ArraySchema;
|
||||||
|
interface ParsedSchema {
|
||||||
|
schema: Schema;
|
||||||
|
validator: (value: unknown) => boolean;
|
||||||
|
parse: (valueString: string) => unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class ParseError extends Error {
|
||||||
|
position?: number | undefined;
|
||||||
|
constructor(message: string, position?: number | undefined);
|
||||||
|
}
|
||||||
|
declare function parseSchema(schemaString: string): Schema;
|
||||||
|
|
||||||
|
declare function parseValue(schema: Schema, valueString: string): unknown;
|
||||||
|
declare function createValidator(schema: Schema): (value: unknown) => boolean;
|
||||||
|
|
||||||
|
declare function defineSchema(schemaString: string): ParsedSchema;
|
||||||
|
|
||||||
|
export { type ArraySchema, ParseError, type ParsedSchema, type PrimitiveSchema, type Schema, type TupleSchema, createValidator, defineSchema, parseSchema, parseValue };
|
||||||
|
|
@ -0,0 +1,408 @@
|
||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
|
||||||
|
// src/index.ts
|
||||||
|
var index_exports = {};
|
||||||
|
__export(index_exports, {
|
||||||
|
ParseError: () => ParseError,
|
||||||
|
createValidator: () => createValidator,
|
||||||
|
defineSchema: () => defineSchema,
|
||||||
|
parseSchema: () => parseSchema,
|
||||||
|
parseValue: () => parseValue
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(index_exports);
|
||||||
|
|
||||||
|
// 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/index.ts
|
||||||
|
function defineSchema(schemaString) {
|
||||||
|
const schema = parseSchema(schemaString);
|
||||||
|
const validator = createValidator(schema);
|
||||||
|
return {
|
||||||
|
schema,
|
||||||
|
validator,
|
||||||
|
parse: (valueString) => parseValue(schema, valueString)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
ParseError,
|
||||||
|
createValidator,
|
||||||
|
defineSchema,
|
||||||
|
parseSchema,
|
||||||
|
parseValue
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,377 @@
|
||||||
|
// 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/index.ts
|
||||||
|
function defineSchema(schemaString) {
|
||||||
|
const schema = parseSchema(schemaString);
|
||||||
|
const validator = createValidator(schema);
|
||||||
|
return {
|
||||||
|
schema,
|
||||||
|
validator,
|
||||||
|
parse: (valueString) => parseValue(schema, valueString)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
ParseError,
|
||||||
|
createValidator,
|
||||||
|
defineSchema,
|
||||||
|
parseSchema,
|
||||||
|
parseValue
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue