jg-fs 1.0.1 → 1.1.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 (46) hide show
  1. package/API.md +1716 -0
  2. package/CHANGELOG.md +26 -16
  3. package/README.md +8 -21
  4. package/RECIPES.md +125 -0
  5. package/dist/async/directories/directories.d.ts +14 -0
  6. package/dist/async/directories/directories.js +20 -2
  7. package/dist/async/directories/directories.js.map +1 -1
  8. package/dist/async/files/files.d.ts +46 -8
  9. package/dist/async/files/files.js +48 -10
  10. package/dist/async/files/files.js.map +1 -1
  11. package/dist/async/files/index.d.ts +1 -1
  12. package/dist/async/files/index.js +1 -1
  13. package/dist/async/files/index.js.map +1 -1
  14. package/dist/async/general/general.d.ts +4 -0
  15. package/dist/async/general/general.js +4 -0
  16. package/dist/async/general/general.js.map +1 -1
  17. package/dist/index.d.ts +2 -2
  18. package/dist/index.js +1 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/shared/index.d.ts +2 -2
  21. package/dist/shared/index.js +1 -1
  22. package/dist/shared/index.js.map +1 -1
  23. package/dist/shared/paths.d.ts +2 -0
  24. package/dist/shared/paths.js +2 -0
  25. package/dist/shared/paths.js.map +1 -1
  26. package/dist/shared/types.d.ts +6 -0
  27. package/dist/shared/types.js.map +1 -1
  28. package/dist/shared/validations.d.ts +5 -3
  29. package/dist/shared/validations.js +30 -14
  30. package/dist/shared/validations.js.map +1 -1
  31. package/dist/sync/directories/directories.d.ts +14 -0
  32. package/dist/sync/directories/directories.js +20 -2
  33. package/dist/sync/directories/directories.js.map +1 -1
  34. package/dist/sync/files/files.d.ts +46 -8
  35. package/dist/sync/files/files.js +48 -10
  36. package/dist/sync/files/files.js.map +1 -1
  37. package/dist/sync/files/index.d.ts +1 -1
  38. package/dist/sync/files/index.js +1 -1
  39. package/dist/sync/files/index.js.map +1 -1
  40. package/dist/sync/general/general.d.ts +4 -0
  41. package/dist/sync/general/general.js +4 -0
  42. package/dist/sync/general/general.js.map +1 -1
  43. package/dist/sync/index.d.ts +2 -2
  44. package/dist/sync/index.js +1 -1
  45. package/dist/sync/index.js.map +1 -1
  46. package/package.json +11 -6
package/CHANGELOG.md CHANGED
@@ -1,30 +1,40 @@
1
1
  # Changelog
2
2
 
3
- ## 1.0.1
3
+ ## 1.1.1
4
4
 
5
- ### Changed
5
+ ### Added
6
6
 
7
- - Published by CI from `jesusgraterol/jg-packages`, with npm provenance. The code is the same as 1.0.0.
7
+ - `RECIPES.md`, patterns for apps that use the package: a config file that's never half-written, and reading uploads with a size limit. Each block is typechecked against the release.
8
8
 
9
- ## 1.0.0
9
+ ## 1.1.0
10
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.
11
+ A fresh start: breaking changes in a minor version, once, because nothing depended on 1.0.x.
12
12
 
13
13
  ### Added
14
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.
15
+ - 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.
16
+ - `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.
17
17
 
18
18
  ### Changed
19
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`.
20
+ - 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`.
21
+ - 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.
22
+ - `readJsonFile` returns `unknown` without a schema or a type argument, instead of `Record<string, unknown>`.
23
+ - `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.
24
+
25
+ ## 1.0.1
27
26
 
28
- ### Fixed
27
+ ### Changed
29
28
 
30
- - Typos in several error messages.
29
+ - Published by CI from `jesusgraterol/jg-packages`, with npm provenance. The code is the same as 1.0.0.
30
+
31
+ ## 1.0.0
32
+
33
+ The first release.
34
+
35
+ ### Added
36
+
37
+ - Async functions for paths, directories, and files, such as `getDirectoryElements`, `readJSONFile`, `writeFileAtomic`, and `readBoundedFile`.
38
+ - The `jg-fs/sync` entry, with the same functions, synchronous.
39
+ - `ERRORS`, the codes of every error the package throws.
40
+ - 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,11 @@ 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/.
56
+
57
+ Recipes for common jobs ship as `RECIPES.md`, with every block typechecked against the release, and are at https://jg-packages.jesusgraterol.dev/packages/jg-fs/recipes/.
71
58
 
72
59
  ## License
73
60
 
