Skip to content

fix: load dorval.config.* under TS 7, and stop emitting dynamic? - #11

Open
barney-ws wants to merge 3 commits into
mainfrom
fix/config-loader
Open

barney-ws wants to merge 3 commits into
mainfrom
fix/config-loader

Conversation

@barney-ws

@barney-ws barney-ws commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Problem

Two separate things stop dorval generate from reading its own config file. Both reproduce in a plain install (npm i dorval@0.10.8 typescript@7) with the dorval.config.ts the README documents:

explicit --config : FAILED -> TypeScript Error in ./dorval.config.ts:
                              typescript.findConfigFile is not a function
auto-discovered   : FAILED -> No configuration file found

The file is never searched for. searchPlaces lists orval.config.* and .orvalrc* — no dorval.config.* at any extension, and no .mjs either. The README says:

Using config file (auto-detects dorval.config.ts/js/mjs/cjs in current directory)

so a config only ever loaded when its path was passed explicitly with --config.

A .ts config cannot be loaded under TypeScript 7. cosmiconfig's built-in TypeScript loader calls into the typescript package:

// cosmiconfig/dist/loaders.js
const filePath = typescript.findConfigFile(directory, ...);
content = typescript.transpileModule(content, config).outputText;

TypeScript 7 moved the compiler to a native binary. Its main entry resolves to lib/version.cjs:

$ node -e "const ts=require('typescript'); console.log(ts.version, Object.keys(ts))"
7.0.2 [ 'version', 'versionMajorMinor' ]

findConfigFile, transpileModule, sys, ScriptTarget are all undefined, so that loader cannot work at all. Upgrading cosmiconfig does not help — v9 loads TypeScript the same way. typescript@7.0.2 is what npm i -D typescript installs today, so this is not an edge case for long.

It reproduces only in a real install: run it from a checkout and require('typescript') resolves to the repo's own TypeScript 5, which works fine.

Change

searchPlaces leads with the documented dorval.config.{ts,mjs,js,cjs} and .dorvalrc* names, keeping the orval.config.* ones behind them so existing configs keep working. The cosmiconfig module name moves from orval to dorval — that only affects which package.json key would be read, and the README does not document configuring through package.json.

For .ts files, the loader tries cosmiconfig's TypeScript loader first, so nothing changes on a runtime where it works, and falls back to Node's own stripTypeScriptTypes when it throws. The stripped module is written next to the config — so its relative imports still resolve — imported, and removed.

The explorer moves from cosmiconfigSync to the async API, which an async loader requires and which loadConfig was already shaped for.

What this does not fix

Node's stripTypeScriptTypes landed in 22.13, and this package supports >=20.8.1. On Node 20 with TypeScript 7 there is no transpiler left, and a .ts config genuinely cannot be loaded. What changes there is the message — instead of a bare typescript.findConfigFile is not a function, it says what happened and what to do:

Could not load ./dorval.config.ts.
The installed typescript package could not transpile it: typescript.findConfigFile is not a function
TypeScript 7 moved the compiler to a native binary and its JavaScript API is gone,
and this Node (v20.x.x) has no built-in type stripping to fall back on.
Use Node 22.13 or newer, install typescript 5.x, or write the config as .mjs, .js or .json.

So: TypeScript 7 works on Node 22.13+; on Node 20 it degrades to an actionable error. The search-places half of this PR is not affected by any of that.

Tests

packages/dorval: 9 tests pass — loading a .ts config from an explicit path, finding it by search under its documented name, and the fallback path with a primary loader that throws exactly what TypeScript 7 throws — asserting the config loads and no temp file is left behind where Node can strip types, and that the error names the way out where it cannot (CI covers the Node 20 branch). The two new behaviours fail without the fix. Lint clean. Monorepo turbo run test: 4/4 tasks pass.

Verified in a real install (npm i dorval@<local pack> typescript@…), which is the only shape where the TypeScript resolution matters:

before after
TypeScript 7, --config findConfigFile is not a function loads
TypeScript 7, auto-discovery No configuration file found loads
TypeScript 5, --config loads loads
TypeScript 5, auto-discovery No configuration file found loads

Notes

stripTypeScriptTypes is marked experimental in Node and prints a warning on the versions that have it. It is only reached when the typescript package has already failed, so the warning appears exactly where the alternative was a hard error.

CI runs Node 20 only, so the loading path is exercised locally and the error path in CI. Adding 22.x to the matrix would cover both, but that is a change to the workflow rather than to this fix.

Reported alongside the $ref alias problem in #10; that one is in @dorval/core and unrelated to this.


