properties-file 2.2.4 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,11 +6,11 @@
6
6
  ![Dependencies](https://img.shields.io/badge/dependencies-0-green)
7
7
  [![Known Vulnerabilities](https://snyk.io/test/github/Avansai/properties-file/badge.svg?targetFile=package.json)](https://snyk.io/test/github/Avansai/properties-file?targetFile=package.json)
8
8
 
9
- `.properties` file JSON converter, serializer, parser and Webpack loader.
9
+ `.properties` file parser, editor, formatter and Webpack loader.
10
10
 
11
11
  ## Installation 💻
12
12
 
13
- > ⚠ in June 2022 we have released version 2 of this package which is not compatible with the previous versions. Make sure to read the documentation before upgrading.
13
+ > ⚠ In April 2023, we released version 3 of this package, which includes breaking changes. Please refer to the [migration guide](./V2-TO-V3-MIGRATION-GUIDE.md) before upgrading.
14
14
 
15
15
  Add the package as a dependency:
16
16
 
@@ -20,30 +20,31 @@ npm install properties-file
20
20
 
21
21
  ## What's in it for me? 🤔
22
22
 
23
- - A modern TypeScript library that reproduces exactly the [Properties Java implementation](/assets/java-implementation.md).
23
+ - A modern library written entirely in TypeScript that exactly reproduces the [Properties Java implementation](/assets/java-implementation.md).
24
+ - Works for both Node.js applications and browsers that support at least [ES5](https://www.w3schools.com/js/js_es5.asp).
24
25
  - Flexible APIs:
25
- - `propertiesToJson` allows quick conversion from `.properties` files to JSON.
26
- - `getProperties` returns a `Properties` object that provides insights into parsing issues such as key collisions.
27
- - `propertiesToJson` & `getProperties` also have a browser-compatible version when passing directly the content of a file using the APIs under `properties-file/content`.
28
- - `escapeKey`, `escapeValue` that can allow you to convert any content to `.properties` compatible format.
29
- - Out of the box Webpack loader to `import` `.properties` files directly in your application.
26
+ - `getProperties` converts the content of `.properties` files to a key-value pair object.
27
+ - A `Properties` class provides insights into parsing data.
28
+ - A `PropertiesEditor` class enables the addition, edition, and removal of entries.
29
+ - `escapeKey` and `escapeValue` allow the conversion of any content to a `.properties` compatible format.
30
+ - The library also includes a Webpack loader to import `.properties` files directly into your application.
31
+ - Tiny ([under 3kB compressed](https://bundlephobia.com/package/properties-file)) with 0 dependencies.
30
32
  - 100% test coverage based on the output from a Java implementation.
31
- - Active maintenance (many popular .properties packages have been inactive years).
33
+ - Active maintenance (many popular `.properties` packages have been inactive for years).
32
34
 
33
35
  ## Usage 🎬
34
36
 
35
- We put a lot of effort into adding [TSDoc](https://tsdoc.org/) to all our APIs. Please check directly in your IDE if you are unsure how to use certain APIs provided in our examples.
37
+ We have put a lot of effort into incorporating [TSDoc](https://tsdoc.org/) into all our APIs. If you are unsure about how to use certain APIs provided in our examples, please check directly in your IDE.
36
38
 
37
- Both APIs (`getProperties` and `propertiesToJson`) directly under `properties-file` depend on [`fs`](https://nodejs.org/api/fs.html) which means they cannot be used by browsers. If you cannot use `fs` and already have a `.properties` file content, the same APIs are available under `properties-file/content`. Instead of taking the `filePath` as the first argument, they take `content`. The example below will use "`fs`" APIs since they are the most common use cases.
39
+ ### `getProperties` (converting `.properties` to an object)
38
40
 
39
- ### `propertiesToJson` (common use case)
40
-
41
- This API is probably the most used. You have a `.properties` file that you want to open and access like a simple key/value JSON object. Here is how this can be done with a single API call:
41
+ 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:
42
42
 
43
43
  ```ts
44
- import { propertiesToJson } from 'properties-file'
44
+ import { readFileSync } from 'node:fs'
45
+ import { getProperties } from 'properties-file'
45
46
 
46
- console.log(propertiesToJson('hello-world.properties'))
47
+ console.log(getProperties(readFileSync('hello-world.properties')))
47
48
  ```
48
49
 
49
50
  Output:
@@ -52,108 +53,139 @@ Output:
52
53
  { hello: 'hello', world: 'world' }
53
54
  ```
54
55
 
55
- If you cannot use [fs](https://nodejs.org/api/fs.html) (e.g., from a browser) and already have the content of a `.properties` file, your code would look like this instead:
56
+ ### `Properties` (using parsing metadata)
57
+
58
+ 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:
56
59
 
57
60
  ```ts
58
- import { propertiesToJson } from 'properties-file/content'
61
+ import { Properties } from 'properties-file'
62
+
63
+ const properties = new Properties(
64
+ 'hello = hello1\nworld = world1\nworld = world2\nhello = hello2\nworld = world3'
65
+ )
66
+ console.log(properties.format())
67
+
68
+ /**
69
+ * Outputs:
70
+ *
71
+ * hello = hello1
72
+ * world = world1
73
+ * world = world2
74
+ * hello = hello2
75
+ * world = world3
76
+ */
77
+
78
+ properties.collection.forEach((property) => {
79
+ console.log(`${property.key} = ${property.value}`)
80
+ })
81
+
82
+ /**
83
+ * Outputs:
84
+ *
85
+ * hello = hello2
86
+ * world = world3
87
+ */
88
+
89
+ const keyCollisions = properties.getKeyCollisions()
59
90
 
60
- // ...some code to get the .properties file content into a variable called `propertiesFileContent`
91
+ keyCollisions.forEach((keyCollision) => {
92
+ console.warn(
93
+ `Found a key collision for key '${
94
+ keyCollision.key
95
+ }' on lines ${keyCollision.startingLineNumbers.join(
96
+ ', '
97
+ )} (will use the value at line ${keyCollision.getApplicableLineNumber()}).`
98
+ )
99
+ })
61
100
 
62
- console.log(propertiesToJson(propertiesFileContent))
101
+ /**
102
+ * Outputs:
103
+ *
104
+ * Found a key collision for key 'hello' on lines 1, 4 (will use the value at line 4).
105
+ * Found a key collision for key 'world' on lines 2, 3, 5 (will use the value at line 5).
106
+ */
63
107
  ```
64
108
 
65
- ### `escapeKey` and `escapeValue` (serializing content to `.properties` format)
109
+ 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.
66
110
 
67
- > This package does not offer a full-fledged `.properties` file writer that would include a variety of options like modifying an existing file while keeping comments and line breaks intact. If you have any interest into adding this in, pull requests are welcomed!
111
+ ### `PropertiesEditor` (editing `.properties` content)
68
112
 
69
- It is possible to use this package serialize content to `.properties.` format by using `escapeKey` and `escapeValue`. Here is an example of how it can be done:
113
+ 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:
70
114
 
71
115
  ```ts
72
- import * as fs from 'node:fs'
73
- import { EOL } from 'node:os'
74
- import { getProperties } from 'properties-file'
116
+ import { Properties } from 'properties-file'
75
117
  import { escapeKey, escapeValue } from 'properties-file/escape'
76
118
 
77
- const properties = getProperties('assets/tests/collisions-test.properties')
119
+ const properties = new Properties('hello = hello\n# This is a comment\nworld = world')
78
120
  const newProperties: string[] = []
79
- console.dir(properties)
80
121
 
81
122
  properties.collection.forEach((property) => {
82
- const value = property.value === 'world3' ? 'new world3' : property.value
83
- newProperties.push(`${escapeKey(property.key)}: ${escapeValue(value)}`)
123
+ const key = property.key === 'world' ? 'new world' : property.key
124
+ const value = property.value === 'world' ? 'new world' : property.value
125
+ newProperties.push(`${escapeKey(key)} = ${escapeValue(value)}`)
84
126
  })
85
127
 
86
- fs.writeFileSync('myNewFile.properties', newProperties.join(EOL))
128
+ console.log(newProperties.join('\n'))
87
129
 
88
130
  /**
89
131
  * Outputs:
90
132
  *
91
- * hello: hello2
92
- * world: new world3
93
- *
133
+ * hello = hello
134
+ * new\ world = new world
94
135
  */
95
136
  ```
96
137
 
97
- ### `getProperties` (advanced use case)
98
-
99
- Java's implementation of `Properties` is quite resilient. In fact, there are only two ways an exception can be thrown:
100
-
101
- - The file is not found.
102
- - A (`\u`) Unicode escape character is malformed.
103
-
104
- This means that almost all files will be valid.
105
-
106
- But what about a file that has duplicate keys? Duplicate keys have no reason to exist and they probably should have thrown errors as well but instead Java decided to simply overwrite the value with the latest occurrence in a file.
107
-
108
- So how can we know if there were duplicate keys if we want to log some warnings? Simply by using `getProperties` which will return all the data that was used to parse the content. Here is an example on how it can be used:
109
-
110
- ```properties
111
- # collisions-test.properties
112
- hello: hello1
113
- world: world1
114
- world: world2
115
- hello: hello2
116
- world: world3
117
- ```
138
+ 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.
118
139
 
119
140
  ```ts
120
- import { getProperties } from 'properties-file'
141
+ import { PropertiesEditor } from 'properties-file/editor'
121
142
 
122
- const properties = getProperties('assets/tests/collisions-test.properties')
123
-
124
- properties.collection.forEach((property) => {
125
- console.log(`${property.key} => '${property.value}'`)
126
- })
143
+ const properties = new PropertiesEditor('hello = hello\n# This is a comment\nworld = world')
144
+ console.log(properties.format())
127
145
 
128
146
  /**
129
147
  * Outputs:
130
148
  *
131
- * hello => 'hello2'
132
- * world => 'world3'
133
- *
149
+ * hello = hello
150
+ * # This is a comment
151
+ * world = world
134
152
  */
135
153
 
136
- const keyCollisions = properties.getKeyCollisions()
154
+ properties.insertComment('This is a multiline\ncomment before `newKey3`')
155
+ properties.insert('newKey3', 'This is my third key')
137
156
 
138
- keyCollisions.forEach((keyCollision) => {
139
- console.warn(
140
- `Found a key collision for key '${
141
- keyCollision.key
142
- }' on lines ${keyCollision.startingLineNumbers.join(
143
- ', '
144
- )} (will use the value at line ${keyCollision.getApplicableLineNumber()}).`
145
- )
157
+ properties.insert('newKey1', 'This is my first new key', {
158
+ referenceKey: 'newKey3',
159
+ position: 'before',
160
+ comment: 'Below are the new keys being edited',
161
+ commentDelimiter: '!',
162
+ })
163
+
164
+ properties.insert('newKey2', 'こんにちは', {
165
+ referenceKey: 'newKey1',
166
+ position: 'after',
167
+ escapeUnicode: true,
146
168
  })
147
169
 
170
+ properties.delete('hello')
171
+ console.log(properties.format())
172
+
148
173
  /**
149
174
  * Outputs:
150
175
  *
151
- * Found a key collision for key 'hello' on lines 1, 4 (will use the value at line 4).
152
- * Found a key collision for key 'world' on lines 2, 3, 5 (will use the value at line 5).
153
- *
176
+ * # This is a comment
177
+ * world = world
178
+ * ! Below are the new keys being edited
179
+ * newKey1 = This is my first new key
180
+ * newKey2 = \u3053\u3093\u306b\u3061\u306f
181
+ * # This is a multiline
182
+ * # comment before `newKey3`
183
+ * newKey3 = This is my third key
154
184
  */
155
185
  ```
156
186
 
187
+ We were not able to show all methods in the example, but the `update` and `upsert` methods can also be useful to modify content.
188
+
157
189
  ### Webpack File Loader
158
190
 
159
191
  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:
@@ -202,17 +234,17 @@ Output:
202
234
 
203
235
  ## Why another `.properties` file package?
204
236
 
205
- There are probably over 20 similar packages available but:
237
+ There are probably over 20 similar packages available, but:
206
238
 
207
- - A lot of the most popular packages have had no activity for over 5 years.
208
- - A large portion of the packages will not replicate the current Java implementation.
239
+ - Many of the most popular packages have had no activity for over 5 years.
240
+ - Most packages will not replicate the current Java implementation.
209
241
  - No package offers the same capabilities as this one.
210
242
 
211
- Unfortunately the `.properties` file specification is not well documented. One reason for this is that it was originally used in Java to store configurations. Most applications will handle this using JSON, YAML or other modern formats today because the formats are more flexible.
243
+ 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.
212
244
 
213
245
  ### So why `.properties` files?
214
246
 
215
- While many options exists today to handle configurations, `.properties` file remain one of the best option 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):
247
+ 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):
216
248
 
217
249
  | File format | Key/value based | Supports inline comments | Built for localization | Good linguistic tools support |
218
250
  | ------------- | ---------------- | ------------------------ | ---------------------- | ----------------------------- |
@@ -220,20 +252,19 @@ While many options exists today to handle configurations, `.properties` file rem
220
252
  | `JSON` | No (can do more) | No (requires JSON5) | No | Depends on the schema |
221
253
  | `YAML` | No (can do more) | Yes | No | Depends on the schema |
222
254
 
223
- By having good JavaScript/TypeScript support for `.properties` files, it provides more options when it comes to i18n.
255
+ Having good JavaScript/TypeScript support for .properties files offers more internationalization (i18n) options.
224
256
 
225
257
  ### How does this package work?
226
258
 
227
- Basically our goal was to offer parity with the Java implementation, which is the closest thing to a specification `.properties` file have. Here is in a nutshell the logic behind this package:
259
+ 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:
228
260
 
229
- 1. Split the file content by lines (create line objects)
230
- 2. Create `LineObjects` by combining multi-line properties and removing trailing backslash
231
- 3. Create `PropertyObjects` from `LineObjects` that combined all lines of a property
232
- 4. Identify the key/value delimiter and populate escaped keys and values.
233
- 5. Unescape keys and values
234
- 6. Create a `PropertiesObject` that will include all `PropertyObjects` while removing collisions
261
+ 1. The content is split by lines, creating an array of strings where each line is an element.
262
+ 2. All lines are parsed to create a collection of `Property` objects that:
263
+ 1. Identify key-value pair lines from the other lines (e.g., comments, blank lines, etc.).
264
+ 2. Merge back multiline key-value pairs on single lines by removing trailing backslashes.
265
+ 3. Unescape the keys and values.
235
266
 
236
- Just like Java, if a Unicode escaped characters (`\u`) is malformed, it will throw an error. But of course, we do not recommend using Unicode escaped characters but rather UTF-8 encoding that supports more characters.
267
+ 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.
237
268
 
238
269
  ## Additional references
239
270
 
@@ -0,0 +1,133 @@
1
+ import { Properties } from '../properties';
2
+ /** The default separator between keys and values. */
3
+ export declare const DEFAULT_SEPARATOR = "=";
4
+ /** The default character used as comment delimiter. */
5
+ export declare const DEFAULT_COMMENT_DELIMITER = "#";
6
+ /** Characters that can be used as key-value pair separators. */
7
+ export type KeyValuePairSeparator = ' ' | ':' | '=';
8
+ /** Characters that can be used as comment delimiters. */
9
+ export type CommentDelimiter = '#' | '!';
10
+ /** Options on the `Properties.insert` method. */
11
+ export type InsertOptions = {
12
+ /** The name of the key to insert before or after. If the key not found, the new property will not be inserted. */
13
+ referenceKey?: string;
14
+ /** The position of the insertion related to the `referenceKey` (default is `after`) */
15
+ position?: 'before' | 'after';
16
+ /** Escape unicode characters into ISO-8859-1 compatible encoding? */
17
+ escapeUnicode?: boolean;
18
+ /** The key/value separator character. */
19
+ separator?: KeyValuePairSeparator;
20
+ /** A comment to insert before. */
21
+ comment?: string;
22
+ /** The comment's delimiter. */
23
+ commentDelimiter?: CommentDelimiter;
24
+ };
25
+ /** Options on the `Properties.insertComment` method. */
26
+ export type InsertCommentOptions = {
27
+ /** The name of the key to insert before or after. If the key not found, the new property will not be inserted. */
28
+ referenceKey?: string;
29
+ /** The position of the insertion related to the `referenceKey` (default is `after`) */
30
+ position?: 'before' | 'after';
31
+ /** The comment's delimiter. */
32
+ commentDelimiter?: CommentDelimiter;
33
+ };
34
+ /** Options on the `Properties.update` method. */
35
+ export type UpdateOptions = {
36
+ /** Optionally replace the existing value with a new value. */
37
+ newValue?: string;
38
+ /** Optionally replace the existing key with a new key name. */
39
+ newKey?: string;
40
+ /** Escape unicode characters into ISO-8859-1 compatible encoding? */
41
+ escapeUnicode?: boolean;
42
+ /** A key/value separator character. */
43
+ separator?: ' ' | ':' | '=';
44
+ /** Optionally insert a new comment, or replace the existing one (including white-space characters). */
45
+ newComment?: string;
46
+ /** The comment's delimiter. */
47
+ commentDelimiter?: CommentDelimiter;
48
+ };
49
+ /** Options on the `Properties.upsert` method. */
50
+ export type UpsertOptions = {
51
+ /** Escape unicode characters into ISO-8859-1 compatible encoding? */
52
+ escapeUnicode?: boolean;
53
+ /** The key/value separator character. */
54
+ separator?: KeyValuePairSeparator;
55
+ /** A comment to insert before. */
56
+ comment?: string;
57
+ /** The comment's delimiter. */
58
+ commentDelimiter?: CommentDelimiter;
59
+ };
60
+ /**
61
+ * A .properties file editor.
62
+ */
63
+ export declare class PropertiesEditor extends Properties {
64
+ /**
65
+ * Create `PropertiesEditor` object.
66
+ *
67
+ * @param content - The content of a `.properties` file.
68
+ */
69
+ constructor(content: string);
70
+ /**
71
+ * Insert a new property in the existing object (by default it will be at the end).
72
+ *
73
+ * @param key - A property key (unescaped).
74
+ * @param value - A property value (unescaped).
75
+ * @param options - Additional options.
76
+ *
77
+ * @returns True if the key was inserted, otherwise false.
78
+ */
79
+ insert(key: string, value: string, options?: InsertOptions): boolean;
80
+ /**
81
+ * Insert a new comment in the existing object (by default it will be at the end).
82
+ *
83
+ * @param comment - The comment to add.
84
+ * @param options - Additional options.
85
+ *
86
+ * @returns True if the comment was inserted, otherwise false.
87
+ */
88
+ insertComment(comment: string, options?: InsertCommentOptions): boolean;
89
+ /**
90
+ * Delete the last occurrence of a given key from the existing object.
91
+ *
92
+ * @param key - The name of the key to delete.
93
+ * @param deleteCommentsAndWhiteSpace - By default, comments and white-space characters before the key will be deleted.
94
+ *
95
+ * @returns True if the key was deleted, otherwise false.
96
+ */
97
+ delete(key: string, deleteCommentsAndWhiteSpace?: boolean): boolean;
98
+ /**
99
+ * Restore the original newline characters of a key.
100
+ *
101
+ * @param property - A property object.
102
+ *
103
+ * @returns The key with its original newlines characters restored.
104
+ */
105
+ private getKeyWithNewlines;
106
+ /**
107
+ * Restore the original newline characters of a value.
108
+ *
109
+ * @param property - A property object.
110
+ *
111
+ * @returns The value with its original newlines characters restored.
112
+ */
113
+ private getValueWithNewlines;
114
+ /**
115
+ * Update the last occurrence of a given key from the existing object.
116
+ *
117
+ * @param key - The name of the key to update.
118
+ * @param options - Additional options.
119
+ *
120
+ * @returns True if the key was updated, otherwise false.
121
+ */
122
+ update(key: string, options?: UpdateOptions): boolean;
123
+ /**
124
+ * Update a key if it exist, otherwise add it at the end.
125
+ *
126
+ * @param key - A property key (unescaped).
127
+ * @param value - A property value (unescaped).
128
+ * @param options - Additional options.
129
+ *
130
+ * @returns True if the key was updated or inserted, otherwise false.
131
+ */
132
+ upsert(key: string, value: string, options?: UpsertOptions): boolean;
133
+ }