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

feat: config provider #51

Draft
wants to merge 7 commits into
base: release
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ node_modules
dist
.rslib
pnpm-lock.yaml
packages/component/index.ts
packages/components/index.ts
packages/components/config-provider/src/*.type.ts
packages/components/config-provider/src/config-provider.type.ts
3 changes: 2 additions & 1 deletion doc/rspress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export default defineConfig({
alias: {
'@qwqui/core': path.join(__dirname, '../packages/components'),
'@qwqui/theme': path.join(__dirname, '../packages/theme/index.scss'),
'@qwqui/tools': path.join(__dirname, '../packages/tools')
'@qwqui/tools': path.join(__dirname, '../packages/tools'),
'@qwqui/config-provider': path.join(__dirname, '../packages/components/config-provider')
},
},
},
Expand Down
199 changes: 199 additions & 0 deletions internal/create-config-provider-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import fg from 'fast-glob';
import { resolve } from 'path';
import { compile } from 'sass';
import { CssNode, parse, walk } from 'css-tree';
import { Project, PropertySignatureStructure, SourceFile } from 'ts-morph';
import camelcase from 'camelcase';
import { existsSync, writeFileSync } from 'fs-extra';

const getComponentStyles = ()=>{
const componentStyles = fg.sync('packages/components/**/*.scss', {ignore: ['**/node_modules', 'packages/components/theme']})
.map((path) => {
const pathArray = path.split('/');
const componentName = pathArray[2];
return [componentName, resolve(pathArray.join('/'))]
})
const componentStylesObject:Record<string,string[]> = {}
for (const [name, path] of componentStyles){
if (componentStylesObject[name]) {
componentStylesObject[name].push(path)
}else{
componentStylesObject[name] = [path];
}
}
return componentStylesObject;
}

const getThemeVars = () => {
const themePaths = fg.sync('packages/theme/**/*.scss', {ignore: ['**/node_modules']})
.map((path) => {
return resolve(path)
})
const properties = [];
for (const path of themePaths){
const {css} = compile(path, {silenceDeprecations: ['import']});
const ast = parse(css);
walk(ast, function(node){
if (node.type === 'Declaration'){
if (!properties.includes(node.property)){
properties.push(node.property);
}
}
})
}
return properties;
}

/**
*
* @param componentName
* @param vars Standardized variable names
*/
const createStyleConfig = (sourceFile: SourceFile, componentName: string, vars: string[]) => {
const properties = vars.map((varName) => {
return {
name: `'${varName}'?`,
type: 'string',
} as PropertySignatureStructure
})
sourceFile.insertInterface(0, {
name: `${camelcase(componentName,{pascalCase: true})}CSSVar`,
properties,
});
}

const template =
`type Seq = {
50: string;
100: string;
200: string;
300: string;
400: string;
500: string;
600: string;
700: string;
800: string;
900: string;
}
type Tokens = {
'zen'?: Partial<Seq>;
'gray'?: Partial<Seq>;
'red'?: Partial<Seq>;
'danger'?: Partial<Seq>;
'pink'?: Partial<Seq>;
'grape'?: Partial<Seq>;
'violet'?: Partial<Seq>;
'indigo'?: Partial<Seq>;
'blue'?: Partial<Seq>;
'primary'?: Partial<Seq>;
'cyan'?: Partial<Seq>;
'teal'?: Partial<Seq>;
'green'?: Partial<Seq>;
'success'?: Partial<Seq>;
'lime'?: Partial<Seq>;
'yellow'?: Partial<Seq>;
'orange'?: Partial<Seq>;
'warning'?: Partial<Seq>;
'radius'?: {
xs?: string
sm?: string
md?: string
lg?: string
xl?: string
full?: string
};
'breakpoint'?: {
xs?: string
sm?: string
md?: string
lg?: string
xl?: string
xxl?: string
};
}

export type ConfigProviderProps = {
direction?: 'ltr' | 'rtl',
tokens?: Partial<Tokens>,
component?: Partial<ComponentVars>
}
`