package/RECIPES.md ADDED
@@ -0,0 +1,125 @@
1
+ # jg-fs recipes
2
+
3
+ Patterns for Node.js apps that keep their files whole through crashes, and read files from users within a size limit.
4
+
5
+ ## A config file that's never half-written
6
+
7
+ Use this for a file the app reads at startup and rewrites while it runs, such as settings changed from an admin page. A plain write that's cut short, by a crash or a full disk, leaves a truncated file that won't parse at the next start. `writeFileAtomic` writes a temporary file next to it, flushes it to disk, and renames it over the old one, so the file holds either the old config or the new one:
8
+
9
+ ```ts
10
+ import { hasErrorCode } from 'jg-exception';
11
+ import { ERRORS, readJsonFile, writeFileAtomic } from 'jg-fs';
12
+
13
+ // the config's type, from your schema
14
+ type IConfig = ReturnType<typeof Config.parse>;
15
+
16
+ const CONFIG_PATH = 'data/config.json';
17
+
18
+ // what the app runs with until someone saves a change
19
+ const DEFAULT_CONFIG: IConfig = { port: 8080 };
20
+
21
+ export const loadConfig = async (): Promise<IConfig> => {
22
+ try {
23
+ // Config is your schema, such as a Zod object, so a file edited by hand into a wrong shape
24
+ // fails here, at startup, instead of somewhere later
25
+ return await readJsonFile(CONFIG_PATH, { schema: Config });
26
+ } catch (error) {
27
+ // nothing was saved yet
28
+ if (hasErrorCode(error, ERRORS.NOT_A_FILE)) {
29
+ return DEFAULT_CONFIG;
30
+ }
31
+ throw error;
32
+ }
33
+ };
34
+
35
+ // the update in progress, which the next one waits for
36
+ let pendingUpdate: Promise<unknown> = Promise.resolve();
37
+
38
+ // reads, changes, and saves the config one update at a time, so two updates never start from
39
+ // the same version and lose one of the changes
40
+ export const updateConfig = (change: (config: IConfig) => IConfig): Promise<IConfig> => {
41
+ const update = pendingUpdate.then(async () => {
42
+ const config = change(await loadConfig());
43
+ await writeFileAtomic(CONFIG_PATH, `${JSON.stringify(config, undefined, 2)}\n`);
44
+ return config;
45
+ });
46
+
47
+ // a failed update rejects for its caller, and the next one still runs
48
+ pendingUpdate = update.catch(() => undefined);
49
+ return update;
50
+ };
51
+
52
+ await updateConfig((config) => ({ ...config, port: 3000 }));
53
+ ```
54
+
55
+ `writeJsonFile` is a plain write, so a file that must survive a crash goes through `writeFileAtomic` with `JSON.stringify` instead. Any error other than a missing file propagates, such as `FILE_CONTENT_IS_EMPTY_OR_INVALID` for a file that isn't JSON, or the schema's own error, so the app never starts from a config it can't trust. The atomic write keeps each save whole, but doesn't order saves: the queue does that inside one process, and when several processes write the file, give it one owner. A crash during a save can leave a temporary `.<random>.tmp` file next to the config, which is safe to delete.
56
+
57
+ `jg-fs/sync` has the same functions without promises, for code that can't await, such as an `exit` handler or a short CLI command. An explicit mode keeps a secret private from the first byte written:
58
+
59
+ ```ts
60
+ import { homedir } from 'node:os';
61
+ import { join } from 'node:path';
62
+ import { writeFileAtomic } from 'jg-fs/sync';
63
+
64
+ // the token your CLI's login command received
65
+ declare const token: string;
66
+
67
+ // kept outside any project, so it's never committed with one
68
+ const credentialsPath = join(homedir(), '.my-cli', 'credentials.json');
69
+
70
+ // on POSIX, only the owner can read it, and the temporary file is never more widely readable
71
+ writeFileAtomic(credentialsPath, JSON.stringify({ token }), 0o600);
72
+ ```
73
+
74
+ Without a mode, a replaced file keeps its permission bits, so a config someone restricted with `chmod` stays restricted after the app saves it.
75
+
76
+ ## Reading uploads with a size limit
77
+
78
+ Use this when a request names a file that someone uploaded earlier, such as an import or an attachment, and the app reads it into memory. The name comes from a client, and the file can be larger than you expect, so the read checks three things: the path stays inside the uploads directory, the file is a regular file and not a symbolic link, and its size is within a limit:
79
+
80
+ ```ts
81
+ import { join, resolve } from 'node:path';
82
+ import { Exception, hasErrorCode } from 'jg-exception';
83
+ import { ERRORS, isPathWithin, readBoundedFile } from 'jg-fs';
84
+
85
+ // where your upload handler saves files, a directory that only the app writes to
86
+ const UPLOADS_DIRECTORY = resolve('uploads');
87
+
88
+ // the largest upload the app loads into memory
89
+ const MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
90
+
91
+ // reads an upload by the name a request gave, such as 'report.csv'. The messages name the upload
92
+ // and not the server's path, and the cause keeps jg-fs's error for the logs
93
+ export const readUpload = async (name: string): Promise<Buffer> => {
94
+ // a name such as '../.env' leads outside the directory, which counts as not found
95
+ const path = join(UPLOADS_DIRECTORY, name);
96
+ if (!isPathWithin(UPLOADS_DIRECTORY, path)) {
97
+ throw new Exception('The upload was not found.', {
98
+ code: 'UPLOAD_NOT_FOUND',
99
+ data: { name },
100
+ });
101
+ }
102
+ try {
103
+ return await readBoundedFile(path, MAX_UPLOAD_BYTES);
104
+ } catch (error) {
105
+ // a missing file, a directory, or a symbolic link someone planted
106
+ if (hasErrorCode(error, ERRORS.NOT_A_FILE)) {
107
+ throw new Exception('The upload was not found.', {
108
+ code: 'UPLOAD_NOT_FOUND',
109
+ data: { name },
110
+ cause: error,
111
+ });
112
+ }
113
+ if (hasErrorCode(error, ERRORS.FILE_TOO_LARGE)) {
114
+ throw new Exception('The upload is too large to read.', {
115
+ code: 'UPLOAD_TOO_LARGE',
116
+ data: { name, maxBytes: MAX_UPLOAD_BYTES },
117
+ cause: error,
118
+ });
119
+ }
120
+ throw error;
121
+ }
122
+ };
123
+ ```
124
+
125
+ `readBoundedFile` checks the size before it reads, and reads at most one byte past the limit, so a file that grows during the read can't make it load more. It refuses a symbolic link as the last part of the path, but follows links in the directories above it, which is why the uploads go in a directory that only the app writes to. A request handler can answer `UPLOAD_NOT_FOUND` with a 404, and `jg-fs/sync` has the same `readBoundedFile` for code that can't await.
@@ -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