Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: generate typed css module #534

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e394c55
chore: generate typed css module
trim21 Apr 4, 2023
929ad83
lint staged
trim21 Apr 4, 2023
3f9a23f
lint staged
trim21 Apr 4, 2023
45c1336
lint staged
trim21 Apr 4, 2023
018da5e
re generate
trim21 Apr 4, 2023
2253949
remove unused var
trim21 Apr 4, 2023
b348a6c
unused function
trim21 Apr 4, 2023
2d7f4f3
deps
trim21 Apr 4, 2023
700dced
revert unexpected change
trim21 Apr 4, 2023
414b018
update
trim21 Apr 4, 2023
9c5c4a8
Merge remote-tracking branch 'upstream/master' into generate-types-cs…
trim21 Apr 4, 2023
364fb8b
diff
trim21 Apr 4, 2023
f70319c
generated
trim21 Apr 4, 2023
98e8a21
fix
trim21 Apr 4, 2023
e070578
remove unused style
FoundTheWOUT Apr 4, 2023
5d4ef01
Update snapshot
FoundTheWOUT Apr 4, 2023
eef8334
Update vite.config.ts
trim21 Apr 4, 2023
7b6c413
generate
trim21 Apr 4, 2023
9158d8e
Merge branch 'master' into generate-types-css-module
trim21 Apr 4, 2023
5bb940a
Merge branch 'master' into generate-types-css-module
trim21 Apr 4, 2023
7579841
Merge branch 'master' into generate-types-css-module
trim21 Apr 4, 2023
161fe82
Merge branch 'master' into generate-types-css-module
trim21 Apr 4, 2023
c22ea4e
Merge branch 'master' into generate-types-css-module
trim21 Apr 4, 2023
6b8f3e2
Merge branch 'master' into generate-types-css-module
trim21 Apr 4, 2023
a902843
Merge remote-tracking branch 'upstream/master' into generate-types-cs…
trim21 Apr 4, 2023
a5e1e9b
Merge branch 'master' into generate-types-css-module
trim21 Apr 4, 2023
339db0e
Update index.mts
trim21 Apr 4, 2023
aa98ade
Update index.mts
trim21 Apr 4, 2023
1fa6b2a
Mergeemote-tracking branch 'upstream/master' into generate-types-css-…
trim21 Apr 5, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pnpm-lock.yaml linguist-generated=true
*.snap linguist-generated=true
**/__snapshots__/* linguist-generated=true
**.module.less.d.ts linguist-generated=true
38 changes: 38 additions & 0 deletions dev/css-typed/gen.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import * as path from 'node:path';
import * as url from 'node:url';

import camelcase from 'camelcase';
import * as css from 'css-tree';
import less from 'less';

const __dirname = url.fileURLToPath(new URL('.', import.meta.url));

// 跟 '<projectRoot>/packages/website/vite.config.ts' 保持同步
const lessAdditionalData = '@import "./src/style/index.less";';

export async function generateDeclaration(p: string, content: string) {
const filename = path.basename(p);
const dirname = path.dirname(p);
if (filename.endsWith('.less')) {
const o = await less.render(lessAdditionalData + '\n' + content, {
filename,
paths: [path.join(__dirname, '../..', 'packages/website'), dirname],
});
content = o.css;
}

let ts = `// Generated by ./dev/css-typed/index.mts, DO NOT EDIT IT!!!\n// source file: ${filename}\n\n`;
const ast = css.parse(content, { filename });
const classNames = new Set<string>();
css.walk(ast, (node) => {
if (node.type === `ClassSelector`) {
classNames.add(camelcase(node.name));
}
});

classNames.forEach((name) => {
ts += `export const ${name}: string;\n`;
});

return ts;
}
103 changes: 103 additions & 0 deletions dev/css-typed/index.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import * as fs from 'node:fs/promises';
import { join, sep } from 'node:path';
import * as process from 'node:process';
import url from 'node:url';

import { minimatch } from 'minimatch';

import { walk } from '../utils/index.mts';
import { generateDeclaration } from './gen.mts';

const __dirname = url.fileURLToPath(new URL('.', import.meta.url));

const cssModulePattern = new minimatch.Minimatch('**/*.module.less');
const dtsPattern = new minimatch.Minimatch('**/*.module.less.d.ts');

async function fileExist(p: string): Promise<boolean> {
try {
await fs.stat(p);
return true;
} catch (err: unknown) {
// @ts-expect-error
if (err.code === 'ENOENT') {
return false;
}
throw err;
}
}

async function generateForCssModuleFile(p: string) {
const dstPath = p + '.d.ts';
let write = false;
const src = await fs.readFile(p);
const generated = await generateDeclaration(p, src.toString('utf-8'));
if (await fileExist(dstPath)) {
const dst = (await fs.readFile(dstPath)).toString('utf-8');
if (dst !== generated) {
write = true;
console.log('updating', dstPath);
await fs.writeFile(dstPath, generated);
}
} else {
write = true;
console.log('create', dstPath);
await fs.writeFile(dstPath, generated);
}

return { dstPath, write };
}

