typed-css-modulesの仕組みを調べる
run.tsにて
DtsCreatorのインスタンスを生成
const creator = new DtsCreator()
chokidar.watchによってcss fileの追加や編集をwatch
const watcher = chokidar.watch([filesPattern.replace(/\\/g, '/')]);
watcher.on('add', writeFile);
watcher.on('change', writeFile);
await waitForever();
creator.createでDtsContentのインスタンスを生成
const content: DtsContent = await creator.create(f, undefined, false);
変更や追加があればDtsContentのwriteFileを実行する
ここでファイルの中身を記述している
resultListの抜き出しが肝っぽい
public get formatted(): string {
if (!this.resultList || !this.resultList.length) return '';
return (
[
'declare const styles: {',
...this.resultList.map((line) => ' ' + line),
'};',
'export = styles;',
''
].join(os.EOL) + this.EOL
);
}
private createResultList(): string[] {
const result = this.rawTokenList.map(
(k) => 'readonly "' + k + '": string;'
);
return result;
}
rawTokenListはこうやって生成される
const res = await this.loader.fetch(filePath, '/', undefined, initialContents);
if (res) {
const tokens = res;
const keys = Object.keys(tokens);
file-system-loader.tsにloaderの詳細がある
source = await readFile(fileRelativePath, 'utf-8');
でファイルの中身取り出して、
const { injectableSource, exportTokens } = await this.core.load(
source,
rootRelativePath,
trace,
this.fetch.bind(this)
);
で何やらtokenっぽいものを取得している。 this.core.loadとは
this.core = new Core();
コンストラクタで宣言されているこの人。
import Core from 'css-modules-loader-core';
にたどり着く
css-modules-loader-core
css-loaderから「exportされているクラスセレクターを抽出するロジック」を切り出してきたライブラリ
との記述が。
参考:https://developer.hatenastaff.com/entry/2022/09/01/093000
typed-css-modules
CSS Modulesファイルの型定義ファイル(TypeScript)を生成するCLIツール
詳しくはQiitaの記事「TypeScript + React JSX + CSS Modules で実現するタイプセーフなWeb開発」を参照
内部でcss-modules-loader-coreを使っている
とも(調べなくてよかった説)
core.load( sourceString , sourcePath , pathFetcher ) =>
Promise({ injectableSource, exportTokens })
Processes the input CSS sourceString, looking for dependencies such as @import or :import. Any localisation will happen by prefixing a sanitised version of sourcePath When dependencies are found, it will ask the pathFetcher for each dependency, resolve & inline any imports, and return the following object:
injectableSource: the final, merged CSS file without @import or :import statements
exportTokens: the mapping from local name to scoped name, as described in the file's :export block
These should map nicely to what your build-tool-specific loader needs to do its job.
とのこと
実際に出てくるのはこういうもの
injectableSource ._applications_flow_src_features_AppForm_Canvas_Field_fieldComponentVariants_MultipleLineText_MultipleLineText_module__appForm-fields-multipleLineText {
height: 100%;
padding: 0px 8px 4px 8px;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
._applications_flow_src_features_AppForm_Canvas_Field_fieldComponentVariants_MultipleLineText_MultipleLineText_module__appForm-dragDroppableField {
height: fit-content;
display: flex;
position: relative;
width: fit-content;
}
._applications_flow_src_features_AppForm_Canvas_Field_fieldComponentVariants_MultipleLineText_MultipleLineText_module__appForm-dragDroppableField--highlighted {
background: var(--c-malibu);
}
exportTokens {
'appForm-fieldLabel-title': '_applications_flow_src_features_AppForm_Canvas_Field_FieldLabel_FieldLabel_module__appForm-fieldLabel-title',
'appForm-fieldLabel-required': '_applications_flow_src_features_AppForm_Canvas_Field_FieldLabel_FieldLabel_module__appForm-fieldLabel-required'
}
const re = new RegExp(/@import\s'(\D+?)';/, 'gm');
const importTokens: Core.ExportTokens = {};
let result;
while ((result = re.exec(injectableSource))) {
const importFile = result?.[1];
if (importFile) {
const importFilePath = isNodeModule(importFile)
? importFile
: path.resolve(path.dirname(fileRelativePath), importFile);
const localTokens = await this.fetch(importFilePath, relativeTo);
Object.assign(importTokens, localTokens);
}
}
const tokens = { ...exportTokens, ...importTokens };
this.sources[trace] = injectableSource;
this.tokensByFile[fileRelativePath] = tokens;
return tokens;
この辺の記述でtoken(クラスネーム)を取得してる?
基本はcss-modules-loader-coreからtokenを取得してるけど、import文がある場合は
再帰的にそのファイルを読んでtokenを取得してるっぽい
this.tokensByFile[fileRelativePath] = tokens;
でキャッシュ?しつつtokenを返却。
それがDtsContentのrawTokenListに詰められる
基本的な仕組みは以上っぽい
this.sources[trace] = injectableSource;
って記述があったけどこれは何に使ってるんだろう
const trace = String.fromCharCode(this.importNr++);
で渡された数値から UTF-16の文字列を生成してる。(コンストラクタ)
importNrはカウントアップされていくけどなぜ数値のままじゃダメなの?
履歴見たりリポジトリ内検索かけたけどわからなかった
sourcesは今のところ使われてない。念の為コンストラクタで持ってるだけかも
おまけとして、css modules削除時にこの仕組みだと型ファイルはゴミとして残ってしまう。
これはrun.tsのchokidar.watchあたりにdeleteの処理を追加してあげれば良い。
watcher.on('add', writeFile);
watcher.on('change', writeFile);
+ watcher.on('unlink', deleteFile);
const deleteFile = async (f: string): Promise<void> => {
try {
const content: DtsContent = await creator.create(
f,
!!options.watch,
true
);
await content.deleteFile();
console.log('Delete ' + chalk.green(content.outputFilePath));
} catch (error) {
console.error(chalk.red('[Error] ' + error));
}
};
ただ、存在しないファイルのcreator.createは無条件で失敗するようになっているので、フラグなどを追加していい感じにしてあげる必要がある。
dts-creator.ts
let keys: string[] = [];
if (!isDelete) {
const res = await this.loader.fetch(filePath, '/');
if (!res) throw res;
keys = Object.keys(res);
}