@magic/fs 0.0.38 → 0.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magic/fs",
3
- "version": "0.0.38",
3
+ "version": "0.0.39",
4
4
  "author": "Wizards & Witches",
5
5
  "description": "nodejs fs promises + goodies",
6
6
  "license": "AGPL-3.0",
@@ -26,7 +26,7 @@
26
26
  "check": "npm run tsc"
27
27
  },
28
28
  "engines": {
29
- "node": ">=14.15.4"
29
+ "node": ">=20.0.0"
30
30
  },
31
31
  "engineStrict": true,
32
32
  "repository": {
@@ -43,9 +43,9 @@
43
43
  "@magic-modules/pre": "0.0.12",
44
44
  "@magic-themes/docs": "0.0.15",
45
45
  "@magic/core": "0.0.158",
46
- "@magic/format": "0.0.73",
47
- "@magic/test": "0.3.12",
48
- "@types/node": "25.9.2",
46
+ "@magic/format": "0.0.74",
47
+ "@magic/test": "0.3.19",
48
+ "@types/node": "26.0.1",
49
49
  "typescript": "6.0.3"
50
50
  },
51
51
  "dependencies": {
package/src/exists.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { fs } from './fs.js'
2
+ import { constants } from 'node:fs'
2
3
 
3
4
  import error from '@magic/error'
4
5
  import is from '@magic/types'
@@ -16,7 +17,7 @@ export const exists = async f => {
16
17
  }
17
18
 
18
19
  try {
19
- await fs.stat(f)
20
+ await fs.access(f, constants.F_OK)
20
21
  return true
21
22
  } catch (e) {
22
23
  const err = /** @type {Error & { code: string }} */ (e)
package/src/fs.js CHANGED
@@ -1,19 +1,24 @@
1
- import fso from 'node:fs'
2
- import util from 'node:util'
3
-
4
- const readDir = fso.promises.readdir
5
- const readFile = fso.promises.readFile
6
- const rmdir = fso.promises.rmdir
1
+ import fso, { constants } from 'node:fs'
2
+ import { access } from 'node:fs/promises'
7
3
 
8
4
  export const fs = /** @type {const} */ ({
9
5
  ...fso,
10
6
  ...fso.promises,
11
- exists: util.promisify(fso.exists),
12
- readdir: readDir,
13
- readDir,
14
- readFile,
15
- readfile: readFile,
16
- rmdir,
17
- rmDir: rmdir,
7
+ exists: async (/** @type {import('node:fs').PathLike} */ f) => {
8
+ try {
9
+ await access(f, constants.F_OK)
10
+ return true
11
+ } catch {
12
+ return false
13
+ }
14
+ },
15
+ readdir: fso.promises.readdir,
16
+ readDir: fso.promises.readdir,
17
+ readFile: fso.promises.readFile,
18
+ readfile: fso.promises.readFile,
19
+ rmdir: fso.promises.rmdir,
20
+ rmDir: fso.promises.rmdir,
18
21
  watch: fso.watch,
22
+ access,
23
+ constants,
19
24
  })
@@ -6,8 +6,6 @@ import error from '@magic/error'
6
6
 
7
7
  import { fs } from './fs.js'
8
8
 
9
- import { getFilePath } from './getFilePath.js'
10
-
11
9
  const libName = '@magic/fs.getDirectories'
12
10
 
13
11
  /**
@@ -28,10 +26,17 @@ export const getDirectories = async (dir, options = {}) => {
28
26
  }
29
27
  }
30
28
 
31
- let { minDepth, maxDepth = false, depth = false, root, noRoot = false } = options
29
+ let { minDepth, maxDepth = false, depth, root, noRoot = false } = options
30
+
31
+ // Only apply depth === false logic if explicitly passed, not if undefined
32
+ const depthExplicitlyFalse = 'depth' in options && depth === false
32
33
 
33
34
  if (!is.number(maxDepth)) {
34
- maxDepth = is.number(depth) ? depth : 200_000
35
+ if (depthExplicitlyFalse) {
36
+ maxDepth = 1
37
+ } else {
38
+ maxDepth = is.number(depth) ? depth : 200_000
39
+ }
35
40
  }
36
41
 
37
42
  if (!is.number(minDepth)) {
@@ -56,68 +61,44 @@ export const getDirectories = async (dir, options = {}) => {
56
61
 
57
62
  try {
58
63
  if (is.array(dir)) {
59
- const dirs = await Promise.all(dir.map(async f => await getDirectories(f, options)))
64
+ const dirs = await Promise.all(dir.map(f => getDirectories(f, options)))
60
65
 
61
66
  return deep.flatten(...dirs).filter(a => a)
62
67
  }
63
68
 
64
- const currentDepth = dir
65
- .replace(root || process.cwd(), '')
66
- .split(path.sep)
67
- .filter(a => a).length
68
-
69
- if (currentDepth > maxDepth) {
70
- return []
71
- }
72
-
73
- const dirContent = await fs.readdir(dir)
69
+ // Use recursive readdir with file types - single syscall, no per-file stat needed
70
+ const entries = await fs.readdir(dir, { recursive: true, withFileTypes: true })
74
71
 
75
72
  /** @type {string[]} */
76
73
  const dirs = []
77
74
 
78
- await Promise.all(
79
- dirContent.map(async file => {
80
- if (!is.string(file)) {
81
- throw error(`${libName}: path was not a string: ${file}`, 'E_ARG_TYPE')
82
- }
75
+ if (!noRoot) {
76
+ // Root dir is depth 0
77
+ if (0 >= minDepth && 0 <= maxDepth) {
78
+ dirs.push(dir)
79
+ }
80
+ }
83
81
 
84
- let filePath = await getFilePath(getDirectories, dir, file, { maxDepth, minDepth, root })
85
-
86
- if (filePath) {
87
- if (!is.array(filePath)) {
88
- filePath = [filePath]
89
- }
90
-
91
- await Promise.all(
92
- filePath.map(async file => {
93
- try {
94
- const stat = await fs.stat(file)
95
- if (stat.isDirectory()) {
96
- if (is.array(filePath)) {
97
- dirs.push(...filePath)
98
- } else if (is.string(filePath)) {
99
- dirs.push(filePath)
100
- }
101
- }
102
- } catch (statErr) {
103
- // File might have been deleted between readdir and stat
104
- // Just skip it
105
- }
106
- }),
107
- )
108
- }
109
- }),
110
- )
82
+ for (const entry of entries) {
83
+ if (entry.isDirectory()) {
84
+ // Use parentPath + name for correct full path with recursive readdir
85
+ const fullPath = path.join(entry.parentPath, entry.name)
111
86
 
112
- let finalDirs = deep.flatten(dirs).filter(a => a)
113
- if (!noRoot) {
114
- finalDirs = [dir, ...finalDirs]
87
+ // Calculate depth relative to root
88
+ const relativePath = path.relative(root || dir, fullPath)
89
+ const entryDepth = relativePath.split(path.sep).filter(Boolean).length
90
+
91
+ // Filter by minDepth and maxDepth
92
+ if (entryDepth >= minDepth && entryDepth <= maxDepth) {
93
+ dirs.push(fullPath)
94
+ }
95
+ }
115
96
  }
116
97
 
117
- const finalized = finalDirs
98
+ const finalized = dirs
118
99
  .filter(a => is.string(a))
119
- .filter(dir => {
120
- const currentDepth = dir
100
+ .filter(d => {
101
+ const currentDepth = d
121
102
  .replace(root || process.cwd(), '')
122
103
  .split(path.sep)
123
104
  .filter(a => a).length
@@ -39,10 +39,17 @@ export const getFilePath = async (fn, dir, file, args = {}) => {
39
39
 
40
40
  const filePath = path.join(dir, file)
41
41
 
42
- const stat = await fs.stat(filePath)
43
- if (stat.isDirectory()) {
42
+ // Use readdir with withFileTypes - more efficient than stat
43
+ const entries = await fs.readdir(dir, { withFileTypes: true })
44
+ const entry = entries.find(e => e.name === file)
45
+
46
+ if (!entry) {
47
+ return undefined
48
+ }
49
+
50
+ if (entry.isDirectory()) {
44
51
  return await fn(filePath, args)
45
- } else if (stat.isFile()) {
52
+ } else if (entry.isFile()) {
46
53
  return filePath
47
54
  }
48
55
  }
package/src/getFiles.js CHANGED
@@ -1,10 +1,8 @@
1
1
  import path from 'node:path'
2
2
 
3
- // import deep from '@magic/deep'
4
3
  import is from '@magic/types'
5
4
  import error from '@magic/error'
6
5
 
7
- import { getFilePath } from './getFilePath.js'
8
6
  import { fs } from './fs.js'
9
7
 
10
8
  const libName = '@magic/fs.getFiles'
@@ -54,60 +52,42 @@ export const getFiles = async (dir, options = {}) => {
54
52
  root = dir
55
53
  }
56
54
 
57
- const currentDepth = dir
58
- .replace(root, '')
59
- .split(path.sep)
60
- .filter(a => a).length
61
-
62
- if (currentDepth > maxDepth) {
63
- return []
64
- }
65
-
66
55
  try {
67
- const dirContent = await fs.readdir(dir)
68
- const files = await Promise.all(
69
- dirContent.map(file => getFilePath(getFiles, dir, file, { maxDepth, minDepth, root })),
70
- )
71
-
72
- const flatFiles = files
73
- .flat(20000)
74
- .filter(a => !is.undef(a))
75
- /*
76
- * if an extension parameter has been passed,
77
- * remove the file if it does not end with extension
78
- */
79
- .filter(a => !extension || a.endsWith(extension))
80
-
81
- /*
82
- * filter nonfiles - use async stat
83
- */
84
- const fileStats = await Promise.all(
85
- flatFiles.map(async f => {
86
- try {
87
- const stat = await fs.stat(f)
88
- return { path: f, isFile: stat.isFile() }
89
- } catch {
90
- return { path: f, isFile: false }
91
- }
92
- }),
93
- )
56
+ // Use recursive readdir with file types - single syscall, no per-file stat needed
57
+ const entries = await fs.readdir(dir, { recursive: true, withFileTypes: true })
94
58
 
95
59
  return (
96
- fileStats
97
- .filter(({ isFile }) => isFile)
98
- .map(({ path }) => path)
99
- /*
100
- * filter files if depth is smaller than minDepth
101
- */
102
- .filter(file => {
103
- const currentDepth =
104
- file
105
- .replace(root ?? '', '')
106
- .split(path.sep)
107
- .filter(a => a).length - 1
108
-
109
- return currentDepth >= minDepth
60
+ entries
61
+ .filter(entry => {
62
+ // Only files
63
+ if (!entry.isFile()) {
64
+ return false
65
+ }
66
+
67
+ // Calculate depth relative to root - based on parent directory, not file name
68
+ const parentPath = path.join(entry.parentPath)
69
+ const relativePath = path.relative(root, parentPath)
70
+ const entryDepth = relativePath.split(path.sep).filter(Boolean).length
71
+
72
+ // Filter by maxDepth
73
+ if (entryDepth > maxDepth) {
74
+ return false
75
+ }
76
+
77
+ // Filter by minDepth
78
+ if (entryDepth < minDepth) {
79
+ return false
80
+ }
81
+
82
+ // Filter by extension
83
+ if (extension && !relativePath.endsWith(extension)) {
84
+ return false
85
+ }
86
+
87
+ return true
110
88
  })
89
+ // Use parentPath + name for correct full path with recursive readdir
90
+ .map(entry => path.join(entry.parentPath, entry.name))
111
91
  )
112
92
  } catch (e) {
113
93
  const err = /** @type {Error & { code?: string }} */ (e)
package/src/mkdirp.js CHANGED
@@ -1,18 +1,17 @@
1
- import path from 'path'
2
-
3
- import error from '@magic/error'
4
1
  import is from '@magic/types'
5
-
6
2
  import { fs } from './fs.js'
3
+ import error from '@magic/error'
7
4
 
8
5
  const libName = '@magic/fs.mkdirp'
9
6
 
10
7
  /**
11
8
  *
12
- * @param {string} p
13
- * @returns {Promise<boolean | void>}
9
+ * @param {import('node:fs').PathLike} p
10
+ * @param {import('node:fs').MakeDirectoryOptions} opts
11
+ *
12
+ * @returns {Promise<boolean>}
14
13
  */
15
- export const mkdirp = async p => {
14
+ export const mkdirp = async (p, opts = {}) => {
16
15
  if (is.empty(p)) {
17
16
  throw error(`${libName} expects a non-empty path string as argument.`, 'E_ARG_EMPTY')
18
17
  }
@@ -21,17 +20,8 @@ export const mkdirp = async p => {
21
20
  throw error(`${libName} expects a path string as argument, got: ${typeof p}`, 'E_ARG_TYPE')
22
21
  }
23
22
 
24
- p = path.resolve(p)
25
-
26
23
  try {
27
- const dir = path.dirname(p)
28
- let exists = await fs.exists(dir)
29
-
30
- if (!exists) {
31
- await mkdirp(dir)
32
- }
33
-
34
- await fs.mkdir(p)
24
+ await fs.mkdir(p, { ...opts, recursive: true })
35
25
  return true
36
26
  } catch (e) {
37
27
  const err = /** @type {Error & { code?: string}} */ (e)
package/src/rmrf.js CHANGED
@@ -1,4 +1,4 @@
1
- import path from 'path'
1
+ import path from 'node:path'
2
2
 
3
3
  import is from '@magic/types'
4
4
  import error from '@magic/error'
@@ -36,23 +36,10 @@ export const rmrf = async (dir, opts = {}) => {
36
36
  }
37
37
 
38
38
  try {
39
- const stat = await fs.stat(dir)
40
-
41
- if (stat.isFile()) {
42
- if (!opts.dryRun) {
43
- await fs.unlink(dir)
44
- }
45
- return true
46
- } else if (stat.isDirectory()) {
47
- const files = await fs.readdir(dir)
48
-
49
- await Promise.all(files.map(async file => await rmrf(path.join(dir, file))))
50
-
51
- if (!opts.dryRun) {
52
- await fs.rmdir(dir)
53
- }
54
- return true
39
+ if (!opts.dryRun) {
40
+ await fs.rm(dir, { recursive: true, force: true })
55
41
  }
42
+ return true
56
43
  } catch (e) {
57
44
  const err = /** @type {Error & { code?: string}} */ (e)
58
45
 
package/types/fs.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export const fs: {
2
- readonly exists: typeof fso.exists.__promisify__
2
+ readonly exists: (f: import('node:fs').PathLike) => Promise<boolean>
3
3
  readonly readdir: typeof fso.promises.readdir
4
4
  readonly readDir: typeof fso.promises.readdir
5
5
  readonly readFile: typeof fso.promises.readFile
@@ -8,6 +8,7 @@ export const fs: {
8
8
  readonly rmDir: typeof fso.promises.rmdir
9
9
  readonly watch: typeof fso.watch
10
10
  readonly access: typeof fso.promises.access
11
+ readonly constants: typeof fso.constants
11
12
  readonly copyFile: typeof fso.promises.copyFile
12
13
  readonly open: typeof fso.promises.open
13
14
  readonly rename: typeof fso.promises.rename
@@ -35,7 +36,6 @@ export const fs: {
35
36
  readonly opendir: typeof fso.promises.opendir
36
37
  readonly cp: typeof fso.promises.cp
37
38
  readonly glob: typeof fso.promises.glob
38
- readonly constants: typeof fso.constants
39
39
  readonly renameSync: typeof fso.renameSync
40
40
  readonly truncateSync: typeof fso.truncateSync
41
41
  readonly ftruncate: typeof fso.ftruncate
package/types/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export const fs: {
2
- readonly mkdirp: (p: string) => Promise<boolean | void>
2
+ readonly mkdirp: (
3
+ p: import('node:fs').PathLike,
4
+ opts?: import('node:fs').MakeDirectoryOptions,
5
+ ) => Promise<boolean>
3
6
  readonly rmrf: (
4
7
  dir: string,
5
8
  opts?: {
@@ -84,6 +87,7 @@ export const fs: {
84
87
  readonly rmDir: typeof import('node:fs/promises').rmdir
85
88
  readonly watch: typeof import('node:fs').watch
86
89
  readonly access: typeof import('node:fs/promises').access
90
+ readonly constants: typeof import('node:fs').constants
87
91
  readonly copyFile: typeof import('node:fs/promises').copyFile
88
92
  readonly open: typeof import('node:fs/promises').open
89
93
  readonly rename: typeof import('node:fs/promises').rename
@@ -111,7 +115,6 @@ export const fs: {
111
115
  readonly opendir: typeof import('node:fs/promises').opendir
112
116
  readonly cp: typeof import('node:fs/promises').cp
113
117
  readonly glob: typeof import('node:fs/promises').glob
114
- readonly constants: typeof import('node:fs').constants
115
118
  readonly renameSync: typeof import('node:fs').renameSync
116
119
  readonly truncateSync: typeof import('node:fs').truncateSync
117
120
  readonly ftruncate: typeof import('node:fs').ftruncate
package/types/mkdirp.d.ts CHANGED
@@ -1 +1,4 @@
1
- export function mkdirp(p: string): Promise<boolean | void>
1
+ export function mkdirp(
2
+ p: import('node:fs').PathLike,
3
+ opts?: import('node:fs').MakeDirectoryOptions,
4
+ ): Promise<boolean>