Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
デッドコード撲滅のためにエンドポイントの棚卸し機能を作った話 〜ESLintカスタムルールとt...
Search
Taiga KATARAO
June 04, 2024
Technology
360
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
デッドコード撲滅のためにエンドポイントの棚卸し機能を作った話 〜ESLintカスタムルールとtypescript-estree利用のすすめ〜
Taiga KATARAO
June 04, 2024
More Decks by Taiga KATARAO
See All by Taiga KATARAO
CloudbaseのTypeScript事情~【TypeScript特集】1時間で6社と出会う!採用ピッチイベント~
tarao1006
0
22
Other Decks in Technology
See All in Technology
初海外で注意すべきこと12選!!!安全快適なre:invent生活にするために
yuuuuuuu168
1
180
スキマバイトという雇用形態は存在しないので、雇用期間1日のアルバイト契約を事業者とワーカーが結ぶ手続きを支えるのがタイミーなんです
akitoshiga
0
210
データエンジニアの次の戦場は "プロセスエンジニアリング"
sansantech
PRO
1
100
10年後、Engineering 行為を剥奪されるとして、わたしたちは、どう、何を創ればいいだろうか。 / If Engineering Is Taken Away in 10 Years, How and What Should We Create?
bitkey
PRO
1
200
WasmをRustにする
sansantech
PRO
0
280
Tofu-Controller lightning talk @ Containerday 2026
gionn
0
120
今さら理解するウェブフォントver.2026
takanorip
3
910
Browser Spec: Testing Hotwire with Ruby
takahashim
1
130
Conditional PED-ANOVA: Hyperparameter Importance in Hierarchical & Dynamic Search Spaces
pfn
PRO
0
140
フィンテック養成勉強会#68
finengine
0
150
NDIでApple Vision Proにリアルタイム映像送信
jyouryuusui
0
130
OptunaHub: ブラックボックス最適化のためのプラットフォーム
pfn
PRO
0
140
Featured
See All Featured
HU Berlin: Industrial-Strength Natural Language Processing with spaCy and Prodigy
inesmontani
PRO
0
760
Refactoring Trust on Your Teams (GOTO; Chicago 2020)
rmw
35
3.8k
Designing for humans not robots
tammielis
254
26k
Avoiding the “Bad Training, Faster” Trap in the Age of AI
tmiket
0
270
Keith and Marios Guide to Fast Websites
keithpitt
413
23k
Fight the Zombie Pattern Library - RWD Summit 2016
marcelosomers
234
18k
The #1 spot is gone: here's how to win anyway
tamaranovitovic
4
1.2k
Creating an realtime collaboration tool: Agile Flush - .NET Oxford
marcduiker
35
2.6k
Evolution of real-time – Irina Nazarova, EuRuKo, 2024
irinanazarova
9
1.6k
Breaking role norms: Why Content Design is so much more than writing copy - Taylor Woolridge
uxyall
1
450
Writing Fast Ruby
sferik
630
63k
Visual Storytelling: How to be a Superhuman Communicator
reverentgeek
2
710
Transcript
デッドコード撲滅のためにエンドポ イントの棚卸し機能を作った話 〜ESLintカスタムルールとtypescript-estree利用の すすめ〜 Cloudbase株式会社 tarao (Taiga Katarao)
© 2024 Cloudbase Inc. tarao (Taiga Katarao) Cloudbase株式会社 ソフトウェアエンジニy x
バックエンド〜Webフロントエンドまで TypeScriptで開6 x より興味があるのはWebフロントエンド @tarao1006 @tarao1006
© 2024 Cloudbase Inc. 話すこ と F ESLintのカスタムルールの一R F 実装方法を逐一解説することはしなB
F 全貌はZennに投稿済y F エッセンス (& 時間が余れば記事投稿後の取り組みの紹介) https://zenn.dev/cloudbase/articles/list-endpoints
Cloudbaseの技術スタック © 2024 Cloudbase Inc. RDB API Server GraphDB Data
Loader Storage スキャナー お客様のクラウド環境 Web Frontend お客様
© 2024 Cloudbase Inc. 素朴にExpressとSWRを使用し ていた T Expressで素朴にルーティング T SWRで素朴にデータフェッチ
T useFetchはuseSWRの薄いwrappe T 今日はClient Componentの話 T エッセンスはServer Componentにも展開可能なはず const = => ... ( , ) { }; app. ( , handler); handler get req res " " /v1/foo/:fooId const = => const = ... () { { } < >( fooId ); }; Component useFetch ResponseType data `/v1/foo/${ }`
© 2024 Cloudbase Inc. 呼び出されていないエンドポイン トあり ませんか ? w 「v2を追加した時にv1を消し忘れた」とか「そのエンドポイン
トを使用しているページがなくなったr w 新任者の無駄なキャッチアップコストになるなど、未使用エンド ポイントはないに越したことはな w Expressのルーティングは変数ではないため、未使用変数として 検出するといったことはできな w 「消し忘れないように注意する」は解決策にならない
© 2024 Cloudbase Inc. 解決を試みる I 定義済みエンドポイント集合から使用エンドポイント集合を引け ば、未使用エンドポイント集合が分かるはず GitHub Actionsで怒られるようになった
使用エンドポイント 未使用エンドポイント
© 2024 Cloudbase Inc. 定義済みエンドポイン トの列挙 8 割愛 t 8
Expressのルーターをゴニョゴニョすることで実現可d 8 Zenn: 状態5: バックエンドでエンドポイントの棚卸しを自動化
© 2024 Cloudbase Inc. 使用エンドポイン トの列挙 v 難儀 q v
APIサーバーと違ってルーターのようなものは存在しなG v ESLint & @typescript-eslint/typescript-estreeが大活躍した https://github.com/typescript-eslint/typescript-eslint
Step1: urlcat導入 フロントエンドでもExpressと同じ文字列を使ってAPIリクエストする ようにするためにurlcatを導入 © 2024 Cloudbase Inc. const =
=> const = ... () { { } ( ( , { fooId })); }; Component useFetch urlcat data " " /v1/foo/:fooId const = ( v1 foo :fooId , { fooId: foo , barId: bar }); console. (path) path urlcat log "/ / / " " " " " // /v1/foo/foo?barId=bar https://github.com/balazsbotond/urlcat
Step1: urlcat導入 、、、全然定着しない © 2024 Cloudbase Inc.
Step2: 型で縛る U エンジニアが増えることも考えると毎回PRで指摘するのはサステナ ブルじゃなa U TypeScriptなので型で縛ろう © 2024 Cloudbase
Inc.
Step2: 型で縛る(定義側) // /foo/:fooId/bar/:barId のような文字列から // { fooId: string }
& { barId: string } のような型を生成する型 type extends = class extends constructor private : private : private : return ... ... const = < > ...; < > { ( , < >, < , >) {} () { ( .pathname, { .params, .searchParams }); } } < >(path: Path< >) => { return (path. ()); } PathParams T Path T T PathParams T Record toString urlcat useFetch useSWR toString string string string any this this this T pathname params searchParams any © 2024 Cloudbase Inc. useFetchの引数をstringからPathに変更し、 Pathクラスを通してurlcatの使用を強制
Step2: 型で縛る(使用側) © 2024 Cloudbase Inc. const = => const
= new () { { } < >( ( , { fooId }) ); } Component useFetch ResponseType Path data "/v1/foo/:fooId" Pathを渡すことを強制
Step2: 型で縛る(使用側) © 2024 Cloudbase Inc. const = => const
= new () { { } < >( ( , { fooId }) ); } Component useFetch ResponseType Path data "/v1/foo/:fooId" const = => const = new () { { } ( (` ) ); } Component useFetch ResponseType Path data < > /v1/foo/${fooId}` Pathを渡すことを強制 Oh... テンプレートリテラルを渡すという抜け道
Step3: ESLintで縛る j 型で縛りきれないのならESLintで縛ろう (詰んだと思っていたが耐えたr j 調べてみたらESLintの世界では文字列リテラルとテンプレートリテ ラルを区別できるぽい!P j
というわけで、PathクラスのNewExpressionの第一引数が TemplateLiteralだった場合にエラーになるルールを作れば良い(実 際には文字列リテラル以外を禁止) © 2024 Cloudbase Inc.
Step3: ESLintで縛る export const = => return for const of
if === && === && !== ESLintUtils.RuleCreator. < , >({ : ( , [ ]) { { ( ) { ( options) { ( node.callee.type node.callee.name option.className node.arguments[option.argumentIndex].type ) { context. ({ node, messageId: , data: { className: option.className, argumentIndex: (option.argumentIndex), }, }); } } }, }; }, }); rule option withoutDocs Options MessageId create NewExpression report ordinal context options node "Identifier" "Literal" "restrict-literal-argument" © 2024 Cloudbase Inc. 実装の雰囲気
Step3: ESLintで縛る useFetch Path useFetch useFetch useFetch useFetch Path useFetch
Path useFetch Path ( ( , { fooId })); ( fooId ); (文字列変数); (文字列を返す関数呼び出し); ( ( fooId )); ( (文字列変数)); ( (文字列を返す関数呼び出し)); new new new new "/v1/foo/:fooId" `/v1/foo/${ }` `/v1/foo/${ }` © 2024 Cloudbase Inc. ESLintでエラー 型でエラー
Step4: typescript-estreeを使って使用エンドポイントを列挙 Pathクラスの第一引数は文字列リテラルしかあり得なくなったので、あとは Pathクラスの第一引数を列挙すれば、使用エンドポイントを列挙できそ う!!c でも、どうやって列挙する、、、? m
気合いで正規表現P というか、ESLintのカスタムルール作った時にエラー吐いてるところをstringの arrayにpushする実装に変えられたらめっちゃ簡単に列挙できるのでは? © 2024 Cloudbase Inc.
Step4: typescript-estreeを使って使用エンドポイントを列挙 g @typescript-eslint/typescript-estree !!!!p g “A parser that produces
an ESTree-compatible AST for TypeScript code.” であり@typescript- eslint/parserの内部で使用されているr g ESLintのカスタムルールとほぼ同じように書けて学習コストが低いのでおすすめ © 2024 Cloudbase Inc. const = new if === && === && === && typeof === < >() (node) { ( node.callee.type node.callee.name node.arguments[ ].type node.arguments[ ].type ) endpoints. (node.arguments[ ].value); } } endopoints string 0 0 0 Set NewExpression add "Identifier" "Path" "Literal" "string" NewExpression report ordinal (node) { ( options) { ( node.callee.type node.callee.name option.className node.arguments[option.argumentIndex].type ) { context. ({ node, messageId: , data: { className: option.className, argumentIndex: (option.argumentIndex), }, }); } } } for const of if === && === && !== option "Identifier" "Literal" "restrict-literal-argument"
Step5: 型パラメータに制約をつける v 実は、Expressから定義済みエンドポイントを列挙する際にユニオン型にしてし まえば、Pathの型引数に制約をつけられu v ただし、この制約をつけたとしても前述したカスタムルールやtypescript- estreeによるASTの走査は依然必P v なぜか
© 2024 Cloudbase Inc. type = | | class extends constructor private : private : private : ; < > { ( , < >, < , >) {} } GatPathname Path T GetPathname T PathParams T Record "/v1/foo/:fooId" "/v2/foo/:fooId" "/v1/bar/:barId" pathname params searchParams string any Expressからユニオン型を生成して型引数に制約をつける
Step5: 型パラメータに制約をつける 4 TypeScriptのユニオン型が必要十分であるかを検証するのは難し& 4 結局、使用している値を列挙する仕組みは必要 © 2024 Cloudbase Inc.
type = | const : = AorB AorB "A" "B" "A" A で十分 type = AorB "A"
© 2024 Cloudbase Inc. 今後の展望と共に取り組みをZennに投稿 Zenn: 今後の展望
© 2024 Cloudbase Inc. 今後の展望と共に取り組みをZennに投稿 Zenn: 今後の展望 やりました やりました
Schema firstに移行 p これまでは「Express → エンドポイント列挙」であったため、 Expressを起動しないとエンドポイントを列挙できず、パフォーマン スの問題があっC p 「エンドポイント列挙
→ Express」のように向きを変えることで問 題を解7 p GraphQLにおけるschema firstのようなイメージ © 2024 Cloudbase Inc.
Schema firstに移行 © 2024 Cloudbase Inc. app. ( , );
app. ( , ); app. ( , ); get get post "/v1/foo/:fooId" "/v2/foo/:fooId" "/v1/foo" ... ... ... app. ( , ); app. ( , ); app. ( , ); get get post "/v1/foo/:fooId" "/v2/foo/:fooId" "/v1/foo" ... ... ... type = type = ; ; GetPathname PostPathname "..." "..." const = { : z. ({ request: z. ({ pathParams: z. ({}), queryParams: z. ({}), body: z. ({}), }), response: z. ({}), }), : z. ({ ... }), : z. ({ ... }), }; schema "GET /v1/foo/:fooId" "GET /v2/foo/:fooId" "POST /v1/foo" object object object object object object object object メソッドとパス リクエスト/レスポンスの情報
Schema firstに移行 ) スキーマにリクエスト/レスポンスの型の情報があるのでgenericsが 不要になった © 2024 Cloudbase Inc. const
= { } < >( fooId ); data useFetch ResponseType `/v1/foo/${ }` const = new { } ( ( , { fooId })); data useFetch Path "/v1/foo/:fooId"
© 2024 Cloudbase Inc. まとめ d コーディングルールはESLintのルールで表現できると良b d ASTにおいてはstringリテラルとテンプレートリテラルを区別で きf
d typescript-estreeはESLintの知識を活かしてASTをいじれるよう になるのでおすすe d (REST APIであってもエンドポイントの定義はSchema firstにし ておくと便利)
We Are Hiring! Engineer Entrance Book