jg-fs 0.0.0-stage → 1.0.1

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 (76) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +73 -2
  4. package/dist/async/directories/directories.d.ts +78 -0
  5. package/dist/async/directories/directories.js +118 -0
  6. package/dist/async/directories/directories.js.map +1 -0
  7. package/dist/async/directories/index.d.ts +1 -0
  8. package/dist/async/directories/index.js +3 -0
  9. package/dist/async/directories/index.js.map +1 -0
  10. package/dist/async/directories/utilities.d.ts +9 -0
  11. package/dist/async/directories/utilities.js +22 -0
  12. package/dist/async/directories/utilities.js.map +1 -0
  13. package/dist/async/files/files.d.ts +164 -0
  14. package/dist/async/files/files.js +252 -0
  15. package/dist/async/files/files.js.map +1 -0
  16. package/dist/async/files/index.d.ts +1 -0
  17. package/dist/async/files/index.js +3 -0
  18. package/dist/async/files/index.js.map +1 -0
  19. package/dist/async/files/utilities.d.ts +21 -0
  20. package/dist/async/files/utilities.js +59 -0
  21. package/dist/async/files/utilities.js.map +1 -0
  22. package/dist/async/general/general.d.ts +18 -0
  23. package/dist/async/general/general.js +35 -0
  24. package/dist/async/general/general.js.map +1 -0
  25. package/dist/async/general/index.d.ts +1 -0
  26. package/dist/async/general/index.js +3 -0
  27. package/dist/async/general/index.js.map +1 -0
  28. package/dist/index.d.ts +6 -0
  29. package/dist/index.js +10 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/shared/constants.d.ts +4 -0
  32. package/dist/shared/constants.js +11 -0
  33. package/dist/shared/constants.js.map +1 -0
  34. package/dist/shared/directory-elements.d.ts +16 -0
  35. package/dist/shared/directory-elements.js +44 -0
  36. package/dist/shared/directory-elements.js.map +1 -0
  37. package/dist/shared/errors.d.ts +9 -0
  38. package/dist/shared/errors.js +11 -0
  39. package/dist/shared/errors.js.map +1 -0
  40. package/dist/shared/index.d.ts +6 -0
  41. package/dist/shared/index.js +11 -0
  42. package/dist/shared/index.js.map +1 -0
  43. package/dist/shared/paths.d.ts +23 -0
  44. package/dist/shared/paths.js +36 -0
  45. package/dist/shared/paths.js.map +1 -0
  46. package/dist/shared/types.d.ts +31 -0
  47. package/dist/shared/types.js +2 -0
  48. package/dist/shared/types.js.map +1 -0
  49. package/dist/shared/validations.d.ts +76 -0
  50. package/dist/shared/validations.js +112 -0
  51. package/dist/shared/validations.js.map +1 -0
  52. package/dist/sync/directories/directories.d.ts +73 -0
  53. package/dist/sync/directories/directories.js +112 -0
  54. package/dist/sync/directories/directories.js.map +1 -0
  55. package/dist/sync/directories/index.d.ts +1 -0
  56. package/dist/sync/directories/index.js +3 -0
  57. package/dist/sync/directories/index.js.map +1 -0
  58. package/dist/sync/files/files.d.ts +156 -0
  59. package/dist/sync/files/files.js +243 -0
  60. package/dist/sync/files/files.js.map +1 -0
  61. package/dist/sync/files/index.d.ts +1 -0
  62. package/dist/sync/files/index.js +3 -0
  63. package/dist/sync/files/index.js.map +1 -0
  64. package/dist/sync/files/utilities.d.ts +19 -0
  65. package/dist/sync/files/utilities.js +58 -0
  66. package/dist/sync/files/utilities.js.map +1 -0
  67. package/dist/sync/general/general.d.ts +18 -0
  68. package/dist/sync/general/general.js +35 -0
  69. package/dist/sync/general/general.js.map +1 -0
  70. package/dist/sync/general/index.d.ts +1 -0
  71. package/dist/sync/general/index.js +3 -0
  72. package/dist/sync/general/index.js.map +1 -0
  73. package/dist/sync/index.d.ts +6 -0
  74. package/dist/sync/index.js +10 -0
  75. package/dist/sync/index.js.map +1 -0
  76. package/package.json +67 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # Changelog
