jg-fs 1.0.1 → 1.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.
Files changed (45) hide show
  1. package/API.md +1716 -0
  2. package/CHANGELOG.md +20 -16
  3. package/README.md +6 -21
  4. package/dist/async/directories/directories.d.ts +14 -0
  5. package/dist/async/directories/directories.js +20 -2
  6. package/dist/async/directories/directories.js.map +1 -1
  7. package/dist/async/files/files.d.ts +46 -8
  8. package/dist/async/files/files.js +48 -10
  9. package/dist/async/files/files.js.map +1 -1
  10. package/dist/async/files/index.d.ts +1 -1
  11. package/dist/async/files/index.js +1 -1
  12. package/dist/async/files/index.js.map +1 -1
  13. package/dist/async/general/general.d.ts +4 -0
  14. package/dist/async/general/general.js +4 -0
  15. package/dist/async/general/general.js.map +1 -1
  16. package/dist/index.d.ts +2 -2
  17. package/dist/index.js +1 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/shared/index.d.ts +2 -2
  20. package/dist/shared/index.js +1 -1
  21. package/dist/shared/index.js.map +1 -1
  22. package/dist/shared/paths.d.ts +2 -0
  23. package/dist/shared/paths.js +2 -0
  24. package/dist/shared/paths.js.map +1 -1
  25. package/dist/shared/types.d.ts +6 -0
  26. package/dist/shared/types.js.map +1 -1
  27. package/dist/shared/validations.d.ts +5 -3
  28. package/dist/shared/validations.js +30 -14
  29. package/dist/shared/validations.js.map +1 -1
  30. package/dist/sync/directories/directories.d.ts +14 -0
  31. package/dist/sync/directories/directories.js +20 -2
  32. package/dist/sync/directories/directories.js.map +1 -1
  33. package/dist/sync/files/files.d.ts +46 -8
  34. package/dist/sync/files/files.js +48 -10
  35. package/dist/sync/files/files.js.map +1 -1
  36. package/dist/sync/files/index.d.ts +1 -1
  37. package/dist/sync/files/index.js +1 -1
  38. package/dist/sync/files/index.js.map +1 -1
  39. package/dist/sync/general/general.d.ts +4 -0
  40. package/dist/sync/general/general.js +4 -0
  41. package/dist/sync/general/general.js.map +1 -1
  42. package/dist/sync/index.d.ts +2 -2
  43. package/dist/sync/index.js +1 -1
  44. package/dist/sync/index.js.map +1 -1
  45. package/package.json +10 -6
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.0
4
+
5
+ A fresh start: breaking changes in a minor version, once, because nothing depended on 1.0.x.
6
+
7
+ ### Added
8
+
9
+ - A `schema` option for `readJsonFile`, in the async and sync APIs, which validates the parsed content and types the result. Its errors propagate unchanged. `ISchema` and `IReadJsonFileOptions` are exported.
10
+ - `API.md`, the full API reference, generated from the TSDoc of the version it ships with, so it can be read from `node_modules` without a browser.
11
+
12
+ ### Changed
13
+
14
+ - Errors are `Exception`s from `jg-exception` 1.1.0: the code, the data, and the cause each have their own field, nothing is encoded into the message, and `toJSON()` and `toString()` follow `jg-exception`.
15
+ - Error messages say what failed at that step and keep the error underneath as `cause`, instead of repeating its message, so `extractMessage` prints each layer of the chain once.
16
+ - `readJsonFile` returns `unknown` without a schema or a type argument, instead of `Record<string, unknown>`.
17
+ - `readJSONFile` and `writeJSONFile` are `readJsonFile` and `writeJsonFile`, in the async and sync APIs, so acronyms are cased as words, like every other name in the packages.
18
+
3
19
  ## 1.0.1
4
20
 
5
21
  ### Changed
@@ -8,23 +24,11 @@
8
24
 
9
25
  ## 1.0.0
10
26
 
11
- The first release of `jg-fs`, which replaces `fs-utils-async` 1.0.1 and, at `jg-fs/sync`, `fs-utils-sync` 1.2.0.
27
+ The first release.
12
28
 
13
29
  ### Added
14
30
 
15
- - The `jg-fs/sync` entry, with the same functions as the async root entry.
31
+ - Async functions for paths, directories, and files, such as `getDirectoryElements`, `readJSONFile`, `writeFileAtomic`, and `readBoundedFile`.
32
+ - The `jg-fs/sync` entry, with the same functions, synchronous.
16
33
  - `ERRORS`, the codes of every error the package throws.
17
-
18
- ### Changed
19
-
20
- - Empty content is allowed when reading and writing text and binary files.
21
- - `createDirectory` succeeds when the directory already exists.
22
- - `includeExts` matches extensions case-insensitively.
23
- - The sync `readDirectory` joins paths with the platform's separator, and the sync `getDirectoryElements` skips entries removed while it reads.
24
- - The async `getDirectoryElements` reads at most 32 entries at once.
25
- - `readFile` returns `string` with an encoding and `Buffer` without one.
26
- - Errors are `Exception`s from `jg-exception`.
27
-
28
- ### Fixed
29
-
30
- - Typos in several error messages.
34
+ - Empty content is allowed when reading and writing text and binary files, and `createDirectory` succeeds when the directory already exists.
package/README.md CHANGED
@@ -13,11 +13,14 @@ npm install jg-fs
13
13
  The root entry is async, and `jg-fs/sync` has the same functions, synchronous:
14
14
 
