@unshared/fs 0.0.20 → 0.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 (42) hide show
  1. package/dist/createTemporaryDirectory.cjs +2 -2
  2. package/dist/createTemporaryDirectory.cjs.map +1 -1
  3. package/dist/createTemporaryDirectory.js +2 -1
  4. package/dist/createTemporaryDirectory.js.map +1 -1
  5. package/dist/createTemporaryFile.cjs +2 -2
  6. package/dist/createTemporaryFile.cjs.map +1 -1
  7. package/dist/createTemporaryFile.js +2 -1
  8. package/dist/createTemporaryFile.js.map +1 -1
  9. package/dist/findAncestor.cjs +2 -2
  10. package/dist/findAncestor.cjs.map +1 -1
  11. package/dist/findAncestor.js +1 -2
  12. package/dist/findAncestor.js.map +1 -1
  13. package/dist/findAncestors.cjs +2 -2
  14. package/dist/findAncestors.cjs.map +1 -1
  15. package/dist/findAncestors.js +1 -2
  16. package/dist/findAncestors.js.map +1 -1
  17. package/dist/glob.cjs +3 -3
  18. package/dist/glob.cjs.map +1 -1
  19. package/dist/glob.js +3 -4
  20. package/dist/glob.js.map +1 -1
  21. package/dist/index.cjs +1 -2
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.js +1 -2
  24. package/dist/index.js.map +1 -1
  25. package/dist/loadObject.cjs +11 -2
  26. package/dist/loadObject.cjs.map +1 -1
  27. package/dist/loadObject.d.ts +7 -0
  28. package/dist/loadObject.js +10 -2
  29. package/dist/loadObject.js.map +1 -1
  30. package/dist/touch.cjs.map +1 -1
  31. package/dist/touch.js.map +1 -1
  32. package/dist/updateFile.cjs.map +1 -1
  33. package/dist/updateFile.js.map +1 -1
  34. package/dist/withTemporaryDirectories.cjs +2 -1
  35. package/dist/withTemporaryDirectories.cjs.map +1 -1
  36. package/dist/withTemporaryDirectories.js +2 -1
  37. package/dist/withTemporaryDirectories.js.map +1 -1
  38. package/dist/withTemporaryFiles.cjs +1 -0
  39. package/dist/withTemporaryFiles.cjs.map +1 -1
  40. package/dist/withTemporaryFiles.js +1 -0
  41. package/dist/withTemporaryFiles.js.map +1 -1
  42. package/package.json +7 -7
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
- var promises = require("node:fs/promises"), node_os = require("node:os"), node_path = require("node:path");
2
+ var node_crypto = require("node:crypto"), promises = require("node:fs/promises"), node_os = require("node:os"), node_path = require("node:path");
3
3
  async function createTemporaryDirectory(options = {}) {
4
4
  const {
5
5
  directory = node_os.tmpdir(),
6
- random = () => Math.random().toString(36).slice(2)
6
+ random = () => node_crypto.randomInt(0, 1e6).toString(36)
7
7
  } = options, name = random(), path = node_path.join(directory, name);
8
8
  return await promises.mkdir(path, { recursive: !0 }), [path, () => promises.rm(path, { force: !0, recursive: !0 })];
9
9
  }
@@ -1 +1 @@
1
- {"version":3,"file":"createTemporaryDirectory.cjs","sources":["../createTemporaryDirectory.ts"],"sourcesContent":["import { mkdir, rm } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\n\nexport interface CreateTemporaryDirectoryOptions {\n\n /**\n * The directory to create the temporary directory in.\n * Defaults to the system's temporary directory.\n *\n * @default tmpdir()\n */\n directory?: string\n\n /**\n * A function that generates a random string.\n *\n * @default () => Math.random().toString(36).slice(2)\n */\n random?: () => string\n}\n\n/**\n * Create a temporary directory with a random name and return\n * an object containing the directory path, and a function to\n * recursively remove the directory.\n *\n * @param options The options to create the temporary directory.\n * @returns A promise that resolves to the temporary directory object.\n * @example\n * // Create a temporary directory.\n * const [path, remove] = await createTemporaryDirectory()\n *\n * // Do something with the directory.\n * exec(`tar -czf ${path}.tar.gz ${path}`)\n *\n * // Remove the directory.\n * await remove()\n */\n\nexport async function createTemporaryDirectory(options: CreateTemporaryDirectoryOptions = {}) {\n const {\n directory = tmpdir(),\n random = () => Math.random().toString(36)\n .slice(2),\n } = options\n\n // --- Generate a random name.\n const name = random()\n const path = join(directory, name)\n\n // --- Create the directory.\n await mkdir(path, { recursive: true })\n\n // --- Return the path and a function to remove the directory.\n const remove = () => rm(path, { force: true, recursive: true })\n return [path, remove] as const\n}\n\n/* v8 ignore start */\nif (import.meta.vitest) {\n const { existsSync, statSync } = await import('node:fs')\n\n test('should create an empty temporary directory in \"/tmp/<random>\"', async() => {\n const [path] = await createTemporaryDirectory()\n const isDirectory = statSync(path).isDirectory()\n expect(path).toMatch(/^\\/tmp\\/[\\da-z]+$/)\n expect(isDirectory).toBe(true)\n })\n\n test('should create a temporary directory in the specified directory', async() => {\n const [path] = await createTemporaryDirectory({ directory: '/cache' })\n expect(path).toMatch(/^\\/cache\\/[\\da-z]+$/)\n })\n\n test('should recursively create the specified directory', async() => {\n const [path] = await createTemporaryDirectory({ directory: '/tmp/foo/bar' })\n expect(path).toMatch(/^\\/tmp\\/foo\\/bar\\/[\\da-z]+$/)\n })\n\n test('should create a temporary file with the given random function', async() => {\n const [path] = await createTemporaryDirectory({ random: () => 'foo' })\n expect(path).toMatch(/^\\/tmp\\/foo$/)\n })\n\n test('should remove the temporary file after calling the remove function', async() => {\n const [path, remove] = await createTemporaryDirectory()\n await remove()\n const exists = existsSync(path)\n expect(exists).toBe(false)\n })\n}\n"],"names":["tmpdir","join","mkdir","rm"],"mappings":";;AAwCsB,eAAA,yBAAyB,UAA2C,IAAI;AACtF,QAAA;AAAA,IACJ,YAAYA,QAAAA,OAAO;AAAA,IACnB,SAAS,MAAM,KAAK,SAAS,SAAS,EAAE,EACrC,MAAM,CAAC;AAAA,EAAA,IACR,SAGE,OAAO,UACP,OAAOC,eAAK,WAAW,IAAI;AAGjC,SAAA,MAAMC,eAAM,MAAM,EAAE,WAAW,GAAK,CAAC,GAI9B,CAAC,MADO,MAAMC,SAAAA,GAAG,MAAM,EAAE,OAAO,IAAM,WAAW,GAAM,CAAA,CAC1C;AACtB;;"}
1
+ {"version":3,"file":"createTemporaryDirectory.cjs","sources":["../createTemporaryDirectory.ts"],"sourcesContent":["import { randomInt } from 'node:crypto'\nimport { mkdir, rm } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\n\nexport interface CreateTemporaryDirectoryOptions {\n\n /**\n * The directory to create the temporary directory in.\n * Defaults to the system's temporary directory.\n *\n * @default tmpdir()\n */\n directory?: string\n\n /**\n * A function that generates a random string.\n *\n * @default () => Math.random().toString(36).slice(2)\n */\n random?: () => string\n}\n\n/**\n * Create a temporary directory with a random name and return\n * an object containing the directory path, and a function to\n * recursively remove the directory.\n *\n * @param options The options to create the temporary directory.\n * @returns A promise that resolves to the temporary directory object.\n * @example\n * // Create a temporary directory.\n * const [path, remove] = await createTemporaryDirectory()\n *\n * // Do something with the directory.\n * exec(`tar -czf ${path}.tar.gz ${path}`)\n *\n * // Remove the directory.\n * await remove()\n */\nexport async function createTemporaryDirectory(options: CreateTemporaryDirectoryOptions = {}) {\n const {\n directory = tmpdir(),\n random = () => randomInt(0, 1e6).toString(36),\n } = options\n\n // --- Generate a random name.\n const name = random()\n const path = join(directory, name)\n\n // --- Create the directory.\n await mkdir(path, { recursive: true })\n\n // --- Return the path and a function to remove the directory.\n const remove = () => rm(path, { force: true, recursive: true })\n return [path, remove] as const\n}\n"],"names":["tmpdir","randomInt","join","mkdir","rm"],"mappings":";;AAwCsB,eAAA,yBAAyB,UAA2C,IAAI;AACtF,QAAA;AAAA,IACJ,YAAYA,QAAAA,OAAO;AAAA,IACnB,SAAS,MAAMC,YAAAA,UAAU,GAAG,GAAG,EAAE,SAAS,EAAE;AAAA,EAAA,IAC1C,SAGE,OAAO,OAAA,GACP,OAAOC,UAAA,KAAK,WAAW,IAAI;AAGjC,SAAA,MAAMC,eAAM,MAAM,EAAE,WAAW,GAAK,CAAC,GAI9B,CAAC,MADO,MAAMC,SAAAA,GAAG,MAAM,EAAE,OAAO,IAAM,WAAW,GAAA,CAAM,CAC1C;AACtB;;"}
@@ -1,10 +1,11 @@
1
+ import { randomInt } from "node:crypto";
1
2
  import { mkdir, rm } from "node:fs/promises";
2
3
  import { tmpdir } from "node:os";
3
4
  import { join } from "node:path";
4
5
  async function createTemporaryDirectory(options = {}) {
5
6
  const {
6
7
  directory = tmpdir(),
7
- random = () => Math.random().toString(36).slice(2)
8
+ random = () => randomInt(0, 1e6).toString(36)
8
9
  } = options, name = random(), path = join(directory, name);
9
10
  return await mkdir(path, { recursive: !0 }), [path, () => rm(path, { force: !0, recursive: !0 })];
10
11
  }
@@ -1 +1 @@
1
- {"version":3,"file":"createTemporaryDirectory.js","sources":["../createTemporaryDirectory.ts"],"sourcesContent":["import { mkdir, rm } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\n\nexport interface CreateTemporaryDirectoryOptions {\n\n /**\n * The directory to create the temporary directory in.\n * Defaults to the system's temporary directory.\n *\n * @default tmpdir()\n */\n directory?: string\n\n /**\n * A function that generates a random string.\n *\n * @default () => Math.random().toString(36).slice(2)\n */\n random?: () => string\n}\n\n/**\n * Create a temporary directory with a random name and return\n * an object containing the directory path, and a function to\n * recursively remove the directory.\n *\n * @param options The options to create the temporary directory.\n * @returns A promise that resolves to the temporary directory object.\n * @example\n * // Create a temporary directory.\n * const [path, remove] = await createTemporaryDirectory()\n *\n * // Do something with the directory.\n * exec(`tar -czf ${path}.tar.gz ${path}`)\n *\n * // Remove the directory.\n * await remove()\n */\n\nexport async function createTemporaryDirectory(options: CreateTemporaryDirectoryOptions = {}) {\n const {\n directory = tmpdir(),\n random = () => Math.random().toString(36)\n .slice(2),\n } = options\n\n // --- Generate a random name.\n const name = random()\n const path = join(directory, name)\n\n // --- Create the directory.\n await mkdir(path, { recursive: true })\n\n // --- Return the path and a function to remove the directory.\n const remove = () => rm(path, { force: true, recursive: true })\n return [path, remove] as const\n}\n\n/* v8 ignore start */\nif (import.meta.vitest) {\n const { existsSync, statSync } = await import('node:fs')\n\n test('should create an empty temporary directory in \"/tmp/<random>\"', async() => {\n const [path] = await createTemporaryDirectory()\n const isDirectory = statSync(path).isDirectory()\n expect(path).toMatch(/^\\/tmp\\/[\\da-z]+$/)\n expect(isDirectory).toBe(true)\n })\n\n test('should create a temporary directory in the specified directory', async() => {\n const [path] = await createTemporaryDirectory({ directory: '/cache' })\n expect(path).toMatch(/^\\/cache\\/[\\da-z]+$/)\n })\n\n test('should recursively create the specified directory', async() => {\n const [path] = await createTemporaryDirectory({ directory: '/tmp/foo/bar' })\n expect(path).toMatch(/^\\/tmp\\/foo\\/bar\\/[\\da-z]+$/)\n })\n\n test('should create a temporary file with the given random function', async() => {\n const [path] = await createTemporaryDirectory({ random: () => 'foo' })\n expect(path).toMatch(/^\\/tmp\\/foo$/)\n })\n\n test('should remove the temporary file after calling the remove function', async() => {\n const [path, remove] = await createTemporaryDirectory()\n await remove()\n const exists = existsSync(path)\n expect(exists).toBe(false)\n })\n}\n"],"names":[],"mappings":";;;AAwCsB,eAAA,yBAAyB,UAA2C,IAAI;AACtF,QAAA;AAAA,IACJ,YAAY,OAAO;AAAA,IACnB,SAAS,MAAM,KAAK,SAAS,SAAS,EAAE,EACrC,MAAM,CAAC;AAAA,EAAA,IACR,SAGE,OAAO,UACP,OAAO,KAAK,WAAW,IAAI;AAGjC,SAAA,MAAM,MAAM,MAAM,EAAE,WAAW,GAAK,CAAC,GAI9B,CAAC,MADO,MAAM,GAAG,MAAM,EAAE,OAAO,IAAM,WAAW,GAAM,CAAA,CAC1C;AACtB;"}
1
+ {"version":3,"file":"createTemporaryDirectory.js","sources":["../createTemporaryDirectory.ts"],"sourcesContent":["import { randomInt } from 'node:crypto'\nimport { mkdir, rm } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\n\nexport interface CreateTemporaryDirectoryOptions {\n\n /**\n * The directory to create the temporary directory in.\n * Defaults to the system's temporary directory.\n *\n * @default tmpdir()\n */\n directory?: string\n\n /**\n * A function that generates a random string.\n *\n * @default () => Math.random().toString(36).slice(2)\n */\n random?: () => string\n}\n\n/**\n * Create a temporary directory with a random name and return\n * an object containing the directory path, and a function to\n * recursively remove the directory.\n *\n * @param options The options to create the temporary directory.\n * @returns A promise that resolves to the temporary directory object.\n * @example\n * // Create a temporary directory.\n * const [path, remove] = await createTemporaryDirectory()\n *\n * // Do something with the directory.\n * exec(`tar -czf ${path}.tar.gz ${path}`)\n *\n * // Remove the directory.\n * await remove()\n */\nexport async function createTemporaryDirectory(options: CreateTemporaryDirectoryOptions = {}) {\n const {\n directory = tmpdir(),\n random = () => randomInt(0, 1e6).toString(36),\n } = options\n\n // --- Generate a random name.\n const name = random()\n const path = join(directory, name)\n\n // --- Create the directory.\n await mkdir(path, { recursive: true })\n\n // --- Return the path and a function to remove the directory.\n const remove = () => rm(path, { force: true, recursive: true })\n return [path, remove] as const\n}\n"],"names":[],"mappings":";;;;AAwCsB,eAAA,yBAAyB,UAA2C,IAAI;AACtF,QAAA;AAAA,IACJ,YAAY,OAAO;AAAA,IACnB,SAAS,MAAM,UAAU,GAAG,GAAG,EAAE,SAAS,EAAE;AAAA,EAAA,IAC1C,SAGE,OAAO,OAAA,GACP,OAAO,KAAK,WAAW,IAAI;AAGjC,SAAA,MAAM,MAAM,MAAM,EAAE,WAAW,GAAK,CAAC,GAI9B,CAAC,MADO,MAAM,GAAG,MAAM,EAAE,OAAO,IAAM,WAAW,GAAA,CAAM,CAC1C;AACtB;"}
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
- var promises = require("node:fs/promises"), node_os = require("node:os"), node_path = require("node:path");
2
+ var node_crypto = require("node:crypto"), promises = require("node:fs/promises"), node_os = require("node:os"), node_path = require("node:path");
3
3
  async function createTemporaryFile(content, options = {}) {
4
4
  const {
5
5
  directory = node_os.tmpdir(),
6
6
  extension,
7
- random = () => Math.random().toString(36).slice(2)
7
+ random = () => node_crypto.randomInt(0, 1e6).toString(36)
8
8
  } = options, rand = random(), name = extension ? `${rand}.${extension}` : rand, path = node_path.join(directory, name);
9
9
  return await promises.mkdir(directory, { recursive: !0 }), await promises.writeFile(path, content ?? ""), [path, () => promises.rm(path, { force: !0 })];
10
10
  }
@@ -1 +1 @@
1
- {"version":3,"file":"createTemporaryFile.cjs","sources":["../createTemporaryFile.ts"],"sourcesContent":["import { mkdir, rm, writeFile } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\n\nexport interface CreateTemporaryFileOptions {\n\n /**\n * The directory to create the temporary file in.\n * Defaults to the system's temporary directory.\n *\n * @default tmpdir()\n */\n directory?: string\n\n /**\n * The file extension to use for the temporary file.\n *\n * @default ''\n */\n extension?: string\n\n /**\n * A function that generates a random string.\n *\n * @default () => Math.random().toString(36).slice(2)\n */\n random?: () => string\n}\n\n/**\n * Create a temporary file with a random name and return\n * an object containing the file path, and a function to\n * remove the file.\n *\n * @param content The content to write to the temporary file.\n * @param options The options to create the temporary file.\n * @returns A promise that resolves to the temporary file object.\n * @example\n * // Create a temporary file with the specified content.\n * const [path, remove] = await createTemporaryFile('Hello, world!')\n *\n * // Do something with the file.\n * exec(`openssl sha1 ${path}`)\n *\n * // Remove the file.\n * await remove()\n */\nexport async function createTemporaryFile(content?: Parameters<typeof writeFile>[1], options: CreateTemporaryFileOptions = {}) {\n const {\n directory = tmpdir(),\n extension,\n random = () => Math.random().toString(36)\n .slice(2),\n } = options\n\n // --- Generate a random name.\n const rand = random()\n const name = extension ? `${rand}.${extension}` : rand\n const path = join(directory, name)\n\n // --- Write the content to the file.\n await mkdir(directory, { recursive: true })\n await writeFile(path, content ?? '')\n\n // --- Return the path and a function to remove the file.\n const remove = () => rm(path, { force: true })\n return [path, remove] as const\n}\n\n/* v8 ignore start */\nif (import.meta.vitest) {\n const { existsSync, readFileSync, statSync } = await import('node:fs')\n\n test('should create an empty temporary file in \"/tmp/<random>\"', async() => {\n const [path] = await createTemporaryFile()\n const isFile = statSync(path).isFile()\n const content = readFileSync(path, 'utf8')\n expect(path).toMatch(/^\\/tmp\\/[\\da-z]+$/)\n expect(isFile).toBe(true)\n expect(content).toBe('')\n })\n\n test('should create a temporary file with the specified content', async() => {\n const [path] = await createTemporaryFile('Hello, world!')\n const content = readFileSync(path, 'utf8')\n expect(content).toBe('Hello, world!')\n })\n\n test('should create a temporary file in the specified directory', async() => {\n const [path] = await createTemporaryFile(undefined, { directory: '/cache' })\n expect(path).toMatch(/^\\/cache\\/[\\da-z]+$/)\n })\n\n test('should recursively create the specified directory', async() => {\n const [path] = await createTemporaryFile(undefined, { directory: '/tmp/foo/bar' })\n expect(path).toMatch(/^\\/tmp\\/foo\\/bar\\/[\\da-z]+$/)\n })\n\n test('should create a temporary file with the specified extension', async() => {\n const [path] = await createTemporaryFile(undefined, { extension: 'txt' })\n expect(path).toMatch(/^\\/tmp\\/[\\da-z]+\\.txt$/)\n })\n\n test('should create a temporary file with the given random function', async() => {\n const [path] = await createTemporaryFile(undefined, { random: () => 'foo' })\n expect(path).toMatch(/^\\/tmp\\/foo$/)\n })\n\n test('should remove the temporary file after calling the remove function', async() => {\n const [path, remove] = await createTemporaryFile()\n await remove()\n const exists = existsSync(path)\n expect(exists).toBe(false)\n })\n}\n"],"names":["tmpdir","join","mkdir","writeFile","rm"],"mappings":";;AA+CA,eAAsB,oBAAoB,SAA2C,UAAsC,IAAI;AACvH,QAAA;AAAA,IACJ,YAAYA,QAAAA,OAAO;AAAA,IACnB;AAAA,IACA,SAAS,MAAM,KAAK,SAAS,SAAS,EAAE,EACrC,MAAM,CAAC;AAAA,EAAA,IACR,SAGE,OAAO,OAAO,GACd,OAAO,YAAY,GAAG,IAAI,IAAI,SAAS,KAAK,MAC5C,OAAOC,UAAAA,KAAK,WAAW,IAAI;AAG3B,SAAA,MAAAC,SAAAA,MAAM,WAAW,EAAE,WAAW,IAAM,GAC1C,MAAMC,SAAA,UAAU,MAAM,WAAW,EAAE,GAI5B,CAAC,MADO,MAAMC,YAAG,MAAM,EAAE,OAAO,GAAM,CAAA,CACzB;AACtB;;"}
1
+ {"version":3,"file":"createTemporaryFile.cjs","sources":["../createTemporaryFile.ts"],"sourcesContent":["import { randomInt } from 'node:crypto'\nimport { mkdir, rm, writeFile } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\n\nexport interface CreateTemporaryFileOptions {\n\n /**\n * The directory to create the temporary file in.\n * Defaults to the system's temporary directory.\n *\n * @default tmpdir()\n */\n directory?: string\n\n /**\n * The file extension to use for the temporary file.\n *\n * @default ''\n */\n extension?: string\n\n /**\n * A function that generates a random string.\n *\n * @default () => Math.random().toString(36).slice(2)\n */\n random?: () => string\n}\n\n/**\n * Create a temporary file with a random name and return\n * an object containing the file path, and a function to\n * remove the file.\n *\n * @param content The content to write to the temporary file.\n * @param options The options to create the temporary file.\n * @returns A promise that resolves to the temporary file object.\n * @example\n * // Create a temporary file with the specified content.\n * const [path, remove] = await createTemporaryFile('Hello, world!')\n *\n * // Do something with the file.\n * exec(`openssl sha1 ${path}`)\n *\n * // Remove the file.\n * await remove()\n */\nexport async function createTemporaryFile(content?: Parameters<typeof writeFile>[1], options: CreateTemporaryFileOptions = {}) {\n const {\n directory = tmpdir(),\n extension,\n random = () => randomInt(0, 1e6).toString(36),\n } = options\n\n // --- Generate a random name.\n const rand = random()\n const name = extension ? `${rand}.${extension}` : rand\n const path = join(directory, name)\n\n // --- Write the content to the file.\n await mkdir(directory, { recursive: true })\n await writeFile(path, content ?? '')\n\n // --- Return the path and a function to remove the file.\n const remove = () => rm(path, { force: true })\n return [path, remove] as const\n}\n"],"names":["tmpdir","randomInt","join","mkdir","writeFile","rm"],"mappings":";;AAgDA,eAAsB,oBAAoB,SAA2C,UAAsC,IAAI;AACvH,QAAA;AAAA,IACJ,YAAYA,QAAAA,OAAO;AAAA,IACnB;AAAA,IACA,SAAS,MAAMC,YAAAA,UAAU,GAAG,GAAG,EAAE,SAAS,EAAE;AAAA,EAAA,IAC1C,SAGE,OAAO,OAAO,GACd,OAAO,YAAY,GAAG,IAAI,IAAI,SAAS,KAAK,MAC5C,OAAOC,UAAA,KAAK,WAAW,IAAI;AAG3B,SAAA,MAAAC,SAAAA,MAAM,WAAW,EAAE,WAAW,GAAM,CAAA,GAC1C,MAAMC,SAAAA,UAAU,MAAM,WAAW,EAAE,GAI5B,CAAC,MADO,MAAMC,SAAA,GAAG,MAAM,EAAE,OAAO,GAAK,CAAC,CACzB;AACtB;;"}
@@ -1,3 +1,4 @@
1
+ import { randomInt } from "node:crypto";
1
2
  import { mkdir, writeFile, rm } from "node:fs/promises";
2
3
  import { tmpdir } from "node:os";
3
4
  import { join } from "node:path";
@@ -5,7 +6,7 @@ async function createTemporaryFile(content, options = {}) {
5
6
  const {
6
7
  directory = tmpdir(),
7
8
  extension,
8
- random = () => Math.random().toString(36).slice(2)
9
+ random = () => randomInt(0, 1e6).toString(36)
9
10
  } = options, rand = random(), name = extension ? `${rand}.${extension}` : rand, path = join(directory, name);
10
11
  return await mkdir(directory, { recursive: !0 }), await writeFile(path, content ?? ""), [path, () => rm(path, { force: !0 })];
11
12
  }
