feat: add optional temp-file materialization for markdown code blocks #595
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds an opt-in capability for the Markdown processor to materialize fenced code blocks as real temporary files on disk, in addition to the existing virtual child file behavior. It introduces a small runtime configuration API to control this behavior and documents how to integrate it, especially for TypeScript typed-linting scenarios. The default behavior remains unchanged for all existing users.
Motivation
Typed linting via
@typescript-eslint/parserwithproject/projectServicerequires real files on disk and cannot operate on ESLint’s virtual child files (e.g.file.md/0.ts). Today,@eslint/markdownonly provides in-memory virtual children, which limits typed linting for Markdown and.mdcexamples. This change introduces an optional materialization mode so that consumers who need typed linting can map Markdown code blocks to stable.tsfiles on disk (e.g. under.eslint-markdown-temp) while keeping the default behavior unchanged for everyone else.This PR is designed to be used together with a small core change in
eslint/eslint(seehttps://github.com/eslint/eslint/pull/20375).@eslint/markdown, this PR adds an opt‑in mode that materializes fenced code blocks as real temp files on disk and returns them to ESLint as children with both a virtualfilenameand a realphysicalFilename.eslint/eslint, the companion PR teaches the processor pipeline and the JS language to:physicalFilenamefrom processors for each child file,VFile.physicalPath, andparserOptions.filePathto the parser while still using the virtual childfilenamefor reporting.Together, these changes allow tools like
@typescript-eslint/parser(withproject/projectService) to typecheck Markdown/MDX/.mdccode blocks via the materialized temp files, while keeping the existing virtual child behavior fully intact for all current users.Type of Change
Detailed Changes
src/processor.js:
MarkdownProcessorOptionstypedef with two optional fields:materializeCodeBlocks?: boolean– whentrue, fenced code blocks are written to temp files on disk as well as returned as virtual children.tempDir?: string– optional base directory for materialized files.processorOptionsobject to hold runtime options, defaulting to:materializeCodeBlocks: falsetempDir: undefinedgetMaterializeBaseDir()to resolve the base directory for materialized files, using eithertempDirorpath.join(os.tmpdir(), "eslint-markdown").getMaterializedFilePath(markdownFilename, index, virtualFilename)to compute a deterministic on-disk path of the form<baseDir>/<sanitizedMarkdownPath>/<index>_<virtualFilename>.materializeCodeBlock(markdownFilename, index, virtualFilename, text)to mkdir recursively and write the block contents to disk.preprocess()to:virtualFilenameas before (includingfilename="..."meta support).materializeCodeBlock(...)whenprocessorOptions.materializeCodeBlocksistrue.{ filename, text, physicalFilename? }, wherephysicalFilenameis only present when materialization is enabled; this preserves full backwards compatibility for existing consumers.src/index.js:
export { setMarkdownProcessorOptions }(alongside the existing default plugin export).eslint.config.*before running ESLint:import markdown, { setMarkdownProcessorOptions } from "@eslint/markdown";.Runtime configuration API (in src/processor.js):
export function setMarkdownProcessorOptions(options = {}):options.materializeCodeBlocksis a boolean (orundefined) and updatesprocessorOptions.materializeCodeBlockswhen provided.options.tempDiris either a string orundefinedand setsprocessorOptions.tempDiraccordingly.Errors when invalid values are passed, to fail fast on misconfiguration.docs/processors/markdown.md:
setMarkdownProcessorOptions:materializeCodeBlocks: true.tempDir: ".eslint-markdown-temp"for a project-local temp directory.tempDiris not provided, the base directory defaults to a subdirectory of the OS temp directory (os.tmpdir()/eslint-markdown)..eslint-markdown-temp), adding it explicitly totsconfiginclude/fileswhen necessary, and ignoring it in version control (e.g..gitignore).C:/Users/<user>/AppData/Local/Temp/eslint-markdown/**) intsconfig, calling out their brittleness across machines, users, and CI environments.README.md:
eslint.config.jsexample using:import markdown, { setMarkdownProcessorOptions } from "@eslint/markdown";setMarkdownProcessorOptions({ materializeCodeBlocks: true, tempDir: ".eslint-markdown-temp" });files: ["**/*.md"].Build artifacts (dist/esm/):**
dist/esm/processor.js,dist/esm/index.js, and related type declarations via the existingnpm run buildpipeline so that published artifacts reflect the new behavior and exports.Tests & Linting:
npm test) passes with the new behavior.npm run lint) to confirm there are no new lint errors introduced by these changes (only pre-existing fixture warnings remain).Testing & Verification
npm testexecuted successfully after the changes.setMarkdownProcessorOptions({ materializeCodeBlocks: true, tempDir: ".eslint-markdown-temp" })in a consumer project’seslint.config.*..tsfiles are created under.eslint-markdown-temp/<sanitized-markdown-path>/<index>_<virtualFilename>.ts.file.md/0.ts,file.mdc/0_0.ts, etc.) continue to be produced as before..eslint-markdown-temp/**/*.tstotsconfigincludeand verify that a TypeScript project service can resolve types for the materialized code blocks.Breaking Changes (if any)
N/A – this feature is fully opt-in:
materializeCodeBlocksdefaults tofalse.physicalFilenamefield is optional and only present when materialization is enabled.