index.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. /* eslint-disable prefer-promise-reject-errors */
  2. import path from 'path';
  3. import sass, { Options as RenderOptions } from 'sass';
  4. import fs from 'fs-extra';
  5. export interface CompileScssOptions {
  6. COMPONENT_SCSS_PATH: string;
  7. OUTPUT_SEMI_SCSS_PATH: string;
  8. OUTPUT_SEMI_CSS_PATH: string;
  9. OUTPUT_SEMI_CSS_MIN_PATH: string;
  10. COMPONENT_EXTRA_SCSS_PATH?: string;
  11. useAbsolutePath?: boolean;
  12. }
  13. const defaultOptions = {
  14. COMPONENT_SCSS_PATH: 'tempory/semi-ui/',
  15. OUTPUT_SEMI_SCSS_PATH: 'tempory/release/semi.scss',
  16. OUTPUT_SEMI_CSS_PATH: 'tempory/release/css/semi.css',
  17. OUTPUT_SEMI_CSS_MIN_PATH: 'tempory/release/css/semi-min.css',
  18. };
  19. export default class CompileScss {
  20. options: CompileScssOptions;
  21. /**
  22. * @param {object} [options]
  23. * @param {string} [options.COMPONENT_SCSS_PATH]
  24. * @param {string} [options.COMPONENT_EXTRA_SCSS_PATH]
  25. * @param {string} [options.OUTPUT_SEMI_SCSS_PATH]
  26. * @param {string} [options.OUTPUT_SEMI_CSS_PATH]
  27. */
  28. constructor(options = defaultOptions) {
  29. // console.log(options)
  30. this.options = options;
  31. }
  32. getScssFolderMap(filepath: string) {
  33. return fs
  34. .readdir(filepath)
  35. .then(files => {
  36. const folderWithScss: string[] = [];
  37. files.forEach(fileName => {
  38. const scssFile = path.join(this.options.COMPONENT_SCSS_PATH, fileName, `${fileName}.scss`);
  39. try {
  40. const stats = fs.statSync(scssFile);
  41. if (stats.isFile()) {
  42. folderWithScss.push(fileName); // Valid file path is pushed
  43. }
  44. } catch (error) {
  45. // console.log(error)
  46. }
  47. });
  48. return folderWithScss;
  49. })
  50. .catch(error => {
  51. console.error(error);
  52. throw error;
  53. });
  54. }
  55. async generateSemiScss() {
  56. const componentScssPath = this.options.COMPONENT_SCSS_PATH;
  57. const outPutSemiScss = this.options.OUTPUT_SEMI_SCSS_PATH;
  58. const outPutScss = outPutSemiScss.split('semi.scss')[0] + 'scss';
  59. const folderWithScss = await this.getScssFolderMap(componentScssPath);
  60. 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
  61. 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
  62. let indexScss = '@import "./scss/index.scss";';
  63. let globalScss = '@import "./scss/global.scss";';
  64. let semiUIPath = this.options.useAbsolutePath ? absolutePath : relativePath;
  65. if (this.options.useAbsolutePath) {
  66. semiUIPath = absolutePath;
  67. indexScss = `@import "${outPutScss}/index.scss";`;
  68. globalScss = `@import "${outPutScss}/global.scss";`;
  69. }
  70. const componentScss = folderWithScss
  71. .map(scssFile => `@import "${semiUIPath}/${scssFile}/${scssFile}.scss"`)
  72. .concat([
  73. `@import "${semiUIPath}/button/iconButton.scss"`,
  74. `@import "${semiUIPath}/input/textarea.scss"`,
  75. ]) // Handle the scss of iconButton/textarea separately
  76. .join(';\n');
  77. const fileContent = `${indexScss}\n${globalScss}\n${componentScss}`;
  78. return fs.outputFile(outPutSemiScss, fileContent);
  79. }
  80. rewriteFile(filePath: string) {
  81. const extraImport = this.options.COMPONENT_EXTRA_SCSS_PATH;
  82. let fileStr = fs.readFileSync(filePath, 'utf-8');
  83. if (extraImport) {
  84. const localImport = `\n@import "${extraImport}";`;
  85. try {
  86. const regex = /(@import '.\/variables.scss';?|@import ".\/variables.scss";?)/g;
  87. const fileSplit = fileStr.split(regex).filter(item => !!item);
  88. if (fileSplit.length > 1) {
  89. fileSplit.splice(fileSplit.length - 1, 0, localImport);
  90. fileStr = fileSplit.join('');
  91. }
  92. } catch (error) { }
  93. }
  94. return fileStr;
  95. }
  96. sassRender(compressed: boolean = false): Promise<boolean> {
  97. let outPutSemiCSS = this.options.OUTPUT_SEMI_CSS_PATH;
  98. const semiScssPath = this.options.OUTPUT_SEMI_SCSS_PATH;
  99. const config: RenderOptions = {
  100. file: semiScssPath,
  101. importer: (url: string) => {
  102. if (url.startsWith('../semi-ui/')) {
  103. const result = this.rewriteFile(url);
  104. return { contents: result };
  105. }
  106. return { file: url };
  107. }
  108. };
  109. if (compressed) {
  110. config.outputStyle = 'compressed';
  111. outPutSemiCSS = this.options.OUTPUT_SEMI_CSS_MIN_PATH;
  112. }
  113. return new Promise((reslove, reject) => {
  114. sass.render(config, function (error, result) {
  115. if (error) {
  116. console.log('error: ', error);
  117. console.log(error.column, error.message);
  118. reject(false);
  119. } else {
  120. fs.outputFile(outPutSemiCSS, result.css)
  121. .then(res => {
  122. reslove(true);
  123. })
  124. .catch(err => {
  125. console.log('err: ', err);
  126. reject(false);
  127. });
  128. }
  129. });
  130. });
  131. }
  132. async compile() {
  133. await this.generateSemiScss();
  134. const compileResult = await this.sassRender();
  135. let compileMinResult = true;
  136. if (this.options.OUTPUT_SEMI_CSS_MIN_PATH) {
  137. compileMinResult = await this.sassRender(true);
  138. }
  139. return compileResult && compileMinResult;
  140. }
  141. }