UNPKG

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
"use strict"; 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