Second fix: dynamic?

Carried on the same branch because it surfaced in the same regeneration.

Problem

dynamic already admits null. A ? on it is redundant, and dart analyze says so:

warning • The '?' is unnecessary because 'dynamic' is nullable without it
        • lib/src/models/stc_clock_in_request_dto.f.dart:29:12
        • unnecessary_question_mark

A property lands on dynamic through several ordinary specs, all of them present in a real nestjs-zod spec:

"clockOut":       { "not": {} },                     // the never type
"publishedShift": { "type": "null", "default": null },
"payload":        {},                                 // no type at all
"unionField":     { "oneOf": [ ... ] }                // too complex to map

Thirteen places in @dorval/core append ? to a type. Three guarded against dynamicTypeMapper.mapTypeWithNullability, ReferenceResolver.getModelType, and one branch of resolvePropertyType. The other ten did not, and the one the model path actually runs through is among them:

// getters/object.ts
if (nullable && !dartType.endsWith('?') && !isOneOfNullable) {
  dartType += '?';        // dartType === 'dynamic' gets a ? here
}

So the guards that existed were the ones not on the path that mattered.

Change

One helper, and every site goes through it:

static toNullable(type: string): string {
  if (type === 'dynamic' || type.endsWith('?')) {
    return type;
  }
  return `${type}?`;
}

`${type}?` now appears exactly once in packages/core/src, inside that helper. The endsWith('?') idempotence check was already duplicated at seven call sites and comes along for free.

The guard is exact equality, not a substring test: List<dynamic>? is a legitimate type and stays nullable.

Tests

models-dynamic-nullable.test.ts, 5 cases: the helper's three behaviours, then a generated model asserting not: {}, type: null and an untyped property all come out bare while String? and List<String>? keep their ?. The generator case fails without the fix.

models-oneof-nullable.test.ts asserted dynamic? unionField — the old output — and is updated to dynamic unionField plus a blanket not.toContain('dynamic?').

packages/core: 455 tests pass. Lint unchanged at its 3 pre-existing errors, none in a touched file. tsc --noEmit unchanged at its 5 pre-existing errors.

End-to-end on a 165-schema production spec, same config both runs:

dynamic? in generated output
0.10.8 4 — clockOut ×2, publishedShift, draftShift
this branch 0

Two things kept `dorval generate` from reading its own config file.

searchPlaces listed orval.config.* and .orvalrc* only, so the dorval.config.ts
the README tells you to write was never found - a config only loaded when its
path was passed with --config. The documented names go first now, with the
orval-style ones kept behind them.

Loading a .ts config then failed outright under TypeScript 7:

    TypeScript Error in ./dorval.config.ts:
    typescript.findConfigFile is not a function

cosmiconfig's TypeScript loader drives the typescript package, and 7.x moved
the compiler to a native binary - its main entry exports `version` and
`versionMajorMinor`, nothing else. That loader still runs first, so nothing
changes where it works; when it throws, Node's own type stripping takes over.

Also switches the explorer to the async API, which the loader needs and which
loadConfig was already shaped for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@barney-ws barney-ws added the bug Something isn't working label Aug 19, 2026
Node's type stripping arrived in 22.13, and this package supports >=20.8.1.
On an older Node with TypeScript 7 there is nothing left to transpile a .ts
config with, so report that plainly along with the ways out instead of letting
`typescript.findConfigFile is not a function` through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@barney-ws

Copy link
Copy Markdown
Collaborator Author

@qwlong take a look when you have time. No hurry, it's not urgent.

`dynamic` already admits null, so `dynamic?` is redundant and the Dart
analyzer reports it as `unnecessary_question_mark`.

Thirteen places appended `?` to a type; three of them guarded against
`dynamic`, the rest did not, and the model path (`getObject`) was among
the unguarded ones. Every site now goes through `TypeMapper.toNullable`,
which is also the only place left that knows how to spell the `?`.

A property reaches `dynamic` through several ordinary specs: `not: {}`
(the never type), `type: 'null'`, a schema with no `type` at all, and a
`oneOf`/`anyOf` too complex to map.
@barney-ws barney-ws changed the title fix(dorval): find dorval.config.* and load TypeScript configs under TS 7 fix: load dorval.config.* under TS 7, and stop emitting dynamic? Aug 24, 2026
@barney-ws
barney-ws requested a review from qwlong August 24, 2026 07:38
@barney-ws

Copy link
Copy Markdown
Collaborator Author

@qwlong @claude Please review this PR when you have time, thanks!

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.

1 participant