@@ -1 +1 @@
1
- {"version":3,"file":"createTemporaryFile.js","sources":["../createTemporaryFile.ts"],"sourcesContent":["import { mkdir, rm, writeFile } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\n\nexport interface CreateTemporaryFileOptions {\n\n /**\n * The directory to create the temporary file in.\n * Defaults to the system's temporary directory.\n *\n * @default tmpdir()\n */\n directory?: string\n\n /**\n * The file extension to use for the temporary file.\n *\n * @default ''\n */\n extension?: string\n\n /**\n * A function that generates a random string.\n *\n * @default () => Math.random().toString(36).slice(2)\n */\n random?: () => string\n}\n\n/**\n * Create a temporary file with a random name and return\n * an object containing the file path, and a function to\n * remove the file.\n *\n * @param content The content to write to the temporary file.\n * @param options The options to create the temporary file.\n * @returns A promise that resolves to the temporary file object.\n * @example\n * // Create a temporary file with the specified content.\n * const [path, remove] = await createTemporaryFile('Hello, world!')\n *\n * // Do something with the file.\n * exec(`openssl sha1 ${path}`)\n *\n * // Remove the file.\n * await remove()\n */\nexport async function createTemporaryFile(content?: Parameters<typeof writeFile>[1], options: CreateTemporaryFileOptions = {}) {\n const {\n directory = tmpdir(),\n extension,\n random = () => Math.random().toString(36)\n .slice(2),\n } = options\n\n // --- Generate a random name.\n const rand = random()\n const name = extension ? `${rand}.${extension}` : rand\n const path = join(directory, name)\n\n // --- Write the content to the file.\n await mkdir(directory, { recursive: true })\n await writeFile(path, content ?? '')\n\n // --- Return the path and a function to remove the file.\n const remove = () => rm(path, { force: true })\n return [path, remove] as const\n}\n\n/* v8 ignore start */\nif (import.meta.vitest) {\n const { existsSync, readFileSync, statSync } = await import('node:fs')\n\n test('should create an empty temporary file in \"/tmp/<random>\"', async() => {\n const [path] = await createTemporaryFile()\n const isFile = statSync(path).isFile()\n const content = readFileSync(path, 'utf8')\n expect(path).toMatch(/^\\/tmp\\/[\\da-z]+$/)\n expect(isFile).toBe(true)\n expect(content).toBe('')\n })\n\n test('should create a temporary file with the specified content', async() => {\n const [path] = await createTemporaryFile('Hello, world!')\n const content = readFileSync(path, 'utf8')\n expect(content).toBe('Hello, world!')\n })\n\n test('should create a temporary file in the specified directory', async() => {\n const [path] = await createTemporaryFile(undefined, { directory: '/cache' })\n expect(path).toMatch(/^\\/cache\\/[\\da-z]+$/)\n })\n\n test('should recursively create the specified directory', async() => {\n const [path] = await createTemporaryFile(undefined, { directory: '/tmp/foo/bar' })\n expect(path).toMatch(/^\\/tmp\\/foo\\/bar\\/[\\da-z]+$/)\n })\n\n test('should create a temporary file with the specified extension', async() => {\n const [path] = await createTemporaryFile(undefined, { extension: 'txt' })\n expect(path).toMatch(/^\\/tmp\\/[\\da-z]+\\.txt$/)\n })\n\n test('should create a temporary file with the given random function', async() => {\n const [path] = await createTemporaryFile(undefined, { random: () => 'foo' })\n expect(path).toMatch(/^\\/tmp\\/foo$/)\n })\n\n test('should remove the temporary file after calling the remove function', async() => {\n const [path, remove] = await createTemporaryFile()\n await remove()\n const exists = existsSync(path)\n expect(exists).toBe(false)\n })\n}\n"],"names":[],"mappings":";;;AA+CA,eAAsB,oBAAoB,SAA2C,UAAsC,IAAI;AACvH,QAAA;AAAA,IACJ,YAAY,OAAO;AAAA,IACnB;AAAA,IACA,SAAS,MAAM,KAAK,SAAS,SAAS,EAAE,EACrC,MAAM,CAAC;AAAA,EAAA,IACR,SAGE,OAAO,OAAO,GACd,OAAO,YAAY,GAAG,IAAI,IAAI,SAAS,KAAK,MAC5C,OAAO,KAAK,WAAW,IAAI;AAG3B,SAAA,MAAA,MAAM,WAAW,EAAE,WAAW,IAAM,GAC1C,MAAM,UAAU,MAAM,WAAW,EAAE,GAI5B,CAAC,MADO,MAAM,GAAG,MAAM,EAAE,OAAO,GAAM,CAAA,CACzB;AACtB;"}
1
+ {"version":3,"file":"createTemporaryFile.js","sources":["../createTemporaryFile.ts"],"sourcesContent":["import { randomInt } from 'node:crypto'\nimport { mkdir, rm, writeFile } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\n\nexport interface CreateTemporaryFileOptions {\n\n /**\n * The directory to create the temporary file in.\n * Defaults to the system's temporary directory.\n *\n * @default tmpdir()\n */\n directory?: string\n\n /**\n * The file extension to use for the temporary file.\n *\n * @default ''\n */\n extension?: string\n\n /**\n * A function that generates a random string.\n *\n * @default () => Math.random().toString(36).slice(2)\n */\n random?: () => string\n}\n\n/**\n * Create a temporary file with a random name and return\n * an object containing the file path, and a function to\n * remove the file.\n *\n * @param content The content to write to the temporary file.\n * @param options The options to create the temporary file.\n * @returns A promise that resolves to the temporary file object.\n * @example\n * // Create a temporary file with the specified content.\n * const [path, remove] = await createTemporaryFile('Hello, world!')\n *\n * // Do something with the file.\n * exec(`openssl sha1 ${path}`)\n *\n * // Remove the file.\n * await remove()\n */\nexport async function createTemporaryFile(content?: Parameters<typeof writeFile>[1], options: CreateTemporaryFileOptions = {}) {\n const {\n directory = tmpdir(),\n extension,\n random = () => randomInt(0, 1e6).toString(36),\n } = options\n\n // --- Generate a random name.\n const rand = random()\n const name = extension ? `${rand}.${extension}` : rand\n const path = join(directory, name)\n\n // --- Write the content to the file.\n await mkdir(directory, { recursive: true })\n await writeFile(path, content ?? '')\n\n // --- Return the path and a function to remove the file.\n const remove = () => rm(path, { force: true })\n return [path, remove] as const\n}\n"],"names":[],"mappings":";;;;AAgDA,eAAsB,oBAAoB,SAA2C,UAAsC,IAAI;AACvH,QAAA;AAAA,IACJ,YAAY,OAAO;AAAA,IACnB;AAAA,IACA,SAAS,MAAM,UAAU,GAAG,GAAG,EAAE,SAAS,EAAE;AAAA,EAAA,IAC1C,SAGE,OAAO,OAAO,GACd,OAAO,YAAY,GAAG,IAAI,IAAI,SAAS,KAAK,MAC5C,OAAO,KAAK,WAAW,IAAI;AAG3B,SAAA,MAAA,MAAM,WAAW,EAAE,WAAW,GAAM,CAAA,GAC1C,MAAM,UAAU,MAAM,WAAW,EAAE,GAI5B,CAAC,MADO,MAAM,GAAG,MAAM,EAAE,OAAO,GAAK,CAAC,CACzB;AACtB;"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
- var promises = require("node:fs/promises"), node_path = require("node:path"), node_process = require("node:process");
3
- async function findAncestor(name, from = node_process.cwd()) {
2
+ var promises = require("node:fs/promises"), node_path = require("node:path");
3
+ async function findAncestor(name, from = process.cwd()) {
4
4
  for (; from !== ""; ) {
5
5
  const absolutePath = node_path.resolve(from, name);
6
6
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"findAncestor.cjs","sources":["../findAncestor.ts"],"sourcesContent":["import { access, constants } from 'node:fs/promises'\nimport { dirname, resolve } from 'node:path'\nimport { cwd } from 'node:process'\n\n/**\n * Find the first ancestor of a file from a given path. The search will start\n * from the given path and will continue until the root directory is reached.\n * If the file is not found, will throw an error.\n *\n * @param name The file name to find.\n * @param from The path to start from.\n * @returns The absolute path of the file found.\n * @example\n * // Create a file in the root directory.\n * await writeFile('/home/user/file.txt', 'Hello, world!')\n *\n * // Find the file from a subdirectory.\n * await findAncestor('file.txt', '/home/user/project') // '/home/user/file.txt'\n */\nexport async function findAncestor(name: string, from: string = cwd()): Promise<string | undefined> {\n while (from !== '') {\n const absolutePath = resolve(from, name)\n try {\n await access(absolutePath, constants.F_OK)\n return absolutePath\n }\n catch {\n\n /** Ignore error. */\n }\n if (from === '/') break\n from = dirname(from)\n }\n}\n\n/* v8 ignore start */\nif (import.meta.vitest) {\n const { vol } = await import('memfs')\n\n test('should resolve ancestor from current directory', async() => {\n vi.mock('node:process', () => ({ cwd: () => '/home/user/project' }))\n vol.fromJSON({ '/home/user/project/.npmrc': '' })\n const result = await findAncestor('.npmrc')\n expect(result).toBe('/home/user/project/.npmrc')\n })\n\n test('should resolve ancestor from given directory', async() => {\n vol.fromJSON({ '/home/user/.npmrc': '' })\n const result = await findAncestor('.npmrc', '/home/user/project')\n expect(result).toBe('/home/user/.npmrc')\n })\n\n test('should resolve ancestor at root directory', async() => {\n vol.fromJSON({ '/.npmrc': '' })\n const result = await findAncestor('.npmrc', '/home/user/project')\n expect(result).toBe('/.npmrc')\n })\n\n test('should resolve the first ancestor', async() => {\n vol.fromJSON({\n '/.npmrc': '',\n '/home/user/.npmrc': '',\n '/home/user/project/.npmrc': '',\n })\n const result = await findAncestor('.npmrc', '/home/user/project')\n expect(result).toBe('/home/user/project/.npmrc')\n })\n\n test('should return undefined if no ancestor was found', async() => {\n vol.fromJSON({})\n const result = await findAncestor('file', '/')\n expect(result).toBeUndefined()\n })\n}\n"],"names":["cwd","resolve","access","constants","dirname"],"mappings":";;AAmBA,eAAsB,aAAa,MAAc,OAAeA,aAAAA,OAAoC;AAClG,SAAO,SAAS,MAAI;AACZ,UAAA,eAAeC,UAAAA,QAAQ,MAAM,IAAI;AACnC,QAAA;AACF,aAAA,MAAMC,SAAO,OAAA,cAAcC,SAAU,UAAA,IAAI,GAClC;AAAA,IAAA,QAEH;AAAA,IAGN;AACA,QAAI,SAAS,IAAK;AAClB,WAAOC,UAAAA,QAAQ,IAAI;AAAA,EACrB;AACF;;"}
1
+ {"version":3,"file":"findAncestor.cjs","sources":["../findAncestor.ts"],"sourcesContent":["import { access, constants } from 'node:fs/promises'\nimport { dirname, resolve } from 'node:path'\n\n/**\n * Find the first ancestor of a file from a given path. The search will start\n * from the given path and will continue until the root directory is reached.\n * If the file is not found, will throw an error.\n *\n * @param name The file name to find.\n * @param from The path to start from.\n * @returns The absolute path of the file found.\n * @example\n * // Create a file in the root directory.\n * await writeFile('/home/user/file.txt', 'Hello, world!')\n *\n * // Find the file from a subdirectory.\n * await findAncestor('file.txt', '/home/user/project') // '/home/user/file.txt'\n */\nexport async function findAncestor(name: string, from = process.cwd()): Promise<string | undefined> {\n while (from !== '') {\n const absolutePath = resolve(from, name)\n try {\n await access(absolutePath, constants.F_OK)\n return absolutePath\n }\n catch { /* Ignore error. */ }\n\n if (from === '/') break\n from = dirname(from)\n }\n}\n"],"names":["resolve","access","constants","dirname"],"mappings":";;AAkBA,eAAsB,aAAa,MAAc,OAAO,QAAQ,OAAoC;AAClG,SAAO,SAAS,MAAI;AACZ,UAAA,eAAeA,UAAAA,QAAQ,MAAM,IAAI;AACnC,QAAA;AACF,aAAA,MAAMC,SAAAA,OAAO,cAAcC,SAAAA,UAAU,IAAI,GAClC;AAAA,IAAA,QAEH;AAAA,IAAA;AAEN,QAAI,SAAS,IAAK;AAClB,WAAOC,kBAAQ,IAAI;AAAA,EAAA;AAEvB;;"}
@@ -1,7 +1,6 @@
1
1
  import { access, constants } from "node:fs/promises";
2
2
  import { resolve, dirname } from "node:path";
3
- import { cwd } from "node:process";
4
- async function findAncestor(name, from = cwd()) {
3
+ async function findAncestor(name, from = process.cwd()) {
5
4
  for (; from !== ""; ) {
6
5
  const absolutePath = resolve(from, name);
7
6
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"findAncestor.js","sources":["../findAncestor.ts"],"sourcesContent":["import { access, constants } from 'node:fs/promises'\nimport { dirname, resolve } from 'node:path'\nimport { cwd } from 'node:process'\n\n/**\n * Find the first ancestor of a file from a given path. The search will start\n * from the given path and will continue until the root directory is reached.\n * If the file is not found, will throw an error.\n *\n * @param name The file name to find.\n * @param from The path to start from.\n * @returns The absolute path of the file found.\n * @example\n * // Create a file in the root directory.\n * await writeFile('/home/user/file.txt', 'Hello, world!')\n *\n * // Find the file from a subdirectory.\n * await findAncestor('file.txt', '/home/user/project') // '/home/user/file.txt'\n */\nexport async function findAncestor(name: string, from: string = cwd()): Promise<string | undefined> {\n while (from !== '') {\n const absolutePath = resolve(from, name)\n try {\n await access(absolutePath, constants.F_OK)\n return absolutePath\n }\n catch {\n\n /** Ignore error. */\n }\n if (from === '/') break\n from = dirname(from)\n }\n}\n\n/* v8 ignore start */\nif (import.meta.vitest) {\n const { vol } = await import('memfs')\n\n test('should resolve ancestor from current directory', async() => {\n vi.mock('node:process', () => ({ cwd: () => '/home/user/project' }))\n vol.fromJSON({ '/home/user/project/.npmrc': '' })\n const result = await findAncestor('.npmrc')\n expect(result).toBe('/home/user/project/.npmrc')\n })\n\n test('should resolve ancestor from given directory', async() => {\n vol.fromJSON({ '/home/user/.npmrc': '' })\n const result = await findAncestor('.npmrc', '/home/user/project')\n expect(result).toBe('/home/user/.npmrc')\n })\n\n test('should resolve ancestor at root directory', async() => {\n vol.fromJSON({ '/.npmrc': '' })\n const result = await findAncestor('.npmrc', '/home/user/project')\n expect(result).toBe('/.npmrc')\n })\n\n test('should resolve the first ancestor', async() => {\n vol.fromJSON({\n '/.npmrc': '',\n '/home/user/.npmrc': '',\n '/home/user/project/.npmrc': '',\n })\n const result = await findAncestor('.npmrc', '/home/user/project')\n expect(result).toBe('/home/user/project/.npmrc')\n })\n\n test('should return undefined if no ancestor was found', async() => {\n vol.fromJSON({})\n const result = await findAncestor('file', '/')\n expect(result).toBeUndefined()\n })\n}\n"],"names":[],"mappings":";;;AAmBA,eAAsB,aAAa,MAAc,OAAe,OAAoC;AAClG,SAAO,SAAS,MAAI;AACZ,UAAA,eAAe,QAAQ,MAAM,IAAI;AACnC,QAAA;AACF,aAAA,MAAM,OAAO,cAAc,UAAU,IAAI,GAClC;AAAA,IAAA,QAEH;AAAA,IAGN;AACA,QAAI,SAAS,IAAK;AAClB,WAAO,QAAQ,IAAI;AAAA,EACrB;AACF;"}
1
+ {"version":3,"file":"findAncestor.js","sources":["../findAncestor.ts"],"sourcesContent":["import { access, constants } from 'node:fs/promises'\nimport { dirname, resolve } from 'node:path'\n\n/**\n * Find the first ancestor of a file from a given path. The search will start\n * from the given path and will continue until the root directory is reached.\n * If the file is not found, will throw an error.\n *\n * @param name The file name to find.\n * @param from The path to start from.\n * @returns The absolute path of the file found.\n * @example\n * // Create a file in the root directory.\n * await writeFile('/home/user/file.txt', 'Hello, world!')\n *\n * // Find the file from a subdirectory.\n * await findAncestor('file.txt', '/home/user/project') // '/home/user/file.txt'\n */\nexport async function findAncestor(name: string, from = process.cwd()): Promise<string | undefined> {\n while (from !== '') {\n const absolutePath = resolve(from, name)\n try {\n await access(absolutePath, constants.F_OK)\n return absolutePath\n }\n catch { /* Ignore error. */ }\n\n if (from === '/') break\n from = dirname(from)\n }\n}\n"],"names":[],"mappings":";;AAkBA,eAAsB,aAAa,MAAc,OAAO,QAAQ,OAAoC;AAClG,SAAO,SAAS,MAAI;AACZ,UAAA,eAAe,QAAQ,MAAM,IAAI;AACnC,QAAA;AACF,aAAA,MAAM,OAAO,cAAc,UAAU,IAAI,GAClC;AAAA,IAAA,QAEH;AAAA,IAAA;AAEN,QAAI,SAAS,IAAK;AAClB,WAAO,QAAQ,IAAI;AAAA,EAAA;AAEvB;"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
- var awaitable = require("@unshared/functions/awaitable"), promises = require("node:fs/promises"), node_path = require("node:path"), node_process = require("node:process");
3
- function findAncestors(name, from = node_process.cwd()) {
2
+ var awaitable = require("@unshared/functions/awaitable"), promises = require("node:fs/promises"), node_path = require("node:path");
3
+ function findAncestors(name, from = process.cwd()) {
4
4
  async function* createIterator() {
5
5
  for (; from !== ""; ) {
6
6
  const absolutePath = node_path.resolve(from, name);
@@ -1 +1 @@
1
- {"version":3,"file":"findAncestors.cjs","sources":["../findAncestors.ts"],"sourcesContent":["import type { Awaitable } from '@unshared/functions/awaitable'\nimport { awaitable } from '@unshared/functions/awaitable'\nimport { access, constants } from 'node:fs/promises'\nimport { dirname, resolve } from 'node:path'\nimport { cwd } from 'node:process'\n\n/**\n * Find all ancestors of a file from a given path. The search will start\n * from the given path and will continue until the root directory is reached.\n * If the file is not found, an empty array will be returned.\n *\n * @param name The file name to find.\n * @param from The path to start from.\n * @returns An awaitable iterator of the absolute paths of the files found.\n * @example\n * // Get all ancestors as an array.\n * const ancestors = await findAncestors('file', '/home/user/project')\n *\n * // Or, iterate over the ancestors one by one.\n * const ancestors = findAncestors('file', '/home/user/project')\n * for await (const ancestor of ancestors) console.log(ancestor)\n */\nexport function findAncestors(name: string, from: string = cwd()): Awaitable<AsyncIterable<string>, string[]> {\n async function * createIterator() {\n while (from !== '') {\n const absolutePath = resolve(from, name)\n try {\n await access(absolutePath, constants.F_OK)\n yield absolutePath\n }\n catch {\n\n /** Ignore error. */\n }\n if (from === '/') break\n from = dirname(from)\n }\n }\n\n // --- Instantiate the iterator and wrap it in an awaitable.\n const iterator = createIterator()\n return awaitable(iterator)\n}\n\n/** v8 ignore start */\nif (import.meta.vitest) {\n const { vol } = await import('memfs')\n\n test('should resolve ancestors from current directory', async() => {\n vi.mock('node:process', () => ({ cwd: () => '/home/user/project' }))\n vol.fromJSON({\n '/file': '',\n '/home/file': '',\n '/home/user/file': '',\n '/home/user/project/file': '',\n })\n const result = await findAncestors('file')\n expect(result).toStrictEqual([\n '/home/user/project/file',\n '/home/user/file',\n '/home/file',\n '/file',\n ])\n })\n\n test('should resolve ancestors at from given directory', async() => {\n vol.fromJSON({\n '/file': '',\n '/home/file': '',\n '/home/user/file': '',\n '/home/user/project/file': '',\n })\n const result = await findAncestors('file', '/home/user/project')\n expect(result).toStrictEqual([\n '/home/user/project/file',\n '/home/user/file',\n '/home/file',\n '/file',\n ])\n })\n\n test('should be iterable', async() => {\n vol.fromJSON({\n '/file': '',\n '/home/file': '',\n '/home/user/file': '',\n '/home/user/project/file': '',\n })\n const result = findAncestors('file', '/home/user/project')\n const items = []\n for await (const item of result) items.push(item)\n expect(items).toStrictEqual([\n '/home/user/project/file',\n '/home/user/file',\n '/home/file',\n '/file',\n ])\n })\n\n test('should return empty array if no ancestors were found', async() => {\n const result = await findAncestors('filename')\n expect(result).toStrictEqual([])\n })\n}\n"],"names":["cwd","resolve","access","constants","dirname","awaitable"],"mappings":";;AAsBO,SAAS,cAAc,MAAc,OAAeA,aAAAA,OAAmD;AAC5G,kBAAiB,iBAAiB;AAChC,WAAO,SAAS,MAAI;AACZ,YAAA,eAAeC,UAAAA,QAAQ,MAAM,IAAI;AACnC,UAAA;AACF,cAAMC,SAAAA,OAAO,cAAcC,SAAAA,UAAU,IAAI,GACzC,MAAM;AAAA,MAAA,QAEF;AAAA,MAGN;AACA,UAAI,SAAS,IAAK;AAClB,aAAOC,UAAAA,QAAQ,IAAI;AAAA,IACrB;AAAA,EACF;AAGA,QAAM,WAAW;AACjB,SAAOC,UAAAA,UAAU,QAAQ;AAC3B;;"}
1
+ {"version":3,"file":"findAncestors.cjs","sources":["../findAncestors.ts"],"sourcesContent":["import type { Awaitable } from '@unshared/functions/awaitable'\nimport { awaitable } from '@unshared/functions/awaitable'\nimport { access, constants } from 'node:fs/promises'\nimport { dirname, resolve } from 'node:path'\n\n/**\n * Find all ancestors of a file from a given path. The search will start\n * from the given path and will continue until the root directory is reached.\n * If the file is not found, an empty array will be returned.\n *\n * @param name The file name to find.\n * @param from The path to start from.\n * @returns An awaitable iterator of the absolute paths of the files found.\n * @example\n * // Get all ancestors as an array.\n * const ancestors = await findAncestors('file', '/home/user/project')\n *\n * // Or, iterate over the ancestors one by one.\n * const ancestors = findAncestors('file', '/home/user/project')\n * for await (const ancestor of ancestors) console.log(ancestor)\n */\nexport function findAncestors(name: string, from = process.cwd()): Awaitable<AsyncIterable<string>, string[]> {\n async function * createIterator() {\n while (from !== '') {\n const absolutePath = resolve(from, name)\n try {\n await access(absolutePath, constants.F_OK)\n yield absolutePath\n }\n catch {\n\n /** Ignore error. */\n }\n if (from === '/') break\n from = dirname(from)\n }\n }\n\n // --- Instantiate the iterator and wrap it in an awaitable.\n const iterator = createIterator()\n return awaitable(iterator)\n}\n"],"names":["resolve","access","constants","dirname","awaitable"],"mappings":";;AAqBO,SAAS,cAAc,MAAc,OAAO,QAAQ,OAAmD;AAC5G,kBAAiB,iBAAiB;AAChC,WAAO,SAAS,MAAI;AACZ,YAAA,eAAeA,UAAAA,QAAQ,MAAM,IAAI;AACnC,UAAA;AACF,cAAMC,SAAO,OAAA,cAAcC,SAAU,UAAA,IAAI,GACzC,MAAM;AAAA,MAAA,QAEF;AAAA,MAAA;AAIN,UAAI,SAAS,IAAK;AAClB,aAAOC,kBAAQ,IAAI;AAAA,IAAA;AAAA,EACrB;AAIF,QAAM,WAAW,eAAe;AAChC,SAAOC,UAAAA,UAAU,QAAQ;AAC3B;;"}
@@ -1,8 +1,7 @@
1
1
  import { awaitable } from "@unshared/functions/awaitable";
2
2
  import { access, constants } from "node:fs/promises";
3
3
  import { resolve, dirname } from "node:path";
4
- import { cwd } from "node:process";
5
- function findAncestors(name, from = cwd()) {
4
+ function findAncestors(name, from = process.cwd()) {
6
5
  async function* createIterator() {
7
6
  for (; from !== ""; ) {
8
7
  const absolutePath = resolve(from, name);
@@ -1 +1 @@
1
- {"version":3,"file":"findAncestors.js","sources":["../findAncestors.ts"],"sourcesContent":["import type { Awaitable } from '@unshared/functions/awaitable'\nimport { awaitable } from '@unshared/functions/awaitable'\nimport { access, constants } from 'node:fs/promises'\nimport { dirname, resolve } from 'node:path'\nimport { cwd } from 'node:process'\n\n/**\n * Find all ancestors of a file from a given path. The search will start\n * from the given path and will continue until the root directory is reached.\n * If the file is not found, an empty array will be returned.\n *\n * @param name The file name to find.\n * @param from The path to start from.\n * @returns An awaitable iterator of the absolute paths of the files found.\n * @example\n * // Get all ancestors as an array.\n * const ancestors = await findAncestors('file', '/home/user/project')\n *\n * // Or, iterate over the ancestors one by one.\n * const ancestors = findAncestors('file', '/home/user/project')\n * for await (const ancestor of ancestors) console.log(ancestor)\n */\nexport function findAncestors(name: string, from: string = cwd()): Awaitable<AsyncIterable<string>, string[]> {\n async function * createIterator() {\n while (from !== '') {\n const absolutePath = resolve(from, name)\n try {\n await access(absolutePath, constants.F_OK)\n yield absolutePath\n }\n catch {\n\n /** Ignore error. */\n }\n if (from === '/') break\n from = dirname(from)\n }\n }\n\n // --- Instantiate the iterator and wrap it in an awaitable.\n const iterator = createIterator()\n return awaitable(iterator)\n}\n\n/** v8 ignore start */\nif (import.meta.vitest) {\n const { vol } = await import('memfs')\n\n test('should resolve ancestors from current directory', async() => {\n vi.mock('node:process', () => ({ cwd: () => '/home/user/project' }))\n vol.fromJSON({\n '/file': '',\n '/home/file': '',\n '/home/user/file': '',\n '/home/user/project/file': '',\n })\n const result = await findAncestors('file')\n expect(result).toStrictEqual([\n '/home/user/project/file',\n '/home/user/file',\n '/home/file',\n '/file',\n ])\n })\n\n test('should resolve ancestors at from given directory', async() => {\n vol.fromJSON({\n '/file': '',\n '/home/file': '',\n '/home/user/file': '',\n '/home/user/project/file': '',\n })\n const result = await findAncestors('file', '/home/user/project')\n expect(result).toStrictEqual([\n '/home/user/project/file',\n '/home/user/file',\n '/home/file',\n '/file',\n ])\n })\n\n test('should be iterable', async() => {\n vol.fromJSON({\n '/file': '',\n '/home/file': '',\n '/home/user/file': '',\n '/home/user/project/file': '',\n })\n const result = findAncestors('file', '/home/user/project')\n const items = []\n for await (const item of result) items.push(item)\n expect(items).toStrictEqual([\n '/home/user/project/file',\n '/home/user/file',\n '/home/file',\n '/file',\n ])\n })\n\n test('should return empty array if no ancestors were found', async() => {\n const result = await findAncestors('filename')\n expect(result).toStrictEqual([])\n })\n}\n"],"names":[],"mappings":";;;;AAsBO,SAAS,cAAc,MAAc,OAAe,OAAmD;AAC5G,kBAAiB,iBAAiB;AAChC,WAAO,SAAS,MAAI;AACZ,YAAA,eAAe,QAAQ,MAAM,IAAI;AACnC,UAAA;AACF,cAAM,OAAO,cAAc,UAAU,IAAI,GACzC,MAAM;AAAA,MAAA,QAEF;AAAA,MAGN;AACA,UAAI,SAAS,IAAK;AAClB,aAAO,QAAQ,IAAI;AAAA,IACrB;AAAA,EACF;AAGA,QAAM,WAAW;AACjB,SAAO,UAAU,QAAQ;AAC3B;"}
1
+ {"version":3,"file":"findAncestors.js","sources":["../findAncestors.ts"],"sourcesContent":["import type { Awaitable } from '@unshared/functions/awaitable'\nimport { awaitable } from '@unshared/functions/awaitable'\nimport { access, constants } from 'node:fs/promises'\nimport { dirname, resolve } from 'node:path'\n\n/**\n * Find all ancestors of a file from a given path. The search will start\n * from the given path and will continue until the root directory is reached.\n * If the file is not found, an empty array will be returned.\n *\n * @param name The file name to find.\n * @param from The path to start from.\n * @returns An awaitable iterator of the absolute paths of the files found.\n * @example\n * // Get all ancestors as an array.\n * const ancestors = await findAncestors('file', '/home/user/project')\n *\n * // Or, iterate over the ancestors one by one.\n * const ancestors = findAncestors('file', '/home/user/project')\n * for await (const ancestor of ancestors) console.log(ancestor)\n */\nexport function findAncestors(name: string, from = process.cwd()): Awaitable<AsyncIterable<string>, string[]> {\n async function * createIterator() {\n while (from !== '') {\n const absolutePath = resolve(from, name)\n try {\n await access(absolutePath, constants.F_OK)\n yield absolutePath\n }\n catch {\n\n /** Ignore error. */\n }\n if (from === '/') break\n from = dirname(from)\n }\n }\n\n // --- Instantiate the iterator and wrap it in an awaitable.\n const iterator = createIterator()\n return awaitable(iterator)\n}\n"],"names":[],"mappings":";;;AAqBO,SAAS,cAAc,MAAc,OAAO,QAAQ,OAAmD;AAC5G,kBAAiB,iBAAiB;AAChC,WAAO,SAAS,MAAI;AACZ,YAAA,eAAe,QAAQ,MAAM,IAAI;AACnC,UAAA;AACF,cAAM,OAAO,cAAc,UAAU,IAAI,GACzC,MAAM;AAAA,MAAA,QAEF;AAAA,MAAA;AAIN,UAAI,SAAS,IAAK;AAClB,aAAO,QAAQ,IAAI;AAAA,IAAA;AAAA,EACrB;AAIF,QAAM,WAAW,eAAe;AAChC,SAAO,UAAU,QAAQ;AAC3B;"}
package/dist/glob.cjs CHANGED
@@ -1,14 +1,14 @@
1
1
  "use strict";
2
- var awaitable = require("@unshared/functions/awaitable"), createPattern = require("@unshared/string/createPattern"), promises = require("node:fs/promises"), node_path = require("node:path"), node_process = require("node:process");
2
+ var awaitable = require("@unshared/functions/awaitable"), createPattern = require("@unshared/string/createPattern"), promises = require("node:fs/promises"), node_path = require("node:path");
3
3
  function glob(pattern, options = {}) {
4
4
  const {
5
- cwd = node_process.cwd(),
5
+ cwd = process.cwd(),
6
6
  exclude = [],
7
7
  getRelative = !1,
8
8
  getStats = !1,
9
9
  onlyDirectories = !1,
10
10
  onlyFiles = !1
11
- } = options, patterns = (Array.isArray(pattern) ? pattern : [pattern]).map(createPattern.createPattern), excludePatterns = (Array.isArray(exclude) ? exclude : [exclude]).map(createPattern.createPattern), searchPool = [cwd];
11
+ } = options, patterns = (Array.isArray(pattern) ? pattern : [pattern]).map((pattern2) => createPattern.createPattern(pattern2)), excludePatterns = (Array.isArray(exclude) ? exclude : [exclude]).map((pattern2) => createPattern.createPattern(pattern2)), searchPool = [cwd];
12
12
  async function* createIterator() {
13
13
  for (; searchPool.length > 0; ) {
14
14
  const directory = searchPool.pop(), entities = await promises.readdir(directory, { withFileTypes: !0 }).catch(() => []);
package/dist/glob.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"glob.cjs","sources":["../glob.ts"],"sourcesContent":["import type { Awaitable } from '@unshared/functions/awaitable'\nimport type { MaybeArray } from '@unshared/types'\nimport type { Stats } from 'node:fs'\nimport { awaitable } from '@unshared/functions/awaitable'\nimport { createPattern } from '@unshared/string/createPattern'\nimport { readdir, stat } from 'node:fs/promises'\nimport { join, relative } from 'node:path'\nimport { cwd as getCwd } from 'node:process'\n\n/**\n * An entry in the glob result iterator or array.\n */\nexport type GlobEntry = Stats | string\n\n/**\n * The result of a glob operation. If `Stat` is `true` the result will be an\n * array of file stats. Otherwise the result will be an array of file paths.\n */\nexport type GlobResult<T extends boolean = boolean> = T extends true\n ? Awaitable<AsyncIterable<Stats>, Stats[]>\n : Awaitable<AsyncIterable<string>, string[]>\n\nexport interface GlobOptions<Stat extends boolean = boolean> {\n\n /**\n * The current working directory. Used to determine the base path for the glob\n * pattern.\n *\n * @default process.cwd()\n */\n cwd?: string\n\n /**\n * A list of patterns to exclude from the result.\n *\n * @default []\n */\n exclude?: MaybeArray<string>\n\n /**\n * Return the paths relative to the current working directory. Will be ignored\n * if `stats` is `true`.\n *\n * @default false\n */\n getRelative?: boolean\n\n /**\n * Return the file stats instead of the file path. Allowing you to filter-out\n * files based on their stats.\n *\n * @default false\n */\n getStats?: Stat\n\n /**\n * If `true` and the glob pattern will only match directories.\n *\n * @default false\n * @example glob('src/**', { onlyDirectories: true }) // ['src/foo', 'src/foo/bar']\n */\n onlyDirectories?: boolean\n\n /**\n * Only return entries that matches the path of a file.\n *\n * @default false\n * @example glob('src/**', { onlyFiles: true }) // ['src/foo.ts', 'src/foo/bar.ts']\n */\n onlyFiles?: boolean\n}\n\n/**\n * Find files matching a glob pattern.\n *\n * @param pattern The glob pattern.\n * @param options The glob options.\n * @returns An awaitable asyncronous iterator of file paths.\n * @example\n * const files = glob('src/*.ts')\n * for await (const file of files) { ... }\n */\nexport function glob(pattern: MaybeArray<string>, options?: GlobOptions<false>): GlobResult<false>\nexport function glob(pattern: MaybeArray<string>, options?: GlobOptions<true>): GlobResult<true>\nexport function glob<T extends boolean>(pattern: MaybeArray<string>, options?: GlobOptions<T>): GlobResult<T>\nexport function glob(pattern: MaybeArray<string>, options: GlobOptions = {}): GlobResult {\n const {\n cwd = getCwd(),\n exclude = [],\n getRelative = false,\n getStats = false,\n onlyDirectories = false,\n onlyFiles = false,\n } = options\n\n // --- Convert the pattern to an array of RegExp.\n const patternArray = Array.isArray(pattern) ? pattern : [pattern]\n const patterns = patternArray.map(createPattern)\n const exludeArray = Array.isArray(exclude) ? exclude : [exclude]\n const excludePatterns = exludeArray.map(createPattern)\n\n // --- Create an iterator that will yield the matching paths.\n const searchPool: string[] = [cwd]\n async function * createIterator() {\n while (searchPool.length > 0) {\n const directory = searchPool.pop()!\n const entities = await readdir(directory, { withFileTypes: true }).catch(() => [])\n\n for (const entity of entities) {\n const pathAbsolute = join(directory, entity.name)\n const pathRelative = relative(cwd, pathAbsolute)\n const isFile = entity.isFile()\n const isDirectory = entity.isDirectory()\n\n // --- Add the directory to the list of directories to check.\n if (isDirectory) searchPool.push(pathAbsolute)\n\n // --- Filter-out the non-matching entries.\n if (onlyFiles && !isFile) continue\n if (onlyDirectories && !isDirectory) continue\n\n // --- Check if the path matches the pattern(s).\n const isMatch = patterns.some(pattern => pattern.test(pathRelative))\n if (!isMatch) continue\n\n // --- Check if the path matches the exclude pattern(s).\n const isExcluded = excludePatterns.some(pattern => pattern.test(pathRelative))\n if (isExcluded) continue\n\n // --- Return the result.\n let result: GlobEntry = pathAbsolute\n if (getStats) result = await stat(pathAbsolute)\n if (getRelative) result = `./${pathRelative}`\n yield result\n }\n }\n }\n\n // --- Instantiate the iterator.\n const iterator = createIterator()\n\n // --- Return the iterator or the result as an array.\n return awaitable(iterator) as GlobResult\n}\n\n/* v8 ignore next */\nif (import.meta.vitest) {\n const { vol } = await import('memfs')\n\n beforeEach(() => {\n vol.fromJSON({\n '/project/bar.ts': '',\n '/project/baz.ts': '',\n '/project/dist/bar.js': '',\n '/project/dist/baz.js': '',\n '/project/dist/docs/CHANGELOG.md': '',\n '/project/dist/docs/README.md': '',\n '/project/dist/foo.js': '',\n '/project/foo.ts': '',\n '/project/README.md': '',\n })\n })\n\n test('should yield the paths matching a glob pattern', async() => {\n const files = glob('*.ts', { cwd: '/project' })\n const result = []\n for await (const file of files) result.push(file)\n expect(result).toStrictEqual([\n '/project/bar.ts',\n '/project/baz.ts',\n '/project/foo.ts',\n ])\n })\n\n test('should find the absolute path matching a glob pattern', async() => {\n const files = await glob('*.ts', { cwd: '/project' })\n expect(files).toStrictEqual([\n '/project/bar.ts',\n '/project/baz.ts',\n '/project/foo.ts',\n ])\n })\n\n test('should find the relative path matching a glob pattern', async() => {\n const files = await glob('*.ts', { cwd: '/project', getRelative: true })\n expect(files).toStrictEqual([\n './bar.ts',\n './baz.ts',\n './foo.ts',\n ])\n })\n\n test('should find the stats matching a glob pattern', async() => {\n const files = await glob('*.ts', { cwd: '/project', getStats: true })\n const expected = [\n vol.statSync('/project/foo.ts'),\n vol.statSync('/project/bar.ts'),\n vol.statSync('/project/baz.ts'),\n ]\n expect(files.map(x => x.uid)).toStrictEqual(expected.map(x => x.uid))\n })\n\n test('should find the paths matching an exclude pattern', async() => {\n const files = await glob('*.ts', { cwd: '/project', exclude: 'baz.ts' })\n const expected = [\n '/project/bar.ts',\n '/project/foo.ts',\n ]\n expect(files).toStrictEqual(expected)\n })\n\n test('should find nested and non-nested files', async() => {\n const files = await glob('**/*', { cwd: '/project', onlyFiles: true })\n expect(files).toStrictEqual([\n '/project/README.md',\n '/project/bar.ts',\n '/project/baz.ts',\n '/project/foo.ts',\n '/project/dist/bar.js',\n '/project/dist/baz.js',\n '/project/dist/foo.js',\n '/project/dist/docs/CHANGELOG.md',\n '/project/dist/docs/README.md',\n ])\n })\n\n test('should find nested and non-nested directories', async() => {\n const files = await glob('**/*', { cwd: '/project', onlyDirectories: true })\n expect(files).toStrictEqual([\n '/project/dist',\n '/project/dist/docs',\n ])\n })\n\n test('should find files but exclude the dist directory', async() => {\n const files = await glob('*', { cwd: '/project', exclude: 'dist/**' })\n expect(files).toStrictEqual([\n '/project/README.md',\n '/project/bar.ts',\n '/project/baz.ts',\n '/project/foo.ts',\n ])\n })\n\n test('should infer the return type as a collection of `Stats`', () => {\n const files = glob('*.ts', { cwd: '/project', getStats: true })\n expectTypeOf(files).toEqualTypeOf<Awaitable<AsyncIterable<Stats>, Stats[]>>()\n })\n\n test('should infer the return type as a collection of `string`', () => {\n const files = glob('*.ts', { cwd: '/project', getStats: false })\n expectTypeOf(files).toEqualTypeOf<Awaitable<AsyncIterable<string>, string[]>>()\n })\n\n test('should infer the return type as a collection of `Stats` or `string`', () => {\n const files = glob('*.ts', { cwd: '/project', getStats: true as boolean })\n expectTypeOf(files).toEqualTypeOf<Awaitable<AsyncIterable<Stats>, Stats[]> | Awaitable<AsyncIterable<string>, string[]>>()\n })\n}\n"],"names":["getCwd","createPattern","readdir","join","relative","pattern","stat","awaitable"],"mappings":";;AAqFO,SAAS,KAAK,SAA6B,UAAuB,IAAgB;AACjF,QAAA;AAAA,IACJ,MAAMA,aAAAA,IAAO;AAAA,IACb,UAAU,CAAC;AAAA,IACX,cAAc;AAAA,IACd,WAAW;AAAA,IACX,kBAAkB;AAAA,IAClB,YAAY;AAAA,EACV,IAAA,SAIE,YADe,MAAM,QAAQ,OAAO,IAAI,UAAU,CAAC,OAAO,GAClC,IAAIC,cAAa,aAAA,GAEzC,mBADc,MAAM,QAAQ,OAAO,IAAI,UAAU,CAAC,OAAO,GAC3B,IAAIA,cAAAA,aAAa,GAG/C,aAAuB,CAAC,GAAG;AACjC,kBAAiB,iBAAiB;AACzB,WAAA,WAAW,SAAS,KAAG;AAC5B,YAAM,YAAY,WAAW,IACvB,GAAA,WAAW,MAAMC,SAAAA,QAAQ,WAAW,EAAE,eAAe,GAAM,CAAA,EAAE,MAAM,MAAM,CAAE,CAAA;AAEjF,iBAAW,UAAU,UAAU;AAC7B,cAAM,eAAeC,UAAAA,KAAK,WAAW,OAAO,IAAI,GAC1C,eAAeC,mBAAS,KAAK,YAAY,GACzC,SAAS,OAAO,OAChB,GAAA,cAAc,OAAO;AAe3B,YAZI,eAAa,WAAW,KAAK,YAAY,GAGzC,aAAa,CAAC,UACd,mBAAmB,CAAC,eAIpB,CADY,SAAS,KAAK,CAAAC,aAAWA,SAAQ,KAAK,YAAY,CAAC,KAIhD,gBAAgB,KAAK,CAAAA,aAAWA,SAAQ,KAAK,YAAY,CAAC,EAC7D;AAGhB,YAAI,SAAoB;AACpB,qBAAU,SAAS,MAAMC,cAAK,YAAY,IAC1C,gBAAa,SAAS,KAAK,YAAY,KAC3C,MAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAGA,QAAM,WAAW;AAGjB,SAAOC,UAAAA,UAAU,QAAQ;AAC3B;;"}
1
+ {"version":3,"file":"glob.cjs","sources":["../glob.ts"],"sourcesContent":["import type { Awaitable } from '@unshared/functions/awaitable'\nimport type { MaybeArray } from '@unshared/types'\nimport type { Stats } from 'node:fs'\nimport { awaitable } from '@unshared/functions/awaitable'\nimport { createPattern } from '@unshared/string/createPattern'\nimport { readdir, stat } from 'node:fs/promises'\nimport { join, relative } from 'node:path'\n\n/**\n * An entry in the glob result iterator or array.\n */\nexport type GlobEntry = Stats | string\n\n/**\n * The result of a glob operation. If `Stat` is `true` the result will be an\n * array of file stats. Otherwise the result will be an array of file paths.\n */\nexport type GlobResult<T extends boolean = boolean> = T extends true\n ? Awaitable<AsyncIterable<Stats>, Stats[]>\n : Awaitable<AsyncIterable<string>, string[]>\n\nexport interface GlobOptions<Stat extends boolean = boolean> {\n\n /**\n * The current working directory. Used to determine the base path for the glob\n * pattern.\n *\n * @default process.cwd()\n */\n cwd?: string\n\n /**\n * A list of patterns to exclude from the result.\n *\n * @default []\n */\n exclude?: MaybeArray<string>\n\n /**\n * Return the paths relative to the current working directory. Will be ignored\n * if `stats` is `true`.\n *\n * @default false\n */\n getRelative?: boolean\n\n /**\n * Return the file stats instead of the file path. Allowing you to filter-out\n * files based on their stats.\n *\n * @default false\n */\n getStats?: Stat\n\n /**\n * If `true` and the glob pattern will only match directories.\n *\n * @default false\n * @example glob('src/**', { onlyDirectories: true }) // ['src/foo', 'src/foo/bar']\n */\n onlyDirectories?: boolean\n\n /**\n * Only return entries that matches the path of a file.\n *\n * @default false\n * @example glob('src/**', { onlyFiles: true }) // ['src/foo.ts', 'src/foo/bar.ts']\n */\n onlyFiles?: boolean\n}\n\n/**\n * Find files matching a glob pattern.\n *\n * @param pattern The glob pattern.\n * @param options The glob options.\n * @returns An awaitable asyncronous iterator of file paths.\n * @example\n * const files = glob('src/*.ts')\n * for await (const file of files) { ... }\n */\nexport function glob(pattern: MaybeArray<string>, options?: GlobOptions<false>): GlobResult<false>\nexport function glob(pattern: MaybeArray<string>, options?: GlobOptions<true>): GlobResult<true>\nexport function glob<T extends boolean>(pattern: MaybeArray<string>, options?: GlobOptions<T>): GlobResult<T>\nexport function glob(pattern: MaybeArray<string>, options: GlobOptions = {}): GlobResult {\n const {\n cwd = process.cwd(),\n exclude = [],\n getRelative = false,\n getStats = false,\n onlyDirectories = false,\n onlyFiles = false,\n } = options\n\n // --- Convert the pattern to an array of RegExp.\n const patternArray = Array.isArray(pattern) ? pattern : [pattern]\n const patterns = patternArray.map(pattern => createPattern(pattern))\n const exludeArray = Array.isArray(exclude) ? exclude : [exclude]\n const excludePatterns = exludeArray.map(pattern => createPattern(pattern))\n\n // --- Create an iterator that will yield the matching paths.\n const searchPool: string[] = [cwd]\n async function * createIterator() {\n while (searchPool.length > 0) {\n const directory = searchPool.pop()!\n const entities = await readdir(directory, { withFileTypes: true }).catch(() => [])\n\n for (const entity of entities) {\n const pathAbsolute = join(directory, entity.name)\n const pathRelative = relative(cwd, pathAbsolute)\n const isFile = entity.isFile()\n const isDirectory = entity.isDirectory()\n\n // --- Add the directory to the list of directories to check.\n if (isDirectory) searchPool.push(pathAbsolute)\n\n // --- Filter-out the non-matching entries.\n if (onlyFiles && !isFile) continue\n if (onlyDirectories && !isDirectory) continue\n\n // --- Check if the path matches the pattern(s).\n const isMatch = patterns.some(pattern => pattern.test(pathRelative))\n if (!isMatch) continue\n\n // --- Check if the path matches the exclude pattern(s).\n const isExcluded = excludePatterns.some(pattern => pattern.test(pathRelative))\n if (isExcluded) continue\n\n // --- Return the result.\n let result: GlobEntry = pathAbsolute\n if (getStats) result = await stat(pathAbsolute)\n if (getRelative) result = `./${pathRelative}`\n yield result\n }\n }\n }\n\n // --- Instantiate the iterator.\n const iterator = createIterator()\n\n // --- Return the iterator or the result as an array.\n return awaitable(iterator) as GlobResult\n}\n"],"names":["pattern","createPattern","readdir","join","relative","stat","awaitable"],"mappings":";;AAoFO,SAAS,KAAK,SAA6B,UAAuB,IAAgB;AACjF,QAAA;AAAA,IACJ,MAAM,QAAQ,IAAI;AAAA,IAClB,UAAU,CAAC;AAAA,IACX,cAAc;AAAA,IACd,WAAW;AAAA,IACX,kBAAkB;AAAA,IAClB,YAAY;AAAA,EAAA,IACV,SAIE,YADe,MAAM,QAAQ,OAAO,IAAI,UAAU,CAAC,OAAO,GAClC,IAAI,CAAAA,aAAWC,cAAA,cAAcD,QAAO,CAAC,GAE7D,mBADc,MAAM,QAAQ,OAAO,IAAI,UAAU,CAAC,OAAO,GAC3B,IAAI,CAAAA,aAAWC,cAAAA,cAAcD,QAAO,CAAC,GAGnE,aAAuB,CAAC,GAAG;AACjC,kBAAiB,iBAAiB;AACzB,WAAA,WAAW,SAAS,KAAG;AAC5B,YAAM,YAAY,WAAW,IAAA,GACvB,WAAW,MAAME,SAAQ,QAAA,WAAW,EAAE,eAAe,IAAM,EAAE,MAAM,MAAM,CAAA,CAAE;AAEjF,iBAAW,UAAU,UAAU;AAC7B,cAAM,eAAeC,UAAAA,KAAK,WAAW,OAAO,IAAI,GAC1C,eAAeC,UAAA,SAAS,KAAK,YAAY,GACzC,SAAS,OAAO,UAChB,cAAc,OAAO,YAAY;AAevC,YAZI,eAAa,WAAW,KAAK,YAAY,GAGzC,aAAa,CAAC,UACd,mBAAmB,CAAC,eAIpB,CADY,SAAS,KAAK,CAAAJ,aAAWA,SAAQ,KAAK,YAAY,CAAC,KAIhD,gBAAgB,KAAK,CAAAA,aAAWA,SAAQ,KAAK,YAAY,CAAC,EAC7D;AAGhB,YAAI,SAAoB;AACpB,qBAAU,SAAS,MAAMK,cAAK,YAAY,IAC1C,gBAAa,SAAS,KAAK,YAAY,KAC3C,MAAM;AAAA,MAAA;AAAA,IACR;AAAA,EACF;AAIF,QAAM,WAAW,eAAe;AAGhC,SAAOC,UAAAA,UAAU,QAAQ;AAC3B;;"}
package/dist/glob.js CHANGED
@@ -2,21 +2,20 @@ import { awaitable } from "@unshared/functions/awaitable";
2
2
  import { createPattern } from "@unshared/string/createPattern";
3
3
  import { readdir, stat } from "node:fs/promises";
4
4
  import { join, relative } from "node:path";
5
- import { cwd } from "node:process";
6
5
  function glob(pattern, options = {}) {
7
6
  const {
8
- cwd: cwd$1 = cwd(),
7
+ cwd = process.cwd(),
9
8
  exclude = [],
10
9
  getRelative = !1,
11
10
  getStats = !1,
12
11
  onlyDirectories = !1,
13
12
  onlyFiles = !1
14
- } = options, patterns = (Array.isArray(pattern) ? pattern : [pattern]).map(createPattern), excludePatterns = (Array.isArray(exclude) ? exclude : [exclude]).map(createPattern), searchPool = [cwd$1];
13
+ } = options, patterns = (Array.isArray(pattern) ? pattern : [pattern]).map((pattern2) => createPattern(pattern2)), excludePatterns = (Array.isArray(exclude) ? exclude : [exclude]).map((pattern2) => createPattern(pattern2)), searchPool = [cwd];
15
14
  async function* createIterator() {
16
15
  for (; searchPool.length > 0; ) {
17
16
  const directory = searchPool.pop(), entities = await readdir(directory, { withFileTypes: !0 }).catch(() => []);
18
17
  for (const entity of entities) {
19
- const pathAbsolute = join(directory, entity.name), pathRelative = relative(cwd$1, pathAbsolute), isFile = entity.isFile(), isDirectory = entity.isDirectory();
18
+ const pathAbsolute = join(directory, entity.name), pathRelative = relative(cwd, pathAbsolute), isFile = entity.isFile(), isDirectory = entity.isDirectory();
20
19
  if (isDirectory && searchPool.push(pathAbsolute), onlyFiles && !isFile || onlyDirectories && !isDirectory || !patterns.some((pattern2) => pattern2.test(pathRelative)) || excludePatterns.some((pattern2) => pattern2.test(pathRelative))) continue;
21
20
  let result = pathAbsolute;
22
21
  getStats && (result = await stat(pathAbsolute)), getRelative && (result = `./${pathRelative}`), yield result;
package/dist/glob.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"glob.js","sources":["../glob.ts"],"sourcesContent":["import type { Awaitable } from '@unshared/functions/awaitable'\nimport type { MaybeArray } from '@unshared/types'\nimport type { Stats } from 'node:fs'\nimport { awaitable } from '@unshared/functions/awaitable'\nimport { createPattern } from '@unshared/string/createPattern'\nimport { readdir, stat } from 'node:fs/promises'\nimport { join, relative } from 'node:path'\nimport { cwd as getCwd } from 'node:process'\n\n/**\n * An entry in the glob result iterator or array.\n */\nexport type GlobEntry = Stats | string\n\n/**\n * The result of a glob operation. If `Stat` is `true` the result will be an\n * array of file stats. Otherwise the result will be an array of file paths.\n */\nexport type GlobResult<T extends boolean = boolean> = T extends true\n ? Awaitable<AsyncIterable<Stats>, Stats[]>\n : Awaitable<AsyncIterable<string>, string[]>\n\nexport interface GlobOptions<Stat extends boolean = boolean> {\n\n /**\n * The current working directory. Used to determine the base path for the glob\n * pattern.\n *\n * @default process.cwd()\n */\n cwd?: string\n\n /**\n * A list of patterns to exclude from the result.\n *\n * @default []\n */\n exclude?: MaybeArray<string>\n\n /**\n * Return the paths relative to the current working directory. Will be ignored\n * if `stats` is `true`.\n *\n * @default false\n */\n getRelative?: boolean\n\n /**\n * Return the file stats instead of the file path. Allowing you to filter-out\n * files based on their stats.\n *\n * @default false\n */\n getStats?: Stat\n\n /**\n * If `true` and the glob pattern will only match directories.\n *\n * @default false\n * @example glob('src/**', { onlyDirectories: true }) // ['src/foo', 'src/foo/bar']\n */\n onlyDirectories?: boolean\n\n /**\n * Only return entries that matches the path of a file.\n *\n * @default false\n * @example glob('src/**', { onlyFiles: true }) // ['src/foo.ts', 'src/foo/bar.ts']\n */\n onlyFiles?: boolean\n}\n\n/**\n * Find files matching a glob pattern.\n *\n * @param pattern The glob pattern.\n * @param options The glob options.\n * @returns An awaitable asyncronous iterator of file paths.\n * @example\n * const files = glob('src/*.ts')\n * for await (const file of files) { ... }\n */\nexport function glob(pattern: MaybeArray<string>, options?: GlobOptions<false>): GlobResult<false>\nexport function glob(pattern: MaybeArray<string>, options?: GlobOptions<true>): GlobResult<true>\nexport function glob<T extends boolean>(pattern: MaybeArray<string>, options?: GlobOptions<T>): GlobResult<T>\nexport function glob(pattern: MaybeArray<string>, options: GlobOptions = {}): GlobResult {\n const {\n cwd = getCwd(),\n exclude = [],\n getRelative = false,\n getStats = false,\n onlyDirectories = false,\n onlyFiles = false,\n } = options\n\n // --- Convert the pattern to an array of RegExp.\n const patternArray = Array.isArray(pattern) ? pattern : [pattern]\n const patterns = patternArray.map(createPattern)\n const exludeArray = Array.isArray(exclude) ? exclude : [exclude]\n const excludePatterns = exludeArray.map(createPattern)\n\n // --- Create an iterator that will yield the matching paths.\n const searchPool: string[] = [cwd]\n async function * createIterator() {\n while (searchPool.length > 0) {\n const directory = searchPool.pop()!\n const entities = await readdir(directory, { withFileTypes: true }).catch(() => [])\n\n for (const entity of entities) {\n const pathAbsolute = join(directory, entity.name)\n const pathRelative = relative(cwd, pathAbsolute)\n const isFile = entity.isFile()\n const isDirectory = entity.isDirectory()\n\n // --- Add the directory to the list of directories to check.\n if (isDirectory) searchPool.push(pathAbsolute)\n\n // --- Filter-out the non-matching entries.\n if (onlyFiles && !isFile) continue\n if (onlyDirectories && !isDirectory) continue\n\n // --- Check if the path matches the pattern(s).\n const isMatch = patterns.some(pattern => pattern.test(pathRelative))\n if (!isMatch) continue\n\n // --- Check if the path matches the exclude pattern(s).\n const isExcluded = excludePatterns.some(pattern => pattern.test(pathRelative))\n if (isExcluded) continue\n\n // --- Return the result.\n let result: GlobEntry = pathAbsolute\n if (getStats) result = await stat(pathAbsolute)\n if (getRelative) result = `./${pathRelative}`\n yield result\n }\n }\n }\n\n // --- Instantiate the iterator.\n const iterator = createIterator()\n\n // --- Return the iterator or the result as an array.\n return awaitable(iterator) as GlobResult\n}\n\n/* v8 ignore next */\nif (import.meta.vitest) {\n const { vol } = await import('memfs')\n\n beforeEach(() => {\n vol.fromJSON({\n '/project/bar.ts': '',\n '/project/baz.ts': '',\n '/project/dist/bar.js': '',\n '/project/dist/baz.js': '',\n '/project/dist/docs/CHANGELOG.md': '',\n '/project/dist/docs/README.md': '',\n '/project/dist/foo.js': '',\n '/project/foo.ts': '',\n '/project/README.md': '',\n })\n })\n\n test('should yield the paths matching a glob pattern', async() => {\n const files = glob('*.ts', { cwd: '/project' })\n const result = []\n for await (const file of files) result.push(file)\n expect(result).toStrictEqual([\n '/project/bar.ts',\n '/project/baz.ts',\n '/project/foo.ts',\n ])\n })\n\n test('should find the absolute path matching a glob pattern', async() => {\n const files = await glob('*.ts', { cwd: '/project' })\n expect(files).toStrictEqual([\n '/project/bar.ts',\n '/project/baz.ts',\n '/project/foo.ts',\n ])\n })\n\n test('should find the relative path matching a glob pattern', async() => {\n const files = await glob('*.ts', { cwd: '/project', getRelative: true })\n expect(files).toStrictEqual([\n './bar.ts',\n './baz.ts',\n './foo.ts',\n ])\n })\n\n test('should find the stats matching a glob pattern', async() => {\n const files = await glob('*.ts', { cwd: '/project', getStats: true })\n const expected = [\n vol.statSync('/project/foo.ts'),\n vol.statSync('/project/bar.ts'),\n vol.statSync('/project/baz.ts'),\n ]\n expect(files.map(x => x.uid)).toStrictEqual(expected.map(x => x.uid))\n })\n\n test('should find the paths matching an exclude pattern', async() => {\n const files = await glob('*.ts', { cwd: '/project', exclude: 'baz.ts' })\n const expected = [\n '/project/bar.ts',\n '/project/foo.ts',\n ]\n expect(files).toStrictEqual(expected)\n })\n\n test('should find nested and non-nested files', async() => {\n const files = await glob('**/*', { cwd: '/project', onlyFiles: true })\n expect(files).toStrictEqual([\n '/project/README.md',\n '/project/bar.ts',\n '/project/baz.ts',\n '/project/foo.ts',\n '/project/dist/bar.js',\n '/project/dist/baz.js',\n '/project/dist/foo.js',\n '/project/dist/docs/CHANGELOG.md',\n '/project/dist/docs/README.md',\n ])\n })\n\n test('should find nested and non-nested directories', async() => {\n const files = await glob('**/*', { cwd: '/project', onlyDirectories: true })\n expect(files).toStrictEqual([\n '/project/dist',\n '/project/dist/docs',\n ])\n })\n\n test('should find files but exclude the dist directory', async() => {\n const files = await glob('*', { cwd: '/project', exclude: 'dist/**' })\n expect(files).toStrictEqual([\n '/project/README.md',\n '/project/bar.ts',\n '/project/baz.ts',\n '/project/foo.ts',\n ])\n })\n\n test('should infer the return type as a collection of `Stats`', () => {\n const files = glob('*.ts', { cwd: '/project', getStats: true })\n expectTypeOf(files).toEqualTypeOf<Awaitable<AsyncIterable<Stats>, Stats[]>>()\n })\n\n test('should infer the return type as a collection of `string`', () => {\n const files = glob('*.ts', { cwd: '/project', getStats: false })\n expectTypeOf(files).toEqualTypeOf<Awaitable<AsyncIterable<string>, string[]>>()\n })\n\n test('should infer the return type as a collection of `Stats` or `string`', () => {\n const files = glob('*.ts', { cwd: '/project', getStats: true as boolean })\n expectTypeOf(files).toEqualTypeOf<Awaitable<AsyncIterable<Stats>, Stats[]> | Awaitable<AsyncIterable<string>, string[]>>()\n })\n}\n"],"names":["cwd","getCwd","pattern"],"mappings":";;;;;AAqFO,SAAS,KAAK,SAA6B,UAAuB,IAAgB;AACjF,QAAA;AAAA,IACJA,KAAAA,QAAMC,IAAO;AAAA,IACb,UAAU,CAAC;AAAA,IACX,cAAc;AAAA,IACd,WAAW;AAAA,IACX,kBAAkB;AAAA,IAClB,YAAY;AAAA,EACV,IAAA,SAIE,YADe,MAAM,QAAQ,OAAO,IAAI,UAAU,CAAC,OAAO,GAClC,IAAI,aAAa,GAEzC,mBADc,MAAM,QAAQ,OAAO,IAAI,UAAU,CAAC,OAAO,GAC3B,IAAI,aAAa,GAG/C,aAAuB,CAACD,KAAG;AACjC,kBAAiB,iBAAiB;AACzB,WAAA,WAAW,SAAS,KAAG;AAC5B,YAAM,YAAY,WAAW,IACvB,GAAA,WAAW,MAAM,QAAQ,WAAW,EAAE,eAAe,GAAM,CAAA,EAAE,MAAM,MAAM,CAAE,CAAA;AAEjF,iBAAW,UAAU,UAAU;AAC7B,cAAM,eAAe,KAAK,WAAW,OAAO,IAAI,GAC1C,eAAe,SAASA,OAAK,YAAY,GACzC,SAAS,OAAO,OAChB,GAAA,cAAc,OAAO;AAe3B,YAZI,eAAa,WAAW,KAAK,YAAY,GAGzC,aAAa,CAAC,UACd,mBAAmB,CAAC,eAIpB,CADY,SAAS,KAAK,CAAAE,aAAWA,SAAQ,KAAK,YAAY,CAAC,KAIhD,gBAAgB,KAAK,CAAAA,aAAWA,SAAQ,KAAK,YAAY,CAAC,EAC7D;AAGhB,YAAI,SAAoB;AACpB,qBAAU,SAAS,MAAM,KAAK,YAAY,IAC1C,gBAAa,SAAS,KAAK,YAAY,KAC3C,MAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAGA,QAAM,WAAW;AAGjB,SAAO,UAAU,QAAQ;AAC3B;"}
1
+ {"version":3,"file":"glob.js","sources":["../glob.ts"],"sourcesContent":["import type { Awaitable } from '@unshared/functions/awaitable'\nimport type { MaybeArray } from '@unshared/types'\nimport type { Stats } from 'node:fs'\nimport { awaitable } from '@unshared/functions/awaitable'\nimport { createPattern } from '@unshared/string/createPattern'\nimport { readdir, stat } from 'node:fs/promises'\nimport { join, relative } from 'node:path'\n\n/**\n * An entry in the glob result iterator or array.\n */\nexport type GlobEntry = Stats | string\n\n/**\n * The result of a glob operation. If `Stat` is `true` the result will be an\n * array of file stats. Otherwise the result will be an array of file paths.\n */\nexport type GlobResult<T extends boolean = boolean> = T extends true\n ? Awaitable<AsyncIterable<Stats>, Stats[]>\n : Awaitable<AsyncIterable<string>, string[]>\n\nexport interface GlobOptions<Stat extends boolean = boolean> {\n\n /**\n * The current working directory. Used to determine the base path for the glob\n * pattern.\n *\n * @default process.cwd()\n */\n cwd?: string\n\n /**\n * A list of patterns to exclude from the result.\n *\n * @default []\n */\n exclude?: MaybeArray<string>\n\n /**\n * Return the paths relative to the current working directory. Will be ignored\n * if `stats` is `true`.\n *\n * @default false\n */\n getRelative?: boolean\n\n /**\n * Return the file stats instead of the file path. Allowing you to filter-out\n * files based on their stats.\n *\n * @default false\n */\n getStats?: Stat\n\n /**\n * If `true` and the glob pattern will only match directories.\n *\n * @default false\n * @example glob('src/**', { onlyDirectories: true }) // ['src/foo', 'src/foo/bar']\n */\n onlyDirectories?: boolean\n\n /**\n * Only return entries that matches the path of a file.\n *\n * @default false\n * @example glob('src/**', { onlyFiles: true }) // ['src/foo.ts', 'src/foo/bar.ts']\n */\n onlyFiles?: boolean\n}\n\n/**\n * Find files matching a glob pattern.\n *\n * @param pattern The glob pattern.\n * @param options The glob options.\n * @returns An awaitable asyncronous iterator of file paths.\n * @example\n * const files = glob('src/*.ts')\n * for await (const file of files) { ... }\n */\nexport function glob(pattern: MaybeArray<string>, options?: GlobOptions<false>): GlobResult<false>\nexport function glob(pattern: MaybeArray<string>, options?: GlobOptions<true>): GlobResult<true>\nexport function glob<T extends boolean>(pattern: MaybeArray<string>, options?: GlobOptions<T>): GlobResult<T>\nexport function glob(pattern: MaybeArray<string>, options: GlobOptions = {}): GlobResult {\n const {\n cwd = process.cwd(),\n exclude = [],\n getRelative = false,\n getStats = false,\n onlyDirectories = false,\n onlyFiles = false,\n } = options\n\n // --- Convert the pattern to an array of RegExp.\n const patternArray = Array.isArray(pattern) ? pattern : [pattern]\n const patterns = patternArray.map(pattern => createPattern(pattern))\n const exludeArray = Array.isArray(exclude) ? exclude : [exclude]\n const excludePatterns = exludeArray.map(pattern => createPattern(pattern))\n\n // --- Create an iterator that will yield the matching paths.\n const searchPool: string[] = [cwd]\n async function * createIterator() {\n while (searchPool.length > 0) {\n const directory = searchPool.pop()!\n const entities = await readdir(directory, { withFileTypes: true }).catch(() => [])\n\n for (const entity of entities) {\n const pathAbsolute = join(directory, entity.name)\n const pathRelative = relative(cwd, pathAbsolute)\n const isFile = entity.isFile()\n const isDirectory = entity.isDirectory()\n\n // --- Add the directory to the list of directories to check.\n if (isDirectory) searchPool.push(pathAbsolute)\n\n // --- Filter-out the non-matching entries.\n if (onlyFiles && !isFile) continue\n if (onlyDirectories && !isDirectory) continue\n\n // --- Check if the path matches the pattern(s).\n const isMatch = patterns.some(pattern => pattern.test(pathRelative))\n if (!isMatch) continue\n\n // --- Check if the path matches the exclude pattern(s).\n const isExcluded = excludePatterns.some(pattern => pattern.test(pathRelative))\n if (isExcluded) continue\n\n // --- Return the result.\n let result: GlobEntry = pathAbsolute\n if (getStats) result = await stat(pathAbsolute)\n if (getRelative) result = `./${pathRelative}`\n yield result\n }\n }\n }\n\n // --- Instantiate the iterator.\n const iterator = createIterator()\n\n // --- Return the iterator or the result as an array.\n return awaitable(iterator) as GlobResult\n}\n"],"names":["pattern"],"mappings":";;;;AAoFO,SAAS,KAAK,SAA6B,UAAuB,IAAgB;AACjF,QAAA;AAAA,IACJ,MAAM,QAAQ,IAAI;AAAA,IAClB,UAAU,CAAC;AAAA,IACX,cAAc;AAAA,IACd,WAAW;AAAA,IACX,kBAAkB;AAAA,IAClB,YAAY;AAAA,EAAA,IACV,SAIE,YADe,MAAM,QAAQ,OAAO,IAAI,UAAU,CAAC,OAAO,GAClC,IAAI,CAAAA,aAAW,cAAcA,QAAO,CAAC,GAE7D,mBADc,MAAM,QAAQ,OAAO,IAAI,UAAU,CAAC,OAAO,GAC3B,IAAI,CAAAA,aAAW,cAAcA,QAAO,CAAC,GAGnE,aAAuB,CAAC,GAAG;AACjC,kBAAiB,iBAAiB;AACzB,WAAA,WAAW,SAAS,KAAG;AAC5B,YAAM,YAAY,WAAW,IAAA,GACvB,WAAW,MAAM,QAAQ,WAAW,EAAE,eAAe,IAAM,EAAE,MAAM,MAAM,CAAA,CAAE;AAEjF,iBAAW,UAAU,UAAU;AAC7B,cAAM,eAAe,KAAK,WAAW,OAAO,IAAI,GAC1C,eAAe,SAAS,KAAK,YAAY,GACzC,SAAS,OAAO,UAChB,cAAc,OAAO,YAAY;AAevC,YAZI,eAAa,WAAW,KAAK,YAAY,GAGzC,aAAa,CAAC,UACd,mBAAmB,CAAC,eAIpB,CADY,SAAS,KAAK,CAAAA,aAAWA,SAAQ,KAAK,YAAY,CAAC,KAIhD,gBAAgB,KAAK,CAAAA,aAAWA,SAAQ,KAAK,YAAY,CAAC,EAC7D;AAGhB,YAAI,SAAoB;AACpB,qBAAU,SAAS,MAAM,KAAK,YAAY,IAC1C,gBAAa,SAAS,KAAK,YAAY,KAC3C,MAAM;AAAA,MAAA;AAAA,IACR;AAAA,EACF;AAIF,QAAM,WAAW,eAAe;AAGhC,SAAO,UAAU,QAAQ;AAC3B;"}
package/dist/index.cjs CHANGED
@@ -1,13 +1,12 @@
1
1
  "use strict";
2
2
  var createTemporaryDirectory = require("./createTemporaryDirectory.cjs"), createTemporaryFile = require("./createTemporaryFile.cjs"), findAncestor = require("./findAncestor.cjs"), findAncestors = require("./findAncestors.cjs"), glob = require("./glob.cjs"), loadObject = require("./loadObject.cjs"), touch = require("./touch.cjs"), updateFile = require("./updateFile.cjs"), withTemporaryDirectories = require("./withTemporaryDirectories.cjs"), withTemporaryFiles = require("./withTemporaryFiles.cjs");
3
+ require("node:crypto");
3
4
  require("node:fs/promises");
4
5
  require("node:os");
5
6
  require("node:path");
6
- require("node:process");
7
7
  require("@unshared/functions/awaitable");
8
8
  require("@unshared/string/createPattern");
9
9
  require("@unshared/collection/overwrite");
10
- require("@unshared/functions/garbageCollected");
11
10
  require("@unshared/reactivity/reactive");
12
11
  require("node:events");
13
12
  require("node:fs");
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;"}
package/dist/index.js CHANGED
@@ -8,14 +8,13 @@ import { touch } from "./touch.js";
8
8
  import { updateFile } from "./updateFile.js";
9
9
  import { withTemporaryDirectories } from "./withTemporaryDirectories.js";
10
10
  import { withTemporaryFiles } from "./withTemporaryFiles.js";
11
+ import "node:crypto";
11
12
  import "node:fs/promises";
12
13
  import "node:os";
13
14
  import "node:path";
14
- import "node:process";
15
15
  import "@unshared/functions/awaitable";
16
16
  import "@unshared/string/createPattern";
17
17
  import "@unshared/collection/overwrite";
18
- import "@unshared/functions/garbageCollected";
19
18
  import "@unshared/reactivity/reactive";
20
19
  import "node:events";
21
20
  import "node:fs";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;"}
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
- var overwrite = require("@unshared/collection/overwrite"), awaitable = require("@unshared/functions/awaitable"), garbageCollected = require("@unshared/functions/garbageCollected"), reactive = require("@unshared/reactivity/reactive"), node_events = require("node:events"), node_fs = require("node:fs"), promises = require("node:fs/promises"), node_path = require("node:path");
2
+ var overwrite = require("@unshared/collection/overwrite"), awaitable = require("@unshared/functions/awaitable"), reactive = require("@unshared/reactivity/reactive"), node_events = require("node:events"), node_fs = require("node:fs"), promises = require("node:fs/promises"), node_path = require("node:path");
3
3
  class FSObject extends node_events.EventEmitter {
4
4
  /**
5
5
  * Load a JSON file and keep it synchronized with it's source file.
@@ -18,7 +18,7 @@ class FSObject extends node_events.EventEmitter {
18
18
  deep: !0,
19
19
  hooks: ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"],
20
20
  ...this.options
21
- }), garbageCollected.garbageCollected(this).then(() => this.destroy());
21
+ });
22
22
  }
23
23
  /** Flag to signal the file is synchronized with the object. */
24
24
  isCommitting = !1;
@@ -32,6 +32,15 @@ class FSObject extends node_events.EventEmitter {
32
32
  stats;
33
33
  /** A watcher that will update the object when the file changes. */
34
34
  watcher;
35
+ /**
36
+ * Close the file and stop watching the file and object for changes.
37
+ * If the file has been created as a temporary file, it will be deleted.
38
+ *
39
+ * @returns A promise that resolves when the file has been destroyed.
40
+ */
41
+ async [Symbol.asyncDispose]() {
42
+ return this.destroy();
43
+ }
35
44
  /**
36
45
  * Create an awaitable instance of `FSObject` that resolves when the file
37
46
  * is synchronized with the object and the object is synchronized with the file.
@@ -1 +1 @@
1
- {"version":3,"file":"loadObject.cjs","sources":["../loadObject.ts"],"sourcesContent":["import type { Awaitable } from '@unshared/functions/awaitable'\nimport type { Reactive, ReactiveOptions } from '@unshared/reactivity/reactive'\nimport type { FSWatcher, PathLike, Stats, WatchOptions } from 'node:fs'\nimport { overwrite } from '@unshared/collection/overwrite'\nimport { awaitable } from '@unshared/functions/awaitable'\nimport { garbageCollected } from '@unshared/functions/garbageCollected'\nimport { reactive } from '@unshared/reactivity/reactive'\nimport { EventEmitter } from 'node:events'\nimport { constants, existsSync, readFileSync, watch, writeFileSync } from 'node:fs'\nimport { access, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'\nimport { dirname } from 'node:path'\n\nexport interface FSObjectOptions<T extends object> extends ReactiveOptions<T>, WatchOptions {\n\n /**\n * If set to `true` and the file does not exist, the file will be created\n * if it does not exist and the object will be initialized with an empty\n * object.\n *\n * @default false\n */\n createIfNotExists?: boolean\n\n /**\n * If set to `true`, the file will be deleted when the instance is destroyed.\n * Allowing you to create temporary files that will be deleted when the\n * instance is garbage collected.\n */\n deleteOnDestroy?: boolean\n\n /**\n * If set to `true`, changes on the file will not be reflected in the object.\n * You can use this to prevent the object from being updated when you are\n * making changes to the file.\n *\n * @default false\n */\n ignoreFileChanges?: boolean\n\n /**\n * If set to `true`, changes on the object will be reflected in the file.\n * You can set this to `false` if you want to make multiple changes to the\n * object without triggering multiple file updates.\n *\n * @default false\n */\n ignoreObjectChanges?: boolean\n\n /**\n * The initial value of the object. If the file does not exist, the object\n * will be initialized with this value.\n *\n * @default {}\n */\n initialValue?: T\n\n /**\n * The parser function to use when reading the file. If not set, the file\n * will be parsed as JSON using the native `JSON.parse` function.\n *\n * @default JSON.parse\n */\n parse?: (json: string) => T\n\n /**\n * The stringifier function to use when writing the file. If not set, the\n * object will be stringified as JSON using the native `JSON.stringify` function.\n *\n * @default JSON.stringify\n */\n serialize?: (object: T) => string\n}\n\nexport interface FSObjectEventMap<T extends object> {\n commit: [T]\n destroy: []\n load: [T]\n lock: []\n unlock: []\n}\n\n// eslint-disable-next-line unicorn/prefer-event-target\nexport class FSObject<T extends object> extends EventEmitter<FSObjectEventMap<T>> {\n\n /** Flag to signal the file is synchronized with the object. */\n public isCommitting = false\n\n /** Flag to signal the instance has been destroyed. */\n public isDestroyed = false\n\n /** Flag to signal the object is synchronized with the file. */\n public isLoading = false\n\n /** The current content of the file. */\n public object: Reactive<T>\n\n /** The current status of the file. */\n public stats: Stats | undefined\n\n /** A watcher that will update the object when the file changes. */\n public watcher: FSWatcher | undefined\n\n /**\n * Load a JSON file and keep it synchronized with it's source file.\n *\n * @param path The path or file descriptor of the file to load.\n * @param options Options for the watcher.\n * @throws If the file is not a JSON object.\n */\n constructor(public path: PathLike, public options: FSObjectOptions<T> = {}) {\n super()\n\n // --- The callback that will be called when the object changes.\n // --- This callback is wrapped in a debounce function to prevent\n // --- multiple writes in a short period of time.\n const callback = async() => {\n if (this.isBusy) return\n if (this.options.ignoreObjectChanges) return\n await this.commit()\n }\n\n // --- Create the reactive object. Each time a nested property is\n // --- changed, the callback will be called with the new object.\n this.object = reactive(this.options.initialValue ?? {} as T, {\n callbacks: [callback],\n deep: true,\n hooks: ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'],\n ...this.options,\n })\n\n // --- Destroy the object once this instance is garbage collected.\n // --- This will also delete the file if it was created as a temporary file.\n void garbageCollected(this).then(() => this.destroy())\n }\n\n /**\n * Create an awaitable instance of `FSObject` that resolves when the file\n * is synchronized with the object and the object is synchronized with the file.\n *\n * This function is a shorthand for creating a new `FSObject` instance and\n * calling the `access`, `load` and `watch` methods in sequence. This allows\n * fast and easy access to the file and object in a single call.\n *\n * @param path The path or file descriptor of the file to load.\n * @param options Options to pass to the `FSObject` constructor.\n * @returns An awaitable instance of `FSObject`.\n * @example\n * const fsObject = FSObject.from('file.json')\n * const object = await fsObject\n *\n * // Change the file and check the object.\n * writeFileSync('file.json', '{\"foo\":\"bar\"}')\n * await fsObject.untilLoaded\n * object // => { foo: 'bar' }\n *\n * // Change the object and check the file.\n * object.foo = 'baz'\n * await fsObject.untilCommitted\n * readFileSync('file.json', 'utf8') // => { \"foo\": \"baz\" }\n */\n static from<T extends object>(path: PathLike, options: FSObjectOptions<T> = {}): Awaitable<FSObject<T>, Reactive<T>> {\n const fsObject = new FSObject<T>(path, options)\n const createPromise = () => fsObject.load().then(() => fsObject.watch().object)\n return awaitable(fsObject, createPromise)\n }\n\n /**\n * Commit the current state of the object to the file. This function\n * **will** write the object to the file and emit a `commit` event.\n *\n * @param writeObject The object to write to the file.\n * @returns A promise that resolves when the file has been written.\n */\n public async commit(writeObject = this.object as T): Promise<void> {\n this.isCommitting = true\n\n // --- Stringify the object and write it to disk.\n const { serialize = (object: unknown) => JSON.stringify(object, undefined, 2) } = this.options\n const writeJson = serialize(writeObject)\n const pathString = this.path.toString()\n const pathDirectory = dirname(pathString)\n await mkdir(pathDirectory, { recursive: true })\n await writeFile(this.path, `${writeJson}\\n`, 'utf8')\n overwrite(this.object, writeObject)\n this.stats = await stat(this.path)\n\n this.emit('commit', writeObject)\n this.isCommitting = false\n }\n\n /**\n * Close the file and stop watching the file and object for changes.\n * If the file has been created as a temporary file, it will be deleted.\n */\n public async destroy(): Promise<void> {\n this.isLoading = false\n this.isCommitting = false\n if (this.watcher) this.watcher.close()\n if (this.options.deleteOnDestroy) await rm(this.path, { force: true })\n this.watcher = undefined\n this.isDestroyed = true\n this.emit('destroy')\n }\n\n /**\n * Load the file and update the object.\n *\n * @returns The loaded object.\n */\n public async load(): Promise<void> {\n this.isLoading = true\n this.isDestroyed = false\n\n // --- If the file does not exist, and the `createIfNotExists` option is\n // --- set to `true`, create the file and initialize the object with the\n // --- `initialValue` option.\n const accessError = await access(this.path, constants.F_OK | constants.R_OK).catch((error: Error) => error)\n if (accessError && this.options.createIfNotExists) {\n await this.commit()\n this.isLoading = false\n this.emit('load', this.object)\n return\n }\n\n // --- If the file does not exist, throw an error.\n if (accessError && !this.options.createIfNotExists) throw accessError\n\n // --- Assert the path points to a file.\n const newStats = await stat(this.path)\n const newIsFile = newStats.isFile()\n if (!newIsFile) throw new Error(`Expected ${this.path.toString()} to be a file`)\n\n // --- If the file has not changed, return the current object.\n if (this.object && this.stats && newStats.mtimeMs < this.stats.mtimeMs) return\n this.stats = newStats\n\n // --- Read and parse the file.\n const { parse = JSON.parse } = this.options\n const newJson = await readFile(this.path, 'utf8')\n const newObject = parse(newJson) as T\n\n // --- Assert JSON is an object.\n if (typeof newObject !== 'object' || newObject === null)\n throw new Error(`Expected ${this.path.toString()} to be a JSON object`)\n\n // --- Update the object by overwriting it's properties.\n overwrite(this.object, newObject)\n this.isLoading = false\n this.emit('load', newObject)\n }\n\n /**\n * Start watching the file for changes and update the object if the content\n * of the file changes.\n *\n * @returns The current instance for chaining.\n * @example\n * const object = new FSObject('file.json').watch()\n *\n * // Change the file and check the object.\n * writeFileSync('file.json', '{\"foo\":\"bar\"}')\n *\n * // Wait until the object is updated.\n * await object.untilLoaded\n *\n * // Check the object.\n * expect(object.object).toStrictEqual({ foo: 'bar' })\n */\n public watch(): this {\n if (this.watcher) return this\n\n // --- Try to watch the file for changes. If an error occurs, the file\n // --- is likely not accessible. In this case, just set the `isWatching`\n // --- flag to `true` and retry watching the file when it becomes accessible.\n this.watcher = watch(this.path, { persistent: false, ...this.options }, (event) => {\n if (this.isBusy) return\n if (this.options.ignoreFileChanges) return\n if (event === 'change') void this.load()\n })\n\n // --- Return the instance for chaining.\n return this\n }\n\n /**\n * Flag to signal the instance is busy doing a commit or a load operation.\n *\n * @returns `true` if the instance is busy, `false` otherwise.\n */\n get isBusy() {\n return this.isLoading || this.isCommitting || this.isDestroyed\n }\n\n /**\n * A promise that resolves when the file is synchronized with the object.\n *\n * @returns A promise that resolves when the file is synchronized.\n * @example\n * const object = new FSObject('file.json')\n * object.commit()\n *\n * // Wait until the file is synchronized.\n * await object.untilCommitted\n */\n get untilCommitted(): Promise<void> {\n if (!this.isCommitting) return Promise.resolve()\n return new Promise<void>(resolve => this.prependOnceListener('commit', () => resolve()))\n }\n\n /**\n * A promise that resolves when the object is destroyed.\n *\n * @returns A promise that resolves when the object is destroyed.\n * @example\n * const object = new FSObject('file.json')\n * object.destroy()\n *\n * // Wait until the object is destroyed.\n * await object.untilDestroyed\n */\n get untilDestroyed(): Promise<void> {\n if (this.isDestroyed) return Promise.resolve()\n return new Promise<void>(resolve => this.prependOnceListener('destroy', resolve))\n }\n\n /**\n * A promise that resolves when the object is synchronized with the file.\n *\n * @returns A promise that resolves when the file is synchronized.\n * @example\n * const object = new FSObject('file.json')\n * object.load()\n *\n * // Wait until the object is synchronized.\n * await object.untilLoaded\n */\n get untilLoaded(): Promise<void> {\n if (!this.isLoading) return Promise.resolve()\n return new Promise<void>(resolve => this.prependOnceListener('load', () => resolve()))\n }\n}\n\n/**\n * Create an awaitable instance of `FSObject` that resolves when the file\n * is synchronized with the object and the object is synchronized with the file.\n *\n * This function is a shorthand for creating a new `FSObject` instance and\n * calling the `access`, `load` and `watch` methods in sequence. This allows\n * fast and easy access to the file and object in a single call.\n *\n * @param path The path or file descriptor of the file to load.\n * @param options Options to pass to the `FSObject` constructor.\n * @returns An awaitable instance of `FSObject`.\n * @example\n * const fsObject = loadObject('file.json')\n * const object = await fsObject\n *\n * // Change the file and check the object.\n * writeFileSync('file.json', '{\"foo\":\"bar\"}')\n * await fsObject.untilLoaded\n * object // => { foo: 'bar' }\n *\n * // Change the object and check the file.\n * object.foo = 'baz'\n * await fsObject.untilCommitted\n * readFileSync('file.json', 'utf8') // => { \"foo\": \"baz\" }\n */\nexport function loadObject<T extends object>(path: PathLike, options: FSObjectOptions<T> = {}): Awaitable<FSObject<T>, Reactive<T>> {\n return FSObject.from(path, options)\n}\n\n/* v8 ignore start */\n/* eslint-disable sonarjs/no-duplicate-string */\nif (import.meta.vitest) {\n const { vol } = await import('memfs')\n\n describe('loadObject', () => {\n it('should return an instance of `FSObject`', () => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const result = loadObject('/app/packages.json')\n expect(result).toBeInstanceOf(FSObject)\n expect(result).toBeInstanceOf(EventEmitter)\n expect(result).toHaveProperty('path', '/app/packages.json')\n expect(result).toHaveProperty('object', reactive({}))\n })\n\n it('should expose the options as properties', () => {\n const options = { initialValue: { foo: 'bar' } }\n const result = loadObject('/app/packages.json', options)\n expect(result.options).toBe(options)\n })\n\n it('should resolve the parsed JSON file', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const result = await loadObject('/app/packages.json')\n expect(result).toMatchObject({ foo: 'bar' })\n })\n\n it('should create the file if it does not exist and the `createIfNotExists` option is set to `true`', async() => {\n const result = await loadObject('/app/packages.json', { createIfNotExists: true })\n expect(result).toMatchObject({})\n const fileContent = readFileSync('/app/packages.json', 'utf8')\n expect(fileContent).toBe('{}\\n')\n })\n\n it('should reject if the file is not a JSON object', async() => {\n vol.fromJSON({ 'file.json': '\"foo\": \"bar\"' })\n const shouldReject = async() => await loadObject('file.json')\n await expect(shouldReject).rejects.toThrow('Unexpected non-whitespace character after JSON at position 5')\n })\n })\n\n describe('load', () => {\n it('should load the file when the `load` method is called', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const result = new FSObject('/app/packages.json')\n const loaded = await result.load()\n expect(loaded).toBeUndefined()\n expect(result.object).toMatchObject({ foo: 'bar' })\n })\n\n it('should set the `isLoading` flag to `true` when loading', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const result = new FSObject('/app/packages.json')\n expect(result.isLoading).toBe(false)\n const loaded = result.load()\n expect(result.isLoading).toBe(true)\n await loaded\n expect(result.isLoading).toBe(false)\n })\n\n it('should call the `load` event when the file is loaded', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const fn = vi.fn()\n const result = new FSObject('/app/packages.json')\n result.addListener('load', fn)\n await result.load()\n expect(fn).toHaveBeenCalledOnce()\n expect(fn).toHaveBeenCalledWith({ foo: 'bar' })\n })\n\n it('should resolve the `untilLoaded` property once the file is loaded', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const result = loadObject('/app/packages.json')\n void result.load()\n expect(result.isLoading).toBe(true)\n await expect(result.untilLoaded).resolves.toBeUndefined()\n expect(result.isLoading).toBe(false)\n })\n\n it('should resolve the `untilLoaded` property immediately if the file is already loaded', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const result = new FSObject('/app/packages.json')\n await result.load()\n expect(result.isLoading).toBe(false)\n await expect(result.untilLoaded).resolves.toBeUndefined()\n expect(result.isLoading).toBe(false)\n })\n\n it('should create the file if it does not exist and the `createIfNotExists` option is set to `true`', async() => {\n const result = new FSObject('/app/packages.json', { createIfNotExists: true })\n await result.load()\n const fileContent = readFileSync('/app/packages.json', 'utf8')\n expect(fileContent).toBe('{}\\n')\n expect(result.object).toMatchObject({})\n })\n\n it('should create with initial value if the file does not exist and the `createIfNotExists` option is set to `true`', async() => {\n const result = new FSObject('/app/packages.json', { createIfNotExists: true, initialValue: { foo: 'bar' } })\n await result.load()\n const fileContent = readFileSync('/app/packages.json', 'utf8')\n expect(fileContent).toBe('{\\n \"foo\": \"bar\"\\n}\\n')\n expect(result.object).toMatchObject({ foo: 'bar' })\n })\n\n it('should use the provided `parse` function to parse the file', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const parse = vi.fn((json: string) => ({ json }))\n const result = new FSObject('/app/packages.json', { parse })\n await result.load()\n expect(result.object).toMatchObject({ json: '{\"foo\":\"bar\"}' })\n expect(parse).toHaveBeenCalledOnce()\n expect(parse).toHaveBeenCalledWith('{\"foo\":\"bar\"}')\n })\n\n it('should reject if the file does not exist', async() => {\n const result = new FSObject('/app/packages.json')\n const shouldReject = () => result.load()\n await expect(shouldReject).rejects.toThrow('ENOENT')\n })\n })\n\n describe('watch', () => {\n it('should return the current instance', () => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const result = new FSObject('/app/packages.json')\n const watch = result.watch()\n expect(watch).toBe(result)\n })\n\n it('should watch for changes on the file', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const fn = vi.fn()\n const result = new FSObject('/app/packages.json')\n result.addListener('load', fn)\n result.watch()\n writeFileSync('/app/packages.json', '{\"bar\":\"baz\"}')\n await result.untilLoaded\n expect(fn).toHaveBeenCalledOnce()\n expect(fn).toHaveBeenCalledWith({ bar: 'baz' })\n })\n\n it('should not watch for changes on the file when `ignoreFileChanges` is `true`', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const fn = vi.fn()\n const result = new FSObject('/app/packages.json', { ignoreFileChanges: true })\n result.watch()\n result.addListener('load', fn)\n writeFileSync('/app/packages.json', '{\"bar\":\"baz\"}')\n await new Promise(resolve => setTimeout(resolve, 10))\n expect(fn).not.toHaveBeenCalled()\n })\n\n it('should throw an error if the file does not exist', () => {\n const result = new FSObject('/app/packages.json')\n const shouldThrow = () => result.watch()\n expect(shouldThrow).toThrow('ENOENT')\n })\n })\n\n describe('commit', () => {\n it('should commit the object to the file when the `commit` method is called', async() => {\n const result = new FSObject('/app/packages.json')\n const commited = await result.commit()\n const fileContent = readFileSync('/app/packages.json', 'utf8')\n expect(commited).toBeUndefined()\n expect(fileContent).toBe('{}\\n')\n })\n\n it('should set the `isCommitting` flag to `true` when committing', () => {\n const result = new FSObject('/app/packages.json')\n expect(result.isCommitting).toBe(false)\n void result.commit()\n expect(result.isCommitting).toBe(true)\n })\n\n it('should call the `commit` event when the file is isCommitting', async() => {\n const result = new FSObject('/app/packages.json', { initialValue: { foo: 'bar' } })\n const fn = vi.fn()\n result.addListener('commit', fn)\n await result.commit()\n expect(fn).toHaveBeenCalledOnce()\n expect(fn).toHaveBeenCalledWith({ foo: 'bar' })\n })\n\n it('should commit the given object to the file', async() => {\n const result = new FSObject('/app/packages.json')\n await result.commit({ foo: 'bar' })\n const fileContent = readFileSync('/app/packages.json', 'utf8')\n expect(fileContent).toBe('{\\n \"foo\": \"bar\"\\n}\\n')\n })\n\n it('should resolve the `untilCommitted` promise once the file is committed', async() => {\n const result = new FSObject('/app/packages.json')\n expect(result.isCommitting).toBe(false)\n void result.commit()\n expect(result.isCommitting).toBe(true)\n await expect(result.untilCommitted).resolves.toBeUndefined()\n expect(result.isCommitting).toBe(false)\n })\n\n it('should resolve the `untilCommitted` promise immediately if the file is already committed', async() => {\n const result = new FSObject('/app/packages.json', { initialValue: { foo: 'bar' } })\n await result.commit()\n const untilCommitted = result.untilCommitted\n await expect(untilCommitted).resolves.toBeUndefined()\n })\n\n it('should commit the object to the file when the object changes', async() => {\n const fn = vi.fn()\n const result = new FSObject('/app/packages.json', { initialValue: { foo: 'bar' } })\n result.addListener('commit', fn)\n result.object.foo = 'baz'\n await result.untilCommitted\n expect(fn).toHaveBeenCalledOnce()\n expect(fn).toHaveBeenCalledWith({ foo: 'baz' })\n })\n\n it('should use the provided `serialize` function to serialize the object', async() => {\n const serialize = vi.fn(String)\n const result = new FSObject('/app/packages.json', { initialValue: { foo: 'bar' }, serialize })\n await result.commit()\n const fileContent = readFileSync('/app/packages.json', 'utf8')\n expect(fileContent).toBe('[object Object]\\n')\n expect(serialize).toHaveBeenCalledOnce()\n expect(serialize).toHaveBeenCalledWith({ foo: 'bar' })\n })\n\n it('should not commit the object to the file when the `ignoreObjectChanges` option is set to `true`', async() => {\n const fn = vi.fn()\n const result = new FSObject<{ foo: string }>('/app/packages.json', { ignoreObjectChanges: true })\n result.addListener('commit', fn)\n result.object.foo = 'baz'\n await new Promise(resolve => setTimeout(resolve, 10))\n expect(fn).not.toHaveBeenCalled()\n })\n })\n\n describe('destroy', () => {\n it('should set the `isDestroyed` flag to `true` when destroyed', async() => {\n const result = new FSObject('/app/packages.json')\n expect(result.isDestroyed).toBe(false)\n await result.destroy()\n expect(result.isDestroyed).toBe(true)\n })\n\n it('should emit the `destroy` event when the object is destroyed', async() => {\n const result = new FSObject('/app/packages.json')\n const fn = vi.fn()\n result.addListener('destroy', fn)\n await result.destroy()\n expect(fn).toHaveBeenCalledOnce()\n })\n\n it('should resolve the `untilDestroyed` promise when the object is destroyed', async() => {\n const result = new FSObject('/app/packages.json')\n expect(result.isDestroyed).toBe(false)\n const untilDestroyed = result.untilDestroyed\n void result.destroy()\n expect(result.isDestroyed).toBe(true)\n await expect(untilDestroyed).resolves.toBeUndefined()\n expect(result.isDestroyed).toBe(true)\n })\n\n it('should resolve the `untilDestroyed` promise immediately if the object is already destroyed', async() => {\n const result = new FSObject('/app/packages.json')\n await result.destroy()\n const untilDestroyed = result.untilDestroyed\n await expect(untilDestroyed).resolves.toBeUndefined()\n })\n\n it('should delete the file when the `deleteOnDestroy` option is set to `true`', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const result = new FSObject('/app/packages.json', { deleteOnDestroy: true })\n await result.destroy()\n const fileExists = existsSync('/app/packages.json')\n expect(fileExists).toBe(false)\n })\n })\n}\n"],"names":["EventEmitter","reactive","garbageCollected","awaitable","dirname","mkdir","writeFile","overwrite","stat","rm","access","constants","readFile","watch"],"mappings":"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"}
1
+ {"version":3,"file":"loadObject.cjs","sources":["../loadObject.ts"],"sourcesContent":["import type { Awaitable } from '@unshared/functions/awaitable'\nimport type { Reactive, ReactiveOptions } from '@unshared/reactivity/reactive'\nimport type { FSWatcher, PathLike, Stats, WatchOptions } from 'node:fs'\nimport { overwrite } from '@unshared/collection/overwrite'\nimport { awaitable } from '@unshared/functions/awaitable'\nimport { reactive } from '@unshared/reactivity/reactive'\nimport { EventEmitter } from 'node:events'\nimport { constants, watch } from 'node:fs'\nimport { access, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'\nimport { dirname } from 'node:path'\n\nexport interface FSObjectOptions<T extends object> extends ReactiveOptions<T>, WatchOptions {\n\n /**\n * If set to `true` and the file does not exist, the file will be created\n * if it does not exist and the object will be initialized with an empty\n * object.\n *\n * @default false\n */\n createIfNotExists?: boolean\n\n /**\n * If set to `true`, the file will be deleted when the instance is destroyed.\n * Allowing you to create temporary files that will be deleted when the\n * instance is garbage collected.\n */\n deleteOnDestroy?: boolean\n\n /**\n * If set to `true`, changes on the file will not be reflected in the object.\n * You can use this to prevent the object from being updated when you are\n * making changes to the file.\n *\n * @default false\n */\n ignoreFileChanges?: boolean\n\n /**\n * If set to `true`, changes on the object will be reflected in the file.\n * You can set this to `false` if you want to make multiple changes to the\n * object without triggering multiple file updates.\n *\n * @default false\n */\n ignoreObjectChanges?: boolean\n\n /**\n * The initial value of the object. If the file does not exist, the object\n * will be initialized with this value.\n *\n * @default {}\n */\n initialValue?: T\n\n /**\n * The parser function to use when reading the file. If not set, the file\n * will be parsed as JSON using the native `JSON.parse` function.\n *\n * @default JSON.parse\n */\n parse?: (json: string) => T\n\n /**\n * The stringifier function to use when writing the file. If not set, the\n * object will be stringified as JSON using the native `JSON.stringify` function.\n *\n * @default JSON.stringify\n */\n serialize?: (object: T) => string\n}\n\nexport interface FSObjectEventMap<T extends object> {\n commit: [T]\n destroy: []\n load: [T]\n lock: []\n unlock: []\n}\n\n// eslint-disable-next-line unicorn/prefer-event-target\nexport class FSObject<T extends object> extends EventEmitter<FSObjectEventMap<T>> {\n\n /** Flag to signal the file is synchronized with the object. */\n public isCommitting = false\n\n /** Flag to signal the instance has been destroyed. */\n public isDestroyed = false\n\n /** Flag to signal the object is synchronized with the file. */\n public isLoading = false\n\n /** The current content of the file. */\n public object: Reactive<T>\n\n /** The current status of the file. */\n public stats: Stats | undefined\n\n /** A watcher that will update the object when the file changes. */\n public watcher: FSWatcher | undefined\n\n /**\n * Load a JSON file and keep it synchronized with it's source file.\n *\n * @param path The path or file descriptor of the file to load.\n * @param options Options for the watcher.\n * @throws If the file is not a JSON object.\n */\n constructor(public path: PathLike, public options: FSObjectOptions<T> = {}) {\n super()\n\n // --- The callback that will be called when the object changes.\n // --- This callback is wrapped in a debounce function to prevent\n // --- multiple writes in a short period of time.\n const callback = async() => {\n if (this.isBusy) return\n if (this.options.ignoreObjectChanges) return\n await this.commit()\n }\n\n // --- Create the reactive object. Each time a nested property is\n // --- changed, the callback will be called with the new object.\n this.object = reactive(this.options.initialValue ?? {} as T, {\n callbacks: [callback],\n deep: true,\n hooks: ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'],\n ...this.options,\n })\n }\n\n /**\n * Close the file and stop watching the file and object for changes.\n * If the file has been created as a temporary file, it will be deleted.\n *\n * @returns A promise that resolves when the file has been destroyed.\n */\n async [Symbol.asyncDispose]() {\n return this.destroy()\n }\n\n /**\n * Create an awaitable instance of `FSObject` that resolves when the file\n * is synchronized with the object and the object is synchronized with the file.\n *\n * This function is a shorthand for creating a new `FSObject` instance and\n * calling the `access`, `load` and `watch` methods in sequence. This allows\n * fast and easy access to the file and object in a single call.\n *\n * @param path The path or file descriptor of the file to load.\n * @param options Options to pass to the `FSObject` constructor.\n * @returns An awaitable instance of `FSObject`.\n * @example\n * const fsObject = FSObject.from('file.json')\n * const object = await fsObject\n *\n * // Change the file and check the object.\n * writeFileSync('file.json', '{\"foo\":\"bar\"}')\n * await fsObject.untilLoaded\n * object // => { foo: 'bar' }\n *\n * // Change the object and check the file.\n * object.foo = 'baz'\n * await fsObject.untilCommitted\n * readFileSync('file.json', 'utf8') // => { \"foo\": \"baz\" }\n */\n static from<T extends object>(path: PathLike, options: FSObjectOptions<T> = {}): Awaitable<FSObject<T>, Reactive<T>> {\n const fsObject = new FSObject<T>(path, options)\n const createPromise = () => fsObject.load().then(() => fsObject.watch().object)\n return awaitable(fsObject, createPromise)\n }\n\n /**\n * Commit the current state of the object to the file. This function\n * **will** write the object to the file and emit a `commit` event.\n *\n * @param writeObject The object to write to the file.\n * @returns A promise that resolves when the file has been written.\n */\n public async commit(writeObject = this.object as T): Promise<void> {\n this.isCommitting = true\n\n // --- Stringify the object and write it to disk.\n const { serialize = (object: unknown) => JSON.stringify(object, undefined, 2) } = this.options\n const writeJson = serialize(writeObject)\n const pathString = this.path.toString()\n const pathDirectory = dirname(pathString)\n await mkdir(pathDirectory, { recursive: true })\n await writeFile(this.path, `${writeJson}\\n`, 'utf8')\n overwrite(this.object, writeObject)\n this.stats = await stat(this.path)\n\n this.emit('commit', writeObject)\n this.isCommitting = false\n }\n\n /**\n * Close the file and stop watching the file and object for changes.\n * If the file has been created as a temporary file, it will be deleted.\n */\n public async destroy(): Promise<void> {\n this.isLoading = false\n this.isCommitting = false\n if (this.watcher) this.watcher.close()\n if (this.options.deleteOnDestroy) await rm(this.path, { force: true })\n this.watcher = undefined\n this.isDestroyed = true\n this.emit('destroy')\n }\n\n /**\n * Load the file and update the object.\n *\n * @returns The loaded object.\n */\n public async load(): Promise<void> {\n this.isLoading = true\n this.isDestroyed = false\n\n // --- If the file does not exist, and the `createIfNotExists` option is\n // --- set to `true`, create the file and initialize the object with the\n // --- `initialValue` option.\n const accessError = await access(this.path, constants.F_OK | constants.R_OK).catch((error: Error) => error)\n if (accessError && this.options.createIfNotExists) {\n await this.commit()\n this.isLoading = false\n this.emit('load', this.object)\n return\n }\n\n // --- If the file does not exist, throw an error.\n if (accessError && !this.options.createIfNotExists) throw accessError\n\n // --- Assert the path points to a file.\n const newStats = await stat(this.path)\n const newIsFile = newStats.isFile()\n if (!newIsFile) throw new Error(`Expected ${this.path.toString()} to be a file`)\n\n // --- If the file has not changed, return the current object.\n if (this.object && this.stats && newStats.mtimeMs < this.stats.mtimeMs) return\n this.stats = newStats\n\n // --- Read and parse the file.\n const { parse = JSON.parse } = this.options\n const newJson = await readFile(this.path, 'utf8')\n const newObject = parse(newJson) as T\n\n // --- Assert JSON is an object.\n if (typeof newObject !== 'object' || newObject === null)\n throw new Error(`Expected ${this.path.toString()} to be a JSON object`)\n\n // --- Update the object by overwriting it's properties.\n overwrite(this.object, newObject)\n this.isLoading = false\n this.emit('load', newObject)\n }\n\n /**\n * Start watching the file for changes and update the object if the content\n * of the file changes.\n *\n * @returns The current instance for chaining.\n * @example\n * const object = new FSObject('file.json').watch()\n *\n * // Change the file and check the object.\n * writeFileSync('file.json', '{\"foo\":\"bar\"}')\n *\n * // Wait until the object is updated.\n * await object.untilLoaded\n *\n * // Check the object.\n * expect(object.object).toStrictEqual({ foo: 'bar' })\n */\n public watch(): this {\n if (this.watcher) return this\n\n // --- Try to watch the file for changes. If an error occurs, the file\n // --- is likely not accessible. In this case, just set the `isWatching`\n // --- flag to `true` and retry watching the file when it becomes accessible.\n this.watcher = watch(this.path, { persistent: false, ...this.options }, (event) => {\n if (this.isBusy) return\n if (this.options.ignoreFileChanges) return\n if (event === 'change') void this.load()\n })\n\n // --- Return the instance for chaining.\n return this\n }\n\n /**\n * Flag to signal the instance is busy doing a commit or a load operation.\n *\n * @returns `true` if the instance is busy, `false` otherwise.\n */\n get isBusy() {\n return this.isLoading || this.isCommitting || this.isDestroyed\n }\n\n /**\n * A promise that resolves when the file is synchronized with the object.\n *\n * @returns A promise that resolves when the file is synchronized.\n * @example\n * const object = new FSObject('file.json')\n * object.commit()\n *\n * // Wait until the file is synchronized.\n * await object.untilCommitted\n */\n get untilCommitted(): Promise<void> {\n if (!this.isCommitting) return Promise.resolve()\n return new Promise<void>(resolve => this.prependOnceListener('commit', () => resolve()))\n }\n\n /**\n * A promise that resolves when the object is destroyed.\n *\n * @returns A promise that resolves when the object is destroyed.\n * @example\n * const object = new FSObject('file.json')\n * object.destroy()\n *\n * // Wait until the object is destroyed.\n * await object.untilDestroyed\n */\n get untilDestroyed(): Promise<void> {\n if (this.isDestroyed) return Promise.resolve()\n return new Promise<void>(resolve => this.prependOnceListener('destroy', resolve))\n }\n\n /**\n * A promise that resolves when the object is synchronized with the file.\n *\n * @returns A promise that resolves when the file is synchronized.\n * @example\n * const object = new FSObject('file.json')\n * object.load()\n *\n * // Wait until the object is synchronized.\n * await object.untilLoaded\n */\n get untilLoaded(): Promise<void> {\n if (!this.isLoading) return Promise.resolve()\n return new Promise<void>(resolve => this.prependOnceListener('load', () => resolve()))\n }\n}\n\n/**\n * Create an awaitable instance of `FSObject` that resolves when the file\n * is synchronized with the object and the object is synchronized with the file.\n *\n * This function is a shorthand for creating a new `FSObject` instance and\n * calling the `access`, `load` and `watch` methods in sequence. This allows\n * fast and easy access to the file and object in a single call.\n *\n * @param path The path or file descriptor of the file to load.\n * @param options Options to pass to the `FSObject` constructor.\n * @returns An awaitable instance of `FSObject`.\n * @example\n * const fsObject = loadObject('file.json')\n * const object = await fsObject\n *\n * // Change the file and check the object.\n * writeFileSync('file.json', '{\"foo\":\"bar\"}')\n * await fsObject.untilLoaded\n * object // => { foo: 'bar' }\n *\n * // Change the object and check the file.\n * object.foo = 'baz'\n * await fsObject.untilCommitted\n * readFileSync('file.json', 'utf8') // => { \"foo\": \"baz\" }\n */\nexport function loadObject<T extends object>(path: PathLike, options: FSObjectOptions<T> = {}): Awaitable<FSObject<T>, Reactive<T>> {\n return FSObject.from(path, options)\n}\n"],"names":["EventEmitter","reactive","awaitable","dirname","mkdir","writeFile","overwrite","stat","rm","access","constants","readFile","watch"],"mappings":"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"}
@@ -86,6 +86,13 @@ declare class FSObject<T extends object> extends EventEmitter<FSObjectEventMap<T
86
86
  * @throws If the file is not a JSON object.
87
87
  */
88
88
  constructor(path: PathLike, options?: FSObjectOptions<T>);
89
+ /**
90
+ * Close the file and stop watching the file and object for changes.
91
+ * If the file has been created as a temporary file, it will be deleted.
92
+ *
93
+ * @returns A promise that resolves when the file has been destroyed.
94
+ */
95
+ [Symbol.asyncDispose](): Promise<void>;
89
96
  /**
90
97
  * Create an awaitable instance of `FSObject` that resolves when the file
91
98
  * is synchronized with the object and the object is synchronized with the file.
@@ -1,6 +1,5 @@
1
1
  import { overwrite } from "@unshared/collection/overwrite";
2
2
  import { awaitable } from "@unshared/functions/awaitable";
3
- import { garbageCollected } from "@unshared/functions/garbageCollected";
4
3
  import { reactive } from "@unshared/reactivity/reactive";
5
4
  import { EventEmitter } from "node:events";
6
5
  import { constants, watch } from "node:fs";
@@ -24,7 +23,7 @@ class FSObject extends EventEmitter {
24
23
  deep: !0,
25
24
  hooks: ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"],
26
25
  ...this.options
27
- }), garbageCollected(this).then(() => this.destroy());
26
+ });
28
27
  }
29
28
  /** Flag to signal the file is synchronized with the object. */
30
29
  isCommitting = !1;
@@ -38,6 +37,15 @@ class FSObject extends EventEmitter {
38
37
  stats;
39
38
  /** A watcher that will update the object when the file changes. */
40
39
  watcher;
40
+ /**
41
+ * Close the file and stop watching the file and object for changes.
42
+ * If the file has been created as a temporary file, it will be deleted.
43
+ *
44
+ * @returns A promise that resolves when the file has been destroyed.
45
+ */
46
+ async [Symbol.asyncDispose]() {
47
+ return this.destroy();
48
+ }
41
49
  /**
42
50
  * Create an awaitable instance of `FSObject` that resolves when the file
43
51
  * is synchronized with the object and the object is synchronized with the file.
@@ -1 +1 @@
1
- {"version":3,"file":"loadObject.js","sources":["../loadObject.ts"],"sourcesContent":["import type { Awaitable } from '@unshared/functions/awaitable'\nimport type { Reactive, ReactiveOptions } from '@unshared/reactivity/reactive'\nimport type { FSWatcher, PathLike, Stats, WatchOptions } from 'node:fs'\nimport { overwrite } from '@unshared/collection/overwrite'\nimport { awaitable } from '@unshared/functions/awaitable'\nimport { garbageCollected } from '@unshared/functions/garbageCollected'\nimport { reactive } from '@unshared/reactivity/reactive'\nimport { EventEmitter } from 'node:events'\nimport { constants, existsSync, readFileSync, watch, writeFileSync } from 'node:fs'\nimport { access, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'\nimport { dirname } from 'node:path'\n\nexport interface FSObjectOptions<T extends object> extends ReactiveOptions<T>, WatchOptions {\n\n /**\n * If set to `true` and the file does not exist, the file will be created\n * if it does not exist and the object will be initialized with an empty\n * object.\n *\n * @default false\n */\n createIfNotExists?: boolean\n\n /**\n * If set to `true`, the file will be deleted when the instance is destroyed.\n * Allowing you to create temporary files that will be deleted when the\n * instance is garbage collected.\n */\n deleteOnDestroy?: boolean\n\n /**\n * If set to `true`, changes on the file will not be reflected in the object.\n * You can use this to prevent the object from being updated when you are\n * making changes to the file.\n *\n * @default false\n */\n ignoreFileChanges?: boolean\n\n /**\n * If set to `true`, changes on the object will be reflected in the file.\n * You can set this to `false` if you want to make multiple changes to the\n * object without triggering multiple file updates.\n *\n * @default false\n */\n ignoreObjectChanges?: boolean\n\n /**\n * The initial value of the object. If the file does not exist, the object\n * will be initialized with this value.\n *\n * @default {}\n */\n initialValue?: T\n\n /**\n * The parser function to use when reading the file. If not set, the file\n * will be parsed as JSON using the native `JSON.parse` function.\n *\n * @default JSON.parse\n */\n parse?: (json: string) => T\n\n /**\n * The stringifier function to use when writing the file. If not set, the\n * object will be stringified as JSON using the native `JSON.stringify` function.\n *\n * @default JSON.stringify\n */\n serialize?: (object: T) => string\n}\n\nexport interface FSObjectEventMap<T extends object> {\n commit: [T]\n destroy: []\n load: [T]\n lock: []\n unlock: []\n}\n\n// eslint-disable-next-line unicorn/prefer-event-target\nexport class FSObject<T extends object> extends EventEmitter<FSObjectEventMap<T>> {\n\n /** Flag to signal the file is synchronized with the object. */\n public isCommitting = false\n\n /** Flag to signal the instance has been destroyed. */\n public isDestroyed = false\n\n /** Flag to signal the object is synchronized with the file. */\n public isLoading = false\n\n /** The current content of the file. */\n public object: Reactive<T>\n\n /** The current status of the file. */\n public stats: Stats | undefined\n\n /** A watcher that will update the object when the file changes. */\n public watcher: FSWatcher | undefined\n\n /**\n * Load a JSON file and keep it synchronized with it's source file.\n *\n * @param path The path or file descriptor of the file to load.\n * @param options Options for the watcher.\n * @throws If the file is not a JSON object.\n */\n constructor(public path: PathLike, public options: FSObjectOptions<T> = {}) {\n super()\n\n // --- The callback that will be called when the object changes.\n // --- This callback is wrapped in a debounce function to prevent\n // --- multiple writes in a short period of time.\n const callback = async() => {\n if (this.isBusy) return\n if (this.options.ignoreObjectChanges) return\n await this.commit()\n }\n\n // --- Create the reactive object. Each time a nested property is\n // --- changed, the callback will be called with the new object.\n this.object = reactive(this.options.initialValue ?? {} as T, {\n callbacks: [callback],\n deep: true,\n hooks: ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'],\n ...this.options,\n })\n\n // --- Destroy the object once this instance is garbage collected.\n // --- This will also delete the file if it was created as a temporary file.\n void garbageCollected(this).then(() => this.destroy())\n }\n\n /**\n * Create an awaitable instance of `FSObject` that resolves when the file\n * is synchronized with the object and the object is synchronized with the file.\n *\n * This function is a shorthand for creating a new `FSObject` instance and\n * calling the `access`, `load` and `watch` methods in sequence. This allows\n * fast and easy access to the file and object in a single call.\n *\n * @param path The path or file descriptor of the file to load.\n * @param options Options to pass to the `FSObject` constructor.\n * @returns An awaitable instance of `FSObject`.\n * @example\n * const fsObject = FSObject.from('file.json')\n * const object = await fsObject\n *\n * // Change the file and check the object.\n * writeFileSync('file.json', '{\"foo\":\"bar\"}')\n * await fsObject.untilLoaded\n * object // => { foo: 'bar' }\n *\n * // Change the object and check the file.\n * object.foo = 'baz'\n * await fsObject.untilCommitted\n * readFileSync('file.json', 'utf8') // => { \"foo\": \"baz\" }\n */\n static from<T extends object>(path: PathLike, options: FSObjectOptions<T> = {}): Awaitable<FSObject<T>, Reactive<T>> {\n const fsObject = new FSObject<T>(path, options)\n const createPromise = () => fsObject.load().then(() => fsObject.watch().object)\n return awaitable(fsObject, createPromise)\n }\n\n /**\n * Commit the current state of the object to the file. This function\n * **will** write the object to the file and emit a `commit` event.\n *\n * @param writeObject The object to write to the file.\n * @returns A promise that resolves when the file has been written.\n */\n public async commit(writeObject = this.object as T): Promise<void> {\n this.isCommitting = true\n\n // --- Stringify the object and write it to disk.\n const { serialize = (object: unknown) => JSON.stringify(object, undefined, 2) } = this.options\n const writeJson = serialize(writeObject)\n const pathString = this.path.toString()\n const pathDirectory = dirname(pathString)\n await mkdir(pathDirectory, { recursive: true })\n await writeFile(this.path, `${writeJson}\\n`, 'utf8')\n overwrite(this.object, writeObject)\n this.stats = await stat(this.path)\n\n this.emit('commit', writeObject)\n this.isCommitting = false\n }\n\n /**\n * Close the file and stop watching the file and object for changes.\n * If the file has been created as a temporary file, it will be deleted.\n */\n public async destroy(): Promise<void> {\n this.isLoading = false\n this.isCommitting = false\n if (this.watcher) this.watcher.close()\n if (this.options.deleteOnDestroy) await rm(this.path, { force: true })\n this.watcher = undefined\n this.isDestroyed = true\n this.emit('destroy')\n }\n\n /**\n * Load the file and update the object.\n *\n * @returns The loaded object.\n */\n public async load(): Promise<void> {\n this.isLoading = true\n this.isDestroyed = false\n\n // --- If the file does not exist, and the `createIfNotExists` option is\n // --- set to `true`, create the file and initialize the object with the\n // --- `initialValue` option.\n const accessError = await access(this.path, constants.F_OK | constants.R_OK).catch((error: Error) => error)\n if (accessError && this.options.createIfNotExists) {\n await this.commit()\n this.isLoading = false\n this.emit('load', this.object)\n return\n }\n\n // --- If the file does not exist, throw an error.\n if (accessError && !this.options.createIfNotExists) throw accessError\n\n // --- Assert the path points to a file.\n const newStats = await stat(this.path)\n const newIsFile = newStats.isFile()\n if (!newIsFile) throw new Error(`Expected ${this.path.toString()} to be a file`)\n\n // --- If the file has not changed, return the current object.\n if (this.object && this.stats && newStats.mtimeMs < this.stats.mtimeMs) return\n this.stats = newStats\n\n // --- Read and parse the file.\n const { parse = JSON.parse } = this.options\n const newJson = await readFile(this.path, 'utf8')\n const newObject = parse(newJson) as T\n\n // --- Assert JSON is an object.\n if (typeof newObject !== 'object' || newObject === null)\n throw new Error(`Expected ${this.path.toString()} to be a JSON object`)\n\n // --- Update the object by overwriting it's properties.\n overwrite(this.object, newObject)\n this.isLoading = false\n this.emit('load', newObject)\n }\n\n /**\n * Start watching the file for changes and update the object if the content\n * of the file changes.\n *\n * @returns The current instance for chaining.\n * @example\n * const object = new FSObject('file.json').watch()\n *\n * // Change the file and check the object.\n * writeFileSync('file.json', '{\"foo\":\"bar\"}')\n *\n * // Wait until the object is updated.\n * await object.untilLoaded\n *\n * // Check the object.\n * expect(object.object).toStrictEqual({ foo: 'bar' })\n */\n public watch(): this {\n if (this.watcher) return this\n\n // --- Try to watch the file for changes. If an error occurs, the file\n // --- is likely not accessible. In this case, just set the `isWatching`\n // --- flag to `true` and retry watching the file when it becomes accessible.\n this.watcher = watch(this.path, { persistent: false, ...this.options }, (event) => {\n if (this.isBusy) return\n if (this.options.ignoreFileChanges) return\n if (event === 'change') void this.load()\n })\n\n // --- Return the instance for chaining.\n return this\n }\n\n /**\n * Flag to signal the instance is busy doing a commit or a load operation.\n *\n * @returns `true` if the instance is busy, `false` otherwise.\n */\n get isBusy() {\n return this.isLoading || this.isCommitting || this.isDestroyed\n }\n\n /**\n * A promise that resolves when the file is synchronized with the object.\n *\n * @returns A promise that resolves when the file is synchronized.\n * @example\n * const object = new FSObject('file.json')\n * object.commit()\n *\n * // Wait until the file is synchronized.\n * await object.untilCommitted\n */\n get untilCommitted(): Promise<void> {\n if (!this.isCommitting) return Promise.resolve()\n return new Promise<void>(resolve => this.prependOnceListener('commit', () => resolve()))\n }\n\n /**\n * A promise that resolves when the object is destroyed.\n *\n * @returns A promise that resolves when the object is destroyed.\n * @example\n * const object = new FSObject('file.json')\n * object.destroy()\n *\n * // Wait until the object is destroyed.\n * await object.untilDestroyed\n */\n get untilDestroyed(): Promise<void> {\n if (this.isDestroyed) return Promise.resolve()\n return new Promise<void>(resolve => this.prependOnceListener('destroy', resolve))\n }\n\n /**\n * A promise that resolves when the object is synchronized with the file.\n *\n * @returns A promise that resolves when the file is synchronized.\n * @example\n * const object = new FSObject('file.json')\n * object.load()\n *\n * // Wait until the object is synchronized.\n * await object.untilLoaded\n */\n get untilLoaded(): Promise<void> {\n if (!this.isLoading) return Promise.resolve()\n return new Promise<void>(resolve => this.prependOnceListener('load', () => resolve()))\n }\n}\n\n/**\n * Create an awaitable instance of `FSObject` that resolves when the file\n * is synchronized with the object and the object is synchronized with the file.\n *\n * This function is a shorthand for creating a new `FSObject` instance and\n * calling the `access`, `load` and `watch` methods in sequence. This allows\n * fast and easy access to the file and object in a single call.\n *\n * @param path The path or file descriptor of the file to load.\n * @param options Options to pass to the `FSObject` constructor.\n * @returns An awaitable instance of `FSObject`.\n * @example\n * const fsObject = loadObject('file.json')\n * const object = await fsObject\n *\n * // Change the file and check the object.\n * writeFileSync('file.json', '{\"foo\":\"bar\"}')\n * await fsObject.untilLoaded\n * object // => { foo: 'bar' }\n *\n * // Change the object and check the file.\n * object.foo = 'baz'\n * await fsObject.untilCommitted\n * readFileSync('file.json', 'utf8') // => { \"foo\": \"baz\" }\n */\nexport function loadObject<T extends object>(path: PathLike, options: FSObjectOptions<T> = {}): Awaitable<FSObject<T>, Reactive<T>> {\n return FSObject.from(path, options)\n}\n\n/* v8 ignore start */\n/* eslint-disable sonarjs/no-duplicate-string */\nif (import.meta.vitest) {\n const { vol } = await import('memfs')\n\n describe('loadObject', () => {\n it('should return an instance of `FSObject`', () => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const result = loadObject('/app/packages.json')\n expect(result).toBeInstanceOf(FSObject)\n expect(result).toBeInstanceOf(EventEmitter)\n expect(result).toHaveProperty('path', '/app/packages.json')\n expect(result).toHaveProperty('object', reactive({}))\n })\n\n it('should expose the options as properties', () => {\n const options = { initialValue: { foo: 'bar' } }\n const result = loadObject('/app/packages.json', options)\n expect(result.options).toBe(options)\n })\n\n it('should resolve the parsed JSON file', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const result = await loadObject('/app/packages.json')\n expect(result).toMatchObject({ foo: 'bar' })\n })\n\n it('should create the file if it does not exist and the `createIfNotExists` option is set to `true`', async() => {\n const result = await loadObject('/app/packages.json', { createIfNotExists: true })\n expect(result).toMatchObject({})\n const fileContent = readFileSync('/app/packages.json', 'utf8')\n expect(fileContent).toBe('{}\\n')\n })\n\n it('should reject if the file is not a JSON object', async() => {\n vol.fromJSON({ 'file.json': '\"foo\": \"bar\"' })\n const shouldReject = async() => await loadObject('file.json')\n await expect(shouldReject).rejects.toThrow('Unexpected non-whitespace character after JSON at position 5')\n })\n })\n\n describe('load', () => {\n it('should load the file when the `load` method is called', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const result = new FSObject('/app/packages.json')\n const loaded = await result.load()\n expect(loaded).toBeUndefined()\n expect(result.object).toMatchObject({ foo: 'bar' })\n })\n\n it('should set the `isLoading` flag to `true` when loading', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const result = new FSObject('/app/packages.json')\n expect(result.isLoading).toBe(false)\n const loaded = result.load()\n expect(result.isLoading).toBe(true)\n await loaded\n expect(result.isLoading).toBe(false)\n })\n\n it('should call the `load` event when the file is loaded', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const fn = vi.fn()\n const result = new FSObject('/app/packages.json')\n result.addListener('load', fn)\n await result.load()\n expect(fn).toHaveBeenCalledOnce()\n expect(fn).toHaveBeenCalledWith({ foo: 'bar' })\n })\n\n it('should resolve the `untilLoaded` property once the file is loaded', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const result = loadObject('/app/packages.json')\n void result.load()\n expect(result.isLoading).toBe(true)\n await expect(result.untilLoaded).resolves.toBeUndefined()\n expect(result.isLoading).toBe(false)\n })\n\n it('should resolve the `untilLoaded` property immediately if the file is already loaded', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const result = new FSObject('/app/packages.json')\n await result.load()\n expect(result.isLoading).toBe(false)\n await expect(result.untilLoaded).resolves.toBeUndefined()\n expect(result.isLoading).toBe(false)\n })\n\n it('should create the file if it does not exist and the `createIfNotExists` option is set to `true`', async() => {\n const result = new FSObject('/app/packages.json', { createIfNotExists: true })\n await result.load()\n const fileContent = readFileSync('/app/packages.json', 'utf8')\n expect(fileContent).toBe('{}\\n')\n expect(result.object).toMatchObject({})\n })\n\n it('should create with initial value if the file does not exist and the `createIfNotExists` option is set to `true`', async() => {\n const result = new FSObject('/app/packages.json', { createIfNotExists: true, initialValue: { foo: 'bar' } })\n await result.load()\n const fileContent = readFileSync('/app/packages.json', 'utf8')\n expect(fileContent).toBe('{\\n \"foo\": \"bar\"\\n}\\n')\n expect(result.object).toMatchObject({ foo: 'bar' })\n })\n\n it('should use the provided `parse` function to parse the file', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const parse = vi.fn((json: string) => ({ json }))\n const result = new FSObject('/app/packages.json', { parse })\n await result.load()\n expect(result.object).toMatchObject({ json: '{\"foo\":\"bar\"}' })\n expect(parse).toHaveBeenCalledOnce()\n expect(parse).toHaveBeenCalledWith('{\"foo\":\"bar\"}')\n })\n\n it('should reject if the file does not exist', async() => {\n const result = new FSObject('/app/packages.json')\n const shouldReject = () => result.load()\n await expect(shouldReject).rejects.toThrow('ENOENT')\n })\n })\n\n describe('watch', () => {\n it('should return the current instance', () => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const result = new FSObject('/app/packages.json')\n const watch = result.watch()\n expect(watch).toBe(result)\n })\n\n it('should watch for changes on the file', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const fn = vi.fn()\n const result = new FSObject('/app/packages.json')\n result.addListener('load', fn)\n result.watch()\n writeFileSync('/app/packages.json', '{\"bar\":\"baz\"}')\n await result.untilLoaded\n expect(fn).toHaveBeenCalledOnce()\n expect(fn).toHaveBeenCalledWith({ bar: 'baz' })\n })\n\n it('should not watch for changes on the file when `ignoreFileChanges` is `true`', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const fn = vi.fn()\n const result = new FSObject('/app/packages.json', { ignoreFileChanges: true })\n result.watch()\n result.addListener('load', fn)\n writeFileSync('/app/packages.json', '{\"bar\":\"baz\"}')\n await new Promise(resolve => setTimeout(resolve, 10))\n expect(fn).not.toHaveBeenCalled()\n })\n\n it('should throw an error if the file does not exist', () => {\n const result = new FSObject('/app/packages.json')\n const shouldThrow = () => result.watch()\n expect(shouldThrow).toThrow('ENOENT')\n })\n })\n\n describe('commit', () => {\n it('should commit the object to the file when the `commit` method is called', async() => {\n const result = new FSObject('/app/packages.json')\n const commited = await result.commit()\n const fileContent = readFileSync('/app/packages.json', 'utf8')\n expect(commited).toBeUndefined()\n expect(fileContent).toBe('{}\\n')\n })\n\n it('should set the `isCommitting` flag to `true` when committing', () => {\n const result = new FSObject('/app/packages.json')\n expect(result.isCommitting).toBe(false)\n void result.commit()\n expect(result.isCommitting).toBe(true)\n })\n\n it('should call the `commit` event when the file is isCommitting', async() => {\n const result = new FSObject('/app/packages.json', { initialValue: { foo: 'bar' } })\n const fn = vi.fn()\n result.addListener('commit', fn)\n await result.commit()\n expect(fn).toHaveBeenCalledOnce()\n expect(fn).toHaveBeenCalledWith({ foo: 'bar' })\n })\n\n it('should commit the given object to the file', async() => {\n const result = new FSObject('/app/packages.json')\n await result.commit({ foo: 'bar' })\n const fileContent = readFileSync('/app/packages.json', 'utf8')\n expect(fileContent).toBe('{\\n \"foo\": \"bar\"\\n}\\n')\n })\n\n it('should resolve the `untilCommitted` promise once the file is committed', async() => {\n const result = new FSObject('/app/packages.json')\n expect(result.isCommitting).toBe(false)\n void result.commit()\n expect(result.isCommitting).toBe(true)\n await expect(result.untilCommitted).resolves.toBeUndefined()\n expect(result.isCommitting).toBe(false)\n })\n\n it('should resolve the `untilCommitted` promise immediately if the file is already committed', async() => {\n const result = new FSObject('/app/packages.json', { initialValue: { foo: 'bar' } })\n await result.commit()\n const untilCommitted = result.untilCommitted\n await expect(untilCommitted).resolves.toBeUndefined()\n })\n\n it('should commit the object to the file when the object changes', async() => {\n const fn = vi.fn()\n const result = new FSObject('/app/packages.json', { initialValue: { foo: 'bar' } })\n result.addListener('commit', fn)\n result.object.foo = 'baz'\n await result.untilCommitted\n expect(fn).toHaveBeenCalledOnce()\n expect(fn).toHaveBeenCalledWith({ foo: 'baz' })\n })\n\n it('should use the provided `serialize` function to serialize the object', async() => {\n const serialize = vi.fn(String)\n const result = new FSObject('/app/packages.json', { initialValue: { foo: 'bar' }, serialize })\n await result.commit()\n const fileContent = readFileSync('/app/packages.json', 'utf8')\n expect(fileContent).toBe('[object Object]\\n')\n expect(serialize).toHaveBeenCalledOnce()\n expect(serialize).toHaveBeenCalledWith({ foo: 'bar' })\n })\n\n it('should not commit the object to the file when the `ignoreObjectChanges` option is set to `true`', async() => {\n const fn = vi.fn()\n const result = new FSObject<{ foo: string }>('/app/packages.json', { ignoreObjectChanges: true })\n result.addListener('commit', fn)\n result.object.foo = 'baz'\n await new Promise(resolve => setTimeout(resolve, 10))\n expect(fn).not.toHaveBeenCalled()\n })\n })\n\n describe('destroy', () => {\n it('should set the `isDestroyed` flag to `true` when destroyed', async() => {\n const result = new FSObject('/app/packages.json')\n expect(result.isDestroyed).toBe(false)\n await result.destroy()\n expect(result.isDestroyed).toBe(true)\n })\n\n it('should emit the `destroy` event when the object is destroyed', async() => {\n const result = new FSObject('/app/packages.json')\n const fn = vi.fn()\n result.addListener('destroy', fn)\n await result.destroy()\n expect(fn).toHaveBeenCalledOnce()\n })\n\n it('should resolve the `untilDestroyed` promise when the object is destroyed', async() => {\n const result = new FSObject('/app/packages.json')\n expect(result.isDestroyed).toBe(false)\n const untilDestroyed = result.untilDestroyed\n void result.destroy()\n expect(result.isDestroyed).toBe(true)\n await expect(untilDestroyed).resolves.toBeUndefined()\n expect(result.isDestroyed).toBe(true)\n })\n\n it('should resolve the `untilDestroyed` promise immediately if the object is already destroyed', async() => {\n const result = new FSObject('/app/packages.json')\n await result.destroy()\n const untilDestroyed = result.untilDestroyed\n await expect(untilDestroyed).resolves.toBeUndefined()\n })\n\n it('should delete the file when the `deleteOnDestroy` option is set to `true`', async() => {\n vol.fromJSON({ '/app/packages.json': '{\"foo\":\"bar\"}' })\n const result = new FSObject('/app/packages.json', { deleteOnDestroy: true })\n await result.destroy()\n const fileExists = existsSync('/app/packages.json')\n expect(fileExists).toBe(false)\n })\n })\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"loadObject.js","sources":["../loadObject.ts"],"sourcesContent":["import type { Awaitable } from '@unshared/functions/awaitable'\nimport type { Reactive, ReactiveOptions } from '@unshared/reactivity/reactive'\nimport type { FSWatcher, PathLike, Stats, WatchOptions } from 'node:fs'\nimport { overwrite } from '@unshared/collection/overwrite'\nimport { awaitable } from '@unshared/functions/awaitable'\nimport { reactive } from '@unshared/reactivity/reactive'\nimport { EventEmitter } from 'node:events'\nimport { constants, watch } from 'node:fs'\nimport { access, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'\nimport { dirname } from 'node:path'\n\nexport interface FSObjectOptions<T extends object> extends ReactiveOptions<T>, WatchOptions {\n\n /**\n * If set to `true` and the file does not exist, the file will be created\n * if it does not exist and the object will be initialized with an empty\n * object.\n *\n * @default false\n */\n createIfNotExists?: boolean\n\n /**\n * If set to `true`, the file will be deleted when the instance is destroyed.\n * Allowing you to create temporary files that will be deleted when the\n * instance is garbage collected.\n */\n deleteOnDestroy?: boolean\n\n /**\n * If set to `true`, changes on the file will not be reflected in the object.\n * You can use this to prevent the object from being updated when you are\n * making changes to the file.\n *\n * @default false\n */\n ignoreFileChanges?: boolean\n\n /**\n * If set to `true`, changes on the object will be reflected in the file.\n * You can set this to `false` if you want to make multiple changes to the\n * object without triggering multiple file updates.\n *\n * @default false\n */\n ignoreObjectChanges?: boolean\n\n /**\n * The initial value of the object. If the file does not exist, the object\n * will be initialized with this value.\n *\n * @default {}\n */\n initialValue?: T\n\n /**\n * The parser function to use when reading the file. If not set, the file\n * will be parsed as JSON using the native `JSON.parse` function.\n *\n * @default JSON.parse\n */\n parse?: (json: string) => T\n\n /**\n * The stringifier function to use when writing the file. If not set, the\n * object will be stringified as JSON using the native `JSON.stringify` function.\n *\n * @default JSON.stringify\n */\n serialize?: (object: T) => string\n}\n\nexport interface FSObjectEventMap<T extends object> {\n commit: [T]\n destroy: []\n load: [T]\n lock: []\n unlock: []\n}\n\n// eslint-disable-next-line unicorn/prefer-event-target\nexport class FSObject<T extends object> extends EventEmitter<FSObjectEventMap<T>> {\n\n /** Flag to signal the file is synchronized with the object. */\n public isCommitting = false\n\n /** Flag to signal the instance has been destroyed. */\n public isDestroyed = false\n\n /** Flag to signal the object is synchronized with the file. */\n public isLoading = false\n\n /** The current content of the file. */\n public object: Reactive<T>\n\n /** The current status of the file. */\n public stats: Stats | undefined\n\n /** A watcher that will update the object when the file changes. */\n public watcher: FSWatcher | undefined\n\n /**\n * Load a JSON file and keep it synchronized with it's source file.\n *\n * @param path The path or file descriptor of the file to load.\n * @param options Options for the watcher.\n * @throws If the file is not a JSON object.\n */\n constructor(public path: PathLike, public options: FSObjectOptions<T> = {}) {\n super()\n\n // --- The callback that will be called when the object changes.\n // --- This callback is wrapped in a debounce function to prevent\n // --- multiple writes in a short period of time.\n const callback = async() => {\n if (this.isBusy) return\n if (this.options.ignoreObjectChanges) return\n await this.commit()\n }\n\n // --- Create the reactive object. Each time a nested property is\n // --- changed, the callback will be called with the new object.\n this.object = reactive(this.options.initialValue ?? {} as T, {\n callbacks: [callback],\n deep: true,\n hooks: ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'],\n ...this.options,\n })\n }\n\n /**\n * Close the file and stop watching the file and object for changes.\n * If the file has been created as a temporary file, it will be deleted.\n *\n * @returns A promise that resolves when the file has been destroyed.\n */\n async [Symbol.asyncDispose]() {\n return this.destroy()\n }\n\n /**\n * Create an awaitable instance of `FSObject` that resolves when the file\n * is synchronized with the object and the object is synchronized with the file.\n *\n * This function is a shorthand for creating a new `FSObject` instance and\n * calling the `access`, `load` and `watch` methods in sequence. This allows\n * fast and easy access to the file and object in a single call.\n *\n * @param path The path or file descriptor of the file to load.\n * @param options Options to pass to the `FSObject` constructor.\n * @returns An awaitable instance of `FSObject`.\n * @example\n * const fsObject = FSObject.from('file.json')\n * const object = await fsObject\n *\n * // Change the file and check the object.\n * writeFileSync('file.json', '{\"foo\":\"bar\"}')\n * await fsObject.untilLoaded\n * object // => { foo: 'bar' }\n *\n * // Change the object and check the file.\n * object.foo = 'baz'\n * await fsObject.untilCommitted\n * readFileSync('file.json', 'utf8') // => { \"foo\": \"baz\" }\n */\n static from<T extends object>(path: PathLike, options: FSObjectOptions<T> = {}): Awaitable<FSObject<T>, Reactive<T>> {\n const fsObject = new FSObject<T>(path, options)\n const createPromise = () => fsObject.load().then(() => fsObject.watch().object)\n return awaitable(fsObject, createPromise)\n }\n\n /**\n * Commit the current state of the object to the file. This function\n * **will** write the object to the file and emit a `commit` event.\n *\n * @param writeObject The object to write to the file.\n * @returns A promise that resolves when the file has been written.\n */\n public async commit(writeObject = this.object as T): Promise<void> {\n this.isCommitting = true\n\n // --- Stringify the object and write it to disk.\n const { serialize = (object: unknown) => JSON.stringify(object, undefined, 2) } = this.options\n const writeJson = serialize(writeObject)\n const pathString = this.path.toString()\n const pathDirectory = dirname(pathString)\n await mkdir(pathDirectory, { recursive: true })\n await writeFile(this.path, `${writeJson}\\n`, 'utf8')\n overwrite(this.object, writeObject)\n this.stats = await stat(this.path)\n\n this.emit('commit', writeObject)\n this.isCommitting = false\n }\n\n /**\n * Close the file and stop watching the file and object for changes.\n * If the file has been created as a temporary file, it will be deleted.\n */\n public async destroy(): Promise<void> {\n this.isLoading = false\n this.isCommitting = false\n if (this.watcher) this.watcher.close()\n if (this.options.deleteOnDestroy) await rm(this.path, { force: true })\n this.watcher = undefined\n this.isDestroyed = true\n this.emit('destroy')\n }\n\n /**\n * Load the file and update the object.\n *\n * @returns The loaded object.\n */\n public async load(): Promise<void> {\n this.isLoading = true\n this.isDestroyed = false\n\n // --- If the file does not exist, and the `createIfNotExists` option is\n // --- set to `true`, create the file and initialize the object with the\n // --- `initialValue` option.\n const accessError = await access(this.path, constants.F_OK | constants.R_OK).catch((error: Error) => error)\n if (accessError && this.options.createIfNotExists) {\n await this.commit()\n this.isLoading = false\n this.emit('load', this.object)\n return\n }\n\n // --- If the file does not exist, throw an error.\n if (accessError && !this.options.createIfNotExists) throw accessError\n\n // --- Assert the path points to a file.\n const newStats = await stat(this.path)\n const newIsFile = newStats.isFile()\n if (!newIsFile) throw new Error(`Expected ${this.path.toString()} to be a file`)\n\n // --- If the file has not changed, return the current object.\n if (this.object && this.stats && newStats.mtimeMs < this.stats.mtimeMs) return\n this.stats = newStats\n\n // --- Read and parse the file.\n const { parse = JSON.parse } = this.options\n const newJson = await readFile(this.path, 'utf8')\n const newObject = parse(newJson) as T\n\n // --- Assert JSON is an object.\n if (typeof newObject !== 'object' || newObject === null)\n throw new Error(`Expected ${this.path.toString()} to be a JSON object`)\n\n // --- Update the object by overwriting it's properties.\n overwrite(this.object, newObject)\n this.isLoading = false\n this.emit('load', newObject)\n }\n\n /**\n * Start watching the file for changes and update the object if the content\n * of the file changes.\n *\n * @returns The current instance for chaining.\n * @example\n * const object = new FSObject('file.json').watch()\n *\n * // Change the file and check the object.\n * writeFileSync('file.json', '{\"foo\":\"bar\"}')\n *\n * // Wait until the object is updated.\n * await object.untilLoaded\n *\n * // Check the object.\n * expect(object.object).toStrictEqual({ foo: 'bar' })\n */\n public watch(): this {\n if (this.watcher) return this\n\n // --- Try to watch the file for changes. If an error occurs, the file\n // --- is likely not accessible. In this case, just set the `isWatching`\n // --- flag to `true` and retry watching the file when it becomes accessible.\n this.watcher = watch(this.path, { persistent: false, ...this.options }, (event) => {\n if (this.isBusy) return\n if (this.options.ignoreFileChanges) return\n if (event === 'change') void this.load()\n })\n\n // --- Return the instance for chaining.\n return this\n }\n\n /**\n * Flag to signal the instance is busy doing a commit or a load operation.\n *\n * @returns `true` if the instance is busy, `false` otherwise.\n */\n get isBusy() {\n return this.isLoading || this.isCommitting || this.isDestroyed\n }\n\n /**\n * A promise that resolves when the file is synchronized with the object.\n *\n * @returns A promise that resolves when the file is synchronized.\n * @example\n * const object = new FSObject('file.json')\n * object.commit()\n *\n * // Wait until the file is synchronized.\n * await object.untilCommitted\n */\n get untilCommitted(): Promise<void> {\n if (!this.isCommitting) return Promise.resolve()\n return new Promise<void>(resolve => this.prependOnceListener('commit', () => resolve()))\n }\n\n /**\n * A promise that resolves when the object is destroyed.\n *\n * @returns A promise that resolves when the object is destroyed.\n * @example\n * const object = new FSObject('file.json')\n * object.destroy()\n *\n * // Wait until the object is destroyed.\n * await object.untilDestroyed\n */\n get untilDestroyed(): Promise<void> {\n if (this.isDestroyed) return Promise.resolve()\n return new Promise<void>(resolve => this.prependOnceListener('destroy', resolve))\n }\n\n /**\n * A promise that resolves when the object is synchronized with the file.\n *\n * @returns A promise that resolves when the file is synchronized.\n * @example\n * const object = new FSObject('file.json')\n * object.load()\n *\n * // Wait until the object is synchronized.\n * await object.untilLoaded\n */\n get untilLoaded(): Promise<void> {\n if (!this.isLoading) return Promise.resolve()\n return new Promise<void>(resolve => this.prependOnceListener('load', () => resolve()))\n }\n}\n\n/**\n * Create an awaitable instance of `FSObject` that resolves when the file\n * is synchronized with the object and the object is synchronized with the file.\n *\n * This function is a shorthand for creating a new `FSObject` instance and\n * calling the `access`, `load` and `watch` methods in sequence. This allows\n * fast and easy access to the file and object in a single call.\n *\n * @param path The path or file descriptor of the file to load.\n * @param options Options to pass to the `FSObject` constructor.\n * @returns An awaitable instance of `FSObject`.\n * @example\n * const fsObject = loadObject('file.json')\n * const object = await fsObject\n *\n * // Change the file and check the object.\n * writeFileSync('file.json', '{\"foo\":\"bar\"}')\n * await fsObject.untilLoaded\n * object // => { foo: 'bar' }\n *\n * // Change the object and check the file.\n * object.foo = 'baz'\n * await fsObject.untilCommitted\n * readFileSync('file.json', 'utf8') // => { \"foo\": \"baz\" }\n */\nexport function loadObject<T extends object>(path: PathLike, options: FSObjectOptions<T> = {}): Awaitable<FSObject<T>, Reactive<T>> {\n return FSObject.from(path, options)\n}\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"touch.cjs","sources":["../touch.ts"],"sourcesContent":["import type { TimeLike } from 'node:fs'\nimport { mkdir, stat, utimes, writeFile } from 'node:fs/promises'\nimport { dirname } from 'node:path'\n\nexport interface TouchOptions {\n\n /**\n * The time to set as the file's last access time.\n *\n * @default Date.now()\n */\n accessTime?: TimeLike\n\n /**\n * The time to set as the file's last modified time.\n *\n * @default Date.now()\n */\n modifiedTime?: TimeLike\n}\n\n/**\n * Touch a file at the given path. You can optionally specify the access and modified times\n * to set on the file. If the file does not exists, an empty file and any missing parent\n * folders will be created.\n *\n * @param path The path to the file to touch.\n * @param options The access and modified times to set on the file.\n * @returns A promise that resolves when the file has been touched.\n * @example\n * // Touch a file with a specific access and modified time.\n * await touch('/foo/bar.txt', { accessTime: 1000, modifiedTime: 2000 })\n *\n * // Check the file's access and modified times.\n * const stats = await stat('/foo/bar.txt')\n * expect(stats.atimeMs).toStrictEqual(1000)\n * expect(stats.mtimeMs).toStrictEqual(2000)\n */\nexport async function touch(path: string, options: TouchOptions = {}): Promise<void> {\n const {\n accessTime = Date.now(),\n modifiedTime = Date.now(),\n } = options\n\n // --- If the path does not exist, then create it.\n const fileExists = await stat(path)\n .then(() => true)\n .catch(() => false)\n\n if (!fileExists) {\n const fileDirectory = dirname(path)\n await mkdir(fileDirectory, { recursive: true })\n await writeFile(path, [])\n }\n\n // --- Update the file's access and modified times.\n await utimes(path, accessTime, modifiedTime)\n}\n\n/* v8 ignore next */\nif (import.meta.vitest) {\n const { vol } = await import('memfs')\n\n beforeAll(() => {\n vi.useFakeTimers()\n })\n\n test('should create a file if it does not exist', async() => {\n await touch('/foo.txt')\n const now = Date.now() * 1000\n const stats = await stat('/foo.txt')\n expect(stats.atimeMs).toStrictEqual(now)\n expect(stats.mtimeMs).toStrictEqual(now)\n })\n\n test('should create a nested file if the parent folder does not exist', async() => {\n await touch('/foo/bar.txt')\n const now = Date.now() * 1000\n const stats = await stat('/foo/bar.txt')\n expect(stats.atimeMs).toStrictEqual(now)\n expect(stats.mtimeMs).toStrictEqual(now)\n })\n\n test('should update the access and modified times of an existing file', async() => {\n vol.fromJSON({ '/foo.txt': 'Hello, world!' })\n await touch('/foo.txt', { accessTime: 1000 })\n const stats = await stat('/foo.txt')\n expect(stats.atimeMs).toStrictEqual(1000 * 1000)\n })\n\n test('should update the modified time of an existing file', async() => {\n vol.fromJSON({ '/foo.txt': 'Hello, world!' })\n await touch('/foo.txt', { modifiedTime: 1000 })\n const stats = await stat('/foo.txt')\n expect(stats.mtimeMs).toStrictEqual(1000 * 1000)\n })\n}\n"],"names":["stat","dirname","mkdir","writeFile","utimes"],"mappings":";;AAsCA,eAAsB,MAAM,MAAc,UAAwB,IAAmB;AAC7E,QAAA;AAAA,IACJ,aAAa,KAAK,IAAI;AAAA,IACtB,eAAe,KAAK,IAAI;AAAA,EACtB,IAAA;AAOJ,MAAI,CAJe,MAAMA,SAAAA,KAAK,IAAI,EAC/B,KAAK,MAAM,EAAI,EACf,MAAM,MAAM,EAAK,GAEH;AACT,UAAA,gBAAgBC,kBAAQ,IAAI;AAC5B,UAAAC,eAAM,eAAe,EAAE,WAAW,IAAM,GAC9C,MAAMC,SAAA,UAAU,MAAM,CAAA,CAAE;AAAA,EAC1B;AAGM,QAAAC,gBAAO,MAAM,YAAY,YAAY;AAC7C;;"}
1
+ {"version":3,"file":"touch.cjs","sources":["../touch.ts"],"sourcesContent":["import type { TimeLike } from 'node:fs'\nimport { mkdir, stat, utimes, writeFile } from 'node:fs/promises'\nimport { dirname } from 'node:path'\n\nexport interface TouchOptions {\n\n /**\n * The time to set as the file's last access time.\n *\n * @default Date.now()\n */\n accessTime?: TimeLike\n\n /**\n * The time to set as the file's last modified time.\n *\n * @default Date.now()\n */\n modifiedTime?: TimeLike\n}\n\n/**\n * Touch a file at the given path. You can optionally specify the access and modified times\n * to set on the file. If the file does not exists, an empty file and any missing parent\n * folders will be created.\n *\n * @param path The path to the file to touch.\n * @param options The access and modified times to set on the file.\n * @returns A promise that resolves when the file has been touched.\n * @example\n * // Touch a file with a specific access and modified time.\n * await touch('/foo/bar.txt', { accessTime: 1000, modifiedTime: 2000 })\n *\n * // Check the file's access and modified times.\n * const stats = await stat('/foo/bar.txt')\n * expect(stats.atimeMs).toStrictEqual(1000)\n * expect(stats.mtimeMs).toStrictEqual(2000)\n */\nexport async function touch(path: string, options: TouchOptions = {}): Promise<void> {\n const {\n accessTime = Date.now(),\n modifiedTime = Date.now(),\n } = options\n\n // --- If the path does not exist, then create it.\n const fileExists = await stat(path)\n .then(() => true)\n .catch(() => false)\n\n if (!fileExists) {\n const fileDirectory = dirname(path)\n await mkdir(fileDirectory, { recursive: true })\n await writeFile(path, [])\n }\n\n // --- Update the file's access and modified times.\n await utimes(path, accessTime, modifiedTime)\n}\n"],"names":["stat","dirname","mkdir","writeFile","utimes"],"mappings":";;AAsCA,eAAsB,MAAM,MAAc,UAAwB,IAAmB;AAC7E,QAAA;AAAA,IACJ,aAAa,KAAK,IAAI;AAAA,IACtB,eAAe,KAAK,IAAI;AAAA,EAAA,IACtB;AAOJ,MAAI,CAJe,MAAMA,SAAAA,KAAK,IAAI,EAC/B,KAAK,MAAM,EAAI,EACf,MAAM,MAAM,EAAK,GAEH;AACT,UAAA,gBAAgBC,kBAAQ,IAAI;AAC5B,UAAAC,SAAA,MAAM,eAAe,EAAE,WAAW,GAAM,CAAA,GAC9C,MAAMC,SAAAA,UAAU,MAAM,EAAE;AAAA,EAAA;AAIpB,QAAAC,gBAAO,MAAM,YAAY,YAAY;AAC7C;;"}
package/dist/touch.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"touch.js","sources":["../touch.ts"],"sourcesContent":["import type { TimeLike } from 'node:fs'\nimport { mkdir, stat, utimes, writeFile } from 'node:fs/promises'\nimport { dirname } from 'node:path'\n\nexport interface TouchOptions {\n\n /**\n * The time to set as the file's last access time.\n *\n * @default Date.now()\n */\n accessTime?: TimeLike\n\n /**\n * The time to set as the file's last modified time.\n *\n * @default Date.now()\n */\n modifiedTime?: TimeLike\n}\n\n/**\n * Touch a file at the given path. You can optionally specify the access and modified times\n * to set on the file. If the file does not exists, an empty file and any missing parent\n * folders will be created.\n *\n * @param path The path to the file to touch.\n * @param options The access and modified times to set on the file.\n * @returns A promise that resolves when the file has been touched.\n * @example\n * // Touch a file with a specific access and modified time.\n * await touch('/foo/bar.txt', { accessTime: 1000, modifiedTime: 2000 })\n *\n * // Check the file's access and modified times.\n * const stats = await stat('/foo/bar.txt')\n * expect(stats.atimeMs).toStrictEqual(1000)\n * expect(stats.mtimeMs).toStrictEqual(2000)\n */\nexport async function touch(path: string, options: TouchOptions = {}): Promise<void> {\n const {\n accessTime = Date.now(),\n modifiedTime = Date.now(),\n } = options\n\n // --- If the path does not exist, then create it.\n const fileExists = await stat(path)\n .then(() => true)\n .catch(() => false)\n\n if (!fileExists) {\n const fileDirectory = dirname(path)\n await mkdir(fileDirectory, { recursive: true })\n await writeFile(path, [])\n }\n\n // --- Update the file's access and modified times.\n await utimes(path, accessTime, modifiedTime)\n}\n\n/* v8 ignore next */\nif (import.meta.vitest) {\n const { vol } = await import('memfs')\n\n beforeAll(() => {\n vi.useFakeTimers()\n })\n\n test('should create a file if it does not exist', async() => {\n await touch('/foo.txt')\n const now = Date.now() * 1000\n const stats = await stat('/foo.txt')\n expect(stats.atimeMs).toStrictEqual(now)\n expect(stats.mtimeMs).toStrictEqual(now)\n })\n\n test('should create a nested file if the parent folder does not exist', async() => {\n await touch('/foo/bar.txt')\n const now = Date.now() * 1000\n const stats = await stat('/foo/bar.txt')\n expect(stats.atimeMs).toStrictEqual(now)\n expect(stats.mtimeMs).toStrictEqual(now)\n })\n\n test('should update the access and modified times of an existing file', async() => {\n vol.fromJSON({ '/foo.txt': 'Hello, world!' })\n await touch('/foo.txt', { accessTime: 1000 })\n const stats = await stat('/foo.txt')\n expect(stats.atimeMs).toStrictEqual(1000 * 1000)\n })\n\n test('should update the modified time of an existing file', async() => {\n vol.fromJSON({ '/foo.txt': 'Hello, world!' })\n await touch('/foo.txt', { modifiedTime: 1000 })\n const stats = await stat('/foo.txt')\n expect(stats.mtimeMs).toStrictEqual(1000 * 1000)\n })\n}\n"],"names":[],"mappings":";;AAsCA,eAAsB,MAAM,MAAc,UAAwB,IAAmB;AAC7E,QAAA;AAAA,IACJ,aAAa,KAAK,IAAI;AAAA,IACtB,eAAe,KAAK,IAAI;AAAA,EACtB,IAAA;AAOJ,MAAI,CAJe,MAAM,KAAK,IAAI,EAC/B,KAAK,MAAM,EAAI,EACf,MAAM,MAAM,EAAK,GAEH;AACT,UAAA,gBAAgB,QAAQ,IAAI;AAC5B,UAAA,MAAM,eAAe,EAAE,WAAW,IAAM,GAC9C,MAAM,UAAU,MAAM,CAAA,CAAE;AAAA,EAC1B;AAGM,QAAA,OAAO,MAAM,YAAY,YAAY;AAC7C;"}
1
+ {"version":3,"file":"touch.js","sources":["../touch.ts"],"sourcesContent":["import type { TimeLike } from 'node:fs'\nimport { mkdir, stat, utimes, writeFile } from 'node:fs/promises'\nimport { dirname } from 'node:path'\n\nexport interface TouchOptions {\n\n /**\n * The time to set as the file's last access time.\n *\n * @default Date.now()\n */\n accessTime?: TimeLike\n\n /**\n * The time to set as the file's last modified time.\n *\n * @default Date.now()\n */\n modifiedTime?: TimeLike\n}\n\n/**\n * Touch a file at the given path. You can optionally specify the access and modified times\n * to set on the file. If the file does not exists, an empty file and any missing parent\n * folders will be created.\n *\n * @param path The path to the file to touch.\n * @param options The access and modified times to set on the file.\n * @returns A promise that resolves when the file has been touched.\n * @example\n * // Touch a file with a specific access and modified time.\n * await touch('/foo/bar.txt', { accessTime: 1000, modifiedTime: 2000 })\n *\n * // Check the file's access and modified times.\n * const stats = await stat('/foo/bar.txt')\n * expect(stats.atimeMs).toStrictEqual(1000)\n * expect(stats.mtimeMs).toStrictEqual(2000)\n */\nexport async function touch(path: string, options: TouchOptions = {}): Promise<void> {\n const {\n accessTime = Date.now(),\n modifiedTime = Date.now(),\n } = options\n\n // --- If the path does not exist, then create it.\n const fileExists = await stat(path)\n .then(() => true)\n .catch(() => false)\n\n if (!fileExists) {\n const fileDirectory = dirname(path)\n await mkdir(fileDirectory, { recursive: true })\n await writeFile(path, [])\n }\n\n // --- Update the file's access and modified times.\n await utimes(path, accessTime, modifiedTime)\n}\n"],"names":[],"mappings":";;AAsCA,eAAsB,MAAM,MAAc,UAAwB,IAAmB;AAC7E,QAAA;AAAA,IACJ,aAAa,KAAK,IAAI;AAAA,IACtB,eAAe,KAAK,IAAI;AAAA,EAAA,IACtB;AAOJ,MAAI,CAJe,MAAM,KAAK,IAAI,EAC/B,KAAK,MAAM,EAAI,EACf,MAAM,MAAM,EAAK,GAEH;AACT,UAAA,gBAAgB,QAAQ,IAAI;AAC5B,UAAA,MAAM,eAAe,EAAE,WAAW,GAAM,CAAA,GAC9C,MAAM,UAAU,MAAM,EAAE;AAAA,EAAA;AAIpB,QAAA,OAAO,MAAM,YAAY,YAAY;AAC7C;"}
@@ -1 +1 @@
1
- {"version":3,"file":"updateFile.cjs","sources":["../updateFile.ts"],"sourcesContent":["/* eslint-disable jsdoc/check-param-names */\nimport type { MaybePromise } from '@unshared/types'\nimport type { PathLike } from 'node:fs'\nimport { open, readFile, writeFile } from 'node:fs/promises'\n\n/**\n * A callback that updates a file's contents.\n *\n * @template T The type of the file's contents.\n * @example UpdateFileCallback<string> // (content: string) => Promise<string> | string\n */\nexport type UpdateFileCallback<T extends Buffer | string> = (content: T) => MaybePromise<Buffer | string>\n\n/**\n * Open a file, update its contents using the provided callback, and close it. The file\n * must exist before calling this function or an error will be thrown.\n *\n * @param path The path to the file to update.\n * @param callback A callback that updates the file's contents.\n * @param encoding The encoding to use when reading the file.\n * @returns A promise that resolves when the file is updated.\n * @example\n * // Create a file.\n * await writeFile('/path/to/file.txt', 'foo')\n *\n * // Update a file's contents using a transform function.\n * await updateFile('/path/to/file.txt', toUpperCase, 'utf8')\n *\n * // Check the file's contents.\n * await readFile('/path/to/file.txt', 'utf8') // 'FOO'\n */\nexport async function updateFile(path: PathLike, callback: UpdateFileCallback<Buffer>): Promise<void>\nexport async function updateFile(path: PathLike, callback: UpdateFileCallback<string>, encoding: BufferEncoding): Promise<void>\nexport async function updateFile(path: PathLike, callback: UpdateFileCallback<any>, encoding?: BufferEncoding): Promise<void> {\n const fileHandle = await open(path, 'r+')\n const fileContents = await readFile(fileHandle, encoding)\n const newFileContents = await callback(fileContents)\n await writeFile(fileHandle, newFileContents)\n await fileHandle.close()\n}\n\n/* v8 ignore start */\nif (import.meta.vitest) {\n const { vol } = await import('memfs')\n\n test('should update a file with a callback using buffer', async() => {\n vol.fromJSON({ '/foo.txt': 'Hello, world!' })\n await updateFile('/foo.txt', content => content.toString('utf8').toUpperCase())\n const result = await readFile('/foo.txt', 'utf8')\n expect(result).toBe('HELLO, WORLD!')\n })\n\n test('should update a file with a callback using utf8 encoding', async() => {\n vol.fromJSON({ '/foo.txt': 'Hello, world!' })\n await updateFile('/foo.txt', content => content.toUpperCase(), 'utf8')\n const result = await readFile('/foo.txt', 'utf8')\n expect(result).toBe('HELLO, WORLD!')\n })\n\n test('should throw an error if the file does not exist', async() => {\n const shouldThrow = updateFile('/foo.txt', content => content)\n await expect(shouldThrow).rejects.toThrow('ENOENT: no such file or directory, open')\n })\n}\n"],"names":["open","readFile","writeFile"],"mappings":";;AAiCsB,eAAA,WAAW,MAAgB,UAAmC,UAA0C;AAC5H,QAAM,aAAa,MAAMA,SAAK,KAAA,MAAM,IAAI,GAClC,eAAe,MAAMC,SAAA,SAAS,YAAY,QAAQ,GAClD,kBAAkB,MAAM,SAAS,YAAY;AACnD,QAAMC,SAAAA,UAAU,YAAY,eAAe,GAC3C,MAAM,WAAW;AACnB;;"}
1
+ {"version":3,"file":"updateFile.cjs","sources":["../updateFile.ts"],"sourcesContent":["/* eslint-disable jsdoc/check-param-names */\nimport type { MaybePromise } from '@unshared/types'\nimport type { PathLike } from 'node:fs'\nimport { open, readFile, writeFile } from 'node:fs/promises'\n\n/**\n * A callback that updates a file's contents.\n *\n * @template T The type of the file's contents.\n * @example UpdateFileCallback<string> // (content: string) => Promise<string> | string\n */\nexport type UpdateFileCallback<T extends Buffer | string> = (content: T) => MaybePromise<Buffer | string>\n\n/**\n * Open a file, update its contents using the provided callback, and close it. The file\n * must exist before calling this function or an error will be thrown.\n *\n * @param path The path to the file to update.\n * @param callback A callback that updates the file's contents.\n * @param encoding The encoding to use when reading the file.\n * @returns A promise that resolves when the file is updated.\n * @example\n * // Create a file.\n * await writeFile('/path/to/file.txt', 'foo')\n *\n * // Update a file's contents using a transform function.\n * await updateFile('/path/to/file.txt', toUpperCase, 'utf8')\n *\n * // Check the file's contents.\n * await readFile('/path/to/file.txt', 'utf8') // 'FOO'\n */\nexport async function updateFile(path: PathLike, callback: UpdateFileCallback<Buffer>): Promise<void>\nexport async function updateFile(path: PathLike, callback: UpdateFileCallback<string>, encoding: BufferEncoding): Promise<void>\nexport async function updateFile(path: PathLike, callback: UpdateFileCallback<any>, encoding?: BufferEncoding): Promise<void> {\n const fileHandle = await open(path, 'r+')\n const fileContents = await readFile(fileHandle, encoding)\n const newFileContents = await callback(fileContents)\n await writeFile(fileHandle, newFileContents)\n await fileHandle.close()\n}\n"],"names":["open","readFile","writeFile"],"mappings":";;AAiCsB,eAAA,WAAW,MAAgB,UAAmC,UAA0C;AAC5H,QAAM,aAAa,MAAMA,SAAAA,KAAK,MAAM,IAAI,GAClC,eAAe,MAAMC,SAAAA,SAAS,YAAY,QAAQ,GAClD,kBAAkB,MAAM,SAAS,YAAY;AACnD,QAAMC,SAAAA,UAAU,YAAY,eAAe,GAC3C,MAAM,WAAW,MAAM;AACzB;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"updateFile.js","sources":["../updateFile.ts"],"sourcesContent":["/* eslint-disable jsdoc/check-param-names */\nimport type { MaybePromise } from '@unshared/types'\nimport type { PathLike } from 'node:fs'\nimport { open, readFile, writeFile } from 'node:fs/promises'\n\n/**\n * A callback that updates a file's contents.\n *\n * @template T The type of the file's contents.\n * @example UpdateFileCallback<string> // (content: string) => Promise<string> | string\n */\nexport type UpdateFileCallback<T extends Buffer | string> = (content: T) => MaybePromise<Buffer | string>\n\n/**\n * Open a file, update its contents using the provided callback, and close it. The file\n * must exist before calling this function or an error will be thrown.\n *\n * @param path The path to the file to update.\n * @param callback A callback that updates the file's contents.\n * @param encoding The encoding to use when reading the file.\n * @returns A promise that resolves when the file is updated.\n * @example\n * // Create a file.\n * await writeFile('/path/to/file.txt', 'foo')\n *\n * // Update a file's contents using a transform function.\n * await updateFile('/path/to/file.txt', toUpperCase, 'utf8')\n *\n * // Check the file's contents.\n * await readFile('/path/to/file.txt', 'utf8') // 'FOO'\n */\nexport async function updateFile(path: PathLike, callback: UpdateFileCallback<Buffer>): Promise<void>\nexport async function updateFile(path: PathLike, callback: UpdateFileCallback<string>, encoding: BufferEncoding): Promise<void>\nexport async function updateFile(path: PathLike, callback: UpdateFileCallback<any>, encoding?: BufferEncoding): Promise<void> {\n const fileHandle = await open(path, 'r+')\n const fileContents = await readFile(fileHandle, encoding)\n const newFileContents = await callback(fileContents)\n await writeFile(fileHandle, newFileContents)\n await fileHandle.close()\n}\n\n/* v8 ignore start */\nif (import.meta.vitest) {\n const { vol } = await import('memfs')\n\n test('should update a file with a callback using buffer', async() => {\n vol.fromJSON({ '/foo.txt': 'Hello, world!' })\n await updateFile('/foo.txt', content => content.toString('utf8').toUpperCase())\n const result = await readFile('/foo.txt', 'utf8')\n expect(result).toBe('HELLO, WORLD!')\n })\n\n test('should update a file with a callback using utf8 encoding', async() => {\n vol.fromJSON({ '/foo.txt': 'Hello, world!' })\n await updateFile('/foo.txt', content => content.toUpperCase(), 'utf8')\n const result = await readFile('/foo.txt', 'utf8')\n expect(result).toBe('HELLO, WORLD!')\n })\n\n test('should throw an error if the file does not exist', async() => {\n const shouldThrow = updateFile('/foo.txt', content => content)\n await expect(shouldThrow).rejects.toThrow('ENOENT: no such file or directory, open')\n })\n}\n"],"names":[],"mappings":";AAiCsB,eAAA,WAAW,MAAgB,UAAmC,UAA0C;AAC5H,QAAM,aAAa,MAAM,KAAK,MAAM,IAAI,GAClC,eAAe,MAAM,SAAS,YAAY,QAAQ,GAClD,kBAAkB,MAAM,SAAS,YAAY;AACnD,QAAM,UAAU,YAAY,eAAe,GAC3C,MAAM,WAAW;AACnB;"}
1
+ {"version":3,"file":"updateFile.js","sources":["../updateFile.ts"],"sourcesContent":["/* eslint-disable jsdoc/check-param-names */\nimport type { MaybePromise } from '@unshared/types'\nimport type { PathLike } from 'node:fs'\nimport { open, readFile, writeFile } from 'node:fs/promises'\n\n/**\n * A callback that updates a file's contents.\n *\n * @template T The type of the file's contents.\n * @example UpdateFileCallback<string> // (content: string) => Promise<string> | string\n */\nexport type UpdateFileCallback<T extends Buffer | string> = (content: T) => MaybePromise<Buffer | string>\n\n/**\n * Open a file, update its contents using the provided callback, and close it. The file\n * must exist before calling this function or an error will be thrown.\n *\n * @param path The path to the file to update.\n * @param callback A callback that updates the file's contents.\n * @param encoding The encoding to use when reading the file.\n * @returns A promise that resolves when the file is updated.\n * @example\n * // Create a file.\n * await writeFile('/path/to/file.txt', 'foo')\n *\n * // Update a file's contents using a transform function.\n * await updateFile('/path/to/file.txt', toUpperCase, 'utf8')\n *\n * // Check the file's contents.\n * await readFile('/path/to/file.txt', 'utf8') // 'FOO'\n */\nexport async function updateFile(path: PathLike, callback: UpdateFileCallback<Buffer>): Promise<void>\nexport async function updateFile(path: PathLike, callback: UpdateFileCallback<string>, encoding: BufferEncoding): Promise<void>\nexport async function updateFile(path: PathLike, callback: UpdateFileCallback<any>, encoding?: BufferEncoding): Promise<void> {\n const fileHandle = await open(path, 'r+')\n const fileContents = await readFile(fileHandle, encoding)\n const newFileContents = await callback(fileContents)\n await writeFile(fileHandle, newFileContents)\n await fileHandle.close()\n}\n"],"names":[],"mappings":";AAiCsB,eAAA,WAAW,MAAgB,UAAmC,UAA0C;AAC5H,QAAM,aAAa,MAAM,KAAK,MAAM,IAAI,GAClC,eAAe,MAAM,SAAS,YAAY,QAAQ,GAClD,kBAAkB,MAAM,SAAS,YAAY;AACnD,QAAM,UAAU,YAAY,eAAe,GAC3C,MAAM,WAAW,MAAM;AACzB;"}
@@ -1,11 +1,12 @@
1
1
  "use strict";
2
2
  var createTemporaryDirectory = require("./createTemporaryDirectory.cjs");
3
+ require("node:crypto");
3
4
  require("node:fs/promises");
4
5
  require("node:os");
5
6
  require("node:path");
6
7
  async function withTemporaryDirectories(options, fn) {
7
8
  typeof options == "number" && (options = Array.from({ length: options }, () => ({}))), Array.isArray(options) || (options = [options]);
8
- const pathsPromises = options.map(createTemporaryDirectory.createTemporaryDirectory), pathsInstances = await Promise.all(pathsPromises), paths = pathsInstances.map((x) => x[0]);
9
+ const pathsPromises = options.map((option) => createTemporaryDirectory.createTemporaryDirectory(option)), pathsInstances = await Promise.all(pathsPromises), paths = pathsInstances.map((x) => x[0]);
9
10
  try {
10
11
  return await fn(...paths);
11
12
  } finally {
@@ -1 +1 @@
1
- {"version":3,"file":"withTemporaryDirectories.cjs","sources":["../withTemporaryDirectories.ts"],"sourcesContent":["import type { Function, MaybeArray, Tuple, TupleLength } from '@unshared/types'\nimport type { CreateTemporaryDirectoryOptions } from './createTemporaryDirectory'\nimport { createTemporaryDirectory } from './createTemporaryDirectory'\n\ntype Callback<U, N extends number> = (...paths: Tuple<N, string>) => Promise<U> | U\n\n/**\n * Wrap a function that will create a temporary directory and\n * recursively remove it after the function has been executed,\n * regardless of whether the function throws an error or not.\n *\n * @param count The number of temporary directories to create.\n * @param fn The function to wrap that takes the temporary directory path(s) as arguments.\n * @returns A promise that resolves to the result of the function.\n */\nexport async function withTemporaryDirectories<U, N extends number>(count: N, fn: Callback<U, N>): Promise<U>\nexport async function withTemporaryDirectories<U, T extends CreateTemporaryDirectoryOptions[]>(options: T, fn: Callback<U, TupleLength<T>>): Promise<U>\nexport async function withTemporaryDirectories<U, T extends CreateTemporaryDirectoryOptions>(option: T, fn: Callback<U, 1>): Promise<U>\nexport async function withTemporaryDirectories(options: MaybeArray<CreateTemporaryDirectoryOptions> | number, fn: Function<unknown>): Promise<unknown> {\n\n // --- Normalize the arguments.\n if (typeof options === 'number') options = Array.from({ length: options }, () => ({}))\n if (!Array.isArray(options)) options = [options]\n\n // --- Create temporary files.\n const pathsPromises = options.map(createTemporaryDirectory)\n const pathsInstances = await Promise.all(pathsPromises)\n const paths = pathsInstances.map(x => x[0])\n\n try {\n return await fn(...paths)\n }\n finally {\n const promises = pathsInstances.map(x => x[1]())\n await Promise.all(promises)\n }\n}\n\n/* v8 ignore start */\nif (import.meta.vitest) {\n const { existsSync } = await import('node:fs')\n\n test('should call a function with one temporary directory', async() => {\n await withTemporaryDirectories(1, (path) => {\n const exists = existsSync(path)\n expect(exists).toBe(true)\n })\n })\n\n test('should call a function with two temporary directories', async() => {\n await withTemporaryDirectories(2, (path1, path2) => {\n const exists1 = existsSync(path1)\n const exists2 = existsSync(path2)\n expect(exists1).toBe(true)\n expect(exists2).toBe(true)\n })\n })\n\n test('should remove the temporary directories after calling the function', async() => {\n let temporaryPath1: string\n let temporaryPath2: string\n await withTemporaryDirectories(2, (path1, path2) => {\n temporaryPath1 = path1\n temporaryPath2 = path2\n })\n const exists1 = existsSync(temporaryPath1!)\n const exists2 = existsSync(temporaryPath2!)\n expect(exists1).toBe(false)\n expect(exists2).toBe(false)\n })\n\n test('should remove the temporary directories even if the function throws an error', async() => {\n let temporaryPath1: string\n let temporaryPath2: string\n await withTemporaryDirectories(2, (path1, path2) => {\n temporaryPath1 = path1\n temporaryPath2 = path2\n throw new Error('Test error')\n }).catch(() => {})\n const exists1 = existsSync(temporaryPath1!)\n const exists2 = existsSync(temporaryPath2!)\n expect(exists1).toBe(false)\n expect(exists2).toBe(false)\n })\n\n test('should call a function with a temporary file in the specified directory', async() => {\n await withTemporaryDirectories({ directory: '/cache' }, (path) => {\n expect(path).toMatch(/^\\/cache\\/[\\da-z]+$/)\n })\n })\n\n test('should call a function with a temporary file with the given random function', async() => {\n await withTemporaryDirectories({ random: () => 'foo' }, (path) => {\n expect(path).toMatch(/^\\/tmp\\/foo$/)\n })\n })\n\n test('should call a function with multiple temporary files with different options', async() => {\n await withTemporaryDirectories(\n [{ directory: '/cache' }, { random: () => 'foo' }],\n (path1, path2) => {\n expect(path1).toMatch(/^\\/cache\\/[\\da-z]+$/)\n expect(path2).toMatch(/^\\/tmp\\/foo$/)\n },\n )\n })\n\n test('should return the result of the function', async() => {\n const result = await withTemporaryDirectories(1, () => 42)\n expect(result).toBe(42)\n })\n\n test('should throw an error if the function throws an error', async() => {\n const shouldReject = withTemporaryDirectories(1, () => { throw new Error('Test error') })\n await expect(shouldReject).rejects.toThrow('Test error')\n })\n}\n"],"names":["createTemporaryDirectory"],"mappings":";;;;;AAkBsB,eAAA,yBAAyB,SAA+D,IAAyC;AAGjJ,SAAO,WAAY,aAAU,UAAU,MAAM,KAAK,EAAE,QAAQ,WAAW,OAAO,CAAC,EAAE,IAChF,MAAM,QAAQ,OAAO,MAAG,UAAU,CAAC,OAAO;AAG/C,QAAM,gBAAgB,QAAQ,IAAIA,yBAAwB,wBAAA,GACpD,iBAAiB,MAAM,QAAQ,IAAI,aAAa,GAChD,QAAQ,eAAe,IAAI,CAAK,MAAA,EAAE,CAAC,CAAC;AAEtC,MAAA;AACK,WAAA,MAAM,GAAG,GAAG,KAAK;AAAA,EAAA,UAE1B;AACE,UAAM,WAAW,eAAe,IAAI,OAAK,EAAE,CAAC,GAAG;AACzC,UAAA,QAAQ,IAAI,QAAQ;AAAA,EAC5B;AACF;;"}
1
+ {"version":3,"file":"withTemporaryDirectories.cjs","sources":["../withTemporaryDirectories.ts"],"sourcesContent":["import type { Function, MaybeArray, Tuple, TupleLength } from '@unshared/types'\nimport type { CreateTemporaryDirectoryOptions } from './createTemporaryDirectory'\nimport { createTemporaryDirectory } from './createTemporaryDirectory'\n\ntype Callback<U, N extends number> = (...paths: Tuple<N, string>) => Promise<U> | U\n\n/**\n * Wrap a function that will create a temporary directory and\n * recursively remove it after the function has been executed,\n * regardless of whether the function throws an error or not.\n *\n * @param count The number of temporary directories to create.\n * @param fn The function to wrap that takes the temporary directory path(s) as arguments.\n * @returns A promise that resolves to the result of the function.\n */\nexport async function withTemporaryDirectories<U, N extends number>(count: N, fn: Callback<U, N>): Promise<U>\nexport async function withTemporaryDirectories<U, T extends CreateTemporaryDirectoryOptions[]>(options: T, fn: Callback<U, TupleLength<T>>): Promise<U>\nexport async function withTemporaryDirectories<U, T extends CreateTemporaryDirectoryOptions>(option: T, fn: Callback<U, 1>): Promise<U>\nexport async function withTemporaryDirectories(options: MaybeArray<CreateTemporaryDirectoryOptions> | number, fn: Function<unknown>): Promise<unknown> {\n\n // --- Normalize the arguments.\n if (typeof options === 'number') options = Array.from({ length: options }, () => ({}))\n if (!Array.isArray(options)) options = [options]\n\n // --- Create temporary files.\n const pathsPromises = options.map(option => createTemporaryDirectory(option))\n const pathsInstances = await Promise.all(pathsPromises)\n const paths = pathsInstances.map(x => x[0])\n\n try {\n return await fn(...paths)\n }\n finally {\n const promises = pathsInstances.map(x => x[1]())\n await Promise.all(promises)\n }\n}\n"],"names":["createTemporaryDirectory"],"mappings":";;;;;;AAkBsB,eAAA,yBAAyB,SAA+D,IAAyC;AAGjJ,SAAO,WAAY,aAAU,UAAU,MAAM,KAAK,EAAE,QAAQ,QAAQ,GAAG,OAAO,CAAA,EAAG,IAChF,MAAM,QAAQ,OAAO,MAAG,UAAU,CAAC,OAAO;AAGzC,QAAA,gBAAgB,QAAQ,IAAI,CAAA,WAAUA,kDAAyB,MAAM,CAAC,GACtE,iBAAiB,MAAM,QAAQ,IAAI,aAAa,GAChD,QAAQ,eAAe,IAAI,CAAK,MAAA,EAAE,CAAC,CAAC;AAEtC,MAAA;AACK,WAAA,MAAM,GAAG,GAAG,KAAK;AAAA,EAAA,UAE1B;AACE,UAAM,WAAW,eAAe,IAAI,OAAK,EAAE,CAAC,GAAG;AACzC,UAAA,QAAQ,IAAI,QAAQ;AAAA,EAAA;AAE9B;;"}
@@ -1,10 +1,11 @@
1
1
  import { createTemporaryDirectory } from "./createTemporaryDirectory.js";
2
+ import "node:crypto";
2
3
  import "node:fs/promises";
3
4
  import "node:os";
4
5
  import "node:path";
5
6
  async function withTemporaryDirectories(options, fn) {
6
7
  typeof options == "number" && (options = Array.from({ length: options }, () => ({}))), Array.isArray(options) || (options = [options]);
7
- const pathsPromises = options.map(createTemporaryDirectory), pathsInstances = await Promise.all(pathsPromises), paths = pathsInstances.map((x) => x[0]);
8
+ const pathsPromises = options.map((option) => createTemporaryDirectory(option)), pathsInstances = await Promise.all(pathsPromises), paths = pathsInstances.map((x) => x[0]);
8
9
  try {
9
10
  return await fn(...paths);
10
11
  } finally {
@@ -1 +1 @@
1
- {"version":3,"file":"withTemporaryDirectories.js","sources":["../withTemporaryDirectories.ts"],"sourcesContent":["import type { Function, MaybeArray, Tuple, TupleLength } from '@unshared/types'\nimport type { CreateTemporaryDirectoryOptions } from './createTemporaryDirectory'\nimport { createTemporaryDirectory } from './createTemporaryDirectory'\n\ntype Callback<U, N extends number> = (...paths: Tuple<N, string>) => Promise<U> | U\n\n/**\n * Wrap a function that will create a temporary directory and\n * recursively remove it after the function has been executed,\n * regardless of whether the function throws an error or not.\n *\n * @param count The number of temporary directories to create.\n * @param fn The function to wrap that takes the temporary directory path(s) as arguments.\n * @returns A promise that resolves to the result of the function.\n */\nexport async function withTemporaryDirectories<U, N extends number>(count: N, fn: Callback<U, N>): Promise<U>\nexport async function withTemporaryDirectories<U, T extends CreateTemporaryDirectoryOptions[]>(options: T, fn: Callback<U, TupleLength<T>>): Promise<U>\nexport async function withTemporaryDirectories<U, T extends CreateTemporaryDirectoryOptions>(option: T, fn: Callback<U, 1>): Promise<U>\nexport async function withTemporaryDirectories(options: MaybeArray<CreateTemporaryDirectoryOptions> | number, fn: Function<unknown>): Promise<unknown> {\n\n // --- Normalize the arguments.\n if (typeof options === 'number') options = Array.from({ length: options }, () => ({}))\n if (!Array.isArray(options)) options = [options]\n\n // --- Create temporary files.\n const pathsPromises = options.map(createTemporaryDirectory)\n const pathsInstances = await Promise.all(pathsPromises)\n const paths = pathsInstances.map(x => x[0])\n\n try {\n return await fn(...paths)\n }\n finally {\n const promises = pathsInstances.map(x => x[1]())\n await Promise.all(promises)\n }\n}\n\n/* v8 ignore start */\nif (import.meta.vitest) {\n const { existsSync } = await import('node:fs')\n\n test('should call a function with one temporary directory', async() => {\n await withTemporaryDirectories(1, (path) => {\n const exists = existsSync(path)\n expect(exists).toBe(true)\n })\n })\n\n test('should call a function with two temporary directories', async() => {\n await withTemporaryDirectories(2, (path1, path2) => {\n const exists1 = existsSync(path1)\n const exists2 = existsSync(path2)\n expect(exists1).toBe(true)\n expect(exists2).toBe(true)\n })\n })\n\n test('should remove the temporary directories after calling the function', async() => {\n let temporaryPath1: string\n let temporaryPath2: string\n await withTemporaryDirectories(2, (path1, path2) => {\n temporaryPath1 = path1\n temporaryPath2 = path2\n })\n const exists1 = existsSync(temporaryPath1!)\n const exists2 = existsSync(temporaryPath2!)\n expect(exists1).toBe(false)\n expect(exists2).toBe(false)\n })\n\n test('should remove the temporary directories even if the function throws an error', async() => {\n let temporaryPath1: string\n let temporaryPath2: string\n await withTemporaryDirectories(2, (path1, path2) => {\n temporaryPath1 = path1\n temporaryPath2 = path2\n throw new Error('Test error')\n }).catch(() => {})\n const exists1 = existsSync(temporaryPath1!)\n const exists2 = existsSync(temporaryPath2!)\n expect(exists1).toBe(false)\n expect(exists2).toBe(false)\n })\n\n test('should call a function with a temporary file in the specified directory', async() => {\n await withTemporaryDirectories({ directory: '/cache' }, (path) => {\n expect(path).toMatch(/^\\/cache\\/[\\da-z]+$/)\n })\n })\n\n test('should call a function with a temporary file with the given random function', async() => {\n await withTemporaryDirectories({ random: () => 'foo' }, (path) => {\n expect(path).toMatch(/^\\/tmp\\/foo$/)\n })\n })\n\n test('should call a function with multiple temporary files with different options', async() => {\n await withTemporaryDirectories(\n [{ directory: '/cache' }, { random: () => 'foo' }],\n (path1, path2) => {\n expect(path1).toMatch(/^\\/cache\\/[\\da-z]+$/)\n expect(path2).toMatch(/^\\/tmp\\/foo$/)\n },\n )\n })\n\n test('should return the result of the function', async() => {\n const result = await withTemporaryDirectories(1, () => 42)\n expect(result).toBe(42)\n })\n\n test('should throw an error if the function throws an error', async() => {\n const shouldReject = withTemporaryDirectories(1, () => { throw new Error('Test error') })\n await expect(shouldReject).rejects.toThrow('Test error')\n })\n}\n"],"names":[],"mappings":";;;;AAkBsB,eAAA,yBAAyB,SAA+D,IAAyC;AAGjJ,SAAO,WAAY,aAAU,UAAU,MAAM,KAAK,EAAE,QAAQ,WAAW,OAAO,CAAC,EAAE,IAChF,MAAM,QAAQ,OAAO,MAAG,UAAU,CAAC,OAAO;AAG/C,QAAM,gBAAgB,QAAQ,IAAI,wBAAwB,GACpD,iBAAiB,MAAM,QAAQ,IAAI,aAAa,GAChD,QAAQ,eAAe,IAAI,CAAK,MAAA,EAAE,CAAC,CAAC;AAEtC,MAAA;AACK,WAAA,MAAM,GAAG,GAAG,KAAK;AAAA,EAAA,UAE1B;AACE,UAAM,WAAW,eAAe,IAAI,OAAK,EAAE,CAAC,GAAG;AACzC,UAAA,QAAQ,IAAI,QAAQ;AAAA,EAC5B;AACF;"}
1
+ {"version":3,"file":"withTemporaryDirectories.js","sources":["../withTemporaryDirectories.ts"],"sourcesContent":["import type { Function, MaybeArray, Tuple, TupleLength } from '@unshared/types'\nimport type { CreateTemporaryDirectoryOptions } from './createTemporaryDirectory'\nimport { createTemporaryDirectory } from './createTemporaryDirectory'\n\ntype Callback<U, N extends number> = (...paths: Tuple<N, string>) => Promise<U> | U\n\n/**\n * Wrap a function that will create a temporary directory and\n * recursively remove it after the function has been executed,\n * regardless of whether the function throws an error or not.\n *\n * @param count The number of temporary directories to create.\n * @param fn The function to wrap that takes the temporary directory path(s) as arguments.\n * @returns A promise that resolves to the result of the function.\n */\nexport async function withTemporaryDirectories<U, N extends number>(count: N, fn: Callback<U, N>): Promise<U>\nexport async function withTemporaryDirectories<U, T extends CreateTemporaryDirectoryOptions[]>(options: T, fn: Callback<U, TupleLength<T>>): Promise<U>\nexport async function withTemporaryDirectories<U, T extends CreateTemporaryDirectoryOptions>(option: T, fn: Callback<U, 1>): Promise<U>\nexport async function withTemporaryDirectories(options: MaybeArray<CreateTemporaryDirectoryOptions> | number, fn: Function<unknown>): Promise<unknown> {\n\n // --- Normalize the arguments.\n if (typeof options === 'number') options = Array.from({ length: options }, () => ({}))\n if (!Array.isArray(options)) options = [options]\n\n // --- Create temporary files.\n const pathsPromises = options.map(option => createTemporaryDirectory(option))\n const pathsInstances = await Promise.all(pathsPromises)\n const paths = pathsInstances.map(x => x[0])\n\n try {\n return await fn(...paths)\n }\n finally {\n const promises = pathsInstances.map(x => x[1]())\n await Promise.all(promises)\n }\n}\n"],"names":[],"mappings":";;;;;AAkBsB,eAAA,yBAAyB,SAA+D,IAAyC;AAGjJ,SAAO,WAAY,aAAU,UAAU,MAAM,KAAK,EAAE,QAAQ,QAAQ,GAAG,OAAO,CAAA,EAAG,IAChF,MAAM,QAAQ,OAAO,MAAG,UAAU,CAAC,OAAO;AAGzC,QAAA,gBAAgB,QAAQ,IAAI,CAAA,WAAU,yBAAyB,MAAM,CAAC,GACtE,iBAAiB,MAAM,QAAQ,IAAI,aAAa,GAChD,QAAQ,eAAe,IAAI,CAAK,MAAA,EAAE,CAAC,CAAC;AAEtC,MAAA;AACK,WAAA,MAAM,GAAG,GAAG,KAAK;AAAA,EAAA,UAE1B;AACE,UAAM,WAAW,eAAe,IAAI,OAAK,EAAE,CAAC,GAAG;AACzC,UAAA,QAAQ,IAAI,QAAQ;AAAA,EAAA;AAE9B;"}
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  var createTemporaryFile = require("./createTemporaryFile.cjs");
3
+ require("node:crypto");
3
4
  require("node:fs/promises");
4
5
  require("node:os");
5
6
  require("node:path");
@@ -1 +1 @@
1
- {"version":3,"file":"withTemporaryFiles.cjs","sources":["../withTemporaryFiles.ts"],"sourcesContent":["import type { Function, MaybeArray, Tuple, TupleLength } from '@unshared/types'\nimport type { CreateTemporaryFileOptions } from './createTemporaryFile'\nimport { createTemporaryFile } from './createTemporaryFile'\n\ntype Callback<U, N extends number> = (...paths: Tuple<N, string>) => Promise<U> | U\n\n/**\n * Wrap a function that will create one or more temporary files and\n * remove them after the function has been executed, regardless of\n * whether the function throws an error or not.\n *\n * @param count The number of temporary files to create.\n * @param fn The function to wrap that takes the temporary directory path(s) as arguments.\n * @returns A promise that resolves to the result of the function.\n */\nexport async function withTemporaryFiles<U, N extends number>(count: N, fn: Callback<U, N>): Promise<U>\nexport async function withTemporaryFiles<U, T extends CreateTemporaryFileOptions[]>(options: T, fn: Callback<U, TupleLength<T>>): Promise<U>\nexport async function withTemporaryFiles<U, T extends CreateTemporaryFileOptions>(option: T, fn: Callback<U, 1>): Promise<U>\nexport async function withTemporaryFiles(options: MaybeArray<CreateTemporaryFileOptions> | number, fn: Function<unknown>): Promise<unknown> {\n\n // --- Normalize the arguments.\n if (typeof options === 'number') options = Array.from({ length: options }, () => ({}))\n if (!Array.isArray(options)) options = [options]\n\n // --- Create temporary files.\n const pathsPromises = options.map(option => createTemporaryFile(undefined, option))\n const pathsInstances = await Promise.all(pathsPromises)\n const paths = pathsInstances.map(x => x[0])\n\n try {\n return await fn(...paths)\n }\n finally {\n const promises = pathsInstances.map(x => x[1]())\n await Promise.all(promises)\n }\n}\n\n/* v8 ignore start */\nif (import.meta.vitest) {\n const { existsSync } = await import('node:fs')\n\n test('should call a function with one temporary file', async() => {\n await withTemporaryFiles(1, (path) => {\n const exists = existsSync(path)\n expect(exists).toBe(true)\n })\n })\n\n test('should call a function with two temporary files', async() => {\n await withTemporaryFiles(2, (path1, path2) => {\n const exists1 = existsSync(path1)\n const exists2 = existsSync(path2)\n expect(exists1).toBe(true)\n expect(exists2).toBe(true)\n })\n })\n\n test('should remove the temporary files after calling the function', async() => {\n let temporaryPath1: string\n let temporaryPath2: string\n await withTemporaryFiles(2, (path1, path2) => {\n temporaryPath1 = path1\n temporaryPath2 = path2\n })\n const exists1 = existsSync(temporaryPath1!)\n const exists2 = existsSync(temporaryPath2!)\n expect(exists1).toBe(false)\n expect(exists2).toBe(false)\n })\n\n test('should remove the temporary files after the function throws an error', async() => {\n let temporaryPath1: string\n let temporaryPath2: string\n await withTemporaryFiles(2, (path1, path2) => {\n temporaryPath1 = path1\n temporaryPath2 = path2\n throw new Error('Test error')\n }).catch(() => {})\n const exists1 = existsSync(temporaryPath1!)\n const exists2 = existsSync(temporaryPath2!)\n expect(exists1).toBe(false)\n expect(exists2).toBe(false)\n })\n\n test('should call a function with a temporary file in the specified directory', async() => {\n await withTemporaryFiles({ directory: '/cache' }, (path) => {\n expect(path).toMatch(/^\\/cache\\/[\\da-z]+$/)\n })\n })\n\n test('should call a function with a temporary file with the specified extension', async() => {\n await withTemporaryFiles({ extension: 'txt' }, (path) => {\n expect(path).toMatch(/^\\/tmp\\/[\\da-z]+\\.txt$/)\n })\n })\n\n test('should call a function with a temporary file with the given random function', async() => {\n await withTemporaryFiles({ random: () => 'foo' }, (path) => {\n expect(path).toMatch(/^\\/tmp\\/foo$/)\n })\n })\n\n test('should call a function with multiple temporary files with different options', async() => {\n await withTemporaryFiles([{ directory: '/cache' }, { extension: 'txt' }, { random: () => 'foo' }], (path1, path2, path3) => {\n expect(path1).toMatch(/^\\/cache\\/[\\da-z]+$/)\n expect(path2).toMatch(/^\\/tmp\\/[\\da-z]+\\.txt$/)\n expect(path3).toMatch(/^\\/tmp\\/foo$/)\n })\n })\n\n test('should return the result of the function', async() => {\n const result = await withTemporaryFiles(1, () => 42)\n expect(result).toBe(42)\n })\n\n test('should throw an error if the function throws an error', async() => {\n const shouldReject = withTemporaryFiles(1, () => { throw new Error('Test error') })\n await expect(shouldReject).rejects.toThrow('Test error')\n })\n}\n"],"names":["createTemporaryFile"],"mappings":";;;;;AAkBsB,eAAA,mBAAmB,SAA0D,IAAyC;AAGtI,SAAO,WAAY,aAAU,UAAU,MAAM,KAAK,EAAE,QAAQ,WAAW,OAAO,CAAC,EAAE,IAChF,MAAM,QAAQ,OAAO,MAAG,UAAU,CAAC,OAAO;AAGzC,QAAA,gBAAgB,QAAQ,IAAI,CAAA,WAAUA,wCAAoB,QAAW,MAAM,CAAC,GAC5E,iBAAiB,MAAM,QAAQ,IAAI,aAAa,GAChD,QAAQ,eAAe,IAAI,CAAA,MAAK,EAAE,CAAC,CAAC;AAEtC,MAAA;AACK,WAAA,MAAM,GAAG,GAAG,KAAK;AAAA,EAAA,UAE1B;AACE,UAAM,WAAW,eAAe,IAAI,OAAK,EAAE,CAAC,GAAG;AACzC,UAAA,QAAQ,IAAI,QAAQ;AAAA,EAC5B;AACF;;"}
1
+ {"version":3,"file":"withTemporaryFiles.cjs","sources":["../withTemporaryFiles.ts"],"sourcesContent":["import type { Function, MaybeArray, Tuple, TupleLength } from '@unshared/types'\nimport type { CreateTemporaryFileOptions } from './createTemporaryFile'\nimport { createTemporaryFile } from './createTemporaryFile'\n\ntype Callback<U, N extends number> = (...paths: Tuple<N, string>) => Promise<U> | U\n\n/**\n * Wrap a function that will create one or more temporary files and\n * remove them after the function has been executed, regardless of\n * whether the function throws an error or not.\n *\n * @param count The number of temporary files to create.\n * @param fn The function to wrap that takes the temporary directory path(s) as arguments.\n * @returns A promise that resolves to the result of the function.\n */\nexport async function withTemporaryFiles<U, N extends number>(count: N, fn: Callback<U, N>): Promise<U>\nexport async function withTemporaryFiles<U, T extends CreateTemporaryFileOptions[]>(options: T, fn: Callback<U, TupleLength<T>>): Promise<U>\nexport async function withTemporaryFiles<U, T extends CreateTemporaryFileOptions>(option: T, fn: Callback<U, 1>): Promise<U>\nexport async function withTemporaryFiles(options: MaybeArray<CreateTemporaryFileOptions> | number, fn: Function<unknown>): Promise<unknown> {\n\n // --- Normalize the arguments.\n if (typeof options === 'number') options = Array.from({ length: options }, () => ({}))\n if (!Array.isArray(options)) options = [options]\n\n // --- Create temporary files.\n const pathsPromises = options.map(option => createTemporaryFile(undefined, option))\n const pathsInstances = await Promise.all(pathsPromises)\n const paths = pathsInstances.map(x => x[0])\n\n // --- Execute the function with the temporary files.\n try {\n return await fn(...paths)\n }\n\n // --- On completion, remove the temporary files.\n finally {\n const promises = pathsInstances.map(x => x[1]())\n await Promise.all(promises)\n }\n}\n"],"names":["createTemporaryFile"],"mappings":";;;;;;AAkBsB,eAAA,mBAAmB,SAA0D,IAAyC;AAGtI,SAAO,WAAY,aAAU,UAAU,MAAM,KAAK,EAAE,QAAQ,QAAQ,GAAG,OAAO,CAAA,EAAG,IAChF,MAAM,QAAQ,OAAO,MAAG,UAAU,CAAC,OAAO;AAGzC,QAAA,gBAAgB,QAAQ,IAAI,CAAA,WAAUA,wCAAoB,QAAW,MAAM,CAAC,GAC5E,iBAAiB,MAAM,QAAQ,IAAI,aAAa,GAChD,QAAQ,eAAe,IAAI,CAAA,MAAK,EAAE,CAAC,CAAC;AAGtC,MAAA;AACK,WAAA,MAAM,GAAG,GAAG,KAAK;AAAA,EAAA,UAI1B;AACE,UAAM,WAAW,eAAe,IAAI,OAAK,EAAE,CAAC,GAAG;AACzC,UAAA,QAAQ,IAAI,QAAQ;AAAA,EAAA;AAE9B;;"}
@@ -1,4 +1,5 @@
1
1
  import { createTemporaryFile } from "./createTemporaryFile.js";
2
+ import "node:crypto";
2
3
  import "node:fs/promises";
3
4
  import "node:os";
4
5
  import "node:path";
@@ -1 +1 @@
1
- {"version":3,"file":"withTemporaryFiles.js","sources":["../withTemporaryFiles.ts"],"sourcesContent":["import type { Function, MaybeArray, Tuple, TupleLength } from '@unshared/types'\nimport type { CreateTemporaryFileOptions } from './createTemporaryFile'\nimport { createTemporaryFile } from './createTemporaryFile'\n\ntype Callback<U, N extends number> = (...paths: Tuple<N, string>) => Promise<U> | U\n\n/**\n * Wrap a function that will create one or more temporary files and\n * remove them after the function has been executed, regardless of\n * whether the function throws an error or not.\n *\n * @param count The number of temporary files to create.\n * @param fn The function to wrap that takes the temporary directory path(s) as arguments.\n * @returns A promise that resolves to the result of the function.\n */\nexport async function withTemporaryFiles<U, N extends number>(count: N, fn: Callback<U, N>): Promise<U>\nexport async function withTemporaryFiles<U, T extends CreateTemporaryFileOptions[]>(options: T, fn: Callback<U, TupleLength<T>>): Promise<U>\nexport async function withTemporaryFiles<U, T extends CreateTemporaryFileOptions>(option: T, fn: Callback<U, 1>): Promise<U>\nexport async function withTemporaryFiles(options: MaybeArray<CreateTemporaryFileOptions> | number, fn: Function<unknown>): Promise<unknown> {\n\n // --- Normalize the arguments.\n if (typeof options === 'number') options = Array.from({ length: options }, () => ({}))\n if (!Array.isArray(options)) options = [options]\n\n // --- Create temporary files.\n const pathsPromises = options.map(option => createTemporaryFile(undefined, option))\n const pathsInstances = await Promise.all(pathsPromises)\n const paths = pathsInstances.map(x => x[0])\n\n try {\n return await fn(...paths)\n }\n finally {\n const promises = pathsInstances.map(x => x[1]())\n await Promise.all(promises)\n }\n}\n\n/* v8 ignore start */\nif (import.meta.vitest) {\n const { existsSync } = await import('node:fs')\n\n test('should call a function with one temporary file', async() => {\n await withTemporaryFiles(1, (path) => {\n const exists = existsSync(path)\n expect(exists).toBe(true)\n })\n })\n\n test('should call a function with two temporary files', async() => {\n await withTemporaryFiles(2, (path1, path2) => {\n const exists1 = existsSync(path1)\n const exists2 = existsSync(path2)\n expect(exists1).toBe(true)\n expect(exists2).toBe(true)\n })\n })\n\n test('should remove the temporary files after calling the function', async() => {\n let temporaryPath1: string\n let temporaryPath2: string\n await withTemporaryFiles(2, (path1, path2) => {\n temporaryPath1 = path1\n temporaryPath2 = path2\n })\n const exists1 = existsSync(temporaryPath1!)\n const exists2 = existsSync(temporaryPath2!)\n expect(exists1).toBe(false)\n expect(exists2).toBe(false)\n })\n\n test('should remove the temporary files after the function throws an error', async() => {\n let temporaryPath1: string\n let temporaryPath2: string\n await withTemporaryFiles(2, (path1, path2) => {\n temporaryPath1 = path1\n temporaryPath2 = path2\n throw new Error('Test error')\n }).catch(() => {})\n const exists1 = existsSync(temporaryPath1!)\n const exists2 = existsSync(temporaryPath2!)\n expect(exists1).toBe(false)\n expect(exists2).toBe(false)\n })\n\n test('should call a function with a temporary file in the specified directory', async() => {\n await withTemporaryFiles({ directory: '/cache' }, (path) => {\n expect(path).toMatch(/^\\/cache\\/[\\da-z]+$/)\n })\n })\n\n test('should call a function with a temporary file with the specified extension', async() => {\n await withTemporaryFiles({ extension: 'txt' }, (path) => {\n expect(path).toMatch(/^\\/tmp\\/[\\da-z]+\\.txt$/)\n })\n })\n\n test('should call a function with a temporary file with the given random function', async() => {\n await withTemporaryFiles({ random: () => 'foo' }, (path) => {\n expect(path).toMatch(/^\\/tmp\\/foo$/)\n })\n })\n\n test('should call a function with multiple temporary files with different options', async() => {\n await withTemporaryFiles([{ directory: '/cache' }, { extension: 'txt' }, { random: () => 'foo' }], (path1, path2, path3) => {\n expect(path1).toMatch(/^\\/cache\\/[\\da-z]+$/)\n expect(path2).toMatch(/^\\/tmp\\/[\\da-z]+\\.txt$/)\n expect(path3).toMatch(/^\\/tmp\\/foo$/)\n })\n })\n\n test('should return the result of the function', async() => {\n const result = await withTemporaryFiles(1, () => 42)\n expect(result).toBe(42)\n })\n\n test('should throw an error if the function throws an error', async() => {\n const shouldReject = withTemporaryFiles(1, () => { throw new Error('Test error') })\n await expect(shouldReject).rejects.toThrow('Test error')\n })\n}\n"],"names":[],"mappings":";;;;AAkBsB,eAAA,mBAAmB,SAA0D,IAAyC;AAGtI,SAAO,WAAY,aAAU,UAAU,MAAM,KAAK,EAAE,QAAQ,WAAW,OAAO,CAAC,EAAE,IAChF,MAAM,QAAQ,OAAO,MAAG,UAAU,CAAC,OAAO;AAGzC,QAAA,gBAAgB,QAAQ,IAAI,CAAA,WAAU,oBAAoB,QAAW,MAAM,CAAC,GAC5E,iBAAiB,MAAM,QAAQ,IAAI,aAAa,GAChD,QAAQ,eAAe,IAAI,CAAA,MAAK,EAAE,CAAC,CAAC;AAEtC,MAAA;AACK,WAAA,MAAM,GAAG,GAAG,KAAK;AAAA,EAAA,UAE1B;AACE,UAAM,WAAW,eAAe,IAAI,OAAK,EAAE,CAAC,GAAG;AACzC,UAAA,QAAQ,IAAI,QAAQ;AAAA,EAC5B;AACF;"}
1
+ {"version":3,"file":"withTemporaryFiles.js","sources":["../withTemporaryFiles.ts"],"sourcesContent":["import type { Function, MaybeArray, Tuple, TupleLength } from '@unshared/types'\nimport type { CreateTemporaryFileOptions } from './createTemporaryFile'\nimport { createTemporaryFile } from './createTemporaryFile'\n\ntype Callback<U, N extends number> = (...paths: Tuple<N, string>) => Promise<U> | U\n\n/**\n * Wrap a function that will create one or more temporary files and\n * remove them after the function has been executed, regardless of\n * whether the function throws an error or not.\n *\n * @param count The number of temporary files to create.\n * @param fn The function to wrap that takes the temporary directory path(s) as arguments.\n * @returns A promise that resolves to the result of the function.\n */\nexport async function withTemporaryFiles<U, N extends number>(count: N, fn: Callback<U, N>): Promise<U>\nexport async function withTemporaryFiles<U, T extends CreateTemporaryFileOptions[]>(options: T, fn: Callback<U, TupleLength<T>>): Promise<U>\nexport async function withTemporaryFiles<U, T extends CreateTemporaryFileOptions>(option: T, fn: Callback<U, 1>): Promise<U>\nexport async function withTemporaryFiles(options: MaybeArray<CreateTemporaryFileOptions> | number, fn: Function<unknown>): Promise<unknown> {\n\n // --- Normalize the arguments.\n if (typeof options === 'number') options = Array.from({ length: options }, () => ({}))\n if (!Array.isArray(options)) options = [options]\n\n // --- Create temporary files.\n const pathsPromises = options.map(option => createTemporaryFile(undefined, option))\n const pathsInstances = await Promise.all(pathsPromises)\n const paths = pathsInstances.map(x => x[0])\n\n // --- Execute the function with the temporary files.\n try {\n return await fn(...paths)\n }\n\n // --- On completion, remove the temporary files.\n finally {\n const promises = pathsInstances.map(x => x[1]())\n await Promise.all(promises)\n }\n}\n"],"names":[],"mappings":";;;;;AAkBsB,eAAA,mBAAmB,SAA0D,IAAyC;AAGtI,SAAO,WAAY,aAAU,UAAU,MAAM,KAAK,EAAE,QAAQ,QAAQ,GAAG,OAAO,CAAA,EAAG,IAChF,MAAM,QAAQ,OAAO,MAAG,UAAU,CAAC,OAAO;AAGzC,QAAA,gBAAgB,QAAQ,IAAI,CAAA,WAAU,oBAAoB,QAAW,MAAM,CAAC,GAC5E,iBAAiB,MAAM,QAAQ,IAAI,aAAa,GAChD,QAAQ,eAAe,IAAI,CAAA,MAAK,EAAE,CAAC,CAAC;AAGtC,MAAA;AACK,WAAA,MAAM,GAAG,GAAG,KAAK;AAAA,EAAA,UAI1B;AACE,UAAM,WAAW,eAAe,IAAI,OAAK,EAAE,CAAC,GAAG;AACzC,UAAA,QAAQ,IAAI,QAAQ;AAAA,EAAA;AAE9B;"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@unshared/fs",
3
3
  "type": "module",
4
- "version": "0.0.20",
4
+ "version": "0.1.0",
5
5
  "license": "MIT",
6
6
  "sideEffects": false,
7
7
  "author": "Stanley Horwood <stanley@hsjm.io>",
@@ -78,13 +78,13 @@
78
78
  "LICENSE.md"
79
79
  ],
80
80
  "dependencies": {
81
- "@unshared/collection": "0.0.20",
82
- "@unshared/functions": "0.0.20",
83
- "@unshared/reactivity": "0.0.20",
84
- "@unshared/string": "0.0.20",
85
- "@unshared/types": "0.0.20"
81
+ "@unshared/collection": "0.1.0",
82
+ "@unshared/functions": "0.1.0",
83
+ "@unshared/reactivity": "0.1.0",
84
+ "@unshared/string": "0.1.0",
85
+ "@unshared/types": "0.1.0"
86
86
  },
87
87
  "devDependencies": {
88
- "memfs": "4.8.2"
88
+ "memfs": "4.14.0"
89
89
  }
90
90
  }