const app = () => {
const componentStyles = getComponentStyles();
const commonVar = getThemeVars();
const astMap = new Map<string,CssNode[]>();
for (const [key, paths] of Object.entries(componentStyles)){
for (const path of paths) {
const {css} = compile(path, {silenceDeprecations: ['import'] })
const ast = parse(css,{ parseAtrulePrelude: true, parseCustomProperty: true, parseRulePrelude: true, parseValue: true });
if (astMap.has(key)) {
astMap.get(key).push(ast)
} else {
astMap.set(key,[ast])
}
}
}
const components = Object.keys(componentStyles);
const cssVars = new Map<string, string[]>();
for (const comp of components) {
const asts = astMap.get(comp);
const names:string[] = [];
for (const ast of asts) {
walk(ast,function(node){
if (node.type === 'Identifier' && node.name.startsWith('--')){
if (commonVar.includes(node.name)){
return;
}
names.push(node.name);
}
})
if(!names.length){
continue;
}
}
const normalizationCSSVar = names.map((name) => {
return name.replace(/^--/, '')
});
cssVars.set(comp, normalizationCSSVar);
}

const componentTokenTypeFilePath = resolve('packages/components/config-provider/src/config-provider-component-token.type.ts');
if (!existsSync(componentTokenTypeFilePath)) {
writeFileSync(componentTokenTypeFilePath, '');
}
const componentFilePath = resolve('packages/components/config-provider/src/config-provider.type.ts');

const proj = new Project();
const tokenSf = proj.createSourceFile(componentTokenTypeFilePath, '', {overwrite: true});
const compsf= proj.createSourceFile(componentFilePath, template,{overwrite: true})

const componentVarsInterface = tokenSf.getInterface('ComponentVars');
if (componentVarsInterface){
componentVarsInterface.remove();
}
tokenSf.insertInterface(0, {
name: 'ComponentVars',
properties: components.filter((compName) => cssVars.get(compName).length).map((comp) => ({
name: comp,
type: `${camelcase(comp, {pascalCase: true})}CSSVar`,
})),
isExported: true
})

for (const [compName, vars] of Array.from(cssVars.entries())){
if (!vars.length) {
continue;
}
createStyleConfig(tokenSf, compName, vars);
}

tokenSf.saveSync();
compsf.addImportDeclaration({
namedImports: ['ComponentVars'],
moduleSpecifier: './config-provider-component-token.type.ts'
})
compsf.saveSync();
}