async function main() {
const dir = join(__dirname, '../..', 'packages/website');

const expectedDST = new Set<string>();
for await (const p of walk(dir)) {
if (p.includes(`${sep}node_modules${sep}`)) {
continue;
}

if (cssModulePattern.match(p)) {
const { dstPath } = await generateForCssModuleFile(p);
expectedDST.add(dstPath);
}
}

// remove orphan
for await (const p of walk(dir)) {
if (p.includes(`${sep}node_modules${sep}`)) {
continue;
}

if (dtsPattern.match(p)) {
if (!expectedDST.has(p)) {
console.log('remove orphan dts', p);
await fs.unlink(p);
}
}
}
}

async function onFiles(files: string[]) {
for (const file of files) {
if (cssModulePattern.match(file)) {
const { write } = await generateForCssModuleFile(file);
if (write) {
console.error(`${file}.d.ts is updated, try again`);
process.exit(1);
}
} else if (dtsPattern.match(file)) {
const sourceFileExist = await fileExist(file.slice(0, file.length - '.d.ts'.length));
if (!sourceFileExist) {
console.error('please remove file ' + file);
process.exit(1);
}
}
}
}

if (process.argv.length === 2) {
await main();
} else {
// with filenames, for lint staged
await onFiles(process.argv.slice(2));
}
10 changes: 10 additions & 0 deletions dev/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../tsconfig.style.json",
"compilerOptions": {
"allowJs": false,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "Node",
"noEmit": true
}
}
11 changes: 11 additions & 0 deletions dev/utils/index.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as fs from 'node:fs';
import * as path from 'node:path';

export async function* walk(dir: string): AsyncIterableIterator<string> {
for await (const d of await fs.promises.opendir(dir)) {
const entry = path.join(dir, d.name);
if (d.isDirectory()) {
yield* walk(entry);
} else if (d.isFile()) yield entry;
}
}
14 changes: 14 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"website": "pnpm --filter=@bangumi/website",
"utils": "pnpm --filter=@bangumi/utils",
"lint-staged": "lint-staged -q",
"generate": "tsx ./dev/css-typed/index.mts",
"type-check": "tsc --noEmit"
},
"engines": {
Expand All @@ -41,6 +42,12 @@
"trailingComma": "all"
},
"lint-staged": {
"*.module.less": [
"tsx ./dev/css-typed/index.mts"
],
"*.module.less.d.ts": [
"tsx ./dev/css-typed/index.mts"
],
"*.{js,ts,mts,cts,tsx,cjs,mjs}": [
"eslint --fix"
],
Expand All @@ -53,9 +60,12 @@
"@actions/core": "^1.10.0",
"@actions/exec": "^1.1.1",
"@actions/github": "^5.1.1",
"@bangumi/design": "workspace:*",
"@octokit/openapi-types": "^16.0.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@types/css-tree": "^2.3.1",
"@types/less": "^3.0.3",
"@types/node": "^18.15.11",
"@types/testing-library__jest-dom": "^5.14.5",
"@types/web": "^0.0.99",
Expand All @@ -64,6 +74,8 @@
"@vitejs/plugin-react": "^3.1.0",
"@vitest/coverage-c8": "^0.29.8",
"@vitest/ui": "^0.29.8",
"camelcase": "^6.0.1",
"css-tree": "^2.3.1",
"eslint": "^8.37.0",
"eslint-config-prettier": "^8.8.0",
"eslint-config-standard": "^17.0.0",
Expand All @@ -81,7 +93,9 @@
"eslint-plugin-unused-imports": "^2.0.0",
"husky": "^8.0.3",
"jsdom": "^21.1.1",
"less": "^4.1.3",
"lint-staged": "^13.2.0",
"minimatch": "^8.0.3",
"postcss": "^8.4.21",
"postcss-less": "^6.0.0",
"prettier": "^2.8.7",
Expand Down
48 changes: 48 additions & 0 deletions packages/website/plugins/typed-css-modules.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* eslint-disable @typescript-eslint/no-misused-promises */

import * as fs from 'node:fs/promises';

import type { Plugin, UserConfig } from 'vite';

import { generateDeclaration } from '../../../dev/css-typed/gen.mts';

export default function plugin(): Plugin {
return {
name: 'typed-css-modules',
config: () => {
const config: UserConfig = {
css: {
modules: {
localsConvention: 'camelCaseOnly',
},
},
};
return config;
},
configureServer: (server) => {
server.watcher.on('change', async (path) => {
if (!path.endsWith('.module.less')) return;
let dts: string | undefined;
try {
const content = await fs.readFile(path);
dts = await generateDeclaration(path, content.toString('utf-8'));
} catch {
/* ignore */
}

if (dts) {
await fs.writeFile(path + '.d.ts', dts);
}
});
server.watcher.on('unlink', async (path) => {
if (!path.endsWith('.module.css')) return;

try {
await fs.unlink(path + '.d.ts');
} catch {
/* ignore */
}
});
},
};
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions packages/website/src/components/Footer/style.module.less.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions packages/website/src/components/Header/SubMenu.module.less.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading