Sfoglia il codice sorgente

feat: rewrite scss-compiler

DaiQiangReal 4 anni fa
parent
commit
50e4473302

+ 39 - 32
packages/semi-scss-compile/README.md

@@ -2,49 +2,56 @@
 
 ## Description
 
-There are mainly the following two usage scenarios: 
+There are mainly the following two usage scenarios:
 
--   For Sever side consumption in Semi Design System.When publishing the theme, call the script on the Node side to compile the custom theme package into a complete semi.css file
--   Before publish `@douyinfe/semi-foundation`,construct a complete semi.css file
+- For Sever side consumption in Semi Design System.When publishing the theme, call the script on the Node side to
+  compile the custom theme package into a complete semi.css file
+- Before publish `@douyinfe/semi-foundation`,construct a complete semi.css file
 
-## Dependencies
+## Usage
 
--   dart-sass
+### Command Line
 
-## Usage
+```shell
+npm i -g @douyinfe/semi-scss-compile
+
+semiScssCompile --foundation="path/to/foundation" --theme="path/to/theme" --output="path/to/output.css" --min=true
+
+# or for short
+
+semiScssCompile -f "path/to/foundation" -t "path/to/theme" -o "path/to/output.css" -m true
+
+```
+
+### JS API
 
 ```js
-const SemiThemeCompile = require('../packages/semi-theme-compile');
-const chalk = require('chalk');
-const path = require('path');
-const log = console.log;
 
-const success = text => chalk.green(text);
-const errors = text => chalk.red(text);
+const {compile} = require('@douyinfe/semi-scss-compile');
+const path = require('path');
 
 function resolve(dir) {
-    return path.join(__dirname, '../', dir);
+    return path.join(__dirname, '../..', dir);
 }
 
-const options = {
-    COMPONENT_SCSS_PATH: resolve('packages/semi-foundation/'),
-    OUTPUT_SEMI_SCSS_PATH: resolve('packages/semi-theme-default/semi.scss'),
-    OUTPUT_SEMI_CSS_PATH: resolve('packages/semi-ui/dist/css/semi.css'),
-    OUTPUT_SEMI_CSS_MIN_PATH: resolve('packages/semi-ui/dist/css/semi.min.css'),
-};
-
-let compiler = new SemiThemeCompile(options);
-compiler
-    .compile()
-    .then(res => {
-        log(success('compile css success'));
-        process.exitCode = 0;
-    })
-    .catch(error => {
-        log(errors('compile css failed'));
-        log(errors(error));
-        process.exitCode = 1;
-    });
+//eg
+
+compile(resolve('semi-foundation/'), resolve('semi-theme-default/'), resolve('semi-ui/dist/css/semi.min.css'), {isMin: true})
+compile(resolve('semi-foundation/'), resolve('semi-theme-default/'), resolve('semi-ui/dist/css/semi.css'), {isMin: false})
+
+```
+
+### extend API
+
+```js
+const {generateScssMap, writeFile, compilerFromScssMap} = require('@douyinfe/semi-scss-compile');
+const fs = require('fs-extra');
+
+const isMin = false;
+const scssMap = generateScssMap("path/to/foundation", "path/to/theme");
+const tempDir = writeFile(scssMap)
+const result = compilerFromScssMap(path.join(tempDir, 'index.scss'), isMin);
+fs.outputFileSync(outputPath, result.css);
 
 ```
 

+ 0 - 152
packages/semi-scss-compile/index.ts

