NEON for JavaScript

The package @nette/neon reads and writes NEON in JavaScript and TypeScript, in Node and in the browser. With it you:

  • decode a configuration file into JavaScript values and encode values back to NEON
  • edit a file while every comment and space you did not touch stays where it was
  • check files with neon-lint in your editor, in CI or after an AI agent

Installation

npm install @nette/neon

It is an ES module with TypeScript types, for Node 22 or newer or any current browser, and it has no dependencies. Only the command neon-lint uses the API of Node.

Decoding and Encoding

The function decode() turns NEON into JavaScript values and encode() turns them back:

import { decode, encode } from '@nette/neon';

decode('roles: [guest, admin]');           // { roles: ['guest', 'admin'] }
encode({ roles: ['guest', 'admin'] });     // '{roles: [guest, admin]}'
encode({ roles: ['guest', 'admin'] }, true); // 'roles:\n\t- guest\n\t- admin\n\n'

The library does not read files itself, so a file is read by Node:

import { readFileSync } from 'node:fs';

const config = decode(readFileSync('config.neon', 'utf8'));

Instead of true, the second argument of encode() can be a function that decides for each array whether to write it as a block. It gets the array and the list of keys on the way to it:

encode(config, (value, path) => !path.length); // blocks for the first level only

The values of the format become these JavaScript values:

NEON JavaScript
null, true, yes … null, boolean
integer number; beyond 253 a bigint
float number
string string
date NeonDate
entity, chain Entity
sequence Array
mapping a plain object

A NeonDate holds the date as it was written, including a missing time zone; toDate(zone) turns it into a Date. A mapping is a plain object with its own properties only, so a key such as __proto__ is just a key.

Two things follow from JavaScript itself. A float without a fractional part, such as 1.0, is the same number as 1 and is written back as 1. And an object lists keys that look like integers first, in ascending order; the syntax tree keeps the order of the file.

Errors

When the input is not valid NEON, decode() and parse() throw NeonError. Its property position holds the line and the column of the problem:

import { decode, NeonError } from '@nette/neon';

try {
	decode('database:\n\thost: localhost\n\thost: db');
} catch (e) {
	if (e instanceof NeonError) {
		e.message;            // "Duplicated key 'host'"
		e.position.line;      // 3
	}
}

Editing Files

The function parse() gives the syntax tree of a file, and through it you change the file so that the rest of it stays as its author wrote it:

import { readFileSync, writeFileSync } from 'node:fs';
import { parse } from '@nette/neon';

const doc = parse(readFileSync('config.neon', 'utf8'));
doc.setValue(['database', 'host'], 'db.example.com');
writeFileSync('config.neon', doc.print());

How to change, add, remove and rename items, and how to find your way around the tree, is described on the page Editing Files.

Checking Files: neon-lint

The package comes with the command neon-lint. Give it files or directories, and it prints one line for each problem:

npx neon-lint config/
config/local.neon:3:2: Duplicated key 'user'

The command, its options and its exit codes are the same as in the PHP library. To run it without installing the package, use npx --package=@nette/neon neon-lint config/.

An AI agent that runs it after every change of a .neon file learns about a broken file right away. The guide for AI agents shows how to set it up as a hook, and it works whatever the language of the project.

More examples, each with its real output, are in the repository of the library.