Giter VIP home page Giter VIP logo

properties-file's Introduction

properties-file

License npm download Coverage Dependencies

.properties file parser, editor, formatter and Webpack loader.

Installation ๐Ÿ’ป

โš  In April 2023, we released version 3 of this package, which includes breaking changes. Please refer to the upgrade guide before upgrading.

Add the package as a dependency:

npm install properties-file

What's in it for me? ๐Ÿค”

  • A modern library written entirely in TypeScript that exactly reproduces the Properties Java implementation.
  • Works for both Node.js applications and browsers that support at least ES5.
  • Flexible APIs:
    • getProperties converts the content of .properties files to a key-value pair object.
    • A Properties class provides insights into parsing data.
    • A PropertiesEditor class enables the addition, edition, and removal of entries.
    • escapeKey and escapeValue allow the conversion of any content to a .properties compatible format.
    • The library also includes a Webpack loader to import .properties files directly into your application.
  • Tiny (under 4kB compressed) with 0 dependencies.
  • 100% test coverage based on the output from a Java implementation.
  • Active maintenance (many popular .properties packages have been inactive for years).

Usage ๐ŸŽฌ

We have put a lot of effort into incorporating TSDoc into all our APIs. If you are unsure about how to use certain APIs provided in our examples, please check directly in your IDE.

getProperties (converting .properties to an object)

The most common use case for .properties files is for Node.js applications that need to read the file's content into a simple key-value pair object. Here is how this can be done with a single API call:

import { readFileSync } from 'node:fs'
import { getProperties } from 'properties-file'

console.log(getProperties(readFileSync('hello-world.properties')))

Output:

{ hello: 'hello', world: 'world' }

Properties (using parsing metadata)

The Properties object is what makes getProperties work under the hood, but when using it directly, you can access granular parsing metadata. Here is an example of how the object can be used to find key collisions:

import { Properties } from 'properties-file'

const properties = new Properties(
  'hello = hello1\nworld = world1\nworld = world2\nhello = hello2\nworld = world3'
)
console.log(properties.format())

/**
 * Outputs:
 *
 * hello = hello1
 * world = world1
 * world = world2
 * hello = hello2
 * world = world3
 */

properties.collection.forEach((property) => {
  console.log(`${property.key} = ${property.value}`)
})

/**
 * Outputs:
 *
 * hello = hello2
 * world = world3
 */

const keyCollisions = properties.getKeyCollisions()

keyCollisions.forEach((keyCollision) => {
  console.warn(
    `Found a key collision for key '${
      keyCollision.key
    }' on lines ${keyCollision.startingLineNumbers.join(
      ', '
    )} (will use the value at line ${keyCollision.getApplicableLineNumber()}).`
  )
})

/**
 * Outputs:
 *
 * Found a key collision for key 'hello' on lines 1, 4 (will use the value at line 4).
 * Found a key collision for key 'world' on lines 2, 3, 5 (will use the value at line 5).
 */

For purposes where you require more parsing metadata, such as building a syntax highlighter, it is recommended that you access the Property objects included in the Properties.collection. These objects provide comprehensive information about each key-value pair.

PropertiesEditor (editing .properties content)

In certain scenarios, it may be necessary to modify the content of the .properties key-value pair objects. This can be achieved easily using the Properties object, with the assistance of the escapeKey and escapeValue APIs, as demonstrated below:

import { Properties } from 'properties-file'
import { escapeKey, escapeValue } from 'properties-file/escape'

const properties = new Properties('hello = hello\n# This is a comment\nworld = world')
const newProperties: string[] = []

properties.collection.forEach((property) => {
  const key = property.key === 'world' ? 'new world' : property.key
  const value = property.value === 'world' ? 'new world' : property.value
  newProperties.push(`${escapeKey(key)} = ${escapeValue(value)}`)
})

console.log(newProperties.join('\n'))

/**
 * Outputs:
 *
 * hello = hello
 * new\ world = new world
 */

The limitation of this approach is that its output contains only valid keys, without any comments or whitespace. However, if you require a more advanced editor that preserves these original elements, then the PropertiesEditor object is exactly what you need.

import { PropertiesEditor } from 'properties-file/editor'

const properties = new PropertiesEditor('hello = hello\n# This is a comment\nworld = world')
console.log(properties.format())

/**
 * Outputs:
 *
 * hello = hello
 * # This is a comment
 * world = world
 */