@@ -1,152 +0,0 @@
-/* eslint-disable prefer-promise-reject-errors */
-import path from 'path';
-import sass, { Options as RenderOptions } from 'sass';
-import fs from 'fs-extra';
-
-export interface CompileScssOptions {
-    COMPONENT_SCSS_PATH: string;
-    OUTPUT_SEMI_SCSS_PATH: string;
-    OUTPUT_SEMI_CSS_PATH: string;
-    OUTPUT_SEMI_CSS_MIN_PATH: string;
-    COMPONENT_EXTRA_SCSS_PATH?: string;
-    useAbsolutePath?: boolean;
-}
-
-const defaultOptions = {
-    COMPONENT_SCSS_PATH: 'tempory/semi-ui/',
-    OUTPUT_SEMI_SCSS_PATH: 'tempory/release/semi.scss',
-    OUTPUT_SEMI_CSS_PATH: 'tempory/release/css/semi.css',
-    OUTPUT_SEMI_CSS_MIN_PATH: 'tempory/release/css/semi-min.css',
-};
-
-export default class CompileScss {
-    options: CompileScssOptions;
-    /**
-     * @param {object} [options]
-     * @param {string} [options.COMPONENT_SCSS_PATH]
-     * @param {string} [options.COMPONENT_EXTRA_SCSS_PATH]
-     * @param {string} [options.OUTPUT_SEMI_SCSS_PATH]
-     * @param {string} [options.OUTPUT_SEMI_CSS_PATH]
-     */
-    constructor(options = defaultOptions) {
-        // console.log(options)
-        this.options = options;
-    }
-
-    getScssFolderMap(filepath: string) {
-        return fs
-            .readdir(filepath)
-            .then(files => {
-                const folderWithScss: string[] = [];
-                files.forEach(fileName => {
-                    const scssFile = path.join(this.options.COMPONENT_SCSS_PATH, fileName, `${fileName}.scss`);
-                    try {
-                        const stats = fs.statSync(scssFile);
-                        if (stats.isFile()) {
-                            folderWithScss.push(fileName); // Valid file path is pushed
-                        }
-                    } catch (error) {
-                        // console.log(error)
-                    }
-                });
-                return folderWithScss;
-            })
-            .catch(error => {
-                console.error(error);
-                throw error;
-            });
-    }
-
-    async generateSemiScss() {
-        const componentScssPath = this.options.COMPONENT_SCSS_PATH;
-        const outPutSemiScss = this.options.OUTPUT_SEMI_SCSS_PATH;
-        const outPutScss = outPutSemiScss.split('semi.scss')[0] + 'scss';
-        const folderWithScss = await this.getScssFolderMap(componentScssPath);
-        const relativePath = '../semi-foundation'; // When used in the Semi main repository, use relative paths to build to avoid different semi.scss built when different people publish versions
-        const absolutePath = componentScssPath; // When used in semi-server, the absolute path is used to build, so that the semiUI path is not located in the same directory structure as the semi.scss built
-        let indexScss = '@import "./scss/index.scss";';
-        let globalScss = '@import "./scss/global.scss";';
-        let semiUIPath = this.options.useAbsolutePath ? absolutePath : relativePath;
-
-        if (this.options.useAbsolutePath) {
-            semiUIPath = absolutePath;
-            indexScss = `@import "${outPutScss}/index.scss";`;
-            globalScss = `@import "${outPutScss}/global.scss";`;
-        }
-
-        const componentScss = folderWithScss
-            .map(scssFile => `@import "${semiUIPath}/${scssFile}/${scssFile}.scss"`)
-            .concat([
-                `@import "${semiUIPath}/button/iconButton.scss"`,
-                `@import "${semiUIPath}/input/textarea.scss"`,
-            ]) // Handle the scss of iconButton/textarea separately
-            .join(';\n');
-        const fileContent = `${indexScss}\n${globalScss}\n${componentScss}`;
-        return fs.outputFile(outPutSemiScss, fileContent);
-    }
-
-    rewriteFile(filePath: string) {
-        const extraImport = this.options.COMPONENT_EXTRA_SCSS_PATH;
-        let fileStr = fs.readFileSync(filePath, 'utf-8');
-        if (extraImport) {
-            const localImport = `\n@import "${extraImport}";`;
-            try {
-                const regex = /(@import '.\/variables.scss';?|@import ".\/variables.scss";?)/g;
-                const fileSplit = fileStr.split(regex).filter(item => !!item);
-                if (fileSplit.length > 1) {
-                    fileSplit.splice(fileSplit.length - 1, 0, localImport);
-                    fileStr = fileSplit.join('');
-                }
-            } catch (error) { }
-        }
-        return fileStr;
-    }
-
-    sassRender(compressed: boolean = false): Promise<boolean> {
-        let outPutSemiCSS = this.options.OUTPUT_SEMI_CSS_PATH;
-        const semiScssPath = this.options.OUTPUT_SEMI_SCSS_PATH;
-        const config: RenderOptions = {
-            file: semiScssPath,
-            importer: (url: string) => {
-                if (url.startsWith('../semi-ui/')) {
-                    const result = this.rewriteFile(url);
-                    return { contents: result };
-                }
-                return { file: url };
-            }
-        };
-        if (compressed) {
-            config.outputStyle = 'compressed';
-            outPutSemiCSS = this.options.OUTPUT_SEMI_CSS_MIN_PATH;
-        }
-
-        return new Promise((reslove, reject) => {
-            sass.render(config, function (error, result) {
-                if (error) {
-                    console.log('error: ', error);
-                    console.log(error.column, error.message);
-                    reject(false);
-                } else {
-                    fs.outputFile(outPutSemiCSS, result.css)
-                        .then(res => {
-                            reslove(true);
-                        })
-                        .catch(err => {
-                            console.log('err: ', err);
-                            reject(false);
-                        });
-                }
-            });
-        });
-    }
-
-    async compile() {
-        await this.generateSemiScss();
-        const compileResult = await this.sassRender();
-        let compileMinResult = true;
-        if (this.options.OUTPUT_SEMI_CSS_MIN_PATH) {
-            compileMinResult = await this.sassRender(true);
-        }
-        return compileResult && compileMinResult;
-    }
-}

