refactor(node): normalize line endings at the file read boundary

Adds readTextFile/readTextFileSync (CRLF/CR -> LF) next to the raw
retrying readFile and switches the markdown-source readers to them
(content loader, dynamic route templates, local search, includes).
Raw reads stay in place where bytes must be preserved (serve, init
scaffolding).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
unmerged-prs-triage
Divyansh Singh 1 month ago
parent b303dd341d
commit 6a4a977ce5

@ -0,0 +1,34 @@
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { readFile, readTextFile, readTextFileSync } from 'node/utils/fs'
describe('node/utils/fs', () => {
let root: string
beforeEach(async () => {
root = await mkdtemp(path.join(tmpdir(), 'vitepress-fs-'))
})
afterEach(async () => {
await rm(root, { recursive: true, force: true })
})
test('readFile keeps line endings as is', async () => {
const file = path.join(root, 'crlf.txt')
await writeFile(file, 'a\r\nb\rc\nd')
expect(await readFile(file)).toBe('a\r\nb\rc\nd')
})
test('readTextFile normalizes CRLF and CR to LF', async () => {
const file = path.join(root, 'crlf.txt')
await writeFile(file, 'a\r\nb\rc\nd')
expect(await readTextFile(file)).toBe('a\nb\nc\nd')
})
test('readTextFileSync normalizes CRLF and CR to LF', async () => {
const file = path.join(root, 'crlf.txt')
await writeFile(file, 'a\r\nb\rc\nd')
expect(readTextFileSync(file)).toBe('a\nb\nc\nd')
})
})

@ -9,7 +9,7 @@ import {
mergeMarkdownLocales mergeMarkdownLocales
} from './markdown/markdown' } from './markdown/markdown'
import type { Awaitable, MarkdownEnv } from './shared' import type { Awaitable, MarkdownEnv } from './shared'
import { readFile } from './utils/fs' import { readTextFile } from './utils/fs'
import { glob, normalizeGlob, type GlobOptions } from './utils/glob' import { glob, normalizeGlob, type GlobOptions } from './utils/glob'
export interface ContentOptions<T = ContentData[]> { export interface ContentOptions<T = ContentData[]> {
@ -122,7 +122,7 @@ export function createContentLoader<T = ContentData[]>(
if (cached && timestamp === cached.timestamp) return cached.data if (cached && timestamp === cached.timestamp) return cached.data
const src = await readFile(file) const src = await readTextFile(file)
const renderExcerpt = options.excerpt const renderExcerpt = options.excerpt
const { data: frontmatter, excerpt } = matter( const { data: frontmatter, excerpt } = matter(

@ -12,7 +12,7 @@ import {
} from 'vite' } from 'vite'
import type { Awaitable } from '../shared' import type { Awaitable } from '../shared'
import { type SiteConfig, type UserConfig } from '../siteConfig' import { type SiteConfig, type UserConfig } from '../siteConfig'
import { readFile } from '../utils/fs' import { readTextFile } from '../utils/fs'
import { glob, normalizeGlob, type GlobOptions } from '../utils/glob' import { glob, normalizeGlob, type GlobOptions } from '../utils/glob'
import { ModuleGraph } from '../utils/moduleGraph' import { ModuleGraph } from '../utils/moduleGraph'
import { resolveRewrites } from './rewritesPlugin' import { resolveRewrites } from './rewritesPlugin'
@ -158,7 +158,7 @@ export const dynamicRoutesPlugin = async (
moduleGraph.add(id, [routeFile]) moduleGraph.add(id, [routeFile])
moduleGraph.add(routeFile, [matched.loaderPath]) moduleGraph.add(routeFile, [matched.loaderPath])
let baseContent = await readFile(routeFile) let baseContent = await readTextFile(routeFile)
// inject raw content // inject raw content
// this is intended for integration with CMS // this is intended for integration with CMS

@ -7,7 +7,7 @@ import type { SiteConfig } from '../config'
import type { DefaultTheme } from '../defaultTheme' import type { DefaultTheme } from '../defaultTheme'
import { createMarkdownRenderer } from '../markdown/markdown' import { createMarkdownRenderer } from '../markdown/markdown'
import { getLocaleForPath, slash, type MarkdownEnv } from '../shared' import { getLocaleForPath, slash, type MarkdownEnv } from '../shared'
import { readFile } from '../utils/fs' import { readTextFile } from '../utils/fs'
import { processIncludes } from '../utils/processIncludes' import { processIncludes } from '../utils/processIncludes'
const debug = createDebug('vitepress:local-search') const debug = createDebug('vitepress:local-search')
@ -55,7 +55,7 @@ export async function localSearchPlugin(
const { srcDir, cleanUrls = false } = siteConfig const { srcDir, cleanUrls = false } = siteConfig
const relativePath = slash(path.relative(srcDir, file)) const relativePath = slash(path.relative(srcDir, file))
const env: MarkdownEnv = { path: file, relativePath, cleanUrls } const env: MarkdownEnv = { path: file, relativePath, cleanUrls }
const raw = await readFile(file).catch((e) => { const raw = await readTextFile(file).catch((e) => {
if (e.code === 'ENOENT') { if (e.code === 'ENOENT') {
debug(`File not found: ${file}`) debug(`File not found: ${file}`)
return '' return ''

@ -1,7 +1,9 @@
import fs from 'node:fs'
import { readFile as fsReadFile } from 'node:fs/promises' import { readFile as fsReadFile } from 'node:fs/promises'
import { setTimeout } from 'node:timers/promises' import { setTimeout } from 'node:timers/promises'
const retryCodes = new Set(['EMFILE', 'ENFILE']) const retryCodes = new Set(['EMFILE', 'ENFILE'])
const newlineRE = /\r\n?/g
/** /**
* Reads a file as utf8, retrying with backoff when the process is * Reads a file as utf8, retrying with backoff when the process is
@ -18,3 +20,17 @@ export async function readFile(file: string): Promise<string> {
} }
} }
} }
/**
* Reads a text file like `readFile`, with line endings normalized to `\n`.
*/
export async function readTextFile(file: string): Promise<string> {
return (await readFile(file)).replace(newlineRE, '\n')
}
/**
* Synchronous `readTextFile`, for use inside synchronous markdown-it rules.
*/
export function readTextFileSync(file: string): string {
return fs.readFileSync(file, 'utf8').replace(newlineRE, '\n')
}

@ -3,7 +3,7 @@ import { replaceAsync, type MarkdownItAsync } from 'markdown-it-async'
import path from 'node:path' import path from 'node:path'
import { findRegion } from '../markdown/plugins/snippet' import { findRegion } from '../markdown/plugins/snippet'
import { slash, type MarkdownEnv } from '../shared' import { slash, type MarkdownEnv } from '../shared'
import { readFile } from './fs' import { readTextFile } from './fs'
export function processIncludes( export function processIncludes(
md: MarkdownItAsync, md: MarkdownItAsync,
@ -41,7 +41,7 @@ export function processIncludes(
// chain are cycles, the same file may still be included by siblings // chain are cycles, the same file may still be included by siblings
if (includePath === file || ancestors.includes(includePath)) return m if (includePath === file || ancestors.includes(includePath)) return m
let content = await readFile(includePath) let content = await readTextFile(includePath)
if (region) { if (region) {
const [regionName] = region const [regionName] = region

Loading…
Cancel
Save