properties.insertComment('This is a multiline\ncomment before `newKey3`')
properties.insert('newKey3', 'This is my third key')

properties.insert('newKey1', 'This is my first new key', {
  referenceKey: 'newKey3',
  position: 'before',
  comment: 'Below are the new keys being edited',
  commentDelimiter: '!',
})

properties.insert('newKey2', 'ใ“ใ‚“ใซใกใฏ', {
  referenceKey: 'newKey1',
  position: 'after',
  escapeUnicode: true,
})

properties.delete('hello')
properties.update('world', {
  newValue: 'new world',
})
console.log(properties.format())

/**
 * Outputs:
 *
 * # This is a comment
 * world = new world
 * ! Below are the new keys being edited
 * newKey1 = This is my first new key
 * newKey2 = \u3053\u3093\u306b\u3061\u306f
 * # This is a multiline
 * # comment before `newKey3`
 * newKey3 = This is my third key
 */

For convenience, we also added an upsert method that allows updating a key if it exists or adding it at the end, when it doesn't. Make sure to check in your IDE for all available methods and options in our TSDoc.

Webpack File Loader

If you would like to import .properties directly using import, this package comes with its own Webpack file loader located under properties-file/webpack-loader. Here is an example of how to configure it:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.properties$/i,
        use: [
          {
            loader: 'properties-file/webpack-loader',
          },
        ],
      },
    ],
  },
}

As soon as you configure Webpack, the .properties type should be available in your IDE when using import. If you ever need to add it manually, you can add a *.properties type declaration file at the root of your application, like this:

declare module '*.properties' {
  /** A key/value object representing the content of a `.properties` file. */
  const properties: {
    /** The value of a `.properties` file key. */
    [key: string]: string
  }
  export { properties }
}

By adding these configurations you should now be able to import directly .properties files just like this:

import { properties as helloWorld } from './hello-world.properties'

console.dir(helloWorld)

Output:

{ "hello": "world" }

Why another .properties file package?

There are probably over 20 similar packages available, but:

  • Many of the most popular packages have had no activity for over 5 years.
  • Most packages will not replicate the current Java implementation.
  • No package offers the same capabilities as this one.

Unfortunately, the .properties file specification is not well-documented. One reason for this is that it was originally used in Java to store configurations. Today, most applications handle this using JSON, YAML, or other modern formats because these formats are more flexible.

So why .properties files?

While many options exist today to handle configurations, .properties files remain one of the best options to store localizable strings (also known as messages). On the Java side, PropertyResourceBundle is how most implementations handle localization today. Because of its simplicity and maturity, .properties files remain one of the best options today when it comes to internationalization (i18n):

File format Key/value based Supports inline comments Built for localization Good linguistic tools support
.properties Yes Yes Yes (Resource Bundles) Yes
JSON No (can do more) No (requires JSON5) No Depends on the schema
YAML No (can do more) Yes No Depends on the schema

Having good JavaScript/TypeScript support for .properties files offers more internationalization (i18n) options.

How does this package work?

Basically, our goal was to offer parity with the Java implementation, which is the closest thing to a specification for .properties files. Here is the logic behind this package in a nutshell:

  1. The content is split by lines, creating an array of strings where each line is an element.
  2. All lines are parsed to create a collection of Property objects that:
    1. Identify key-value pair lines from the other lines (e.g., comments, blank lines, etc.).
    2. Merge back multiline key-value pairs on single lines by removing trailing backslashes.
    3. Unescape the keys and values.

Just like Java, if a Unicode-escaped character (\u) is malformed, an error will be thrown. However, we do not recommend using Unicode-escaped characters, but rather using UTF-8 encoding that supports more characters.

Additional references

Special mention

Thanks to @calibr, the creator of properties-file version 1.0, for letting us use the https://www.npmjs.com/package/properties-file package name. We hope that it will make it easier to find our package.

properties-file's People

Contributors

dependabot[bot] avatar inpermutation avatar mdvorak avatar nbouvrette avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

properties-file's Issues

Regression 3.4.1 for ESM environments: TS1203

../../../node_modules/properties-file/lib/esm/properties-file.d.ts:7:3 - error TS1203: Export assignment cannot be used when targeting ECMAScript modules. Consider using 'export default' or another module format instead.

7   export = keyValuePairObject
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~


Found 1 error in ../../../node_modules/properties-file/lib/esm/properties-file.d.ts:7

package.json

blah 
"type": "module"
blah
"dependencies": {
  "properties-file": "^3.4.1",
},
"devDependencies": {
  "typescript": "^5.3.3"
}

tsConfig.json

{
  "compilerOptions": {
    "module": "NodeNext",
    "strict": true,
    "target": "ES6",
    "resolveJsonModule": true
  },
  "exclude": ["node_modules"]
}

Lmk if you need more elements for debug but at first sight it will probably be an easy one :)

ReferenceError: Can't find variable: Buffer

Hi!

I was exited to see a maintained package for dealing with property files.
When i install it into a brand new vite project and try to call getProperties i get an error in my browser that Buffer is not found? Missing Dependency maybe?

import { getProperties } from 'properties-file'

console.log(getProperties("hello = hello1\nworld = world1\nworld = world2"))

Screenshot 2023-04-28 at 22 14 23

Feature Request: Add support for client-only usage

I want to use this for parsing a properties file from the client side to JSON and then upload only the parsed, validated results. Is this all under the scope of this project?

The primary change would be to support File Blob as input and remove a required dependency for node:fs or expose the parser function only.

Serializing back to properties file?

Thanks for the great library! I have been looking into possibly using this package in an application that needs to modify values in a properties file and write it back to disk, curious if there's any plans to support this?

If not perhaps adding an endingLineNumber to the property could help with implementing this externally.

fs.exists should not be used before readFile

https://github.com/Avansai/properties-file/blob/main/src/file/index.ts#L17

It is anti-pattern to call existsSync before readFileSync, since it introduces a race-condition - readFileSync still might fail, even with ENOENT error.
You should simply let it fail directly, and remove explicit check with custom Error.

See
https://nodejs.org/api/fs.html#fsexistspath-callback

Using fs.exists() to check for the existence of a file before calling fs.open(), fs.readFile(), or fs.writeFile() is not recommended. Doing so introduces a race condition, since other processes may change the file's state between the two calls. Instead, user code should open/read/write the file directly and handle the error raised if the file does not exist.

Property.findSeparator is scanning strings character-by-character

Related to #16 #17 #18 - findSeparator is also showing up in my profiler as a significant CPU user; I think it can be significantly sped up by using e.g.