15
15
  ```ts
16
- import { readJSONFile, writeFileAtomic, writeTextFile } from 'jg-fs';
16
+ import { readJsonFile, writeFileAtomic, writeTextFile } from 'jg-fs';
17
17
  import { readTextFile } from 'jg-fs/sync';
18
+ import { z } from 'zod';
19
+
20
+ const Config = z.object({ port: z.number() });
18
21
 
19
22
  await writeTextFile('notes/today.md', '# Today'); // creates notes/ when it's missing
20
- const config = await readJSONFile<{ port: number }>('config.json');
23
+ const config = await readJsonFile('config.json', { schema: Config }); // validated, and typed by the schema
21
24
  await writeFileAtomic('state.json', JSON.stringify(state)); // readers never see a partial file
22
25
 
23
26
  const readme = readTextFile('README.md');
@@ -47,27 +50,9 @@ if (isPathWithin(uploadsDirectory, path)) {
47
50
 
48
51
  Failures throw `Exception`s from `jg-exception`, with the codes in `ERRORS`.
49
52
 
50
- ## Coming from `fs-utils-async` and `fs-utils-sync`
51
-
52
- | Old | `jg-fs` |
53
- | -------------------------------- | ---------------------------- |
54
- | `import … from 'fs-utils-async'` | `import … from 'jg-fs'` |
55
- | `import … from 'fs-utils-sync'` | `import … from 'jg-fs/sync'` |
56
- | the codes, not exported | `ERRORS` |
57
-
58
- Changed behavior:
59
-
60
- - Empty content is allowed. `readTextFile`, `readBufferFile`, and `readFile` return empty content instead of throwing, and `writeTextFile('')` and `writeBufferFile` with an empty Buffer write empty files. `FILE_CONTENT_IS_EMPTY_OR_INVALID` now means data of the wrong type, or JSON that is empty or can't be parsed or stringified. Code that relied on the throw to detect empty files needs its own check.
61
- - `createDirectory` succeeds when the directory already exists, keeping its contents. It throws `DIRECTORY_ALREADY_EXISTS` only when a file or something else that isn't a directory is at the path.
62
- - `includeExts` matches extensions case-insensitively, so `.JSON` and `.json` are the same.
63
- - The sync `readDirectory` joins paths with the platform's separator, and the sync `getDirectoryElements` skips entries removed while it reads the directory, as the async one already did.
64
- - The async `getDirectoryElements` reads at most 32 entries at once.
65
- - `readFile` returns `string` when given an encoding and `Buffer` otherwise, instead of `string | Buffer`.
66
- - Errors are `Exception`s from `jg-exception`, with the same codes.
67
-
68
53
  ## Documentation
69
54
 
70
- The full API reference is at https://jg-packages.jesusgraterol.dev/packages/jg-fs/.
55
+ The full API reference ships in the package as `API.md`, generated from its TSDoc for the installed version, and is at https://jg-packages.jesusgraterol.dev/packages/jg-fs/.
71
56
 
72
57
  ## License
73
58
 
@@ -4,6 +4,8 @@ import { type IDirectoryElementsOptions, type IDirectoryPathElements } from '../
4
4
  * @param path The path.
5
5
  * @returns A promise that resolves to whether the path is a directory.
6
6
  * @example
7
+ * import { isDirectory } from 'jg-fs';
8
+ *
7
9
  * await isDirectory('dist'); // true
8
10
  */
9
11
  export declare const isDirectory: (path: string) => Promise<boolean>;
@@ -12,6 +14,8 @@ export declare const isDirectory: (path: string) => Promise<boolean>;
12
14
  * @param path The directory's path.
13
15
  * @returns A promise that resolves once the directory is gone.
14
16
  * @example
17
+ * import { deleteDirectory } from 'jg-fs';
18
+ *
15
19
  * await deleteDirectory('dist');
16
20
  */
17
21
  export declare const deleteDirectory: (path: string) => Promise<void>;
@@ -22,6 +26,8 @@ export declare const deleteDirectory: (path: string) => Promise<void>;
22
26
  * @param deleteIfExists Whether to delete an existing directory and its contents first.
23
27
  * @returns A promise that resolves once the directory exists.
24
28
  * @example
29
+ * import { createDirectory } from 'jg-fs';
30
+ *
25
31
  * await createDirectory('dist/assets'); // creates missing parents too
26
32
  * await createDirectory('dist', true); // replaces it with an empty directory
27
33
  * @throws
@@ -34,6 +40,8 @@ export declare const createDirectory: (path: string, deleteIfExists?: boolean) =
34
40
  * @param destPath Where to copy it.
35
41
  * @returns A promise that resolves once the copy is complete.
36
42
  * @example
43
+ * import { copyDirectory } from 'jg-fs';
44
+ *
37
45
  * await copyDirectory('templates', 'dist/templates');
38
46
  * @throws
39
47
  * - NOT_A_DIRECTORY: if the srcPath is not a directory
@@ -45,6 +53,8 @@ export declare const copyDirectory: (srcPath: string, destPath: string) => Promi
45
53
  * @param path Where to create the link.
46
54
  * @returns A promise that resolves once the link exists.
47
55
  * @example
56
+ * import { createDirectorySymLink } from 'jg-fs';
57
+ *
48
58
  * await createDirectorySymLink('releases/1.4.0', 'current');
49
59
  * @throws
50
60
  * - NON_EXISTENT_DIRECTORY: if the target directory doesn't exist
@@ -57,6 +67,8 @@ export declare const createDirectorySymLink: (target: string, path: string) => P
57
67
  * @param recursive Whether to include the contents of subdirectories.
58
68
  * @returns A promise that resolves to the content paths.
59
69
  * @example
70
+ * import { readDirectory } from 'jg-fs';
71
+ *
60
72
  * await readDirectory('dist'); // ['dist/index.js', 'dist/assets']
61
73
  * await readDirectory('dist', true); // includes the contents of dist/assets
62
74
  * @throws
@@ -71,6 +83,8 @@ export declare const readDirectory: (path: string, recursive?: boolean) => Promi
71
83
  * @param options The extensions to include and how to sort.
72
84
  * @returns A promise that resolves to the grouped elements.
73
85
  * @example
86
+ * import { getDirectoryElements } from 'jg-fs';
87
+ *
74
88
  * const { files } = await getDirectoryElements('content', { includeExts: ['.md'], sortByKey: 'creation', sortOrder: 'desc' });
75
89
  * @throws
76
90
  * - NOT_A_DIRECTORY: if the path doesn't exist or is not a directory
@@ -9,6 +9,8 @@ import { mapWithConcurrency } from './utilities.js';
9
9
  * @param path The path.
10
10
  * @returns A promise that resolves to whether the path is a directory.
11
11
  * @example
12
+ * import { isDirectory } from 'jg-fs';
13
+ *
12
14
  * await isDirectory('dist'); // true
13
15
  */
14
16
  export const isDirectory = async (path) => (await getPathElement(path))?.isDirectory ?? false;
@@ -17,6 +19,8 @@ export const isDirectory = async (path) => (await getPathElement(path))?.isDirec
17
19
  * @param path The directory's path.
18
20
  * @returns A promise that resolves once the directory is gone.
19
21
  * @example
22
+ * import { deleteDirectory } from 'jg-fs';
23
+ *
20
24
  * await deleteDirectory('dist');
21
25
  */
22
26
  export const deleteDirectory = (path) => rm(path, { recursive: true, force: true });
@@ -27,6 +31,8 @@ export const deleteDirectory = (path) => rm(path, { recursive: true, force: true
27
31
  * @param deleteIfExists Whether to delete an existing directory and its contents first.
28
32
  * @returns A promise that resolves once the directory exists.
29
33
  * @example
34
+ * import { createDirectory } from 'jg-fs';
35
+ *
30
36
  * await createDirectory('dist/assets'); // creates missing parents too
31
37
  * await createDirectory('dist', true); // replaces it with an empty directory
32
38
  * @throws
@@ -35,7 +41,9 @@ export const deleteDirectory = (path) => rm(path, { recursive: true, force: true
35
41
  export const createDirectory = async (path, deleteIfExists) => {
36
42
  const element = await getPathElement(path);
37
43
  if (element !== null && !element.isDirectory) {
38
- throw new Exception(`The path '${path}' already exists and is not a directory.`, ERRORS.DIRECTORY_ALREADY_EXISTS);
44
+ throw new Exception(`The path '${path}' already exists and is not a directory.`, {
45
+ code: ERRORS.DIRECTORY_ALREADY_EXISTS,
46
+ });
39
47
  }
40
48
  if (element !== null && deleteIfExists) {
41
49
  await deleteDirectory(path);
@@ -48,6 +56,8 @@ export const createDirectory = async (path, deleteIfExists) => {
48
56
  * @param destPath Where to copy it.
49
57
  * @returns A promise that resolves once the copy is complete.
50
58
  * @example
59
+ * import { copyDirectory } from 'jg-fs';
60
+ *
51
61
  * await copyDirectory('templates', 'dist/templates');
52
62
  * @throws
53
63
  * - NOT_A_DIRECTORY: if the srcPath is not a directory
@@ -65,6 +75,8 @@ export const copyDirectory = async (srcPath, destPath) => {
65
75
  * @param path Where to create the link.
66
76
  * @returns A promise that resolves once the link exists.
67
77
  * @example
78
+ * import { createDirectorySymLink } from 'jg-fs';
79
+ *
68
80
  * await createDirectorySymLink('releases/1.4.0', 'current');
69
81
  * @throws
70
82
  * - NON_EXISTENT_DIRECTORY: if the target directory doesn't exist
@@ -73,7 +85,9 @@ export const copyDirectory = async (srcPath, destPath) => {
73
85
  export const createDirectorySymLink = async (target, path) => {
74
86
  const element = await getPathElement(target);
75
87
  if (element === null) {
76
- throw new Exception(`The target directory '${target}' does not exist.`, ERRORS.NON_EXISTENT_DIRECTORY);
88
+ throw new Exception(`The target directory '${target}' does not exist.`, {
89
+ code: ERRORS.NON_EXISTENT_DIRECTORY,
90
+ });
77
91
  }
78
92
  if (!element.isDirectory) {
79
93
  throw notADirectory(target);
@@ -86,6 +100,8 @@ export const createDirectorySymLink = async (target, path) => {
86
100
  * @param recursive Whether to include the contents of subdirectories.
87
101
  * @returns A promise that resolves to the content paths.
88
102
  * @example
103
+ * import { readDirectory } from 'jg-fs';
104
+ *
89
105
  * await readDirectory('dist'); // ['dist/index.js', 'dist/assets']
90
106
  * await readDirectory('dist', true); // includes the contents of dist/assets
91
107
  * @throws
@@ -106,6 +122,8 @@ export const readDirectory = async (path, recursive = false) => {
106
122
  * @param options The extensions to include and how to sort.
107
123
  * @returns A promise that resolves to the grouped elements.
108
124
  * @example
125
+ * import { getDirectoryElements } from 'jg-fs';
126
+ *
109
127
  * const { files } = await getDirectoryElements('content', { includeExts: ['.md'], sortByKey: 'creation', sortOrder: 'desc' });
110
128
  * @throws
111
129
  * - NOT_A_DIRECTORY: if the path doesn't exist or is not a directory
@@ -1 +1 @@
1
- {"version":3,"file":"directories.js","sourceRoot":"","sources":["../../../src/async/directories/directories.ts"],"names":[],"mappings":"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","sourcesContent":["import { cp, mkdir, readdir, rm, symlink } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { Exception } from 'jg-exception';\n\nimport {\n buildDirectoryElementsOptions,\n DIRECTORY_ELEMENTS_CONCURRENCY,\n ERRORS,\n groupDirectoryElements,\n notADirectory,\n type IDirectoryElementsOptions,\n type IDirectoryPathElements,\n type IPathElement,\n} from '../../shared/index.js';\nimport { getPathElement } from '../general/index.js';\n\nimport { mapWithConcurrency } from './utilities.js';\n\n/**\n * Checks whether a path exists and is a directory.\n * @param path The path.\n * @returns A promise that resolves to whether the path is a directory.\n * @example\n * await isDirectory('dist'); // true\n */\nexport const isDirectory = async (path: string): Promise<boolean> =>\n (await getPathElement(path))?.isDirectory ?? false;\n\n/**\n * Deletes a directory and everything in it. A missing directory is not an error.\n * @param path The directory's path.\n * @returns A promise that resolves once the directory is gone.\n * @example\n * await deleteDirectory('dist');\n */\nexport const deleteDirectory = (path: string): Promise<void> =>\n rm(path, { recursive: true, force: true });\n\n/**\n * Creates a directory, with any missing parents. An existing directory is kept, or replaced with\n * an empty one when `deleteIfExists` is set.\n * @param path The directory's path.\n * @param deleteIfExists Whether to delete an existing directory and its contents first.\n * @returns A promise that resolves once the directory exists.\n * @example\n * await createDirectory('dist/assets'); // creates missing parents too\n * await createDirectory('dist', true); // replaces it with an empty directory\n * @throws\n * - DIRECTORY_ALREADY_EXISTS: if something other than a directory exists at the path\n */\nexport const createDirectory = async (path: string, deleteIfExists?: boolean): Promise<void> => {\n const element = await getPathElement(path);\n if (element !== null && !element.isDirectory) {\n throw new Exception(\n `The path '${path}' already exists and is not a directory.`,\n ERRORS.DIRECTORY_ALREADY_EXISTS,\n );\n }\n if (element !== null && deleteIfExists) {\n await deleteDirectory(path);\n }\n await mkdir(path, { recursive: true });\n};\n\n/**\n * Copies a directory and everything in it, replacing the destination completely.\n * @param srcPath The directory to copy.\n * @param destPath Where to copy it.\n * @returns A promise that resolves once the copy is complete.\n * @example\n * await copyDirectory('templates', 'dist/templates');\n * @throws\n * - NOT_A_DIRECTORY: if the srcPath is not a directory\n */\nexport const copyDirectory = async (srcPath: string, destPath: string): Promise<void> => {\n if (!(await isDirectory(srcPath))) {\n throw notADirectory(srcPath);\n }\n await deleteDirectory(destPath);\n await cp(srcPath, destPath, { recursive: true });\n};\n\n/**\n * Creates a symbolic link to a directory.\n * @param target The directory the link points to.\n * @param path Where to create the link.\n * @returns A promise that resolves once the link exists.\n * @example\n * await createDirectorySymLink('releases/1.4.0', 'current');\n * @throws\n * - NON_EXISTENT_DIRECTORY: if the target directory doesn't exist\n * - NOT_A_DIRECTORY: if the target directory is not considered a directory by the OS\n */\nexport const createDirectorySymLink = async (target: string, path: string): Promise<void> => {\n const element = await getPathElement(target);\n if (element === null) {\n throw new Exception(\n `The target directory '${target}' does not exist.`,\n ERRORS.NON_EXISTENT_DIRECTORY,\n );\n }\n if (!element.isDirectory) {\n throw notADirectory(target);\n }\n await symlink(target, path, 'dir');\n};\n\n/**\n * Lists a directory's contents as paths joined to the directory's path.\n * @param path The directory's path.\n * @param recursive Whether to include the contents of subdirectories.\n * @returns A promise that resolves to the content paths.\n * @example\n * await readDirectory('dist'); // ['dist/index.js', 'dist/assets']\n * await readDirectory('dist', true); // includes the contents of dist/assets\n * @throws\n * - NOT_A_DIRECTORY: if the path doesn't exist or is not a directory\n */\nexport const readDirectory = async (\n path: string,\n recursive: boolean = false,\n): Promise<string[]> => {\n if (!(await isDirectory(path))) {\n throw notADirectory(path);\n }\n const contentPaths = await readdir(path, { encoding: 'utf-8', recursive });\n return contentPaths.map((contentPath) => join(path, contentPath));\n};\n\n/**\n * Lists a directory's elements grouped by kind: directories, files (filtered by extension), and\n * symbolic links, each group sorted. Entries removed while the directory is read are left out, and\n * at most 32 entries are read at once.\n * @param path The directory's path.\n * @param options The extensions to include and how to sort.\n * @returns A promise that resolves to the grouped elements.\n * @example\n * const { files } = await getDirectoryElements('content', { includeExts: ['.md'], sortByKey: 'creation', sortOrder: 'desc' });\n * @throws\n * - NOT_A_DIRECTORY: if the path doesn't exist or is not a directory\n */\nexport const getDirectoryElements = async (\n path: string,\n options?: Partial<IDirectoryElementsOptions>,\n): Promise<IDirectoryPathElements> => {\n const contentPaths = await readDirectory(path);\n const elements = (\n await mapWithConcurrency(contentPaths, DIRECTORY_ELEMENTS_CONCURRENCY, getPathElement)\n ).filter((element): element is IPathElement => element !== null);\n return groupDirectoryElements(elements, buildDirectoryElementsOptions(options));\n};\n"]}
1
+ {"version":3,"file":"directories.js","sourceRoot":"","sources":["../../../src/async/directories/directories.ts"],"names":[],"mappings":"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","sourcesContent":["import { cp, mkdir, readdir, rm, symlink } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { Exception } from 'jg-exception';\n\nimport {\n buildDirectoryElementsOptions,\n DIRECTORY_ELEMENTS_CONCURRENCY,\n ERRORS,\n groupDirectoryElements,\n notADirectory,\n type IDirectoryElementsOptions,\n type IDirectoryPathElements,\n type IPathElement,\n} from '../../shared/index.js';\nimport { getPathElement } from '../general/index.js';\n\nimport { mapWithConcurrency } from './utilities.js';\n\n/**\n * Checks whether a path exists and is a directory.\n * @param path The path.\n * @returns A promise that resolves to whether the path is a directory.\n * @example\n * import { isDirectory } from 'jg-fs';\n *\n * await isDirectory('dist'); // true\n */\nexport const isDirectory = async (path: string): Promise<boolean> =>\n (await getPathElement(path))?.isDirectory ?? false;\n\n/**\n * Deletes a directory and everything in it. A missing directory is not an error.\n * @param path The directory's path.\n * @returns A promise that resolves once the directory is gone.\n * @example\n * import { deleteDirectory } from 'jg-fs';\n *\n * await deleteDirectory('dist');\n */\nexport const deleteDirectory = (path: string): Promise<void> =>\n rm(path, { recursive: true, force: true });\n\n/**\n * Creates a directory, with any missing parents. An existing directory is kept, or replaced with\n * an empty one when `deleteIfExists` is set.\n * @param path The directory's path.\n * @param deleteIfExists Whether to delete an existing directory and its contents first.\n * @returns A promise that resolves once the directory exists.\n * @example\n * import { createDirectory } from 'jg-fs';\n *\n * await createDirectory('dist/assets'); // creates missing parents too\n * await createDirectory('dist', true); // replaces it with an empty directory\n * @throws\n * - DIRECTORY_ALREADY_EXISTS: if something other than a directory exists at the path\n */\nexport const createDirectory = async (path: string, deleteIfExists?: boolean): Promise<void> => {\n const element = await getPathElement(path);\n if (element !== null && !element.isDirectory) {\n throw new Exception(`The path '${path}' already exists and is not a directory.`, {\n code: ERRORS.DIRECTORY_ALREADY_EXISTS,\n });\n }\n if (element !== null && deleteIfExists) {\n await deleteDirectory(path);\n }\n await mkdir(path, { recursive: true });\n};\n\n/**\n * Copies a directory and everything in it, replacing the destination completely.\n * @param srcPath The directory to copy.\n * @param destPath Where to copy it.\n * @returns A promise that resolves once the copy is complete.\n * @example\n * import { copyDirectory } from 'jg-fs';\n *\n * await copyDirectory('templates', 'dist/templates');\n * @throws\n * - NOT_A_DIRECTORY: if the srcPath is not a directory\n */\nexport const copyDirectory = async (srcPath: string, destPath: string): Promise<void> => {\n if (!(await isDirectory(srcPath))) {\n throw notADirectory(srcPath);\n }\n await deleteDirectory(destPath);\n await cp(srcPath, destPath, { recursive: true });\n};\n\n/**\n * Creates a symbolic link to a directory.\n * @param target The directory the link points to.\n * @param path Where to create the link.\n * @returns A promise that resolves once the link exists.\n * @example\n * import { createDirectorySymLink } from 'jg-fs';\n *\n * await createDirectorySymLink('releases/1.4.0', 'current');\n * @throws\n * - NON_EXISTENT_DIRECTORY: if the target directory doesn't exist\n * - NOT_A_DIRECTORY: if the target directory is not considered a directory by the OS\n */\nexport const createDirectorySymLink = async (target: string, path: string): Promise<void> => {\n const element = await getPathElement(target);\n if (element === null) {\n throw new Exception(`The target directory '${target}' does not exist.`, {\n code: ERRORS.NON_EXISTENT_DIRECTORY,\n });\n }\n if (!element.isDirectory) {\n throw notADirectory(target);\n }\n await symlink(target, path, 'dir');\n};\n\n/**\n * Lists a directory's contents as paths joined to the directory's path.\n * @param path The directory's path.\n * @param recursive Whether to include the contents of subdirectories.\n * @returns A promise that resolves to the content paths.\n * @example\n * import { readDirectory } from 'jg-fs';\n *\n * await readDirectory('dist'); // ['dist/index.js', 'dist/assets']\n * await readDirectory('dist', true); // includes the contents of dist/assets\n * @throws\n * - NOT_A_DIRECTORY: if the path doesn't exist or is not a directory\n */\nexport const readDirectory = async (\n path: string,\n recursive: boolean = false,\n): Promise<string[]> => {\n if (!(await isDirectory(path))) {\n throw notADirectory(path);\n }\n const contentPaths = await readdir(path, { encoding: 'utf-8', recursive });\n return contentPaths.map((contentPath) => join(path, contentPath));\n};\n\n/**\n * Lists a directory's elements grouped by kind: directories, files (filtered by extension), and\n * symbolic links, each group sorted. Entries removed while the directory is read are left out, and\n * at most 32 entries are read at once.\n * @param path The directory's path.\n * @param options The extensions to include and how to sort.\n * @returns A promise that resolves to the grouped elements.\n * @example\n * import { getDirectoryElements } from 'jg-fs';\n *\n * const { files } = await getDirectoryElements('content', { includeExts: ['.md'], sortByKey: 'creation', sortOrder: 'desc' });\n * @throws\n * - NOT_A_DIRECTORY: if the path doesn't exist or is not a directory\n */\nexport const getDirectoryElements = async (\n path: string,\n options?: Partial<IDirectoryElementsOptions>,\n): Promise<IDirectoryPathElements> => {\n const contentPaths = await readDirectory(path);\n const elements = (\n await mapWithConcurrency(contentPaths, DIRECTORY_ELEMENTS_CONCURRENCY, getPathElement)\n ).filter((element): element is IPathElement => element !== null);\n return groupDirectoryElements(elements, buildDirectoryElementsOptions(options));\n};\n"]}
@@ -1,11 +1,13 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import type { WriteFileOptions } from 'node:fs';
3
- import { type IReadBufferFileOptions, type IReadFileOptions, type IReadStringFileOptions } from '../../shared/index.js';
3
+ import { type IReadBufferFileOptions, type IReadFileOptions, type IReadJsonFileOptions, type IReadStringFileOptions } from '../../shared/index.js';
4
4
  /**
5
5
  * Checks whether a path exists and is a regular file.
6
6
  * @param path The path.
7
7
  * @returns A promise that resolves to whether the path is a file.
8
8
  * @example
9
+ * import { isFile } from 'jg-fs';
10
+ *
9
11
  * await isFile('package.json'); // true
10
12
  */
11
13
  export declare const isFile: (path: string) => Promise<boolean>;
@@ -16,6 +18,9 @@ export declare const isFile: (path: string) => Promise<boolean>;
16
18
  * @param options Node.js's writeFile options, such as the encoding or mode.
17
19
  * @returns A promise that resolves once the file is written.
18
20
  * @example
21
+ * import { writeFile } from 'jg-fs';
22
+ *
23
+ * const csv = 'id,name\n1,Ada\n';
19
24
  * await writeFile('out/report.csv', csv, { encoding: 'utf-8' });
20
25
  */
21
26
  export declare const writeFile: (path: string, data: string | NodeJS.ArrayBufferView, options?: WriteFileOptions) => Promise<void>;
@@ -25,6 +30,8 @@ export declare const writeFile: (path: string, data: string | NodeJS.ArrayBuffer
25
30
  * @param data The text.
26
31
  * @returns A promise that resolves once the file is written.
27
32
  * @example
33
+ * import { writeTextFile } from 'jg-fs';
34
+ *
28
35
  * await writeTextFile('notes/today.md', '# Today');
29
36
  * @throws
30
37
  * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the data isn't a string
@@ -37,17 +44,22 @@ export declare const writeTextFile: (path: string, data: string) => Promise<void
37
44
  * @param space The indentation for stringified values.
38
45
  * @returns A promise that resolves once the file is written.
39
46
  * @example
40
- * await writeJSONFile('config.json', { port: 8080 });
47
+ * import { writeJsonFile } from 'jg-fs';
48
+ *
49
+ * await writeJsonFile('config.json', { port: 8080 });
41
50
  * @throws
42
51
  * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the JSON content cannot be stringified or is empty
43
52
  */
44
- export declare const writeJSONFile: <T = Record<string, unknown>>(path: string, data: T | string, space?: number) => Promise<void>;
53
+ export declare const writeJsonFile: <T = Record<string, unknown>>(path: string, data: T | string, space?: number) => Promise<void>;
45
54
  /**
46
55
  * Writes a binary file. An empty Buffer writes an empty file.
47
56
  * @param path The file's path.
48
57
  * @param data The content.
49
58
  * @returns A promise that resolves once the file is written.
50
59
  * @example
60
+ * import { writeBufferFile } from 'jg-fs';
61
+ *
62
+ * const imageBuffer = Buffer.from('iVBORw0KGgo=', 'base64');
51
63
  * await writeBufferFile('avatar.png', imageBuffer);
52
64
  * @throws
53
65
  * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the data isn't a Buffer
@@ -59,6 +71,8 @@ export declare const writeBufferFile: (path: string, data: Buffer) => Promise<vo
59
71
  * @param options Node.js's readFile options, such as the encoding.
60
72
  * @returns A promise that resolves to the content.
61
73
  * @example
74
+ * import { readFile } from 'jg-fs';
75
+ *
62
76
  * await readFile('notes.md', 'utf-8'); // a string
63
77
  * await readFile('avatar.png'); // a Buffer
64
78
  * @throws
@@ -72,27 +86,39 @@ export declare function readFile(path: string, options?: IReadFileOptions): Prom
72
86
  * @param path The file's path.
73
87
  * @returns A promise that resolves to the text.
74
88
  * @example
89
+ * import { readTextFile } from 'jg-fs';
90
+ *
75
91
  * const markdown = await readTextFile('README.md');
76
92
  * @throws
77
93
  * - NOT_A_FILE: if the path doesn't exist or is not a file
78
94
  */
79
95
  export declare const readTextFile: (path: string) => Promise<string>;
80
96
  /**
81
- * Reads and parses a JSON file.
97
+ * Reads and parses a JSON file, and validates it with the schema when there is one. Without a
98
+ * schema, the value is `unknown`, or the type you pass.
82
99
  * @param path The file's path.
100
+ * @param options The schema, such as a Zod schema, whose errors propagate unchanged.
83
101
  * @returns A promise that resolves to the parsed value.
84
102
  * @example
85
- * const config = await readJSONFile<{ port: number }>('config.json');
103
+ * import { readJsonFile } from 'jg-fs';
104
+ *
105
+ * const config = await readJsonFile('config.json', { schema: Config }); // Config's type
106
+ * @example
107
+ * import { readJsonFile } from 'jg-fs';
108
+ *
109
+ * const value = await readJsonFile('data.json'); // unknown
86
110
  * @throws
87
111
  * - NOT_A_FILE: if the path doesn't exist or is not a file
88
112
  * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the file's JSON content cannot be parsed
89
113
  */
90
- export declare const readJSONFile: <T = Record<string, unknown>>(path: string) => Promise<T>;
114
+ export declare const readJsonFile: <T = unknown>(path: string, options?: IReadJsonFileOptions<T>) => Promise<T>;
91
115
  /**
92
116
  * Reads a binary file. An empty file reads as an empty Buffer.
93
117
  * @param path The file's path.
94
118
  * @returns A promise that resolves to the content.
95
119
  * @example
120
+ * import { readBufferFile } from 'jg-fs';
121
+ *
96
122
  * const image = await readBufferFile('avatar.png');
97
123
  * @throws
98
124
  * - NOT_A_FILE: if the path doesn't exist or is not a file
@@ -104,6 +130,8 @@ export declare const readBufferFile: (path: string) => Promise<Buffer>;
104
130
  * @param destPath Where to copy it.
105
131
  * @returns A promise that resolves once the copy exists.
106
132
  * @example
133
+ * import { copyFile } from 'jg-fs';
134
+ *
107
135
  * await copyFile('config.example.json', 'config.json');
108
136
  * @throws
109
137
  * - NOT_A_FILE: if the srcPath doesn't exist or is not a file
@@ -114,6 +142,8 @@ export declare const copyFile: (srcPath: string, destPath: string) => Promise<vo
114
142
  * @param path The file's path.
115
143
  * @returns A promise that resolves once the file is gone.
116
144
  * @example
145
+ * import { deleteFile } from 'jg-fs';
146
+ *
117
147
  * await deleteFile('cache.json');
118
148
  * @throws
119
149
  * - NOT_A_FILE: if the path doesn't exist or is not a file
@@ -125,6 +155,8 @@ export declare const deleteFile: (path: string) => Promise<void>;
125
155
  * @param path Where to create the link.
126
156
  * @returns A promise that resolves once the link exists.
127
157
  * @example
158
+ * import { createFileSymLink } from 'jg-fs';
159
+ *
128
160
  * await createFileSymLink('config.production.json', 'config.json');
129
161
  * @throws
130
162
  * - NON_EXISTENT_FILE: if the target file does not exist
@@ -143,8 +175,11 @@ export declare const createFileSymLink: (target: string, path: string) => Promis
143
175
  * @param mode The permission bits of the written file, such as `0o600`.
144
176
  * @returns A promise that resolves once the file is written and flushed.
145
177
  * @example
178
+ * import { writeFileAtomic } from 'jg-fs';
179
+ *
180
+ * const state = { step: 2 };
146
181
  * await writeFileAtomic('state.json', JSON.stringify(state)); // readers never see a partial file
147
- * await writeFileAtomic('secrets.env', contents, 0o600);
182
+ * await writeFileAtomic('secrets.env', 'TOKEN=abc\n', 0o600); // only the owner can read it
148
183
  */
149
184
  export declare const writeFileAtomic: (path: string, data: string | NodeJS.ArrayBufferView, mode?: number) => Promise<void>;
150
185
  /**
@@ -156,7 +191,10 @@ export declare const writeFileAtomic: (path: string, data: string | NodeJS.Array
156
191
  * @param maxBytes The largest size accepted, in bytes.
157
192
  * @returns A promise that resolves to the content.
158
193
  * @example
159
- * const upload = await readBoundedFile(join(uploadsDirectory, name), 5 * 1024 * 1024); // at most 5 MB
194
+ * import { join } from 'node:path';
195
+ * import { readBoundedFile } from 'jg-fs';
196
+ *
197
+ * const upload = await readBoundedFile(join('uploads', 'avatar.png'), 5 * 1024 * 1024); // at most 5 MB
160
198
  * @throws
161
199
  * - NOT_A_FILE: if the path doesn't exist, its final component is a symbolic link, or it isn't a regular file
162
200
  * - FILE_TOO_LARGE: if the file holds more than maxBytes bytes
@@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto';
3
3
  import { copyFile as copyFileNode, mkdir, open, readFile as readFileNode, rename, rm, symlink, unlink, writeFile as writeFileNode, } from 'node:fs/promises';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { Exception } from 'jg-exception';
6
- import { assertBufferData, assertTextData, BOUNDED_READ_CHUNK_SIZE, ERRORS, fileTooLarge, notAFile, parseJSONContent, serializeJSONData, } from '../../shared/index.js';
6
+ import { assertBufferData, assertTextData, BOUNDED_READ_CHUNK_SIZE, ERRORS, fileTooLarge, notAFile, parseJsonContent, serializeJsonData, } from '../../shared/index.js';
7
7
  import { getPathElement } from '../general/index.js';
8
8
  import { openWithoutFollowing, readPermissionBits, syncDirectory } from './utilities.js';
9
9
  /**
@@ -11,6 +11,8 @@ import { openWithoutFollowing, readPermissionBits, syncDirectory } from './utili
11
11
  * @param path The path.
12
12
  * @returns A promise that resolves to whether the path is a file.
13
13
  * @example
14
+ * import { isFile } from 'jg-fs';
15
+ *
14
16
  * await isFile('package.json'); // true
15
17
  */
16
18
  export const isFile = async (path) => (await getPathElement(path))?.isFile ?? false;
@@ -21,6 +23,9 @@ export const isFile = async (path) => (await getPathElement(path))?.isFile ?? fa
21
23
  * @param options Node.js's writeFile options, such as the encoding or mode.
22
24
  * @returns A promise that resolves once the file is written.
23
25
  * @example
26
+ * import { writeFile } from 'jg-fs';
27
+ *
28
+ * const csv = 'id,name\n1,Ada\n';
24
29
  * await writeFile('out/report.csv', csv, { encoding: 'utf-8' });
25
30
  */
26
31
  export const writeFile = async (path, data, options) => {
@@ -34,6 +39,8 @@ export const writeFile = async (path, data, options) => {
34
39
  * @param data The text.
35
40
  * @returns A promise that resolves once the file is written.
36
41
  * @example
42
+ * import { writeTextFile } from 'jg-fs';
43
+ *
37
44
  * await writeTextFile('notes/today.md', '# Today');
38
45
  * @throws
39
46
  * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the data isn't a string
@@ -49,12 +56,14 @@ export const writeTextFile = async (path, data) => {
49
56
  * @param space The indentation for stringified values.
50
57
  * @returns A promise that resolves once the file is written.
51
58
  * @example
52
- * await writeJSONFile('config.json', { port: 8080 });
59
+ * import { writeJsonFile } from 'jg-fs';
60
+ *
61
+ * await writeJsonFile('config.json', { port: 8080 });
53
62
  * @throws
54
63
  * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the JSON content cannot be stringified or is empty
55
64
  */
56
- export const writeJSONFile = async (path, data, space = 2) => {
57
- await writeFile(path, serializeJSONData(path, data, space), { encoding: 'utf-8' });
65
+ export const writeJsonFile = async (path, data, space = 2) => {
66
+ await writeFile(path, serializeJsonData(path, data, space), { encoding: 'utf-8' });
58
67
  };
59
68
  /**
60
69
  * Writes a binary file. An empty Buffer writes an empty file.
@@ -62,6 +71,9 @@ export const writeJSONFile = async (path, data, space = 2) => {
62
71
  * @param data The content.
63
72
  * @returns A promise that resolves once the file is written.
64
73
  * @example
74
+ * import { writeBufferFile } from 'jg-fs';
75
+ *
76
+ * const imageBuffer = Buffer.from('iVBORw0KGgo=', 'base64');
65
77
  * await writeBufferFile('avatar.png', imageBuffer);
66
78
  * @throws
67
79
  * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the data isn't a Buffer
@@ -81,27 +93,39 @@ export async function readFile(path, options = null) {
81
93
  * @param path The file's path.
82
94
  * @returns A promise that resolves to the text.
83
95
  * @example
96
+ * import { readTextFile } from 'jg-fs';
97
+ *
84
98
  * const markdown = await readTextFile('README.md');
85
99
  * @throws
86
100
  * - NOT_A_FILE: if the path doesn't exist or is not a file
87
101
  */
88
102
  export const readTextFile = (path) => readFile(path, { encoding: 'utf-8' });
89
103
  /**
90
- * Reads and parses a JSON file.
104
+ * Reads and parses a JSON file, and validates it with the schema when there is one. Without a
105
+ * schema, the value is `unknown`, or the type you pass.
91
106
  * @param path The file's path.
107
+ * @param options The schema, such as a Zod schema, whose errors propagate unchanged.
92
108
  * @returns A promise that resolves to the parsed value.
93
109
  * @example
94
- * const config = await readJSONFile<{ port: number }>('config.json');
110
+ * import { readJsonFile } from 'jg-fs';
111
+ *
112
+ * const config = await readJsonFile('config.json', { schema: Config }); // Config's type
113
+ * @example
114
+ * import { readJsonFile } from 'jg-fs';
115
+ *
116
+ * const value = await readJsonFile('data.json'); // unknown
95
117
  * @throws
96
118
  * - NOT_A_FILE: if the path doesn't exist or is not a file
97
119
  * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the file's JSON content cannot be parsed
98
120
  */
99
- export const readJSONFile = async (path) => parseJSONContent(path, await readTextFile(path));
121
+ export const readJsonFile = async (path, options = {}) => parseJsonContent(path, await readTextFile(path), options.schema);
100
122
  /**
101
123
  * Reads a binary file. An empty file reads as an empty Buffer.
102
124
  * @param path The file's path.
103
125
  * @returns A promise that resolves to the content.
104
126
  * @example
127
+ * import { readBufferFile } from 'jg-fs';
128
+ *
105
129
  * const image = await readBufferFile('avatar.png');
106
130
  * @throws
107
131
  * - NOT_A_FILE: if the path doesn't exist or is not a file
@@ -113,6 +137,8 @@ export const readBufferFile = (path) => readFile(path, null);
113
137
  * @param destPath Where to copy it.
114
138
  * @returns A promise that resolves once the copy exists.
115
139
  * @example
140
+ * import { copyFile } from 'jg-fs';
141
+ *
116
142
  * await copyFile('config.example.json', 'config.json');
117
143
  * @throws
118
144
  * - NOT_A_FILE: if the srcPath doesn't exist or is not a file
@@ -128,6 +154,8 @@ export const copyFile = async (srcPath, destPath) => {
128
154
  * @param path The file's path.
129
155
  * @returns A promise that resolves once the file is gone.
130
156
  * @example
157
+ * import { deleteFile } from 'jg-fs';
158
+ *
131
159
  * await deleteFile('cache.json');
132
160
  * @throws
133
161
  * - NOT_A_FILE: if the path doesn't exist or is not a file
@@ -144,6 +172,8 @@ export const deleteFile = async (path) => {
144
172
  * @param path Where to create the link.
145
173
  * @returns A promise that resolves once the link exists.
146
174
  * @example
175
+ * import { createFileSymLink } from 'jg-fs';
176
+ *
147
177
  * await createFileSymLink('config.production.json', 'config.json');
148
178
  * @throws
149
179
  * - NON_EXISTENT_FILE: if the target file does not exist
@@ -152,7 +182,9 @@ export const deleteFile = async (path) => {
152
182
  export const createFileSymLink = async (target, path) => {
153
183
  const element = await getPathElement(target);
154
184
  if (element === null) {
155
- throw new Exception(`The target file '${target}' does not exist.`, ERRORS.NON_EXISTENT_FILE);
185
+ throw new Exception(`The target file '${target}' does not exist.`, {
186
+ code: ERRORS.NON_EXISTENT_FILE,
187
+ });
156
188
  }
157
189
  if (!element.isFile) {
158
190
  throw notAFile(target);
@@ -171,8 +203,11 @@ export const createFileSymLink = async (target, path) => {
171
203
  * @param mode The permission bits of the written file, such as `0o600`.
172
204
  * @returns A promise that resolves once the file is written and flushed.
173
205
  * @example
206
+ * import { writeFileAtomic } from 'jg-fs';
207
+ *
208
+ * const state = { step: 2 };
174
209
  * await writeFileAtomic('state.json', JSON.stringify(state)); // readers never see a partial file
175
- * await writeFileAtomic('secrets.env', contents, 0o600);
210
+ * await writeFileAtomic('secrets.env', 'TOKEN=abc\n', 0o600); // only the owner can read it
176
211
  */
177
212
  export const writeFileAtomic = async (path, data, mode) => {
178
213
  const directory = dirname(path);
@@ -214,7 +249,10 @@ export const writeFileAtomic = async (path, data, mode) => {
214
249
  * @param maxBytes The largest size accepted, in bytes.
215
250
  * @returns A promise that resolves to the content.
216
251
  * @example
217
- * const upload = await readBoundedFile(join(uploadsDirectory, name), 5 * 1024 * 1024); // at most 5 MB
252
+ * import { join } from 'node:path';
253
+ * import { readBoundedFile } from 'jg-fs';
254
+ *
255
+ * const upload = await readBoundedFile(join('uploads', 'avatar.png'), 5 * 1024 * 1024); // at most 5 MB
218
256
  * @throws
219
257
  * - NOT_A_FILE: if the path doesn't exist, its final component is a symbolic link, or it isn't a regular file
220
258
  * - FILE_TOO_LARGE: if the file holds more than maxBytes bytes