TypeScript Compiler APIでASTからコード生成しFormatter CLIを作る

はじめに
こんにちは。サイバーエージェントのソフトウェアエンジニアの平井柊太(@did0es)です。
前回は、TypeScriptで書かれたソースコードのASTの解析と、これを応用したLinterの開発を行いました。
今回は、ASTからTypeScriptのソースコードを生成します。この生成の仕組みを利用し、前回開発したLinterをFormatterとして使えるようにします。
プロジェクトをセットアップする
前回と同様に、プロジェクトをセットアップします。Node.jsは「v23.10.0」を使います。build-your-own-ast-tools-by-typescript
ディレクトリの中に、以下で新たな作業用ディレクトリ(004-generate-code
ディレクトリ)を作成します。
mkdir -p build-your-own-ast-tools-by-typescript/004-generate-code cd build-your-own-ast-tools-by-typescript/004-generate-code
今回は手順を簡単にするため、GitHubリポジトリにある basic-project ディレクトリにあるファイル一式を、作業用ディレクトリにコピーして使用します。以下の構造になります。
004-generate-code ├── console.ts ├── node.config.json ├── package-lock.json ├── package.json └── tsconfig.json
環境構築の詳細は、前回の「セットアップ」の項を参照ください。
ASTからTypeScriptのソースコードを生成してみる
今まではTypeScriptのソースコードからASTを生成し、内容を解析してきました。今回はその逆で、ASTからソースコードを生成するような仕組みを実装します。
TypeScript Compiler APIを用いてASTからソースコードを生成する
TypeScript Compiler APIには、createPrinter
というASTからソースコードの文字列を生成する機能があります。この機能を用いて、簡単なソースコードを生成してみましょう。
作業用ディレクトリにtsPrinterSample.ts
ファイルを作成し、以下を記述します。
import * as ts from 'typescript'; // ts.factory にある関数を使用して AST を作成します const f = ts.factory; // ASTの種類に対応したcreate関数を呼び出します const ast = f.createVariableStatement( undefined, f.createVariableDeclarationList( [ f.createVariableDeclaration( 'greeting', undefined, f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), f.createStringLiteral('Hello, World!') ), ], ts.NodeFlags.Const ) ); // ASTをコンソールに出力するためのプリンターを作成します const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); const source = ts.createSourceFile('sample.ts', '', ts.ScriptTarget.Latest); const code = printer.printNode(ts.EmitHint.Unspecified, ast, source); console.log(code);
ターミナルを開き、npm run dev -- ./tsPrinterSample.ts
を実行します。
$ npm run dev -- ./tsPrinterSample.ts > basic-project@1.0.0 dev > node --experimental-default-config-file ./tsPrinterSample.ts const greeting: string = "Hello, World!";
create関数で構築したASTを元にconst greeting: string = "Hello, World!";
が出力されます。
ASTからソースコードを生成するCLIを実装する
前項の応用として、入力されたデータからASTを構築し、ソースコードを出力するCLIを実装します。
作業用ディレクトリの中に__mocks__
ディレクトリを作成し、その中にinput.json
ファイルを作成します。このファイルをCLIの入力として使います。
004-generate-code └── __mocks__ └── input.json
__mocks__
ディレクトリの内容はnpmに公開する際に含めないように、tsconfig.json
を開いてexcludes
フィールドに__mocks__
ディレクトリを追記します。
{ // ~~~ "exclude": ["node_modules", "dist", "__mocks__"] // node_modules、出力先のdistフォルダ、__mocks__フォルダは対象外にする }
__mocks__
ディレクトリに追加したinput.json
ファイルを開き、以下を記述します。
{ "kind": "SourceFile", "children": [ { "kind": "FirstStatement", "children": [ { "kind": "VariableDeclarationList", "children": [ { "kind": "VariableDeclaration", "children": [ { "kind": "Identifier", "text": "greeting", "children": [] }, { "kind": "StringKeyword", "text": "string", "children": [] }, { "kind": "StringLiteral", "text": "\"Hello, World!\"", "children": [] } ] } ] } ] }, { "kind": "EndOfFileToken", "children": [] } ] }
このJSONはconst greeting: string = "Hello, World!";
のASTの構造をJSON形式で再現したものです。
作業用ディレクトリの中にgenerateCodeCli.ts
ファイルを作成します。このファイルでは、JSON形式のASTを入力としてTypeScriptのソースコードを出力するCLIを実装します。
作成したgenerateCodeCli.ts
ファイルを開き、以下を記述します。
#!/usr/bin/env node // Node.jsのモジュールをインポート import fs from 'node:fs/promises'; import path from 'node:path'; // カスタムコンソールモジュールをインポート import { debug, error, info } from './console.ts'; import { createSourceFromAst } from './createSourceFromAst.ts'; console.log('Welcome to the Generate Code CLI!'); const helpMessage = 'Usage: basic-cli <file-path> [--out <output-path>] [--debug]'; // コマンドの引数を取得(0番目はnodeの実行パス、1番目はスクリプトのパスなので省く) const args = process.argv.slice(2); // 引数が不足している場合はヘルプメッセージを表示 if (args.length === 0) { info(helpMessage); process.exit(1); } let [inputFilePath, ...restArgs] = args; // コマンドの1つ目の引数に、入力ファイルパスが指定されていない場合はエラーにする if (!inputFilePath || inputFilePath.startsWith('--')) { error('Input file path is required'); info(helpMessage); process.exit(1); } let outputFilePath = ''; for (let i = 0; i < restArgs.length; i++) { const arg = restArgs[i]; if (arg === '--out') { // --outオプションが指定された場合、次の引数を出力ファイルパスとして取得する // 次の引数がない、または次の引数がオプション形式(--で始まる)ならエラーにする if (i + 1 >= args.length || restArgs[i + 1].startsWith('--')) { error('--out requires a file path'); process.exit(1); } outputFilePath = restArgs[++i]; } else if (arg === '--debug') { // --debugオプションが指定された場合、デバッグモードを有効にする process.env.DEBUG = 'true'; } else { error(`Unknown argument: ${arg}`); process.exit(1); } } // 入力ファイルを読み込む try { const absoluteInputPath = path.resolve(inputFilePath); const data = (await fs.readFile(absoluteInputPath)).toString(); debug(`Input file path: ${absoluteInputPath}`); debug(`Input file content:\n${data}`); const ast = JSON.parse(data); const source = createSourceFromAst(ast); if (outputFilePath) { // 出力ファイルパスが指定されている場合、内容を逆順にしてファイルに書き込む const absoluteOutputPath = path.resolve(outputFilePath); await fs.writeFile(absoluteOutputPath, source); debug(`Output written to: ${absoluteOutputPath}`); } else { // 出力ファイルパスが指定されていない場合、コンソールに出力する info(`Source:\n${source}`); } } catch (err) { if (err instanceof Error) { error(err.message); } else { error('An unknown error occurred'); } process.exit(1); }
第2回で実装したBasic CLIをベースに受け取ったJSONをcreateSourceFromAst
関数に渡して実行し、結果を出力するCLIになっています。
次に、createSourceFromAst
関数を実装します。作業用ディレクトリの中にcreateSourceFromAst.ts
ファイルを作成し、以下を記述します。
import * as ts from 'typescript'; import { debug } from './console.ts'; export type AstNode = { kind: string; text: string; children: AstNode[]; }; const f = ts.factory; export function createSourceFromAst(astNode: AstNode): string { const tsNode = buildTsNodeFromAst(astNode); if (tsNode === null) { return ''; } const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); if (ts.isSourceFile(tsNode)) { return printer.printFile(tsNode); } const source = ts.createSourceFile( 'generated.ts', '', ts.ScriptTarget.Latest ); return printer.printNode(ts.EmitHint.Unspecified, tsNode, source); } function buildTsNodeFromAst(astNode: AstNode, depth = 0): ts.Node | null { const indent = ' '.repeat(depth); debug(`${indent}Kind: ${astNode.kind}, Text: ${astNode.text}`); switch (astNode.kind) { // SourceFileノードを生成します case 'SourceFile': { const statements: ts.Statement[] = []; for (const child of astNode.children) { if (child.kind === 'FirstStatement') { // FirstStatement が複数ある場合、すべての子ノードを処理します const inner = child.children .map((c) => buildTsNodeFromAst(c, depth + 2)) .filter((n): n is ts.Statement => n !== null); statements.push(...inner); } else { // それ以外の子ノードは個別に処理します const node = buildTsNodeFromAst(child, depth + 1); if (node && ts.isStatement(node)) { statements.push(node); } } } return f.createSourceFile( statements, f.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None ); } // 変数宣言のリストからVariableStatementノードを生成します case 'VariableDeclarationList': { const letHead = /^(\s*)let\b/; const useLet = letHead.test(astNode.text); const varDeclaration = astNode.children .map((child) => buildTsNodeFromAst(child, depth + 1)) .filter((node): node is ts.VariableDeclaration => node !== null); const varDeclarationList = f.createVariableDeclarationList( varDeclaration, useLet ? ts.NodeFlags.Let : ts.NodeFlags.Const ); return f.createVariableStatement(undefined, varDeclarationList); } // 変数宣言の場合、VariableDeclarationノードを生成します case 'VariableDeclaration': { const [identifierNode, typeNode, initializerNode] = astNode.children; return f.createVariableDeclaration( identifierNode.text, undefined, toTsTypeNode(typeNode), toTsPrimitiveNode(initializerNode) ); } // 識別子の場合、Identifierノードを生成します case 'Identifier': return f.createIdentifier(astNode.text); // 型の場合、TypeNodeノードを生成します case 'StringKeyword': return toTsTypeNode(astNode); //Primitiveな値の場合、Literalノードを生成します case 'StringLiteral': return toTsPrimitiveNode(astNode); // ファイルの末尾の改行またはサポートされていないノードの場合、nullを返します case 'EndOfFileToken': default: return null; } } function toTsTypeNode(node: AstNode): ts.TypeNode { switch (node.kind) { case 'StringKeyword': return f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword); default: throw new Error(`Unsupported type: ${node.kind}`); } } function toTsPrimitiveNode(node: AstNode): ts.Expression { switch (node.kind) { case 'StringLiteral': const isSingleQuote = node.text.startsWith("'") && node.text.endsWith("'"); return f.createStringLiteral(node.text.slice(1, -1), isSingleQuote); default: throw new Error(`Unsupported literal: ${node.kind}`); } }
このファイルではinput.json
に存在するASTのkind
ごとに、対応するTypeScriptのノードを生成するbuildTsNodeFromAst
関数を実装しています。また、これを呼び出してソースコードを生成する関数として、createSourceFromAst
関数を実装しています。
では、generateCodeCli.ts
ファイルを実行してみましょう。ターミナルを開き、以下のコマンドを実行します。
$ npm run dev -- ./generateCodeCli.ts ./__mocks__/input.json > basic-project@1.0.0 dev > node --experimental-default-config-file ./generateCodeCli.ts ./__mocks__/input.json Welcome to the Generate Code CLI! [INFO] Source: const greeting: string = "Hello, World!";
JSON形式のASTを入力として、ソースコードを出力するCLIが完成しました。
ソースコードをFormatするCLIを実装する
前回実装したLinter CLIと、今回実装したASTからソースコードを生成するCLIを応用して、Formatter CLIを実装しましょう。Formatter CLIでは、以下のフローで処理を行います。
入力されたソースコードをASTに変換し、Linterと同様に検査します。ルールに違反している箇所のうち自動修正が可能なものは修正し、ASTからソースコードに変換します。
以上の処理を実装に落とし込んでみましょう。作業用ディレクトリの中にconfig.json
ファイル、formatterCli.ts
ファイル、createAst.ts ファイルを作成して以下を記述します。前回のLinter CLIの実装を一部改変しています。
config.json
ファイル:
{ "use-let-never-reassigned": "error", "double-quotes": "error" }
formatterCli.ts
ファイル:
#!/usr/bin/env node // Node.jsのモジュールをインポートします import fs from 'node:fs/promises'; import path from 'node:path'; // カスタムコンソールモジュールをインポートします import { debug, error, info } from './console.ts'; import { createAst } from './createAst.ts'; import { createSourceFromAst } from './createSourceFromAst.ts'; import { fixAst } from './fixAst.ts'; // configの型を定義します export type Config = { 'use-let-never-reassigned': 'error' | 'warn' | 'off'; 'double-quotes': 'error' | 'warn' | 'off'; }; console.log('Welcome to the Formatter CLI!'); const helpMessage = 'Usage: formatter-cli <file-path> [--out <output-path>] [--debug]'; // コマンドの引数を取得(0番目はnodeの実行パス、1番目はスクリプトのパスなので省きます) const args = process.argv.slice(2); // 引数が不足している場合はヘルプメッセージを表示します if (args.length === 0) { info(helpMessage); process.exit(1); } let [inputFilePath, ...restArgs] = args; // コマンドの1つ目の引数に、入力ファイルパスが指定されていない場合はエラーにします if (!inputFilePath || inputFilePath.startsWith('--')) { error('Input file path is required'); info(helpMessage); process.exit(1); } let outputFilePath = ''; for (let i = 0; i < restArgs.length; i++) { const arg = restArgs[i]; if (arg === '--out') { // --outオプションが指定された場合、次の引数を出力ファイルパスとして取得します // 次の引数がない、または次の引数がオプション形式(--で始まる)ならエラーにします if (i + 1 >= args.length || restArgs[i + 1].startsWith('--')) { error('--out requires a file path'); process.exit(1); } outputFilePath = restArgs[++i]; } else if (arg === '--debug') { // --debugオプションが指定された場合、デバッグモードを有効にします process.env.DEBUG = 'true'; } else { error(`Unknown argument: ${arg}`); process.exit(1); } } // コマンドが実行されたディレクトリにconfigファイルがない場合はエラーにします const configFilePath = path.resolve('config.json'); const isConfigFileExists = await fs .access(configFilePath) .then(() => true) .catch(() => false); if (!isConfigFileExists) { error('Config file not found: config.json'); process.exit(1); } // 入力ファイルを読み込む try { const absoluteInputPath = path.resolve(inputFilePath); const data = (await fs.readFile(absoluteInputPath)).toString(); debug(`Input file path: ${absoluteInputPath}`); debug(`Input file content:\n${data}`); // configファイルを読み込みます const config: Config = JSON.parse( (await fs.readFile(configFilePath)).toString() ); debug(`Config file content:\n${JSON.stringify(config, null, 2)}`); const ast = await createAst(absoluteInputPath); // AST を走査してルールを元に修正します const fixedAst = fixAst(ast, 0, config, ast); // AST をソースコードに変換し、ファイルに書き込みます const source = createSourceFromAst(fixedAst); if (outputFilePath) { const absoluteOutputPath = path.resolve(outputFilePath); await fs.writeFile(absoluteOutputPath, source); debug(`Output written to: ${absoluteOutputPath}`); } else { // 出力ファイルパスが指定されていない場合、コンソールに出力します info(`Formatted source:\n${source}`); } } catch (err) { if (err instanceof Error) { error(err.message); } else { error('An unknown error occurred'); } process.exit(1); }
createAst.ts ファイル:
import { readFile } from 'fs/promises'; import ts from 'typescript'; export type AstNode = { kind: string; text: string; children: AstNode[]; }; function toAstNode(node: ts.Node) { const result: AstNode = { kind: ts.SyntaxKind[node.kind], text: node.getText(), children: [], }; ts.forEachChild(node, (child) => { result.children.push(toAstNode(child)); }); return result; } export async function createAst(fileName: string) { const sourceCode = await readFile(fileName, 'utf8'); const sourceFile = ts.createSourceFile( fileName, sourceCode, ts.ScriptTarget.Latest, true ); return toAstNode(sourceFile); }
作業用ディレクトリ内の__mocks__
フォルダにinvalidInput.ts
ファイル、validInput1.ts
ファイル、validInput2.ts
ファイルを追加し、それぞれ以下を記述します。
invalidInput.ts
ファイル:
let message: string = "This method has fixable errors."; console.log(message);
validInput1.ts
ファイル:
const message: string = 'This method has fixable errors.'; console.log(message);
validInput2.ts
ファイル:
let message: string = 'This method has fixable errors.'; message = 'Reassigned'; console.log(message);
前項で作成したcreateSourceFromAst.ts
ファイルを開き、以下のように改変します。この改変で、先ほど追加した__mocks__
以下の3つの入力向けTypeScriptファイルのASTをソースコードに変換できます。
import * as ts from 'typescript'; import { debug } from './console.ts'; export type AstNode = { kind: string; text: string; children: AstNode[]; }; const f = ts.factory; export function createSourceFromAst(astNode: AstNode): string { const tsNode = buildTsNodeFromAst(astNode); if (tsNode === null) { return ''; } const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); if (ts.isSourceFile(tsNode)) { return printer.printFile(tsNode); } const source = ts.createSourceFile( 'generated.ts', '', ts.ScriptTarget.Latest ); return printer.printNode(ts.EmitHint.Unspecified, tsNode, source); } function buildTsNodeFromAst(astNode: AstNode, depth = 0): ts.Node | null { const indent = ' '.repeat(depth); debug(`${indent}Kind: ${astNode.kind}, Text: ${astNode.text}`); switch (astNode.kind) { // SourceFileノードを生成します case 'SourceFile': { const statements: ts.Statement[] = []; for (const child of astNode.children) { if (child.kind === 'FirstStatement') { // FirstStatement が複数ある場合、すべての子ノードを処理します const inner = child.children .map((c) => buildTsNodeFromAst(c, depth + 2)) .filter((n): n is ts.Statement => n !== null); statements.push(...inner); } else { // それ以外の子ノードは個別に処理します const node = buildTsNodeFromAst(child, depth + 1); if (node && ts.isStatement(node)) { statements.push(node); } } } return f.createSourceFile( statements, f.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None ); } // 変数宣言のリストからVariableStatementノードを生成します case 'VariableDeclarationList': { const letHead = /^(\s*)let\b/; const useLet = letHead.test(astNode.text); const varDeclaration = astNode.children .map((child) => buildTsNodeFromAst(child, depth + 1)) .filter((node): node is ts.VariableDeclaration => node !== null); const varDeclarationList = f.createVariableDeclarationList( varDeclaration, useLet ? ts.NodeFlags.Let : ts.NodeFlags.Const ); return f.createVariableStatement(undefined, varDeclarationList); } // 変数宣言の場合、VariableDeclarationノードを生成します case 'VariableDeclaration': { const [identifierNode, typeNode, initializerNode] = astNode.children; return f.createVariableDeclaration( identifierNode.text, undefined, toTsTypeNode(typeNode), toTsPrimitiveNode(initializerNode) ); } // 識別子の場合、Identifierノードを生成します case 'Identifier': return f.createIdentifier(astNode.text); // 型の場合、TypeNodeノードを生成します case 'StringKeyword': return toTsTypeNode(astNode); //Primitiveな値の場合、Literalノードを生成します case 'StringLiteral': return toTsPrimitiveNode(astNode); // 式文の場合、ExpressionStatementノードを生成します case 'ExpressionStatement': { const expr = buildTsNodeFromAst(astNode.children[0], depth + 1); return expr ? f.createExpressionStatement(expr as ts.Expression) : null; } // 代入式の場合、BinaryExpressionノードを生成します case 'BinaryExpression': { const [leftNode, _, rightNode] = astNode.children; const left = buildTsNodeFromAst(leftNode, depth + 1) as ts.Expression; const right = buildTsNodeFromAst(rightNode, depth + 1) as ts.Expression; return f.createBinaryExpression( left, f.createToken(ts.SyntaxKind.EqualsToken), right ); } // 関数呼び出しの場合、CallExpressionノードを生成します case 'CallExpression': { const [calleeNode, argNode] = astNode.children; const callee = buildTsNodeFromAst(calleeNode, depth + 1) as ts.Expression; const arg = buildTsNodeFromAst(argNode, depth + 1) as ts.Expression; return f.createCallExpression(callee, undefined, [arg]); } // プロパティへのアクセスの場合、PropertyAccessExpressionノードを生成します case 'PropertyAccessExpression': { const [objNode, propNode] = astNode.children; const obj = buildTsNodeFromAst(objNode, depth + 1) as ts.Expression; const prop = buildTsNodeFromAst(propNode, depth + 1) as ts.Identifier; return f.createPropertyAccessExpression(obj, prop); } // BinaryExpressionで生成するため、FirstAssignmentは無視します case 'FirstAssignment': // ファイルの末尾の改行またはサポートされていないノードの場合、nullを返します case 'EndOfFileToken': default: return null; } } function toTsTypeNode(node: AstNode): ts.TypeNode { switch (node.kind) { case 'StringKeyword': return f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword); default: throw new Error(`Unsupported type: ${node.kind}`); } } function toTsPrimitiveNode(node: AstNode): ts.Expression { switch (node.kind) { case 'StringLiteral': const isSingleQuote = node.text.startsWith("'") && node.text.endsWith("'"); return f.createStringLiteral(node.text.slice(1, -1), isSingleQuote); default: throw new Error(`Unsupported literal: ${node.kind}`); } }
作業用ディレクトリにfixAst.ts
ファイルを作成し、以下を記述します。Formatter CLIのコアの実装です。
import type { AstNode } from './createAst.ts'; import type { Config } from './formatterCli.ts'; import { debug, info } from './console.ts'; export function fixAst(astNode: AstNode, depth = 0, config: Config, root: AstNode): AstNode { const indent = ' '.repeat(depth); debug( `${indent}Kind: ${astNode.kind}, Text: ${astNode.text.replaceAll('\n', ' ')}` ); // configのuse-let-never-reassignedが有効な場合、ルールを適用してASTを修正します if (config['use-let-never-reassigned'] !== 'off') { const letHead = /^(\s*)let\b/; if ( astNode.kind === 'VariableDeclarationList' && letHead.test(astNode.text) ) { // 宣言に含まれる識別子名を抽出します const identifiers = astNode.children .filter((c) => c.kind === 'VariableDeclaration') .map( (v) => v.children.find((n) => n.kind === 'Identifier')?.text || '__dummy__' ); // 後続で再代入が無ければ const に置換します if (isNeverReassigned(root, astNode, identifiers)) { astNode.text = astNode.text.replace(letHead, '$1const'); info('[use-let-never-reassigned] letをconstに置換しました'); } } } // configのdouble-quotesが有効な場合、ルールを適用してASTを修正します if (config['double-quotes'] !== 'off') { if (astNode.kind === 'StringLiteral') { const text = astNode.text.trim(); if (text.startsWith('"') && text.endsWith('"')) { // 本体を取り出し、シングルクォートをエスケープします const inner = text.slice(1, -1).replace(/'/g, "\\'"); astNode.text = `'${inner}'`; info('[double-quotes] ダブルクオートをシングルクオートに修正しました'); } } } for (const child of astNode.children) { // 子ノードを再帰的に修正します fixAst(child, depth + 1, config, root); } return astNode; } /** * let → const に変換して良いか判定するユーティリティ関数です */ function isNeverReassigned( root: AstNode, declNode: AstNode, identifiers: string[] ): boolean { /** 変数宣言ノード自身は検索対象から除外したいので引数で受け取り比較します */ const walk = (node: AstNode): boolean => { if (node === declNode) return false; // 宣言部分は無視します // `Identifier = ...` という BinaryExpression があれば再代入とみなします if ( node.kind === 'BinaryExpression' && node.children.length >= 1 && node.children[0].kind === 'Identifier' && identifiers.includes(node.children[0].text) ) { return true; } return node.children.some(walk); }; return !walk(root); }
ターミナルを開き、__mocks__
ディレクトリのinvalidInput.ts
ファイルがFormatされるか確かめましょう。
$ npm run dev -- ./formatterCli.ts ./__mocks__/invalidInput.ts > basic-project@1.0.0 dev > node --experimental-default-config-file ./formatterCli.ts ./__mocks__/invalidInput.ts Welcome to the Formatter CLI! [INFO] [use-let-never-reassigned] letをconstに置換しました [INFO] [double-quotes] ダブルクオートをシングルクオートに修正しました [INFO] Formatted source: const message: string = 'This method has fixable errors.'; console.log(message);
ダブルクオートがシングルクオートに、再代入されていないletによる変数宣言がconstに書き換わっています。
では、引き続きターミナルで__mocks__
ディレクトリのvalidInput1.ts
ファイルとvalidInput2.ts
ファイルがFormatされないか確かめましょう。
validInput1.ts
ファイルを入力した結果:
$ npm run dev -- ./formatterCli.ts ./__mocks__/validInput1.ts > basic-project@1.0.0 dev > node --experimental-default-config-file ./formatterCli.ts ./__mocks__/validInput1.ts Welcome to the Formatter CLI! [INFO] Formatted source: const message: string = 'This method has fixable errors.'; console.log(message);
validInput2.ts
ファイルを入力した結果:
$ npm run dev -- ./formatterCli.ts ./__mocks__/validInput2.ts > basic-project@1.0.0 dev > node --experimental-default-config-file ./formatterCli.ts ./__mocks__/validInput2.ts Welcome to the Formatter CLI! [INFO] Formatted source: let message: string = 'This method has fixable errors.'; message = 'Reassigned'; console.log(message);
どちらの入力ファイルもルールに違反していないので、Formatは行われません。
これでFormatter CLIが完成しました。CLIを公開する場合は、第2回のnpmへの公開手順を参照ください。
以上のコードはGitHubのリポジトリに掲載しています。併せてご覧ください。
次回は、最終回として今まで実装してきた内容の振り返りと、現場で広く用いられているLinterやFormatterのソースコードの一部をリーディングし、どういった処理になっているのかを概観します。
連載バックナンバー
Think ITメルマガ会員登録受付中
全文検索エンジンによるおすすめ記事
- ASTの解析とCLIの実装によるTypeScript製Lintツール開発
- TypeScript×Node.jsでCLIを開発してnpmで公開する基本手順を学ぶ
- ASTの基礎知識とTypeScriptの環境構築から始めるCLIツール開発
- 「Ace」を使って「TAURI」で「テキストエディタ」アプリを作ろう
- 「K8sGPT」の未来と生成AIを用いたKubernetes運用の最前線
- ECMAScript
- ES2015で導入された、より洗練された構文 Part 1
- ES2015が備えるモダンな非同期処理
- 「WebGPU」でシェーダーを使って三角形を1つだけ描画してみよう
- 「Flutter」のプロジェクト構造と状態管理でアプリ開発を標準化する