@unshared/fs 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- package/dist/createTemporaryDirectory.cjs.map +1 -1
- package/dist/createTemporaryDirectory.js.map +1 -1
- package/dist/createTemporaryFile.cjs.map +1 -1
- package/dist/createTemporaryFile.js.map +1 -1
- package/dist/findAncestor.cjs.map +1 -1
- package/dist/findAncestor.js.map +1 -1
- package/dist/findAncestors.cjs.map +1 -1
- package/dist/findAncestors.js.map +1 -1
- package/dist/glob.cjs.map +1 -1
- package/dist/glob.js.map +1 -1
- package/dist/loadObject.cjs.map +1 -1
- package/dist/loadObject.js.map +1 -1
- package/dist/touch.cjs.map +1 -1
- package/dist/touch.js.map +1 -1
- package/package.json +7 -7
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"createTemporaryDirectory.cjs","sources":["../createTemporaryDirectory.ts"],"sourcesContent":["import { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { mkdir, rm } from 'node:fs/promises'\n\nexport interface CreateTemporaryDirectoryOptions {\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 * A function that generates a random string.\n *\n * @default () => Math.random().toString(36).slice(2)\n */\n random?: () => string\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).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
|
1
|
+
{"version":3,"file":"createTemporaryDirectory.cjs","sources":["../createTemporaryDirectory.ts"],"sourcesContent":["import { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { mkdir, rm } from 'node:fs/promises'\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).toBeTruthy()\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).toBeFalsy()\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 +1 @@
|
|
1
|
-
{"version":3,"file":"createTemporaryDirectory.js","sources":["../createTemporaryDirectory.ts"],"sourcesContent":["import { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { mkdir, rm } from 'node:fs/promises'\n\nexport interface CreateTemporaryDirectoryOptions {\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 * A function that generates a random string.\n *\n * @default () => Math.random().toString(36).slice(2)\n */\n random?: () => string\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).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
|
1
|
+
{"version":3,"file":"createTemporaryDirectory.js","sources":["../createTemporaryDirectory.ts"],"sourcesContent":["import { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { mkdir, rm } from 'node:fs/promises'\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).toBeTruthy()\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).toBeFalsy()\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 +1 @@
|
|
1
|
-
{"version":3,"file":"createTemporaryFile.cjs","sources":["../createTemporaryFile.ts"],"sourcesContent":["import { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { mkdir, rm, writeFile } from 'node:fs/promises'\n\nexport interface CreateTemporaryFileOptions {\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 * The file extension to use for the temporary file.\n *\n * @default ''\n */\n extension?: string\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).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
|
1
|
+
{"version":3,"file":"createTemporaryFile.cjs","sources":["../createTemporaryFile.ts"],"sourcesContent":["import { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { mkdir, rm, writeFile } from 'node:fs/promises'\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).toBeTruthy()\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).toBeFalsy()\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 +1 @@
|
|
1
|
-
{"version":3,"file":"createTemporaryFile.js","sources":["../createTemporaryFile.ts"],"sourcesContent":["import { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { mkdir, rm, writeFile } from 'node:fs/promises'\n\nexport interface CreateTemporaryFileOptions {\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 * The file extension to use for the temporary file.\n *\n * @default ''\n */\n extension?: string\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).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
|
1
|
+
{"version":3,"file":"createTemporaryFile.js","sources":["../createTemporaryFile.ts"],"sourcesContent":["import { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { mkdir, rm, writeFile } from 'node:fs/promises'\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).toBeTruthy()\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).toBeFalsy()\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 +1 @@
|
|
1
|
-
{"version":3,"file":"findAncestor.cjs","sources":["../findAncestor.ts"],"sourcesContent":["import { cwd } from 'node:process'\nimport { dirname, resolve } from 'node:path'\nimport { access, constants } from 'node:fs/promises'\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 /** 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,
|
1
|
+
{"version":3,"file":"findAncestor.cjs","sources":["../findAncestor.ts"],"sourcesContent":["import { cwd } from 'node:process'\nimport { dirname, resolve } from 'node:path'\nimport { access, constants } from 'node:fs/promises'\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;AAAK;AAClB,WAAOC,UAAAA,QAAQ,IAAI;AAAA,EACrB;AACF;;"}
|
package/dist/findAncestor.js.map
CHANGED
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"findAncestor.js","sources":["../findAncestor.ts"],"sourcesContent":["import { cwd } from 'node:process'\nimport { dirname, resolve } from 'node:path'\nimport { access, constants } from 'node:fs/promises'\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 /** 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,
|
1
|
+
{"version":3,"file":"findAncestor.js","sources":["../findAncestor.ts"],"sourcesContent":["import { cwd } from 'node:process'\nimport { dirname, resolve } from 'node:path'\nimport { access, constants } from 'node:fs/promises'\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;AAAK;AAClB,WAAO,QAAQ,IAAI;AAAA,EACrB;AACF;"}
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"findAncestors.cjs","sources":["../findAncestors.ts"],"sourcesContent":["import { cwd } from 'node:process'\nimport { dirname, resolve } from 'node:path'\nimport { access, constants } from 'node:fs/promises'\nimport { Awaitable, awaitable } from '@unshared/functions/awaitable'\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 /** 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":";;AAqBO,SAAS,cAAc,MAAc,OAAeA,aAAAA,OAAmD;AAC5G,
|
1
|
+
{"version":3,"file":"findAncestors.cjs","sources":["../findAncestors.ts"],"sourcesContent":["import { cwd } from 'node:process'\nimport { dirname, resolve } from 'node:path'\nimport { access, constants } from 'node:fs/promises'\nimport { Awaitable, awaitable } from '@unshared/functions/awaitable'\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":";;AAqBO,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;AAAK;AAClB,aAAOC,UAAAA,QAAQ,IAAI;AAAA,IACrB;AAAA,EACF;AAGA,QAAM,WAAW;AACjB,SAAOC,UAAAA,UAAU,QAAQ;AAC3B;;"}
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"findAncestors.js","sources":["../findAncestors.ts"],"sourcesContent":["import { cwd } from 'node:process'\nimport { dirname, resolve } from 'node:path'\nimport { access, constants } from 'node:fs/promises'\nimport { Awaitable, awaitable } from '@unshared/functions/awaitable'\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 /** 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":";;;;AAqBO,SAAS,cAAc,MAAc,OAAe,OAAmD;AAC5G,
|
1
|
+
{"version":3,"file":"findAncestors.js","sources":["../findAncestors.ts"],"sourcesContent":["import { cwd } from 'node:process'\nimport { dirname, resolve } from 'node:path'\nimport { access, constants } from 'node:fs/promises'\nimport { Awaitable, awaitable } from '@unshared/functions/awaitable'\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":";;;;AAqBO,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;AAAK;AAClB,aAAO,QAAQ,IAAI;AAAA,IACrB;AAAA,EACF;AAGA,QAAM,WAAW;AACjB,SAAO,UAAU,QAAQ;AAC3B;"}
|
package/dist/glob.cjs.map
CHANGED
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"glob.cjs","sources":["../glob.ts"],"sourcesContent":["import { cwd as getCwd } from 'node:process'\nimport { join, relative } from 'node:path'\nimport { readdir, stat } from 'node:fs/promises'\nimport { Stats } from 'node:fs'\nimport { MaybeArray } from '@unshared/types'\nimport { createPattern } from '@unshared/string/createPattern'\nimport { Awaitable, awaitable } from '@unshared/functions/awaitable'\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 * 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 * A list of patterns to exclude from the result.\n *\n * @default []\n */\n exclude?: MaybeArray<string>\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 * 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 * 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 * 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":";;
|
1
|
+
{"version":3,"file":"glob.cjs","sources":["../glob.ts"],"sourcesContent":["import { cwd as getCwd } from 'node:process'\nimport { join, relative } from 'node:path'\nimport { readdir, stat } from 'node:fs/promises'\nimport { Stats } from 'node:fs'\nimport { MaybeArray } from '@unshared/types'\nimport { createPattern } from '@unshared/string/createPattern'\nimport { Awaitable, awaitable } from '@unshared/functions/awaitable'\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":";;AAoFO,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;AAC7D;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;;"}
|
package/dist/glob.js.map
CHANGED
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"glob.js","sources":["../glob.ts"],"sourcesContent":["import { cwd as getCwd } from 'node:process'\nimport { join, relative } from 'node:path'\nimport { readdir, stat } from 'node:fs/promises'\nimport { Stats } from 'node:fs'\nimport { MaybeArray } from '@unshared/types'\nimport { createPattern } from '@unshared/string/createPattern'\nimport { Awaitable, awaitable } from '@unshared/functions/awaitable'\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 * 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 * A list of patterns to exclude from the result.\n *\n * @default []\n */\n exclude?: MaybeArray<string>\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 * 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 * 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 * 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":";;;;;
|
1
|
+
{"version":3,"file":"glob.js","sources":["../glob.ts"],"sourcesContent":["import { cwd as getCwd } from 'node:process'\nimport { join, relative } from 'node:path'\nimport { readdir, stat } from 'node:fs/promises'\nimport { Stats } from 'node:fs'\nimport { MaybeArray } from '@unshared/types'\nimport { createPattern } from '@unshared/string/createPattern'\nimport { Awaitable, awaitable } from '@unshared/functions/awaitable'\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":";;;;;AAoFO,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;AAC7D;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;"}
|
package/dist/loadObject.cjs.map
CHANGED
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"loadObject.cjs","sources":["../loadObject.ts"],"sourcesContent":["import { dirname } from 'node:path'\nimport { access, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'\nimport { FSWatcher, PathLike, Stats, WatchOptions, constants, existsSync, readFileSync, watch, writeFileSync } from 'node:fs'\nimport { EventEmitter } from 'node:events'\nimport { Reactive, ReactiveOptions, reactive } from '@unshared/reactivity/reactive'\nimport { garbageCollected } from '@unshared/functions/garbageCollected'\nimport { Awaitable, awaitable } from '@unshared/functions/awaitable'\nimport { overwrite } from '@unshared/collection/overwrite'\n\nexport interface FSObjectOptions<T extends object> extends ReactiveOptions<T>, WatchOptions {\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 * 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 * 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 * 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 * 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 * 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 * 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 /** 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 * 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).toBeFalsy()\n const loaded = result.load()\n expect(result.isLoading).toBeTruthy()\n await loaded\n expect(result.isLoading).toBeFalsy()\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).toBeTruthy()\n await expect(result.untilLoaded).resolves.toBeUndefined()\n expect(result.isLoading).toBeFalsy()\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).toBeFalsy()\n await expect(result.untilLoaded).resolves.toBeUndefined()\n expect(result.isLoading).toBeFalsy()\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).toBeFalsy()\n void result.commit()\n expect(result.isCommitting).toBeTruthy()\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).toBeFalsy()\n void result.commit()\n expect(result.isCommitting).toBeTruthy()\n await expect(result.untilCommitted).resolves.toBeUndefined()\n expect(result.isCommitting).toBeFalsy()\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).toBeFalsy()\n await result.destroy()\n expect(result.isDestroyed).toBeTruthy()\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).toBeFalsy()\n const untilDestroyed = result.untilDestroyed\n void result.destroy()\n expect(result.isDestroyed).toBeTruthy()\n await expect(untilDestroyed).resolves.toBeUndefined()\n expect(result.isDestroyed).toBeTruthy()\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).toBeFalsy()\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 { dirname } from 'node:path'\nimport { access, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'\nimport { FSWatcher, PathLike, Stats, WatchOptions, constants, existsSync, readFileSync, watch, writeFileSync } from 'node:fs'\nimport { EventEmitter } from 'node:events'\nimport { Reactive, ReactiveOptions, reactive } from '@unshared/reactivity/reactive'\nimport { garbageCollected } from '@unshared/functions/garbageCollected'\nimport { Awaitable, awaitable } from '@unshared/functions/awaitable'\nimport { overwrite } from '@unshared/collection/overwrite'\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).toBeFalsy()\n const loaded = result.load()\n expect(result.isLoading).toBeTruthy()\n await loaded\n expect(result.isLoading).toBeFalsy()\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).toBeTruthy()\n await expect(result.untilLoaded).resolves.toBeUndefined()\n expect(result.isLoading).toBeFalsy()\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).toBeFalsy()\n await expect(result.untilLoaded).resolves.toBeUndefined()\n expect(result.isLoading).toBeFalsy()\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).toBeFalsy()\n void result.commit()\n expect(result.isCommitting).toBeTruthy()\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).toBeFalsy()\n void result.commit()\n expect(result.isCommitting).toBeTruthy()\n await expect(result.untilCommitted).resolves.toBeUndefined()\n expect(result.isCommitting).toBeFalsy()\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).toBeFalsy()\n await result.destroy()\n expect(result.isDestroyed).toBeTruthy()\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).toBeFalsy()\n const untilDestroyed = result.untilDestroyed\n void result.destroy()\n expect(result.isDestroyed).toBeTruthy()\n await expect(untilDestroyed).resolves.toBeUndefined()\n expect(result.isDestroyed).toBeTruthy()\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).toBeFalsy()\n })\n })\n}\n"],"names":["EventEmitter","reactive","garbageCollected","awaitable","dirname","mkdir","writeFile","overwrite","stat","rm","access","constants","readFile","watch"],"mappings":"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"}
|
package/dist/loadObject.js.map
CHANGED
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"loadObject.js","sources":["../loadObject.ts"],"sourcesContent":["import { dirname } from 'node:path'\nimport { access, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'\nimport { FSWatcher, PathLike, Stats, WatchOptions, constants, existsSync, readFileSync, watch, writeFileSync } from 'node:fs'\nimport { EventEmitter } from 'node:events'\nimport { Reactive, ReactiveOptions, reactive } from '@unshared/reactivity/reactive'\nimport { garbageCollected } from '@unshared/functions/garbageCollected'\nimport { Awaitable, awaitable } from '@unshared/functions/awaitable'\nimport { overwrite } from '@unshared/collection/overwrite'\n\nexport interface FSObjectOptions<T extends object> extends ReactiveOptions<T>, WatchOptions {\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 * 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 * 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 * 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 * 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 * 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 * 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 /** 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 * 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).toBeFalsy()\n const loaded = result.load()\n expect(result.isLoading).toBeTruthy()\n await loaded\n expect(result.isLoading).toBeFalsy()\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).toBeTruthy()\n await expect(result.untilLoaded).resolves.toBeUndefined()\n expect(result.isLoading).toBeFalsy()\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).toBeFalsy()\n await expect(result.untilLoaded).resolves.toBeUndefined()\n expect(result.isLoading).toBeFalsy()\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).toBeFalsy()\n void result.commit()\n expect(result.isCommitting).toBeTruthy()\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).toBeFalsy()\n void result.commit()\n expect(result.isCommitting).toBeTruthy()\n await expect(result.untilCommitted).resolves.toBeUndefined()\n expect(result.isCommitting).toBeFalsy()\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).toBeFalsy()\n await result.destroy()\n expect(result.isDestroyed).toBeTruthy()\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).toBeFalsy()\n const untilDestroyed = result.untilDestroyed\n void result.destroy()\n expect(result.isDestroyed).toBeTruthy()\n await expect(untilDestroyed).resolves.toBeUndefined()\n expect(result.isDestroyed).toBeTruthy()\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).toBeFalsy()\n })\n })\n}\n"],"names":[],"mappings":"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"}
|
1
|
+
{"version":3,"file":"loadObject.js","sources":["../loadObject.ts"],"sourcesContent":["import { dirname } from 'node:path'\nimport { access, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'\nimport { FSWatcher, PathLike, Stats, WatchOptions, constants, existsSync, readFileSync, watch, writeFileSync } from 'node:fs'\nimport { EventEmitter } from 'node:events'\nimport { Reactive, ReactiveOptions, reactive } from '@unshared/reactivity/reactive'\nimport { garbageCollected } from '@unshared/functions/garbageCollected'\nimport { Awaitable, awaitable } from '@unshared/functions/awaitable'\nimport { overwrite } from '@unshared/collection/overwrite'\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).toBeFalsy()\n const loaded = result.load()\n expect(result.isLoading).toBeTruthy()\n await loaded\n expect(result.isLoading).toBeFalsy()\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).toBeTruthy()\n await expect(result.untilLoaded).resolves.toBeUndefined()\n expect(result.isLoading).toBeFalsy()\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).toBeFalsy()\n await expect(result.untilLoaded).resolves.toBeUndefined()\n expect(result.isLoading).toBeFalsy()\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).toBeFalsy()\n void result.commit()\n expect(result.isCommitting).toBeTruthy()\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).toBeFalsy()\n void result.commit()\n expect(result.isCommitting).toBeTruthy()\n await expect(result.untilCommitted).resolves.toBeUndefined()\n expect(result.isCommitting).toBeFalsy()\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).toBeFalsy()\n await result.destroy()\n expect(result.isDestroyed).toBeTruthy()\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).toBeFalsy()\n const untilDestroyed = result.untilDestroyed\n void result.destroy()\n expect(result.isDestroyed).toBeTruthy()\n await expect(untilDestroyed).resolves.toBeUndefined()\n expect(result.isDestroyed).toBeTruthy()\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).toBeFalsy()\n })\n })\n}\n"],"names":[],"mappings":"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"}
|
package/dist/touch.cjs.map
CHANGED
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"touch.cjs","sources":["../touch.ts"],"sourcesContent":["import { dirname } from 'node:path'\nimport { mkdir, stat, utimes, writeFile } from 'node:fs/promises'\nimport { TimeLike } from 'node:fs'\n\nexport interface TouchOptions {\n /**\n * The time to set as the file's last access time.\n *\n * @default Date.now()\n */\n accessTime?: TimeLike\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":";;
|
1
|
+
{"version":3,"file":"touch.cjs","sources":["../touch.ts"],"sourcesContent":["import { dirname } from 'node:path'\nimport { mkdir, stat, utimes, writeFile } from 'node:fs/promises'\nimport { TimeLike } from 'node:fs'\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;;"}
|
package/dist/touch.js.map
CHANGED
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"touch.js","sources":["../touch.ts"],"sourcesContent":["import { dirname } from 'node:path'\nimport { mkdir, stat, utimes, writeFile } from 'node:fs/promises'\nimport { TimeLike } from 'node:fs'\n\nexport interface TouchOptions {\n /**\n * The time to set as the file's last access time.\n *\n * @default Date.now()\n */\n accessTime?: TimeLike\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":";;
|
1
|
+
{"version":3,"file":"touch.js","sources":["../touch.ts"],"sourcesContent":["import { dirname } from 'node:path'\nimport { mkdir, stat, utimes, writeFile } from 'node:fs/promises'\nimport { TimeLike } from 'node:fs'\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;"}
|
package/package.json
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
{
|
2
2
|
"name": "@unshared/fs",
|
3
3
|
"type": "module",
|
4
|
-
"version": "0.0.
|
4
|
+
"version": "0.0.2",
|
5
5
|
"license": "MIT",
|
6
6
|
"sideEffects": false,
|
7
7
|
"author": "Stanley Horwood <stanley@hsjm.io>",
|
@@ -10,7 +10,7 @@
|
|
10
10
|
"repository": {
|
11
11
|
"directory": "packages/fs",
|
12
12
|
"type": "git",
|
13
|
-
"url": "
|
13
|
+
"url": "https://github.com/shorwood/unshared"
|
14
14
|
},
|
15
15
|
"main": "./dist/index.cjs",
|
16
16
|
"module": "./dist/index.js",
|
@@ -78,11 +78,11 @@
|
|
78
78
|
"LICENSE.md"
|
79
79
|
],
|
80
80
|
"dependencies": {
|
81
|
-
"@unshared/collection": "0.0.
|
82
|
-
"@unshared/functions": "0.0.
|
83
|
-
"@unshared/reactivity": "0.0.
|
84
|
-
"@unshared/string": "0.0.
|
85
|
-
"@unshared/types": "0.0.
|
81
|
+
"@unshared/collection": "0.0.2",
|
82
|
+
"@unshared/functions": "0.0.2",
|
83
|
+
"@unshared/reactivity": "0.0.2",
|
84
|
+
"@unshared/string": "0.0.2",
|
85
|
+
"@unshared/types": "0.0.2"
|
86
86
|
},
|
87
87
|
"devDependencies": {
|
88
88
|
"memfs": "4.8.2"
|