+ 33 - 26
packages/semi-scss-compile/package.json

@@ -1,28 +1,35 @@
 {
-    "name": "@douyinfe/semi-scss-compile",
-    "version": "2.0.1",
-    "description": "compile semi scss to css",
-    "author": "[email protected]",
-    "license": "MIT",
-    "main": "lib/index.js",
-    "files": [
-        "lib"
-    ],
-    "keywords": [
-        "semi-scss-compiler",
-        "scss"
-    ],
-    "scripts": {
-        "build:lib": "tsc",
-        "prepublishOnly": "npm run build:lib"
-    },
-    "dependencies": {
-        "fs-extra": "^8.1.0",
-        "sass": "1.32.13"
-    },
-    "devDependencies": {
-        "@types/sass": "^1.16.1",
-        "typescript": "^4.4.4"
-    },
-    "gitHead": "661ac2de772e28c3b6f934f70e4e08efbe2387b2"
+  "name": "@douyinfe/semi-scss-compile",
+  "version": "2.0.1",
+  "description": "compile semi scss to css",
+  "author": "[email protected]",
+  "license": "MIT",
+  "main": "lib/index.js",
+  "types": "lib/index.d.ts",
+  "bin": {
+    "semiScssCompile": "lib/bin.js"
+  },
+  "files": [
+    "lib",
+    "src"
+  ],
+  "keywords": [
+    "semi-scss-compiler",
+    "scss"
+  ],
+  "scripts": {
+    "build:lib": "tsc",
+    "prepublishOnly": "rm -rf lib && npm run build:lib"
+  },
+  "dependencies": {
+    "arg": "^5.0.1",
+    "fs-extra": "^8.1.0",
+    "lodash": "^4.17.21",
+    "sass": "^1.43.4"
+  },
+  "devDependencies": {
+    "@types/lodash": "^4.14.176",
+    "@types/sass": "^1.43.0",
+    "typescript": "^4.4.4"
+  }
 }

+ 27 - 0
packages/semi-scss-compile/src/bin.ts

@@ -0,0 +1,27 @@
+import arg from "arg";
+import {compile} from "./index";
+
+const main = () => {
+    const userArgs = arg({
+        "--foundation": String,
+        "--theme": String,
+        "--output": String,
+        "--min": Boolean,
+
+        "-f": "--foundation",
+        "-t": "--theme",
+        "-o": "--output",
+        "-m": "--min"
+    }, {permissive: true});
+    const {"--foundation": foundationPath, '--theme': themePath, '--output': outputPath, '--min': isMin} = userArgs;
+    console.log(`foundationPath: ${foundationPath},\nthemePath: ${themePath},\noutputPath: ${outputPath}\n`);
+    if (foundationPath && themePath && outputPath) {
+        compile(foundationPath, themePath, outputPath, {
+            isMin
+        });
+    } else {
+        console.error('Error: lack of args.')
+    }
+}
+
+main();

+ 26 - 0
packages/semi-scss-compile/src/index.ts

@@ -0,0 +1,26 @@
+import generateScssMap from "./utils/generateSCSSMap";
+import writeFile from "./utils/writeFile";
+import compilerFromScssMap from "./utils/compiler";
+import path from "path";
+import fs from 'fs-extra';
+
+
+export interface Options {
+    isMin?: boolean,
+}
+
+const compile = (foundationPath: string, themePath: string, outputPath: string, {isMin = false}: Options = {}) => {
+    const scssMap = generateScssMap(foundationPath, themePath);
+    const tempDir = writeFile(scssMap)
+    const result = compilerFromScssMap(path.join(tempDir, 'index.scss'), isMin);
+    fs.outputFileSync(outputPath, result.css);
+}
+
+
+export {
+    compile,
+    generateScssMap,
+    compilerFromScssMap,
+    writeFile
+};
+

+ 9 - 0
packages/semi-scss-compile/src/utils/compiler.ts

@@ -0,0 +1,9 @@
+import sass from 'sass';
+
+
+const compile = (entryScssPath: string, isMin: boolean = false) => {
+    const result = sass.renderSync({file: entryScssPath, outputStyle: isMin ? 'compressed' : 'expanded'});
+    return result
+}
+
+export default compile;

