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
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
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
18
Other Decks in Technology
See All in Technology
HRC_Frontend_Conference_Fukuoka_2026.pdf
ts020
0
750
株式会社シーエーシー エンジニア向け会社紹介資料
cac
0
57k
越境するなら専門用語を使うな高校校歌 / If you wanna cross border, you shouldn't use jargon
vtryo
0
150
AgentCore Runtime上にAgentic Coding基盤を構築・展開する際の設計ポイントと限界点 / Design considerations and limitations when building an agentic coding platform on AgentCore Runtime
har1101
5
320
AIに任せた品質は、誰が見立てるのか - AI時代のテストマネジメント
nakanao
3
2.2k
現場で役立つ技術負債の効果的な返済方法
masuda220
PRO
9
4.6k
【技術的負債conf】事業成長に伴う技術的負債の説明責任とAIによるモニタリング、認知的負債について
i35_267
3
1.8k
GoのInterface内部構造から学ぶ!最高パフォーマンスを出すコード設計
yappli_developers
0
160
アプリログインとWeb認証基盤をつなぐ ASWebAuthenticationSession 作法
shimastripe
1
340
Issue 駆動でスペシャリストの意図を届ける、AI 実装のアクセシビリティ向上
thkt
0
120
データ_AIの事業の勝敗をわけるもの
nek0128
1
460
すぐできる衛星通信対応 あとは山奥に行くだけ
tatetate55
0
130
Featured
See All Featured
ピンチをチャンスに:未来をつくるプロダクトロードマップ #pmconf2020
aki_iinuma
128
56k
Max Prin - Stacking Signals: How International SEO Comes Together (And Falls Apart)
techseoconnect
PRO
0
470
Everyday Curiosity
cassininazir
0
320
Making Projects Easy
brettharned
120
6.8k
We Have a Design System, Now What?
morganepeng
55
8.3k
What the history of the web can teach us about the future of AI
inesmontani
PRO
1
690
Put a Button on it: Removing Barriers to Going Fast.
kastner
60
4.6k
Creating an realtime collaboration tool: Agile Flush - .NET Oxford
marcduiker
35
2.6k
jQuery: Nuts, Bolts and Bling
dougneiner
66
8.6k
How to train your dragon (web standard)
notwaldorf
97
6.8k
Between Models and Reality
mayunak
4
460
Designing Powerful Visuals for Engaging Learning
tmiket
1
570
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