2022/7/24 checkable-css-modules/リファクタリングの作戦を考える
どういうインターフェイスが良さそうとか考えよう。
リファクタリングの作戦を考える
Round 1
すべての css modules の結果を watch 中はキャッシュしておきたい
キャッシュストアが必要
そのキャッシュストアは、少なくとも watcher が知っている
極力 state は排除したい
型定義ファイルとソースマップの生成君はそれぞれ関数として設計する
type DtsCreator = (sourceFilePath: string, tokens: CSSModuleToken[], options: RunOptions) => string
type SourceMapCreator = (sourceFilePath: string, tokens: CSSModuleToken[], options: RunOptions) => string
DtsCreator や SourceMapCreator は Emitter が管理する
type Emitter = (sourceFilePath: string, tokens: CSSModuleToken[], options: RunOptions) => Promise<void>
Dts と Sourcemap の結合も emitter の責務
出力するファイルに差分がなかったら出力しない、という振る舞いも emitter の責務
ソースファイルを読み取って、tokensを返すのはLoaderの責務
type SourceFileLoader = (sourceFilePath: string, cacheStore: CacheStore) => Promise<tokens: CSSModuleToken[]>
他のファイルの依存関係も再帰的に辿って cacheStoreに token を詰め込んでいく
あとは全部 watcher の仕事
code:tsx
const cacheStore = new CacheStore();
watcher.watch(patterns, (file) => {
await loadSourceFile(file, cacheStore);
const tokens = cacheStore.get(file);
await emitter.emit(file, tokens, options);
});
感想:
loadSourceFileはtokensだけじゃなくて、あるファイルがどのファイルに依存しているかの情報も集めないといけない
じゃないとexport * as './other-file.module.less';が書き込めない
つまりモジュールグラフを構築する役割もある
CacheStoreはただのSetなのでこれをわざわざモデルにする必要なさそう
モジュールグラフ構築+トークン解析器の中に組み込む形で良さそう
(モジュールを渡り歩いて情報をかき集めていくので) Walkerが正しい? Loaderでも良いとは思うけど
import文を辿ってモジュールグラフを構築する + 1ファイル単位でパースしてモジュールの依存情報と token を抜き出す、という2つの仕事があることを示唆している
前者が Walker で、後者が Loader ということ?
そこまで分けなくても良いような気はする...
こういうインターフェイスになる?
code:tsx
type CacheEntry = {
mtime: number,
result: LoadResult,
};
class Loader {
cache: Map<string, CacheEntry>;
// ...
}
type Token = {
name: '...',
positions: Position[],
};
type LoadResult = {
filePath: '...',
imports: LoadResult[],
localTokens: Token[],
};
const loader = new Loader();
const result = loader.load(file);
ファイルを入力として受け取ってなんかする、みたいなインターフェイスは以下の OSS を参考にすると良いのでは
graphql code generator
eslint
prettier
Round 2
試しに Round 1+感想に書いた改善案で擬似コードを書いてみる。実際に書いてみて、使い心地に違和感があったら、ちょっとインターフェイスを調整したりして改善してみる。
code:runner.ts
import chokidar from 'chokidar';
import fg from 'fast-glob';
function run(options: RunnerOptions) {
const loader = new Loader(options);
async function processFile(filePath: string) {
const result = await loader.load(path);
await emitGeneratedFiles(result, options);
}
if (options.watch) {
chokidar.watch(options.pattern).on('change', async (event, filePath) => {
void processFile(filePath);
});
} else {
filePaths.forEach((filePath) => {
void processFile(filePath);
});
}
}
code:loader.ts
type CacheEntry = {
mtime: number,
result: LoadResult,
};
type Token = {
name: '...',
positions: Position[],
};
type LoadResult = {
filePath: '...',
imports: LoadResult[],
localTokens: Token[],
};
class Loader {
cache: Map<string, CacheEntry>;
async load(filePath: string): Promise<LoadResult> { ... }
}
code:emitter.ts
async function emitGeneratedFiles(result: LoadResult, options: RunnerOptions) {
const dtsFilePath = getDtsFilePath(result, options);
const { dtsContent, sourceMap } = generateDtsContentWithSourceMap(result, options);
if (options.declarationMap) {
const sourceMapFilePath = getSourceMapFilePath(result, options);
const sourceMappingURLComment = generateSourceMappingURLComment(dtsFilePath, sourceMapFilePath);
await writeFileIfChanged(dtsFilePath, dtsContent + sourceMappingURLComment);
await writeFileIfChanged(sourceMapFilePath, sourceMap.toString());
} else {
await writeFileIfChanged(dtsFilePath, dtsContent);
}
}
感想:
概ね良いのでは
optionsそこかしこに渡していてこれで良いのかとかは気になる
RunnerOptionsをrunner以外に渡している…
レイヤーごとにXXXOptionsみたいな型 (引数オブジェクト) を作る? いやでも面倒だし過剰な気もする。
過剰だなと思ったら引数オブジェクトは作らず直で flat なリストで書いていったほうが良いだろう
typescript の API とかもそうなってる
code:ts
emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult;
順序は界隈の一般的な仕草に合わせる
ファイル書き込み API だったらファイルのパス => コンテンツの順だよね、みたいな
code:ts
writeFile(filePath: string, content: string): Promise<void>;
まああとは書きながら考えればよいだろう
Round 2 で打ち止めにする
実際にリファクタリングしてみる
Round 1
まずは末端 (emitter) からちょっとずつ書き換えていく。
Round 2
次は第二の末端、loader を作る。