+ 47 - 0
packages/semi-scss-compile/src/utils/generateSCSSMap.ts

@@ -0,0 +1,47 @@
+import path from 'path';
+import fs from "fs-extra";
+import {set} from 'lodash';
+
+const lodash = {set};
+
+const generateComponentsScssMap = (foundationPath: string) => {
+    const foundationComponentList = fs.readdirSync(foundationPath);
+    const componentScssMap: { [componentName: string]: { [scssFileName: string]: string } } = {};
+    foundationComponentList.forEach(fileName => {
+        const fileAbsolutePath = path.join(foundationPath, fileName);
+        if (fs.existsSync(fileAbsolutePath) && fs.statSync(fileAbsolutePath).isDirectory()) {
+            //in component folder
+            const componentPath = fileAbsolutePath;
+            const scssFileList = fs.readdirSync(componentPath).filter((fileName) => fileName.endsWith('.scss'));
+            scssFileList.forEach(scssFileName => {
+                const scssRaw = fs.readFileSync(path.join(componentPath, scssFileName), {encoding: 'utf-8'});
+                lodash.set(componentScssMap, [fileName, scssFileName], scssRaw);
+            })
+        }
+    });
+    return componentScssMap;
+}
+
+
+const generateThemeScssMap = (themePath: string) => {
+    const fileList = ['_font.scss', '_palette.scss', 'global.scss', 'index.scss', 'local.scss', 'mixin.scss', 'variables.scss'] as const;
+    const themeScssMap: { [key in typeof fileList[number]]?: string } = {}
+    for (const fileName of fileList) {
+        const scssAbsolutePath = path.join(themePath, 'scss', fileName);
+        if (fs.existsSync(scssAbsolutePath)) {
+            //in theme folder
+            themeScssMap[fileName] = fs.readFileSync(scssAbsolutePath, {encoding: "utf8"});
+        }
+    }
+    // console.log(themeScssMap)
+    return themeScssMap;
+};
+
+const generateScssMap = (foundationPath: string, themePath: string) => {
+    return {
+        components: generateComponentsScssMap(foundationPath),
+        theme: generateThemeScssMap(themePath)
+    }
+}
+
+export default generateScssMap;

+ 86 - 0
packages/semi-scss-compile/src/utils/writeFile.ts

@@ -0,0 +1,86 @@
+import fs from 'fs-extra';
+import path from 'path';
+import os from 'os';
+import generateScssMap from "./generateSCSSMap";
+import {cloneDeep, omit} from "lodash";
+
+const lodash = {cloneDeep, omit};
+
+const writeComponentScss = (scssMap: { [p: string]: { [p: string]: string } }, tempDir: string) => {
+    for (const componentName of Object.keys(scssMap)) {
+        const componentDirPath = path.join(tempDir, 'components', componentName);
+        fs.emptyDirSync(componentDirPath);
+        for (const scssFileName of Object.keys(scssMap[componentName])) {
+            fs.writeFileSync(path.join(componentDirPath, scssFileName), scssMap[componentName][scssFileName], {encoding: 'utf-8'});
+        }
+    }
+    return;
+};
+
+const writeThemeScss = (scssMap: (ReturnType<typeof generateScssMap>)['theme'], tempDir: string) => {
+    const themeDirPath = path.join(tempDir, 'theme');
+    fs.emptyDirSync(themeDirPath);
+
+    for (const scssFileName of Object.keys(scssMap)) {
+        fs.writeFileSync(path.join(themeDirPath, scssFileName), scssMap[scssFileName as keyof typeof scssMap] as string, {encoding: "utf8"});
+    }
+    return;
+}
+
+const preProcessScssMap = (scssMapOrigin: ReturnType<typeof generateScssMap>) => {
+    const scssMap = lodash.cloneDeep(scssMapOrigin);
+
+    //----- generate entry -----
+    let compilerEntryContent = '';
+    compilerEntryContent += `@import "./theme/index.scss";\n`
+    compilerEntryContent += `@import "./theme/global.scss";\n`;
+
+    for (const componentName of Object.keys(scssMap['components'])) {
+        let scssFileName = `${componentName}.scss`;
+        //edge case portal keyframes, cause their folderName and scssFilename not match.
+        if (componentName === '_portal') {
+            scssFileName = 'portal.scss'
+        } else if (componentName === 'keyframes') {
+            scssFileName = 'rotate.scss'
+        }
+        compilerEntryContent += `@import "./components/${componentName}/${scssFileName}";\n`;
+    }
+    //edge case for iconButton and textarea
+    compilerEntryContent += `@import "./components/button/iconButton.scss";\n`
+    compilerEntryContent += `@import "./components/input/textarea.scss";\n`
+    //----- generate entry end -----
+
+
+    //----- inject component token file local.scss to component's variables.scss -----
+    const themeLocalRaw = scssMap.theme["local.scss"];
+    if (themeLocalRaw) {
+        for (const componentName of Object.keys(scssMap['components'])) {
+            if (scssMap["components"][componentName]['variables.scss']) {
+                scssMap["components"][componentName]['variables.scss'] += `\n\n\n\n//inject custom theme variables\n${themeLocalRaw}`;
+            }
+        }
+    }
+    //----- inject end -----
+
+    return {
+        ...{
+            components: scssMap["components"],
+            theme: lodash.omit(scssMap['theme'], 'local.scss')
+        },
+        index: compilerEntryContent
+    };
+}
+
+
+const writeFile = (scssMap: ReturnType<typeof generateScssMap>, tempDir: string = path.join(os.tmpdir(), `semi_scss_compile_${Date.now()}`)) => {
+    fs.emptyDirSync(tempDir);
+
+    const finalScssMapWaitForCompiling = preProcessScssMap(scssMap);
+    writeComponentScss(finalScssMapWaitForCompiling["components"], tempDir);
+    writeThemeScss(finalScssMapWaitForCompiling['theme'], tempDir);
+    //write compile entry
+    fs.writeFileSync(path.join(tempDir, 'index.scss'), finalScssMapWaitForCompiling['index'], {encoding: "utf8"});
+    return tempDir;
+}
+
+export default writeFile;