for (const match of this.linesContent[0].matchAll(/[\t\f :=]/g)) {
  if (!match.index) { continue; }

  const position = match?.index

but I haven't finished drafting the PR, so I'm filing this Issue instead ๐Ÿ˜Ž

Node 18.x support

Hi @nbouvrette, I'd like to use your package for a project that requires node engine version 18.7.0. Do you have plans on supporting node 18.x?

Getting value for a key

In Java , we can use Property.getproperty("key") to get the value but here we are in a position to iterate the collection to fetch a value.
Is there any similar method or I should role back to V2 to achieve the same?

ERR_PACKAGE_PATH_NOT_EXPORTED

Hey,
I'm getting this error when starting my program inside a docker container(docker-compose) and from command-line.
OS: Debian 10

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './lib/editor' is not defined by "exports" in /program/node_modules/properties-file/package.json
    at new NodeError (node:internal/errors:388:5)
    at throwExportsNotFound (node:internal/modules/esm/resolve:440:9)
    at packageExportsResolve (node:internal/modules/esm/resolve:719:3)
    at resolveExports (node:internal/modules/cjs/loader:488:36)
    at Module._findPath (node:internal/modules/cjs/loader:528:31)
    at Module._resolveFilename (node:internal/modules/cjs/loader:932:27)
    at Module._load (node:internal/modules/cjs/loader:787:27)
    at Module.require (node:internal/modules/cjs/loader:1012:19)
    at require (node:internal/modules/cjs/helpers:102:18)
    at Object.<anonymous> (/program/src/servers/Server.js:22:18) {
  code: 'ERR_PACKAGE_PATH_NOT_EXPORTED'
}

Node.js: v18.4.0

Code as provided does not compile

I have written a simple Typescript function intended to read the contents of a properties file. The code i question is test code and is provided below:

import { readFileSync } from 'node:fs'
import { Properties } from 'properties-file'

export const readProperties = () => {
    const props = new Properties(readFileSync('client.properties'));

    props.collection.forEach(property =>{
        console.log('Property: '+property.key+' '+property.value);
    });
}

When I attempt to compile this code, I get the error messages included below. There is clearly something wrong (missing?) with the properties-file library. Until the problem is fixed, this library is unusable.

Can this problem be fixed soon? Or perhaps there is a workaround that can make this code compile?

------------------------------------------Error Messages (3 errors)----------------------------------------------------------------

Compiled with problems:
ร—
ERROR in ./src/Network/PropManager.ts 6:20-30
export 'Properties' (imported as 'Properties') was not found in 'properties-file' (possible exports: getProperties)
ERROR in ./node_modules/properties-file/lib/esm/index.js 1:0-42
Module not found: Error: Can't resolve './properties' in 'C:\Research\Project 30\demoui\node_modules\properties-file\lib\esm'
Did you mean 'properties.js'?
BREAKING CHANGE: The request './properties' failed to resolve only because it was resolved as fully specified
(probably because the origin is strict EcmaScript Module, e. g. a module with javascript mimetype, a '*.mjs' file, or a '*.js' file where the package.json contains '"type": "module"').
The extension in the request is mandatory for it to be fully specified.
Add the extension to the request.
ERROR in node:fs
Module build failed: UnhandledSchemeError: Reading from "node:fs" is not handled by plugins (Unhandled scheme).
Webpack supports "data:" and "file:" URIs by default.
You may need an additional plugin to handle "node:" URIs.
    at C:\Research\Project 30\demoui\node_modules\webpack\lib\NormalModule.js:838:25
    at Hook.eval [as callAsync] (eval at create (C:\Research\Project 30\demoui\node_modules\tapable\lib\HookCodeFactory.js:33:10), <anonymous>:6:1)
    at Hook.CALL_ASYNC_DELEGATE [as _callAsync] (C:\Research\Project 30\demoui\node_modules\tapable\lib\Hook.js:18:14)
    at Object.processResource (C:\Research\Project 30\demoui\node_modules\webpack\lib\NormalModule.js:835:8)
    at processResource (C:\Research\Project 30\demoui\node_modules\loader-runner\lib\LoaderRunner.js:220:11)
    at iteratePitchingLoaders (C:\Research\Project 30\demoui\node_modules\loader-runner\lib\LoaderRunner.js:171:10)
    at runLoaders (C:\Research\Project 30\demoui\node_modules\loader-runner\lib\LoaderRunner.js:398:2)
    at NormalModule._doBuild (C:\Research\Project 30\demoui\node_modules\webpack\lib\NormalModule.js:825:3)
    at NormalModule.build (C:\Research\Project 30\demoui\node_modules\webpack\lib\NormalModule.js:969:15)
    at C:\Research\Project 30\demoui\node_modules\webpack\lib\Compilation.js:1374:12
    at NormalModule.needBuild (C:\Research\Project 30\demoui\node_modules\webpack\lib\NormalModule.js:1262:26)
    at Compilation._buildModule (C:\Research\Project 30\demoui\node_modules\webpack\lib\Compilation.js:1355:10)
    at C:\Research\Project 30\demoui\node_modules\webpack\lib\util\AsyncQueue.js:305:10
    at Hook.eval [as callAsync] (eval at create (C:\Research\Project 30\demoui\node_modules\tapable\lib\HookCodeFactory.js:33:10), <anonymous>:6:1)
    at AsyncQueue._startProcessing (C:\Research\Project 30\demoui\node_modules\webpack\lib\util\AsyncQueue.js:295:26)
    at AsyncQueue._ensureProcessing (C:\Research\Project 30\demoui\node_modules\webpack\lib\util\AsyncQueue.js:282:12)
    at process.processImmediate (node:internal/timers:476:21)

O(N^2) when calling PropertiesEditor .insert() N times

Related to #16, my Lambda is still timing out on that โ‰ค1000 lines x โ‰ค400 characters file.

I have read my profiler output more carefully, and found what I think is the core performance issue: PropertiesEditor calls this.parseLines() on every .insert() call.

On the first insert, parse the first line.
On the second insert, parse the first and second lines.
On the third insert, parse the first, second, and third lines.
โ€ฆ
On the Nth insert, parse lines 0...N.

This is Shlemiel the painter's algorithm.

I would submit a PR, but I'm not yet sure what the right approach is to fixing this. (Maybe an intermediate PropertiesBuilder that only parses at the end?)

WORKAROUND: Since I am not editing a .properties file, but rather generating a brand new one, I can avoid PropertiesEditor, and instead just linearly generate my own output, using the functions in properties-file/escape.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.