TypeScript/tsconfig.jsonのセマンティクス
#honey-css-modules #typescript
はじめに
honey-css-modules でも tsconfig.json を解析することになった
tsc/tsserver が tsconfig.json を解析する挙動にできるだけ合わせたい
そこでその挙動がどうなってるのか調べておきたい
include オプション
includeオプションを省略すると、"include": ["**/*"]と書いた時と同じ挙動になる
includeオプションに含まれるファイルでも、node_modules/bower_components/jspm_packages配下のファイルはコンパイル対象から除外される
https://github.com/microsoft/TypeScript/blob/caf1aee269d1660b4d2a8b555c2d602c97cb28d7/src/compiler/utilities.ts#L9506
"include": []と書くと、node_modulesなどの配下のファイルが型チェックの範囲に含まれてしまう
"include": ["src"]と書いた時や、includeを省略した時はnode_modulesは除外されるが、1つもパスがない時は含まれてしまう
TypeScript のバグ...な気がするが"include": []と書くことないのでどうでも良すぎる
ts.readJsonConfigFile
tsConfigSourceFile.parseDiagnostics: 構文エラーの情報
internal API であり、型定義上は公開されていない
mizdra.icon ts.getConfigFileParsingDiagnosticsで取れるかも?
構文エラーがあっても、パースを継続して、それ以降のトークンもパースされる
愚直に JSONC としてパースするだけで、本来 string が来るべきところに number が来ていても、そのままパースされる
tsConfigSourceFile.extendedSourceFilesにextendsにリストされたファイルのパスが入ってる
ただし、ts.readJsonConfigFileを呼び出した直後の時点では入ってない
ts.parseJsonSourceFileConfigFileContentにtsConfigSourceFileを渡した時に代入される
extendedSourceFilesは絶対パスに解決されたものが入ってる
"extends": "@tsconfig/recommended/tsconfig.json"と書かれてたらextendedSourceFiles: ["/path/to/node_modules/@tsconfig/recommended/tsconfig.json"]になる
tsConfigSourceFile.configFileSpecsにはinclude/excludeを解析した情報が入ってる
ただし、ts.readJsonConfigFileを呼び出した直後の時点では入ってない
ts.parseJsonSourceFileConfigFileContentにtsConfigSourceFileを渡した時に代入される
internal API であり、型定義上は公開されていない
code:tsconfig.json
{
"exclude": "src/test", 1,
}
code:configFileSpecs.json
{
"includeSpecs": "**/*",
"excludeSpecs": "src/test",1,
"validatedIncludeSpecs": "**/*",
"validatedExcludeSpecs": "src/test",
"validatedIncludeSpecsBeforeSubstitution": "**/*",
"validatedExcludeSpecsBeforeSubstitution": "src/test",
"isDefaultIncludeSpec": true
}
ts.parseJsonSourceFileConfigFileContent
parsedCommandLine.errors: セマンティックエラー
構文エラーは含まれない
parsedCommandLine.options: compilerOptionsに関するデータ
tsconfig.json のスキーマに一致しないものは省かれてる
無効なオプションは原則としてundefinedに fallback される
例: "module": 1=>"module": undefined
libのような配列の要素が無効な場合、配列から取り除かれる
例: "lib": ["esnext", 1]=>"lib": ["lib.esnext.d.ts"]
ただし"paths"は例外で、無効なデータが何故か含まれる
例: "paths": { "@/*": ["./*", 1], "#/*": 1 } => "paths": { "@/*": ["./*", 1], "#/*": 1 }
ts.ParsedCommandLine['options']['paths']型に一致してないので、おそらく TypeScript のバグ
code:tsconfig.json
{
"exclude": "src/test", 1,
"compilerOptions": {
"lib": "esnext", 1,
"module": 1,
"paths": {
"@/*": 1,
"#/*": 1
}
},
"hcmOptions": {
"dtsOutDir": 1,
"arbitraryExtensions": 1
}
}
code:options.json
{
"lib": "lib.esnext.d.ts",
"module": undefined,
"paths": {
"@/*": 1,
"#/*": 1
},
"pathsBasePath": "/app",
"configFilePath": "/app/tsconfig.json"
}
parsedCommandLine.rawにtsconfig.jsonの生のパース結果が入ってる
hcmOptionsとかライブラリ拡張のオプションを解析したかったら、ここを見ると良い
code:tsconfig.json
{
"exclude": "src/test", 1,
"compilerOptions": {
"lib": "esnext", 1,
"module": 1,
"paths": {
"@/*": 1,
"#/*": 1
}
},
"hcmOptions": {
"dtsOutDir": 1,
"arbitraryExtensions": 1
}
}
code:raw.json
{
"exclude": "src/test", 1,
"compilerOptions": {
"lib": "esnext", 1,
"module": 1,
"paths": { "@/*": 1, "#/*": 1 }
},
"hcmOptions": { "dtsOutDir": 1, "arbitraryExtensions": 1 }
}
エラーのある tsconfig.json の扱い
tsserver は構文エラーやセマンティックエラーがあっても、エラーがある部分を無視して動いてくれる
https://github.com/mizdra/honey-css-modules/pull/99
構文エラーやセマンティックエラーは tsc によりコンソールに、tsserver によりエディタ上で報告される
extendsに指定されたファイルが存在しない、もしくは権限不足でアクセス不能の時、単にそのファイルからの値を引き継がない挙動となる
include/exclude オプションの正規化
TypeScript ではincludeが省略されたら、"include": ["**/*"]へと正規化する処理が内部的に行われる
この正規化を行う public API はない
internal API であれば honey-css-modules/tsconfig.jsonのセマンティクス#67bbf92ac75fd30000db6950 がある
include/exclude オプションの glob の展開
内部的にはmatchFiles関数で展開される
typescriptpackageからはts.matchFilesとして export されてるが...型定義上は存在せず、internal API になってる
glob にマッチするファイルのうち、ファイルシステムに存在するものなら、ts.sys.readDirectoryで取得可能
これは public API
あるファイルパスが glob にマッチするかを判定する public API はない
internal API のmatchFilesを使うしかない
ts-morph はそうしている
API 公開に関する Issue はある: https://github.com/microsoft/TypeScript/issues/34545
extends オプションによるオプションの継承
extendsで指定されたファイルのオプションを、extends元のオプションで上書きする挙動となる
array や object のマージはされない
code:tsconfig.base.json
{
"include": "src/dir1",
"exclude": "src/test",
"compilerOptions": {
"target": "es2020",
"lib": "es2020",
"paths": {
"@/*": "./src/*",
}
}
}
code:tsconfig.json
{
"extends": "./tsconfig.base.json",
"include": "src/dir2",
"compilerOptions": {
"lib": "es2021",
"paths": {
"#/*": "./src/*",
}
}
}
このtsconfig.jsonは以下と等価になる
code:tsconfig.resolved.json
{
"include": "src/dir2",
"exclude": "src/test",
"compilerOptions": {
"target": "es2020",
"lib": "es2021",
"paths": {
"#/*": "./src/*",
}
}
}
referencesはextendsにより引き継がれない唯一の例外であると、ドキュメントで触れられている
https://www.typescriptlang.org/tsconfig/#extends
Currently, the only top-level property that is excluded from inheritance is references.
parsedCommandLine.optionsやtsConfigSourceFile.configFileSpecsに、引き継がれたあとの値が入ってる
ドキュメントでは触れられていないが、ライブラリの独自拡張のオプションは引き継がれない
vueCompilerOptionsやts-nodeなど
そのため、vue language tools は自前で extends でvueCompilerOptionsを引き継ぐロジックを実装してる
https://github.com/vuejs/language-tools/blob/b6580514e659cc1aab0676fe418ba0a5e2f0ead3/packages/language-core/lib/utils/ts.ts#L84-L91