+ 16 - 22
packages/semi-scss-compile/tsconfig.json

@@ -1,24 +1,18 @@
 {
-    "compilerOptions": {
-        "target": "es6",
-        "baseUrl": "./",
-        "outDir": "lib",
-        "sourceMap": true,
-        "allowJs": true,
-        "module": "commonjs",
-        "lib": ["es7", "dom"],
-        "moduleResolution": "node",
-        "noImplicitAny": true,
-        "suppressImplicitAnyIndexErrors": true,
-        "forceConsistentCasingInFileNames": true,
-        "allowSyntheticDefaultImports": true,
-        "experimentalDecorators": true,
-        "noImplicitReturns": true,
-        "noImplicitThis": false,
-        "strictNullChecks": false,
-        "esModuleInterop": true,
-        "skipLibCheck": true,
-    },
-    "include": ["**/*.ts"],
-    "exclude": ["node_modules"]
+  "compilerOptions": {
+    "sourceMap": true,
+    "strict": true,
+    "target": "ES2017",
+    "module": "commonjs",
+    "esModuleInterop":true,
+    "moduleResolution": "Node",
+    "declaration": true,
+      "outDir": "./lib",
+      "rootDir": "./src",
+    "types": []
+
+  },
+  "include": [
+    "src/**/*"
+  ]
 }

+ 4 - 25
packages/semi-ui/scripts/compileScss.js

@@ -1,32 +1,11 @@
-const SemiThemeCompile = require('@douyinfe/semi-scss-compile').default;
-const chalk = require('chalk');
+const { compile } = require('@douyinfe/semi-scss-compile');
 const path = require('path');
-const log = console.log;
-
-const success = text => chalk.green(text);
-const errors = text => chalk.red(text);
 
 function resolve(dir) {
     return path.join(__dirname, '../..', dir);
 }
 
-const options = {
-    COMPONENT_SCSS_PATH: resolve('semi-foundation/'),
-    OUTPUT_SEMI_SCSS_PATH: resolve('semi-theme-default/semi.scss'),
-    OUTPUT_SEMI_CSS_PATH: resolve('semi-ui/dist/css/semi.css'),
-    OUTPUT_SEMI_CSS_MIN_PATH: resolve('semi-ui/dist/css/semi.min.css'),
-};
 
-const compiler = new SemiThemeCompile(options);
-compiler
-    .compile()
-    .then(res => {
-        log(success('compile css success'));
-        // console.log(res);
-        process.exitCode = 0;
-    })
-    .catch(error => {
-        log(errors('compile css failed'));
-        log(errors(error));
-        process.exitCode = 1;
-    });
+compile(resolve('semi-foundation/'), resolve('semi-theme-default/'), resolve('semi-ui/dist/css/semi.min.css'), {isMin: true})
+compile(resolve('semi-foundation/'), resolve('semi-theme-default/'), resolve('semi-ui/dist/css/semi.css'), {isMin: false})
+