yini-parser
Version:
Readable configuration without YAML foot-guns or JSON noise. The official Node.js parser for YINI config format — An INI-inspired configuration format with clear nesting, explicit types, and predictable parsing.
216 lines • 12 kB
JavaScript
;
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var _YiniRuntime_runtime;
Object.defineProperty(exports, "__esModule", { value: true });
exports.YiniRuntime = void 0;
const fs_1 = __importDefault(require("fs"));
const env_1 = require("../config/env");
const pathAndFileName_1 = require("../utils/pathAndFileName");
const print_1 = require("../utils/print");
const string_1 = require("../utils/string");
const defaultParserOptions_1 = require("./options/defaultParserOptions");
const failLevel_1 = require("./options/failLevel");
const optionsFunctions_1 = require("./options/optionsFunctions");
const pipeline_1 = require("./pipeline/pipeline");
/**
* Private class representing a runtime context for a single parse call.
*
* @note This design prevents race conditions: each call gets its own
* runtimeInfo and related state. Without this, multiple calls
* (especially in parallel) could overwrite each other's data.
*
* @note The following options MUST be respected!
* quiet?: boolean // Reduce output (show only errors, does not effect warnings and etc. in meta data).
* silent?: boolean // Suppress all output (even errors, exit code only).
*/
class YiniRuntime {
constructor(sourceType) {
/**
* @note Leading # makes the property "truly private" at runtime.
*/
_YiniRuntime_runtime.set(this, void 0);
__classPrivateFieldSet(this, _YiniRuntime_runtime, this.makeRuntimeInfo(), "f");
__classPrivateFieldGet(this, _YiniRuntime_runtime, "f").sourceType = sourceType;
}
makeRuntimeInfo() {
return {
sourceType: 'Inline',
fileName: undefined,
fileByteSize: null,
lineCount: null,
timeIoMs: null,
preferredBailSensitivity: null,
sha256: null,
};
}
// --- Single implementation --------------------------------------------
// Implementation method (not declared with arrow function) for both method overload signatures.
// NOTE: Must be method declaration with NO =, arrow functions not (currently) supported for this type of method overloading.
runParse(yiniContent, arg2, // strictMode | options
failLevel = 'auto', includeMetadata = false) {
(0, print_1.debugPrint)('-> Entered runParse(..) in YiniRuntime class\n');
// Handle optional UTF-8 BOM content of file.
if (yiniContent.startsWith('\uFEFF')) {
// (!) NOTE: slice(1) only because UTF-8 BOM appears as one single Unicode code characte, even though it is 3 bytes (EF BB BF) on disk.
yiniContent = yiniContent.slice(1);
(0, print_1.devPrint)('runParse(..): BOM was detected and stripped BOM in UTF-8 content');
}
// Handle optional shebang line (if line starts with "#!").
if (yiniContent.startsWith('#!')) {
const newlineIndex = yiniContent.indexOf('\n');
(0, print_1.devPrint)('runParse(..): Shebang detected at first line, stripped line 1.');
if (newlineIndex < 2) {
throw new Error('Syntax-Error: Unexpected YINI input');
}
yiniContent = yiniContent.slice(newlineIndex + 1);
}
// Runtime guard to catch illegal/ambiguous calls coming from JS or any-cast code
if ((0, optionsFunctions_1.isOptionsObjectForm)(arg2) &&
(failLevel !== 'auto' || includeMetadata !== false)) {
throw new TypeError('Invalid call: when providing an options object, do not also pass positional parameters.');
}
const mode = (0, optionsFunctions_1.inferModeFromArgs)(arg2);
const defaultOptions = (0, defaultParserOptions_1.getDefaultUserOptions)(mode);
// Normalize to a fully-required options object.
let userOpts;
// Required, makes all properties in T required, no undefined.
if ((0, optionsFunctions_1.isOptionsObjectForm)(arg2)) {
userOpts = {
...defaultOptions, // Sets the default options.
...arg2,
};
}
else {
// Positional form.
userOpts = {
...defaultOptions, // Sets the default options.
strictMode: arg2 ?? defaultOptions.strictMode,
failLevel,
includeMetadata,
};
}
if (userOpts.includeMetadata && __classPrivateFieldGet(this, _YiniRuntime_runtime, "f").sourceType === 'Inline') {
const lineCount = yiniContent.split(/\r?\n/).length; // Counts the lines.
const sha256 = (0, string_1.computeSha256)(yiniContent); // NOTE: Compute BEFORE any possible tampering of content.
__classPrivateFieldGet(this, _YiniRuntime_runtime, "f").lineCount = lineCount;
__classPrivateFieldGet(this, _YiniRuntime_runtime, "f").preferredBailSensitivity = userOpts.failLevel;
__classPrivateFieldGet(this, _YiniRuntime_runtime, "f").sha256 = sha256;
}
// NOTE: Important: Do not trim or mutate the yiniContent here, due
// to it will mess up the line numbers in error reporting.
if (!yiniContent) {
throw new Error('Syntax-Error: Unexpected blank YINI input');
}
if (!yiniContent.endsWith('\n')) {
yiniContent += '\n';
}
let level = (0, failLevel_1.mapFailLevelToBail)(userOpts.strictMode, userOpts.failLevel);
const coreOpts = (0, optionsFunctions_1.toCoreOptions)(userOpts, level);
(0, print_1.debugPrint)();
(0, print_1.debugPrint)('==== Call runPipeline(..) ==========================');
const result = (0, pipeline_1.runPipeline)(yiniContent, coreOpts, __classPrivateFieldGet(this, _YiniRuntime_runtime, "f"), userOpts);
(0, print_1.debugPrint)('==== End call runPipeline ==========================\n');
if ((0, env_1.isDev)()) {
console.log();
(0, print_1.devPrint)('runParse(..): result:');
console.log(result);
(0, print_1.devPrint)('Complete result:');
(0, print_1.printObject)(result);
}
return result;
}
// --- Single implementation --------------------------------------------
// Implementation method (not declared with arrow function) for both method overload signatures.
// NOTE: Must be method declaration with NO =, arrow functions not (currently) supported for this type of method overloading.
doParseFile(filePath, arg2, // strictMode | options
failLevel = 'auto', includeMetadata = false) {
(0, print_1.debugPrint)('-> Entered doParseFile(..) in YiniRuntime class\n');
(0, print_1.debugPrint)('Current directory = ' + process.cwd());
// Runtime guard to catch illegal/ambiguous calls coming from JS or any-cast code
if ((0, optionsFunctions_1.isOptionsObjectForm)(arg2) &&
(failLevel !== 'auto' || includeMetadata !== false)) {
throw new TypeError('Invalid call: when providing an options object, do not also pass positional parameters.');
}
const mode = (0, optionsFunctions_1.inferModeFromArgs)(arg2);
const defaultOptions = (0, defaultParserOptions_1.getDefaultUserOptions)(mode);
// Normalize to a fully-required options object.
let userOpts;
// Required, makes all properties in T required, no undefined.
if ((0, optionsFunctions_1.isOptionsObjectForm)(arg2)) {
// Options-object Form.
userOpts = {
...defaultOptions, // Sets the default options.
...arg2,
};
}
else {
// Positional form.
userOpts = {
...defaultOptions, // Sets the default options.
strictMode: arg2 ?? defaultOptions.strictMode,
failLevel,
includeMetadata,
};
}
if ((0, pathAndFileName_1.getFileNameExtension)(filePath).toLowerCase() !== '.yini') {
// IMPORTANT: If "silent" option is set, do not log anything to console!
if (!userOpts.silent) {
// In quiet-mode we still show errors (these are fine).
console.error('Invalid file extension for YINI file:');
console.error(`"${filePath}"`);
console.error('File does not have a valid ".yini" extension (case-insensitive).');
}
throw new Error('Error: Unexpected file extension for YINI file');
}
// ---- Phase 0: I/O ----
const timeStartMs = performance.now();
// let content = fs.readFileSync(filePath, 'utf8')
const rawBuffer = fs_1.default.readFileSync(filePath); // Raw buffer for size.
const fileByteSize = rawBuffer.byteLength; // Byte size in UTF-8.
let content = rawBuffer.toString('utf8');
const timeEndMs = performance.now();
// this.#runtime.sourceType = 'File'
__classPrivateFieldGet(this, _YiniRuntime_runtime, "f").fileName = filePath;
__classPrivateFieldGet(this, _YiniRuntime_runtime, "f").timeIoMs = +(timeEndMs - timeStartMs).toFixed(3); // NOTE: (!) Dependent of isWithTiming.
if (userOpts.includeMetadata) {
__classPrivateFieldGet(this, _YiniRuntime_runtime, "f").lineCount = content.split(/\r?\n/).length; // Counts the lines.
__classPrivateFieldGet(this, _YiniRuntime_runtime, "f").fileByteSize = fileByteSize;
// this.#runtime.timeIoMs = +(timeEndMs - timeStartMs).toFixed(3) // NOTE: (!) Dependent of isWithTiming.
__classPrivateFieldGet(this, _YiniRuntime_runtime, "f").preferredBailSensitivity = userOpts.failLevel;
__classPrivateFieldGet(this, _YiniRuntime_runtime, "f").sha256 = (0, string_1.computeSha256)(content); // NOTE: Compute BEFORE any possible tampering of content.
}
let hasNoNewlineAtEOF = false;
if (!content.endsWith('\n')) {
content += '\n';
hasNoNewlineAtEOF = true;
}
const result = this.runParse(content, {
...userOpts,
});
// if (hasNoNewlineAtEOF && !userOpts.quiet && !userOpts.silent) {
if (hasNoNewlineAtEOF && !userOpts.quiet) {
// IMPORTANT: If "silent" option is set, do not log anything to console!
if (!userOpts.silent) {
//@todo: (or maybe not, 20250917) Maybe let errorHandler emit message
console.warn(`No newline at end of file, it's recommended to end a file with a newline. File:\n"${filePath}"`);
}
}
return result;
}
}
exports.YiniRuntime = YiniRuntime;
_YiniRuntime_runtime = new WeakMap();
//# sourceMappingURL=runtime.js.map