気になったので調べてみました。
tsconfig.jsonと普通のJSONの大きな違い
tsconfig.jsonには、コメントが書けます。
tsc --init
した時に生成されるtsconfig.jsonに、大量にコメントが付けられているので、すぐに気付くことと思います。
例)
{ "compilerOptions": { "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, /* Strict Type-Checking Options */ "strict": true /* Enable all strict type-checking options. */, "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */ } }
これについて深く考えたことも無く、どこかでこれは JSON5 だと言う噂を見ていた*1のでそうだと思っていました。
ところが、試してみたところJSON5なら使えるはずの豊富な文法が全然使えなかったり、JSONとJSON5にかけていたformatOnSaveの設定が効かなかったので、違うということに気付きました。
tsconfig.jsonにPrettierかからんなーと思ったけど、そもそもtsconfig.jsonはJSONじゃなかった事を思い出した。
— syumai (@__syumai) 2020年3月29日
tsconfig.jsonの正体は一体何なのか
結論から入りますが、どうやらこれは JSON with Comments (JSONC) と言うフォーマットのようです。
少なくとも、VS Codeはtsconfig.jsonをJSON with Commentsとして解釈しています。
右下に注目ください。
自動的にJSON with Commentsの編集モードになっていることが確認出来ると思います。
JSON with Commentsとは
VS Codeのドキュメントでは、JSON with Commentsについて下記のように説明されています。
JSON with Comments
In addition to the default JSON mode following the JSON specification, VS Code also has a JSON with Comments (jsonc) mode. This mode is used for the VS Code configuration files such as settings.json, tasks.json, or launch.json. When in the JSON with Comments mode, you can use single line (//) as well as block comments (/* */) as used in JavaScript.
https://code.visualstudio.com/docs/languages/json#_json-with-comments
どうやら、VS Codeの設定ファイルを記述するために使われているフォーマットのようです。
VS Codeは、内部で node-jsonc-parser
と言うpackageを利用してJSON with Commentsを解析しているようでした。
node-jsonc-parserの使い方
node-jsonc-parserは、 parse
関数を提供しており、JSON with Commentsを下記のように簡単にObjectとして読み取ることが出来ます。
import { parse } from "jsonc-parser"; import { promises as fsPromises } from "fs"; (async () => { const buf = await fsPromises.readFile("./data.jsonc"); const data = parse(buf.toString()); console.log(data); // => 結果のObjectが出力される })();
parse関数は、ParseOptionsを提供しており、ここに下記のように disallowComments
が含まれています。どうやら、VS Codeは通常のJSONもこのparserのOptionを使い分け解析しているようでした。*2
/** * Parses the given text and returns the object the JSON content represents. On invalid input, the parser tries to be as fault tolerant as possible, but still return a result. * Therefore, always check the errors list to find out if the input was valid. */ export declare const parse: (text: string, errors?: ParseError[], options?: ParseOptions) => any;
export interface ParseOptions { disallowComments?: boolean; allowTrailingComma?: boolean; allowEmptyContent?: boolean; }
ここに allowTrailingComma
が含まれていることからおわかりいただけると思いますが、 なんと JSONC
ではtrailing commaが使えます。 これもJSONとの大きな違いですね。
allowEmptyContent
の使い方については調べられていません。
TypeScript本体はどのようにしてtsconfig.jsonをパースしているのか
では、TypeScript本体もこの node-jsonc-parser
を利用しているのかと言うと、リポジトリ内にこのpackageへの依存が見付からず、そうではないようでした。
見つかったのは下記の parseJsonText
と言う関数で、これを使ってJSONを解析しているようでした。
createSourceFile
に渡しているオプションを見る限り、TypeScriptはJSONをES2015のJavaScriptとして解析しているように見えました。
export function parseJsonText(fileName: string, sourceText: string, languageVersion: ScriptTarget = ScriptTarget.ES2015, syntaxCursor?: IncrementalParser.SyntaxCursor, setParentNodes?: boolean): JsonSourceFile { initializeState(sourceText, languageVersion, syntaxCursor, ScriptKind.JSON); // Set source file so that errors will be reported with this file name sourceFile = createSourceFile(fileName, ScriptTarget.ES2015, ScriptKind.JSON, /*isDeclaration*/ false); sourceFile.flags = contextFlags;
ここに続くのは、TypeScriptがJSONとして受け付けられるトークンを分岐するswitch文で、受け付けていたのは 配列、boolean、null、Number (正負)、文字列
で、それらのどれにも当てはまらなかったらObjectとして解析する。と言う風に見受けられました。
switch (token()) { case SyntaxKind.OpenBracketToken: statement.expression = parseArrayLiteralExpression(); break; case SyntaxKind.TrueKeyword: case SyntaxKind.FalseKeyword: case SyntaxKind.NullKeyword: statement.expression = parseTokenNode<BooleanLiteral | NullLiteral>(); break; case SyntaxKind.MinusToken: if (lookAhead(() => nextToken() === SyntaxKind.NumericLiteral && nextToken() !== SyntaxKind.ColonToken)) { statement.expression = parsePrefixUnaryExpression() as JsonMinusNumericLiteral; } else { statement.expression = parseObjectLiteralExpression(); } break; case SyntaxKind.NumericLiteral: case SyntaxKind.StringLiteral: if (lookAhead(() => nextToken() !== SyntaxKind.ColonToken)) { statement.expression = parseLiteralNode() as StringLiteral | NumericLiteral; break; } // falls through default: statement.expression = parseObjectLiteralExpression(); break; }
ここから想像したのは、 TypeScriptはES2015のObjectの一部文法を制限してJSONCを表現している というものでしたが、やはりコメントとtrailing comma以外の要素は含めることが出来ず、追いかけきれなかったので断念しました。
誰か知ってたら教えてください。
調べた感じ、tsconfig.jsonと紐付ける形でJSON with Commentsに触れている情報ソースはかなり少なく、TypeScript本体も特に言及していないので情報収集がとても困難でした。
JSON with Commentsと言う規格について
この規格は、どうも標準化されていない気がします。
JSONCで検索して見つかるのは、Goで実装された野良Parserのようなものばかりでした。
node-jsonc-parserが代表的な実装なのかなと思っています。
見つかったParserとか
- GoのJSONCパーサー GitHub - muhammadmuzzammil1998/jsonc: JSON with comments for Go!
- 3年前とかに作られたGoのJSONCパーサー GitHub - komkom/jsonc: json config.
- Rust製のCode formatterのJSONCプラグイン(これはTSで書かれてるっぽい) dprint/packages/dprint-plugin-jsonc at master · dsherret/dprint · GitHub
下記の記事を見る限り、Windows Terminalの設定ファイルとかでも使われているようです。
tsconfig.jsonや、VS Codeのsettings.jsonなど、ここまで広く使われているもののフォーマットがこうフワッとしているように感じられるのは、何だか面白いなあと思った話でした。
*1:.babelrcはJSON5らしい https://babeljs.io/docs/en/config-files#supported-file-extensions
*2:VS Codeのリポジトリ内に、JSON Language Serverがjsonc-parserを利用していることについて記述があります https://github.com/microsoft/vscode/blob/1a55cd072acf651a321cdd7d94d324c186eb5af7/extensions/json-language-features/server/README.md#participate