Skip to content

perf(core): skip generated-file writes when the content is unchanged - #3824

Open
luantaraschi wants to merge 3 commits into
orval-labs:masterfrom
luantaraschi:perf/skip-identical-writes
Open

perf(core): skip generated-file writes when the content is unchanged#3824
luantaraschi wants to merge 3 commits into
orval-labs:masterfrom
luantaraschi:perf/skip-identical-writes

Conversation

@luantaraschi

@luantaraschi luantaraschi commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Addresses #3787 by extending behaviour that already exists in this repo rather than introducing a policy.

reconcileBarrel and reconcileZodBarrel in packages/orval/src/utils/barrel.ts already read the file and skip the write when the content matches, added for #3756 with the same reasoning about mtime churn. writeGeneratedFile did not, so every artifact that goes through it was rewritten on each run. That is the inconsistency the issue describes.

writeGeneratedFile now compares before writing, which covers the schema, single-mode and split-mode writers.

The comparison runs after trailing whitespace is stripped, so what is compared is the bytes that would land on disk rather than the generator's raw string. That matters: two runs can produce different source strings and identical output, and the naive comparison would miss it. One of the tests exercises exactly that case.

On the open question in the issue, default versus onlyUpdateWhenDifferent: I made it the default, since that is what the barrels already do and a second option would leave two writers in the codebase disagreeing about the same thing. A file whose content genuinely differs, including from a version stamp in its header, is still rewritten, so this does not suppress real changes. Happy to put it behind a flag instead if you would rather preserve write events for consumers who depend on them.

Three tests in packages/core/src/writers/file.test.ts, a new file, covering creation with whitespace stripping, an unchanged rewrite leaving mtime alone, and a changed rewrite still landing. The mtime test fails on the commit before this change.

packages/core goes from 2272 to 2275 tests. The full package suite passes, including the #3756 barrel idempotency tests in packages/orval that exercise repeat generation. tsc --noEmit and vp fmt --check are clean.

Summary by CodeRabbit

  • Improvements

    • Improved generated-file formatting with file-specific Prettier configuration.
    • Missing generated files are now created automatically during specification and schema generation.
    • Generated barrel and schema files are updated consistently.
  • Bug Fixes

    • Prevented unnecessary rewrites when generated content is unchanged.
    • Preserved file modification times when no content changes are needed.
    • Continued stripping trailing whitespace, including in nested files.
  • Tests

    • Added coverage for formatting, file creation, unchanged content, and content updates.

Regenerating with unchanged inputs rewrote every artifact, updating mtime
across the output directory. That wakes file watchers, triggers rebuilds
downstream and does filesystem work that produces no new bytes.