app();
6 changes: 5 additions & 1 deletion internal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@
"dependencies": {
"@types/inquirer": "^9.0.7",
"camelcase": "^8.0.0",
"css-tree": "^3.0.1",
"dashify": "^2.0.0",
"esbuild-sass-plugin": "^3.3.1",
"fast-glob": "^3.3.2",
"inquirer": "^11.0.2"
"inquirer": "^11.0.2",
"sass": "^1.81.0",
"ts-morph": "^24.0.0"
},
"devDependencies": {
"@types/css-tree": "^2.3.9",
"@types/dashify": "^1.0.3",
"@types/fs-extra": "^11.0.4",
"@types/node": "^22.7.2",
Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@
"typescript": "^5.6.2"
},
"scripts": {
"prebuild": "esno internal/create-core-entry.ts && pnpm --filter @qwqui/* clean:dist",
"predev": "esno internal/create-core-entry.ts && pnpm --filter @qwqui/* clean:dist",
"postinstall": "esno internal/create-core-entry.ts && esno internal/create-config-provider-type.ts",
"prebuild": "esno internal/create-core-entry.ts && esno internal/create-config-provider-type.ts && pnpm --filter @qwqui/* clean:dist",
"predev": "esno internal/create-core-entry.ts && esno internal/create-config-provider-type.ts && pnpm --filter @qwqui/* clean:dist",
"test:type": "tsc --noEmit",
"test": "jest",
"build": "pnpm --filter !@qwqui/core build && pnpm --filter @qwqui/core build",
Expand All @@ -46,7 +47,8 @@
"create:comp": "esno internal/create-component.ts",
"dev": "pnpm -C doc dev",
"build:site": "pnpm prebuild && pnpm --filter @qwqui/doc build",
"lint": "eslint ."
"lint": "eslint .",
"esno": "esno"
},
"devDependencies": {
"@changesets/changelog-github": "^0.5.0",
Expand Down
13 changes: 13 additions & 0 deletions packages/components/config-provider/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# @qwqui/config-provider

<!-- Description -->

## Install

```bash
pnpm add @qwqui/config-provider
```

## License

MIT
1 change: 1 addition & 0 deletions packages/components/config-provider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './src/config-provider'
29 changes: 29 additions & 0 deletions packages/components/config-provider/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@qwqui/config-provider",
"version": "1.0.0",
"description": "",
"scripts": {
"build": "rslib build",
"clean:dist": "rimraf dist .rslib",
"clean:deps": "rimraf node_modules"
},
"keywords": [],
"author": "",
"license": "MIT",
"types": "./dist/index.d.mts",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.mts"
}
},
"files": [
"dist"
],
"peerDependencies": {
"react": "18.3.1"
}
}
2 changes: 2 additions & 0 deletions packages/components/config-provider/rslib.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { defineBuild } from '@qwqui/build';
export default defineBuild();
6 changes: 6 additions & 0 deletions packages/components/config-provider/src/config-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createContext } from "react";
import { ConfigProviderProps } from "./config-provider.type";

export const ConfigContext = createContext<Partial<ConfigProviderProps>>({});

export const {Consumer} = ConfigContext;
54 changes: 54 additions & 0 deletions packages/components/config-provider/src/config-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useEffect } from "react";
import { ConfigProviderProps } from "./config-provider.type";
import { ConfigContext } from "./config-context";

const overwriteComponentToken = (element:HTMLElement,token: ConfigProviderProps['component']) => {
if (JSON.stringify(token) === '{}') {
return;
}
if (!element){
return;
}
for (const [, overwriteToken] of Object.entries(token)) {
// TODO: 后续会重构为 --comp--key: value
for (const [key, value] of Object.entries(overwriteToken)){
element.style.setProperty(`--${key}`, value);
console.log(element.style.getPropertyValue(`--${key}`));
}
}
}

const overwriteToken = (element:HTMLElement, token: ConfigProviderProps['tokens']) => {
if (JSON.stringify(token) === '{}') {
return;
}
if (!element){
return;
}
for (const [color, steps] of Object.entries(token)) {
for (const [step, value] of Object.entries(steps)) {
element.style.setProperty(`--${color}-${step}`,value);
}
}
}

export const ConfigProvider = (props: Partial<ConfigProviderProps> & {children: React.ReactNode}) => {
const {
direction='ltr',
tokens={},
component={}
} = props;
useEffect(()=>{
const root = document.querySelector(':root') as HTMLElement;
overwriteToken(root, tokens)
overwriteComponentToken(root, component)
root.style.direction = direction;
}, [tokens, direction, component]);
return (
<ConfigContext.Provider value={props}>
{props.children}
</ConfigContext.Provider>
)
}

ConfigProvider.ConfigContext = ConfigContext;
10 changes: 10 additions & 0 deletions packages/components/config-provider/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"include": [
"index.ts",
"src"
],
"exclude": [
"node_modules"
],
"extends": "../../../tsconfig.json"
}
1 change: 1 addition & 0 deletions packages/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import './code/src/styles/header.module.scss'
export * from './button/index.ts'
export * from './center/index.ts'
export * from './code/index.ts'
export * from './config-provider/index.ts'
export * from './flex/index.ts'
export * from './group/index.ts'
export * from './ripple/index.ts'
Expand Down
Loading