2
+
3
+ ## 1.0.1
4
+
5
+ ### Changed
6
+
7
+ - Published by CI from `jesusgraterol/jg-packages`, with npm provenance. The code is the same as 1.0.0.
8
+
9
+ ## 1.0.0
10
+
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.
12
+
13
+ ### Added
14
+
15
+ - The `jg-fs/sync` entry, with the same functions as the async root entry.
16
+ - `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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jesus Graterol
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,74 @@
1
- # Temporary Holding Version
1
+ # jg-fs
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Filesystem helpers for Node.js, async at the root and sync at `jg-fs/sync`: paths, directories, text, JSON, atomic and bounded files.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install jg-fs
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ The root entry is async, and `jg-fs/sync` has the same functions, synchronous:
14
+
15
+ ```ts
16
+ import { readJSONFile, writeFileAtomic, writeTextFile } from 'jg-fs';
17
+ import { readTextFile } from 'jg-fs/sync';
18
+
19
+ await writeTextFile('notes/today.md', '# Today'); // creates notes/ when it's missing
20
+ const config = await readJSONFile<{ port: number }>('config.json');
21
+ await writeFileAtomic('state.json', JSON.stringify(state)); // readers never see a partial file
22
+
23
+ const readme = readTextFile('README.md');
24
+ ```
25
+
26
+ List, filter, and sort a directory:
27
+
28
+ ```ts
29
+ import { getDirectoryElements } from 'jg-fs';
30
+
31
+ const { files, directories } = await getDirectoryElements('content', {
32
+ includeExts: ['.md'],
33
+ sortByKey: 'creation',
34
+ sortOrder: 'desc',
35
+ });
36
+ ```
37
+
38
+ Read files from untrusted paths with a size limit, without following a symbolic link:
39
+
40
+ ```ts
41
+ import { isPathWithin, readBoundedFile } from 'jg-fs';
42
+
43
+ if (isPathWithin(uploadsDirectory, path)) {
44
+ const upload = await readBoundedFile(path, 5 * 1024 * 1024);
45
+ }
46
+ ```
47
+
48
+ Failures throw `Exception`s from `jg-exception`, with the codes in `ERRORS`.
49
+
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
+ ## Documentation
69
+
70
+ The full API reference is at https://jg-packages.jesusgraterol.dev/packages/jg-fs/.
71
+
72
+ ## License
73
+
74
+ [MIT](https://github.com/jesusgraterol/jg-packages/blob/main/packages/jg-fs/LICENSE)
@@ -0,0 +1,78 @@
1
+ import { type IDirectoryElementsOptions, type IDirectoryPathElements } from '../../shared/index.js';
2
+ /**
3
+ * Checks whether a path exists and is a directory.
4
+ * @param path The path.
5
+ * @returns A promise that resolves to whether the path is a directory.
6
+ * @example
7
+ * await isDirectory('dist'); // true
8
+ */
9
+ export declare const isDirectory: (path: string) => Promise<boolean>;
10
+ /**
11
+ * Deletes a directory and everything in it. A missing directory is not an error.
12
+ * @param path The directory's path.
13
+ * @returns A promise that resolves once the directory is gone.
14
+ * @example
15
+ * await deleteDirectory('dist');
16
+ */
17
+ export declare const deleteDirectory: (path: string) => Promise<void>;
18
+ /**
19
+ * Creates a directory, with any missing parents. An existing directory is kept, or replaced with
20
+ * an empty one when `deleteIfExists` is set.
21
+ * @param path The directory's path.
22
+ * @param deleteIfExists Whether to delete an existing directory and its contents first.
23
+ * @returns A promise that resolves once the directory exists.
24
+ * @example
25
+ * await createDirectory('dist/assets'); // creates missing parents too
26
+ * await createDirectory('dist', true); // replaces it with an empty directory
27
+ * @throws
28
+ * - DIRECTORY_ALREADY_EXISTS: if something other than a directory exists at the path
29
+ */
30
+ export declare const createDirectory: (path: string, deleteIfExists?: boolean) => Promise<void>;
31
+ /**
32
+ * Copies a directory and everything in it, replacing the destination completely.
33
+ * @param srcPath The directory to copy.
34
+ * @param destPath Where to copy it.
35
+ * @returns A promise that resolves once the copy is complete.
36
+ * @example
37
+ * await copyDirectory('templates', 'dist/templates');
38
+ * @throws
39
+ * - NOT_A_DIRECTORY: if the srcPath is not a directory
40
+ */
41
+ export declare const copyDirectory: (srcPath: string, destPath: string) => Promise<void>;
42
+ /**
43
+ * Creates a symbolic link to a directory.
44
+ * @param target The directory the link points to.
45
+ * @param path Where to create the link.
46
+ * @returns A promise that resolves once the link exists.
47
+ * @example
48
+ * await createDirectorySymLink('releases/1.4.0', 'current');
49
+ * @throws
50
+ * - NON_EXISTENT_DIRECTORY: if the target directory doesn't exist
51
+ * - NOT_A_DIRECTORY: if the target directory is not considered a directory by the OS
52
+ */
53
+ export declare const createDirectorySymLink: (target: string, path: string) => Promise<void>;
54
+ /**
55
+ * Lists a directory's contents as paths joined to the directory's path.
56
+ * @param path The directory's path.
57
+ * @param recursive Whether to include the contents of subdirectories.
58
+ * @returns A promise that resolves to the content paths.
59
+ * @example
60
+ * await readDirectory('dist'); // ['dist/index.js', 'dist/assets']
61
+ * await readDirectory('dist', true); // includes the contents of dist/assets
62
+ * @throws
63
+ * - NOT_A_DIRECTORY: if the path doesn't exist or is not a directory
64
+ */
65
+ export declare const readDirectory: (path: string, recursive?: boolean) => Promise<string[]>;
66
+ /**
67
+ * Lists a directory's elements grouped by kind: directories, files (filtered by extension), and
68
+ * symbolic links, each group sorted. Entries removed while the directory is read are left out, and
69
+ * at most 32 entries are read at once.
70
+ * @param path The directory's path.
71
+ * @param options The extensions to include and how to sort.
72
+ * @returns A promise that resolves to the grouped elements.
73
+ * @example
74
+ * const { files } = await getDirectoryElements('content', { includeExts: ['.md'], sortByKey: 'creation', sortOrder: 'desc' });
75
+ * @throws
76
+ * - NOT_A_DIRECTORY: if the path doesn't exist or is not a directory
77
+ */
78
+ export declare const getDirectoryElements: (path: string, options?: Partial<IDirectoryElementsOptions>) => Promise<IDirectoryPathElements>;
@@ -0,0 +1,118 @@
1
+ import { cp, mkdir, readdir, rm, symlink } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { Exception } from 'jg-exception';
4
+ import { buildDirectoryElementsOptions, DIRECTORY_ELEMENTS_CONCURRENCY, ERRORS, groupDirectoryElements, notADirectory, } from '../../shared/index.js';
5
+ import { getPathElement } from '../general/index.js';
6
+ import { mapWithConcurrency } from './utilities.js';
7
+ /**
8
+ * Checks whether a path exists and is a directory.
9
+ * @param path The path.
10
+ * @returns A promise that resolves to whether the path is a directory.
11
+ * @example
12
+ * await isDirectory('dist'); // true
13
+ */
14
+ export const isDirectory = async (path) => (await getPathElement(path))?.isDirectory ?? false;
15
+ /**
16
+ * Deletes a directory and everything in it. A missing directory is not an error.
17
+ * @param path The directory's path.
18
+ * @returns A promise that resolves once the directory is gone.
19
+ * @example
20
+ * await deleteDirectory('dist');
21
+ */
22
+ export const deleteDirectory = (path) => rm(path, { recursive: true, force: true });
23
+ /**
24
+ * Creates a directory, with any missing parents. An existing directory is kept, or replaced with
25
+ * an empty one when `deleteIfExists` is set.
26
+ * @param path The directory's path.
27
+ * @param deleteIfExists Whether to delete an existing directory and its contents first.
28
+ * @returns A promise that resolves once the directory exists.
29
+ * @example
30
+ * await createDirectory('dist/assets'); // creates missing parents too
31
+ * await createDirectory('dist', true); // replaces it with an empty directory
32
+ * @throws
33
+ * - DIRECTORY_ALREADY_EXISTS: if something other than a directory exists at the path
34
+ */
35
+ export const createDirectory = async (path, deleteIfExists) => {
36
+ const element = await getPathElement(path);
37
+ if (element !== null && !element.isDirectory) {
38
+ throw new Exception(`The path '${path}' already exists and is not a directory.`, ERRORS.DIRECTORY_ALREADY_EXISTS);
39
+ }
40
+ if (element !== null && deleteIfExists) {
41
+ await deleteDirectory(path);
42
+ }
43
+ await mkdir(path, { recursive: true });
44
+ };
45
+ /**
46
+ * Copies a directory and everything in it, replacing the destination completely.
47
+ * @param srcPath The directory to copy.
48
+ * @param destPath Where to copy it.
49
+ * @returns A promise that resolves once the copy is complete.
50
+ * @example
51
+ * await copyDirectory('templates', 'dist/templates');
52
+ * @throws
53
+ * - NOT_A_DIRECTORY: if the srcPath is not a directory
54
+ */
55
+ export const copyDirectory = async (srcPath, destPath) => {
56
+ if (!(await isDirectory(srcPath))) {
57
+ throw notADirectory(srcPath);
58
+ }
59
+ await deleteDirectory(destPath);
60
+ await cp(srcPath, destPath, { recursive: true });
61
+ };
62
+ /**
63
+ * Creates a symbolic link to a directory.
64
+ * @param target The directory the link points to.
65
+ * @param path Where to create the link.
66
+ * @returns A promise that resolves once the link exists.
67
+ * @example
68
+ * await createDirectorySymLink('releases/1.4.0', 'current');
69
+ * @throws
70
+ * - NON_EXISTENT_DIRECTORY: if the target directory doesn't exist
71
+ * - NOT_A_DIRECTORY: if the target directory is not considered a directory by the OS
72
+ */
73
+ export const createDirectorySymLink = async (target, path) => {
74
+ const element = await getPathElement(target);
75
+ if (element === null) {
76
+ throw new Exception(`The target directory '${target}' does not exist.`, ERRORS.NON_EXISTENT_DIRECTORY);
77
+ }
78
+ if (!element.isDirectory) {
79
+ throw notADirectory(target);
80
+ }
81
+ await symlink(target, path, 'dir');
82
+ };
83
+ /**
84
+ * Lists a directory's contents as paths joined to the directory's path.
85
+ * @param path The directory's path.
86
+ * @param recursive Whether to include the contents of subdirectories.
87
+ * @returns A promise that resolves to the content paths.
88
+ * @example
89
+ * await readDirectory('dist'); // ['dist/index.js', 'dist/assets']
90
+ * await readDirectory('dist', true); // includes the contents of dist/assets
91
+ * @throws
92
+ * - NOT_A_DIRECTORY: if the path doesn't exist or is not a directory
93
+ */
94
+ export const readDirectory = async (path, recursive = false) => {
95
+ if (!(await isDirectory(path))) {
96
+ throw notADirectory(path);
97
+ }
98
+ const contentPaths = await readdir(path, { encoding: 'utf-8', recursive });
99
+ return contentPaths.map((contentPath) => join(path, contentPath));
100
+ };
101
+ /**
102
+ * Lists a directory's elements grouped by kind: directories, files (filtered by extension), and
103
+ * symbolic links, each group sorted. Entries removed while the directory is read are left out, and
104
+ * at most 32 entries are read at once.
105
+ * @param path The directory's path.
106
+ * @param options The extensions to include and how to sort.
107
+ * @returns A promise that resolves to the grouped elements.
108
+ * @example
109
+ * const { files } = await getDirectoryElements('content', { includeExts: ['.md'], sortByKey: 'creation', sortOrder: 'desc' });
110
+ * @throws
111
+ * - NOT_A_DIRECTORY: if the path doesn't exist or is not a directory
112
+ */
113
+ export const getDirectoryElements = async (path, options) => {
114
+ const contentPaths = await readDirectory(path);
115
+ const elements = (await mapWithConcurrency(contentPaths, DIRECTORY_ELEMENTS_CONCURRENCY, getPathElement)).filter((element) => element !== null);
116
+ return groupDirectoryElements(elements, buildDirectoryElementsOptions(options));
117
+ };
118
+ //# sourceMappingURL=directories.js.map
@@ -0,0 +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"]}
@@ -0,0 +1 @@
1
+ export { copyDirectory, createDirectory, createDirectorySymLink, deleteDirectory, getDirectoryElements, isDirectory, readDirectory, } from './directories.js';
@@ -0,0 +1,3 @@
1
+ // directory actions
2
+ export { copyDirectory, createDirectory, createDirectorySymLink, deleteDirectory, getDirectoryElements, isDirectory, readDirectory, } from './directories.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/async/directories/index.ts"],"names":[],"mappings":"AAAA,oBAAoB;AACpB,OAAO,EACL,aAAa,EACb,eAAe,EACf,sBAAsB,EACtB,eAAe,EACf,oBAAoB,EACpB,WAAW,EACX,aAAa,GACd,MAAM,kBAAkB,CAAC","sourcesContent":["// directory actions\nexport {\n copyDirectory,\n createDirectory,\n createDirectorySymLink,\n deleteDirectory,\n getDirectoryElements,\n isDirectory,\n readDirectory,\n} from './directories.js';\n"]}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Maps items with an async function, running at most a fixed number of calls at once, and keeps
3
+ * the results in the items' order.
4
+ * @param items The items.
5
+ * @param concurrency The most calls running at once.
6
+ * @param mapper The async function.
7
+ * @returns A promise that resolves to the results.
8
+ */
9
+ export declare const mapWithConcurrency: <TItem, TResult>(items: TItem[], concurrency: number, mapper: (item: TItem) => Promise<TResult>) => Promise<TResult[]>;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Maps items with an async function, running at most a fixed number of calls at once, and keeps
3
+ * the results in the items' order.
4
+ * @param items The items.
5
+ * @param concurrency The most calls running at once.
6
+ * @param mapper The async function.
7
+ * @returns A promise that resolves to the results.
8
+ */
9
+ export const mapWithConcurrency = async (items, concurrency, mapper) => {
10
+ const results = new Array(items.length);
11
+ let nextIndex = 0;
12
+ const worker = async () => {
13
+ while (nextIndex < items.length) {
14
+ const index = nextIndex;
15
+ nextIndex += 1;
16
+ results[index] = await mapper(items[index]);
17
+ }
18
+ };
19
+ await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker));
20
+ return results;
21
+ };
22
+ //# sourceMappingURL=utilities.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utilities.js","sourceRoot":"","sources":["../../../src/async/directories/utilities.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,EACrC,KAAc,EACd,WAAmB,EACnB,MAAyC,EACrB,EAAE;IACtB,MAAM,OAAO,GAAc,IAAI,KAAK,CAAU,KAAK,CAAC,MAAM,CAAC,CAAC;IAC5D,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,MAAM,MAAM,GAAG,KAAK,IAAmB,EAAE;QACvC,OAAO,SAAS,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;YAChC,MAAM,KAAK,GAAG,SAAS,CAAC;YACxB,SAAS,IAAI,CAAC,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,KAAK,CAAU,CAAC,CAAC;QACvD,CAAC;IACH,CAAC,CAAC;IACF,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;IACvF,OAAO,OAAO,CAAC;AACjB,CAAC,CAAC","sourcesContent":["/**\n * Maps items with an async function, running at most a fixed number of calls at once, and keeps\n * the results in the items' order.\n * @param items The items.\n * @param concurrency The most calls running at once.\n * @param mapper The async function.\n * @returns A promise that resolves to the results.\n */\nexport const mapWithConcurrency = async <TItem, TResult>(\n items: TItem[],\n concurrency: number,\n mapper: (item: TItem) => Promise<TResult>,\n): Promise<TResult[]> => {\n const results: TResult[] = new Array<TResult>(items.length);\n let nextIndex = 0;\n const worker = async (): Promise<void> => {\n while (nextIndex < items.length) {\n const index = nextIndex;\n nextIndex += 1;\n results[index] = await mapper(items[index] as TItem);\n }\n };\n await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker));\n return results;\n};\n"]}
@@ -0,0 +1,164 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import type { WriteFileOptions } from 'node:fs';
3
+ import { type IReadBufferFileOptions, type IReadFileOptions, type IReadStringFileOptions } from '../../shared/index.js';
4
+ /**
5
+ * Checks whether a path exists and is a regular file.
6
+ * @param path The path.
7
+ * @returns A promise that resolves to whether the path is a file.
8
+ * @example
9
+ * await isFile('package.json'); // true
10
+ */
11
+ export declare const isFile: (path: string) => Promise<boolean>;
12
+ /**
13
+ * Writes a file, creating its directory first when it doesn't exist.
14
+ * @param path The file's path.
15
+ * @param data The content.
16
+ * @param options Node.js's writeFile options, such as the encoding or mode.
17
+ * @returns A promise that resolves once the file is written.
18
+ * @example
19
+ * await writeFile('out/report.csv', csv, { encoding: 'utf-8' });
20
+ */
21
+ export declare const writeFile: (path: string, data: string | NodeJS.ArrayBufferView, options?: WriteFileOptions) => Promise<void>;
22
+ /**
23
+ * Writes a UTF-8 text file. An empty string writes an empty file.
24
+ * @param path The file's path.
25
+ * @param data The text.
26
+ * @returns A promise that resolves once the file is written.
27
+ * @example
28
+ * await writeTextFile('notes/today.md', '# Today');
29
+ * @throws
30
+ * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the data isn't a string
31
+ */
32
+ export declare const writeTextFile: (path: string, data: string) => Promise<void>;
33
+ /**
34
+ * Writes a JSON file. A string is written as given; anything else is stringified.
35
+ * @param path The file's path.
36
+ * @param data The value, or its JSON text.
37
+ * @param space The indentation for stringified values.
38
+ * @returns A promise that resolves once the file is written.
39
+ * @example
40
+ * await writeJSONFile('config.json', { port: 8080 });
41
+ * @throws
42
+ * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the JSON content cannot be stringified or is empty
43
+ */
44
+ export declare const writeJSONFile: <T = Record<string, unknown>>(path: string, data: T | string, space?: number) => Promise<void>;
45
+ /**
46
+ * Writes a binary file. An empty Buffer writes an empty file.
47
+ * @param path The file's path.
48
+ * @param data The content.
49
+ * @returns A promise that resolves once the file is written.
50
+ * @example
51
+ * await writeBufferFile('avatar.png', imageBuffer);
52
+ * @throws
53
+ * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the data isn't a Buffer
54
+ */
55
+ export declare const writeBufferFile: (path: string, data: Buffer) => Promise<void>;
56
+ /**
57
+ * Reads a file as a string when an encoding is given, or as a Buffer otherwise.
58
+ * @param path The file's path.
59
+ * @param options Node.js's readFile options, such as the encoding.
60
+ * @returns A promise that resolves to the content.
61
+ * @example
62
+ * await readFile('notes.md', 'utf-8'); // a string
63
+ * await readFile('avatar.png'); // a Buffer
64
+ * @throws
65
+ * - NOT_A_FILE: if the path doesn't exist or is not a file
66
+ */
67
+ export declare function readFile(path: string, options: IReadStringFileOptions): Promise<string>;
68
+ export declare function readFile(path: string, options?: IReadBufferFileOptions): Promise<Buffer>;
69
+ export declare function readFile(path: string, options?: IReadFileOptions): Promise<string | Buffer>;
70
+ /**
71
+ * Reads a UTF-8 text file. An empty file reads as an empty string.
72
+ * @param path The file's path.
73
+ * @returns A promise that resolves to the text.
74
+ * @example
75
+ * const markdown = await readTextFile('README.md');
76
+ * @throws
77
+ * - NOT_A_FILE: if the path doesn't exist or is not a file
78
+ */
79
+ export declare const readTextFile: (path: string) => Promise<string>;
80
+ /**
81
+ * Reads and parses a JSON file.
82
+ * @param path The file's path.
83
+ * @returns A promise that resolves to the parsed value.
84
+ * @example
85
+ * const config = await readJSONFile<{ port: number }>('config.json');
86
+ * @throws
87
+ * - NOT_A_FILE: if the path doesn't exist or is not a file
88
+ * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the file's JSON content cannot be parsed
89
+ */
90
+ export declare const readJSONFile: <T = Record<string, unknown>>(path: string) => Promise<T>;
91
+ /**
92
+ * Reads a binary file. An empty file reads as an empty Buffer.
93
+ * @param path The file's path.
94
+ * @returns A promise that resolves to the content.
95
+ * @example
96
+ * const image = await readBufferFile('avatar.png');
97
+ * @throws
98
+ * - NOT_A_FILE: if the path doesn't exist or is not a file
99
+ */
100
+ export declare const readBufferFile: (path: string) => Promise<Buffer>;
101
+ /**
102
+ * Copies a file, replacing the destination when it exists.
103
+ * @param srcPath The file to copy.
104
+ * @param destPath Where to copy it.
105
+ * @returns A promise that resolves once the copy exists.
106
+ * @example
107
+ * await copyFile('config.example.json', 'config.json');
108
+ * @throws
109
+ * - NOT_A_FILE: if the srcPath doesn't exist or is not a file
110
+ */
111
+ export declare const copyFile: (srcPath: string, destPath: string) => Promise<void>;
112
+ /**
113
+ * Deletes a file.
114
+ * @param path The file's path.
115
+ * @returns A promise that resolves once the file is gone.
116
+ * @example
117
+ * await deleteFile('cache.json');
118
+ * @throws
119
+ * - NOT_A_FILE: if the path doesn't exist or is not a file
120
+ */
121
+ export declare const deleteFile: (path: string) => Promise<void>;
122
+ /**
123
+ * Creates a symbolic link to a file.
124
+ * @param target The file the link points to.
125
+ * @param path Where to create the link.
126
+ * @returns A promise that resolves once the link exists.
127
+ * @example
128
+ * await createFileSymLink('config.production.json', 'config.json');
129
+ * @throws
130
+ * - NON_EXISTENT_FILE: if the target file does not exist
131
+ * - NOT_A_FILE: if the target is not a file
132
+ */
133
+ export declare const createFileSymLink: (target: string, path: string) => Promise<void>;
134
+ /**
135
+ * Writes a file atomically: readers see either the previous content or the complete new content,
136
+ * never a partial write. The data goes to a temporary file in the same directory, which is flushed
137
+ * to disk and renamed over the path; on POSIX, the directory is flushed too. An explicit mode wins;
138
+ * without one, a replaced file keeps its permission bits and a new file gets the platform default.
139
+ * The temporary file is never more widely readable than the final mode. When writing fails before
140
+ * the rename, the temporary file is removed and the destination is untouched.
141
+ * @param path The file's path.
142
+ * @param data The content.
143
+ * @param mode The permission bits of the written file, such as `0o600`.
144
+ * @returns A promise that resolves once the file is written and flushed.
145
+ * @example
146
+ * await writeFileAtomic('state.json', JSON.stringify(state)); // readers never see a partial file
147
+ * await writeFileAtomic('secrets.env', contents, 0o600);
148
+ */
149
+ export declare const writeFileAtomic: (path: string, data: string | NodeJS.ArrayBufferView, mode?: number) => Promise<void>;
150
+ /**
151
+ * Reads a regular file of at most `maxBytes` bytes. It never follows a symbolic link in the
152
+ * path's last component, though links in parent directories are followed, so callers that need
153
+ * containment combine it with `isPathWithin`. It reads at most `maxBytes + 1` bytes, even when the
154
+ * file grows while it's read.
155
+ * @param path The file's path.
156
+ * @param maxBytes The largest size accepted, in bytes.
157
+ * @returns A promise that resolves to the content.
158
+ * @example
159
+ * const upload = await readBoundedFile(join(uploadsDirectory, name), 5 * 1024 * 1024); // at most 5 MB
160
+ * @throws
161
+ * - NOT_A_FILE: if the path doesn't exist, its final component is a symbolic link, or it isn't a regular file
162
+ * - FILE_TOO_LARGE: if the file holds more than maxBytes bytes
163
+ */
164
+ export declare const readBoundedFile: (path: string, maxBytes: number) => Promise<Buffer>;