@nifrajs/content 4.0.2 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/fs.ts +21 -15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nifrajs/content",
3
- "version": "4.0.2",
3
+ "version": "5.0.0",
4
4
  "description": "Typed, schema-validated content collections for nifra - Markdown + frontmatter, framework-agnostic.",
5
5
  "keywords": [
6
6
  "bun",
package/src/fs.ts CHANGED
@@ -3,7 +3,8 @@
3
3
  * gives you a typed, validated API over them. Build/server-time (Bun/Node/Deno); for the edge, bake the
4
4
  * entries at build (the manifest approach `@nifrajs/web/fs` uses for routes) - tracked as a follow-up.
5
5
  */
6
- import { readdirSync, readFileSync, statSync } from "node:fs"
6
+ import type { Stats } from "node:fs"
7
+ import { readdir, readFile, stat as statPath } from "node:fs/promises"
7
8
  import { resolve, sep } from "node:path"
8
9
  import { type Entry, type InferSchema, parseEntry, type StandardSchemaV1 } from "./index.ts"
9
10
 
@@ -21,7 +22,8 @@ export interface Collection<Frontmatter> {
21
22
  /** Every entry in the directory (unordered - sort/filter in your loader). Throws if any file's
22
23
  * frontmatter is invalid. */
23
24
  all(): Promise<Array<Entry<Frontmatter>>>
24
- /** One entry by slug (filename without extension), or `null` if not found. */
25
+ /** One entry by slug (filename without extension), or `null` if not found. Throws, as `all()`
26
+ * does, if the file's frontmatter is invalid. */
25
27
  get(slug: string): Promise<Entry<Frontmatter> | null>
26
28
  }
27
29
 
@@ -87,7 +89,7 @@ export function defineCollection<S extends StandardSchemaV1>(
87
89
  const cached = fileCache.get(path)
88
90
  if (cached?.stamp === stamp) return cached.entry
89
91
  const entry = await parseEntry({
90
- raw: readFileSync(path, "utf8"),
92
+ raw: await readFile(path, "utf8"),
91
93
  slug,
92
94
  schema: config.schema,
93
95
  })
@@ -95,19 +97,21 @@ export function defineCollection<S extends StandardSchemaV1>(
95
97
  return entry
96
98
  }
97
99
 
98
- const listFiles = (): FileRecord[] =>
99
- readdirSync(root)
100
- .filter(matches)
101
- .sort()
102
- .map((file) => {
100
+ // Async throughout: a collection read on a long-lived server must not block its event loop.
101
+ const listFiles = async (): Promise<FileRecord[]> => {
102
+ const files = (await readdir(root)).filter(matches).sort()
103
+ return Promise.all(
104
+ files.map(async (file) => {
103
105
  const path = resolve(root, file)
104
- const stat = statSync(path)
106
+ const stat = await statPath(path)
105
107
  return { file, path, stamp: `${stat.mtimeMs}:${stat.size}` }
106
- })
108
+ }),
109
+ )
110
+ }
107
111
 
108
112
  return {
109
113
  async all() {
110
- const files = listFiles()
114
+ const files = await listFiles()
111
115
  const fingerprint = files.map((file) => `${file.file}\0${file.stamp}`).join("\0")
112
116
  if (collectionCache?.fingerprint === fingerprint) return [...collectionCache.entries]
113
117
 
@@ -126,13 +130,15 @@ export function defineCollection<S extends StandardSchemaV1>(
126
130
  for (const ext of extensions) {
127
131
  const file = resolve(root, `${slug}${ext}`)
128
132
  if (!insideRoot(root, file)) return null
133
+ let stat: Stats
129
134
  try {
130
- const stat = statSync(file)
131
- if (!stat.isFile()) continue
132
- return await readEntry(file, slug, `${stat.mtimeMs}:${stat.size}`)
135
+ stat = await statPath(file)
133
136
  } catch {
134
- // Not this extension or unreadable; try the next configured extension.
137
+ continue // Not this extension; try the next configured one.
135
138
  }
139
+ if (!stat.isFile()) continue
140
+ // Outside the try: a file that exists but fails to parse is an error, not a missing entry.
141
+ return await readEntry(file, slug, `${stat.mtimeMs}:${stat.size}`)
136
142
  }
137
143
  return null
138
144
  },