The barrel writers already compare against the file on disk before
writing (orval-labs#3756). writeGeneratedFile now does the same, which covers the
schema, single-mode and split-mode writers that go through it.

The comparison happens after trailing whitespace is stripped, so it is
the final output that is compared, not the generator's raw string. A file
whose content genuinely differs, including because of a version stamp in
its header, is still rewritten.

Refs orval-labs#3787
Copilot AI lite review requested due to automatic review settings August 7, 2026 11:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: af98a36e-85df-42a7-887e-ea60250276fe

📥 Commits

Reviewing files that changed from the base of the PR and between 5328db5 and 282ff74.

📒 Files selected for processing (3)
  • packages/core/src/writers/file.test.ts
  • packages/orval/src/formatters/prettier.test.ts
  • packages/orval/src/formatters/prettier.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/writers/file.test.ts

📝 Walkthrough

Walkthrough

writeGeneratedFile now supports scoped asynchronous transforms, normalizes content, and skips unchanged writes. Prettier and generator outputs use this writer. Tests verify formatting, missing-file handling, content updates, and modification-time preservation.

Changes

Generated file writing

Layer / File(s) Summary
Normalize, transform, and compare generated files
packages/core/src/writers/file.ts, packages/core/src/writers/file.test.ts
writeGeneratedFile applies scoped transforms, handles missing files, compares normalized content, and writes only when content differs.
Apply Prettier through generated-file transforms
packages/orval/src/formatters/*, packages/orval/src/write-specs.ts, packages/orval/src/generate-spec.test.ts
Prettier formatting uses a reusable file transform during spec generation. Tests cover per-file configuration and unchanged output.
Route generated outputs through the shared writer
packages/orval/src/utils/barrel.ts, packages/orval/src/write-specs.ts, packages/orval/src/write-zod-specs.ts, related tests
Barrel, schema index, extra-file, and Zod schema writes use writeGeneratedFile with explicit ENOENT handling. Integration tests verify unchanged outputs retain their modification times.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: melloware

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: skipping generated-file writes when content is unchanged.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@melloware melloware added the bug Something isn't working label Aug 7, 2026
@melloware

Copy link
Copy Markdown
Collaborator

@aqeelat please review!

@pkg-pr-new

pkg-pr-new Bot commented Aug 7, 2026

Copy link
Copy Markdown

Open in StackBlitz

@orval/angular

bun add https://pkg.pr.new/@orval/angular@282ff74

@orval/axios

bun add https://pkg.pr.new/@orval/axios@282ff74

@orval/core

bun add https://pkg.pr.new/@orval/core@282ff74

@orval/effect

bun add https://pkg.pr.new/@orval/effect@282ff74

@orval/fetch

bun add https://pkg.pr.new/@orval/fetch@282ff74

@orval/hono

bun add https://pkg.pr.new/@orval/hono@282ff74

@orval/mcp

bun add https://pkg.pr.new/@orval/mcp@282ff74

@orval/mock

bun add https://pkg.pr.new/@orval/mock@282ff74

orval

bun add https://pkg.pr.new/orval@282ff74

@orval/query

bun add https://pkg.pr.new/@orval/query@282ff74

@orval/solid-start

bun add https://pkg.pr.new/@orval/solid-start@282ff74

@orval/swr

bun add https://pkg.pr.new/@orval/swr@282ff74

@orval/zod

bun add https://pkg.pr.new/@orval/zod@282ff74

commit: 282ff74

@melloware melloware added this to the 8.25.0 milestone Aug 8, 2026

@aqeelat aqeelat left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The helper is the right primitive, but this is not complete enough for #3787 yet.

  1. Apply the same conditional-write behavior to every generated artifact. write-zod-specs.ts still writes directly with fs.outputFile, and write-specs.ts does the same for schema-index creation and extraFiles. Those outputs will continue to churn mtime.

  2. Replace pathExists() followed by readFile(). It adds an unnecessary syscall and has a check-then-read race if the file disappears between calls. Read once and treat ENOENT as a missing file.

  3. Replace the timing-based mtime assertion with utimes() to set a known past mtime before the no-op generation. The test should assert the invariant without relying on a delay.

  4. Add repeated-generation coverage for at least one direct Zod output and one extraFiles output.

  5. Add coverage for formatter: 'prettier'. Comparing raw generated bytes does not prevent the raw-write-then-format-write cycle when the final formatted bytes are unchanged. Please handle the no-hook Prettier path in memory before comparison, while preserving the existing write → hook → formatter ordering when hooks are configured.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/core/src/writers/file.test.ts (1)

42-59: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Cover normalization after the transform.

This test verifies transformed-content comparison, but format does not add trailing whitespace. Add trailing spaces in the transformed output, then assert that the stored content is normalized and the second generation preserves mtime.

Suggested regression coverage
     const format = async (_filePath: string, content: string) =>
-      content.replaceAll("'", '"');
+      content.replace(";\n", ";   \n").replaceAll("'", '"');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/writers/file.test.ts` around lines 42 - 59, Update the
“compares transformed content before writing” test so its format transform adds
trailing spaces to the transformed output, then assert the file stores the
normalized content without those spaces and that the second generation still
preserves the original mtime.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/orval/src/formatters/prettier.ts`:
- Around line 19-30: Update the formatter returned by the exported formatter
function to remove the directory-based configs cache and call
prettier.resolveConfig(filePath) once for each transformed file, preserving the
existing file-specific filepath: filePath argument passed to prettier.format.

---

Nitpick comments:
In `@packages/core/src/writers/file.test.ts`:
- Around line 42-59: Update the “compares transformed content before writing”
test so its format transform adds trailing spaces to the transformed output,
then assert the file stores the normalized content without those spaces and that
the second generation still preserves the original mtime.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a00641e2-aca2-4cac-8409-592f1f7c69ec

📥 Commits

Reviewing files that changed from the base of the PR and between 9a4986b and 5328db5.

📒 Files selected for processing (10)
  • packages/core/src/writers/file.test.ts
  • packages/core/src/writers/file.ts
  • packages/orval/src/formatters/prettier.test.ts
  • packages/orval/src/formatters/prettier.ts
  • packages/orval/src/generate-spec.test.ts
  • packages/orval/src/utils/barrel.ts
  • packages/orval/src/write-specs.test.ts
  • packages/orval/src/write-specs.ts
  • packages/orval/src/write-zod-specs.test.ts
  • packages/orval/src/write-zod-specs.ts

Comment on lines +19 to +30
const configs = new Map<
string,
Awaited<ReturnType<typeof prettier.resolveConfig>>
>();

return async (filePath, content) => {
try {
const directory = path.dirname(filePath);
let config = configs.get(directory);
if (!configs.has(directory)) {
config = await prettier.resolveConfig(filePath);
configs.set(directory, config);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate file =="
fd -a 'prettier\.ts$' . | sed 's#^\./##' || true

echo "== relevant lines =="
file=$(fd 'prettier\.ts$' . | head -n1)
if [ -n "$file" ]; then
  cat -n "$file" | sed -n '1,120p'
fi

echo "== search usage of createPrettierFileTransform =="
rg -n "createPrettierFileTransform|configs = new Map|resolveConfig\\(filePath\\)" -S .

Repository: orval-labs/orval

Length of output: 5069


🌐 Web query:

Prettier CLI API resolveConfig file path overrides returns different config per file

💡 Result:

When using the Prettier CLI API, the prettier.resolveConfig(filePath) function is designed to resolve configuration on a per-file basis [1][2]. It works by starting at the directory of the provided filePath and searching up the file tree until a configuration file is found [1][3]. If you observe different configurations being returned for different files, this is the expected behavior when those files are located in different directory structures or have their own local configuration files (such as .prettierrc) [3][4]. To manage per-file configuration overrides effectively: 1. Use the overrides property in your configuration file: This allows you to specify different settings for certain file extensions, folders, or specific files within a single configuration file [3][5]. Because resolveConfig is file-aware, it will evaluate these overrides based on the filePath you pass to it [1][5]. 2. Verify the resolved configuration: If you need to debug which configuration is being applied, you can programmatically inspect the result of await prettier.resolveConfig(filePath) for different files in your script [1][2]. 3. Explicit configuration: If you do not want Prettier to search the file tree, you can bypass the automatic resolution for specific files by passing the path of a specific configuration file to the options.config property when calling resolveConfig [1][2]. The resolveConfig function automatically handles the application of overrides by checking the file path against the files patterns defined in your configuration [3][5]. It is not a bug that it returns different results for different files; rather, it is how Prettier ensures that project-specific or file-specific rules are correctly respected based on their location [4].

Citations:


🌐 Web query:

site:prettier.io/docs/api resolveConfig config cache per file documentation

💡 Result:

The prettier.resolveConfig function in the Prettier API resolves the configuration for a specific file by searching from the file's directory upwards through the directory structure [1]. By default, Prettier caches configuration files and plugins to improve performance [1]. Regarding cache management: 1. Per-file Caching: Prettier internally caches configuration results. You can disable this behavior by setting the useCache option to false in the options object passed to resolveConfig [1]. 2. Manual Cache Clearing: Because configuration and plugin loading is cached, you may need to manually clear the cache if the file system structure changes (such as during editor integrations). The prettier.clearConfigCache function is provided for this purpose to flush the existing configuration cache [1]. For more details on these functions and options, you can refer to the official Prettier API documentation [1].

Citations:


🌐 Web query:

Prettier v3 prettier.resolveConfig accepts options config resolveConfigCache false documentation

💡 Result:

In Prettier v3, the correct option to bypass caching when using prettier.resolveConfig is useCache, not resolveConfigCache [1][2]. By setting options.useCache to false, all configuration caching will be bypassed [1][3]. The documentation for prettier.resolveConfig(fileUrlOrPath [, options]) explicitly states: "If options.useCache is false, all caching will be bypassed" [1][2]. Note that there is also an options.resolveConfig property (which defaults to true), but this is used to enable or disable the searching for a configuration file entirely, rather than controlling the cache [1][4]. If set to false, Prettier will not search for a configuration file at all, which can be useful if the function is only being used to check if a file is ignored [1][2].

Citations:


Resolve Prettier config by file path instead of directory.

resolveConfig(filePath) can return different options for different files in the same directory when using overrides, but the directory key makes later files reuse the first file’s resolved config. Call prettier.resolveConfig(filePath) once per transformed file and keep filepath: filePath in prettier.format; Prettier caches config files internally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/orval/src/formatters/prettier.ts` around lines 19 - 30, Update the
formatter returned by the exported formatter function to remove the
directory-based configs cache and call prettier.resolveConfig(filePath) once for
each transformed file, preserving the existing file-specific filepath: filePath
argument passed to prettier.format.

@melloware
melloware requested a review from aqeelat August 10, 2026 11:53
@luantaraschi

Copy link
Copy Markdown
Contributor Author

Thanks for going through it properly, the list was clear enough to work from. All five are in, in 5328db5 and 282ff74.

  1. Conditional writes now cover every generated artifact. writeGeneratedFile is the only write path left in the writers, so the direct Zod outputs, the schema index creation and extraFiles all go through it. No fs.outputFile left in write-specs.ts or write-zod-specs.ts.

  2. pathExists() is gone. The helper reads once and treats ENOENT as a missing file, so there is no extra syscall and no window between the check and the read.

  3. The mtime assertions use fs.utimes() to set a known past mtime before the no-op run, so nothing depends on a delay.

  4. Added repeated-generation coverage for a direct Zod output (does not rewrite unchanged direct zod output in write-zod-specs.test.ts) and for an extraFiles output in write-specs.test.ts.

  5. For formatter: 'prettier' without hooks, the content is formatted in memory before the comparison, so the raw write followed by a formatted write no longer happens. When hooks are configured the existing write → hook → formatter order is kept. generate-spec.test.ts covers it with keeps mtimes when prettier produces the same final files.

Locally the five touched files run 73 tests green. Full packages/core and packages/orval is green except resolve-version.test.ts, which fails the same three tests on master here, so it looks unrelated to this branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Avoid rewriting generated files when their final content is unchanged

4 participants