Skip to content

Commit 026df10

Browse files
fix executor for dex-scss
1 parent 22d0a62 commit 026df10

4 files changed

Lines changed: 73 additions & 39 deletions

File tree

packages/devextreme-themebuilder/src/modules/compile-manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export default class CompileManager {
6969
css = removeExternalResources(css);
7070
}
7171

72-
css = addInfoHeader(css, version);
72+
css = addInfoHeader(css, version, true);
7373

7474
return {
7575
compiledMetadata: compileData.changedVariables,

packages/devextreme-themebuilder/src/modules/post-compiler.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,39 @@ export function addBasePath(css: string | Buffer, basePath: string): string {
1010
return css.toString().replace(/(url\()("|')?(icons|fonts)/g, `$1$2${normalizedPath}$3`);
1111
}
1212

13-
export function addInfoHeader(css: string | Buffer, version: string): string {
13+
function buildThemeBuilderInfoHeader(version: string): string {
1414
const generatedBy = '* Generated by the DevExpress ThemeBuilder';
1515
const versionString = `* Version: ${version}`;
1616
const link = '* http://js.devexpress.com/ThemeBuilder/';
1717

18-
const header = `/*${generatedBy}\n${versionString}\n${link}\n*/\n\n`;
18+
return `/*${generatedBy}\n${versionString}\n${link}\n*/\n\n`;
19+
}
20+
21+
export function addInfoHeader(
22+
css: string | Buffer,
23+
version: string,
24+
appendInfoHeaderAfterBody = false,
25+
): string {
26+
const header = buildThemeBuilderInfoHeader(version);
1927
const source = css.toString();
2028
const encoding = '@charset "UTF-8";';
2129

22-
if (source.startsWith(encoding)) {
23-
return `${encoding}\n${header}${source.replace(`${encoding}\n`, '')}`;
30+
// clean-css may emit @charset immediately followed by :root / @import with no newline.
31+
const charsetPrefix = /^@charset\s+"utf-8";\s*/i;
32+
const match = source.match(charsetPrefix);
33+
if (match) {
34+
const rest = source.slice(match[0].length).trimStart();
35+
36+
if (appendInfoHeaderAfterBody) {
37+
const joined = `${encoding.trimEnd()}${rest}`.replace(
38+
/^(@charset\s+"utf-8";)\s+/i,
39+
'$1',
40+
);
41+
return `${joined}\n${header}`;
42+
}
43+
return `${encoding}\n${header}${rest}`;
2444
}
25-
return `${header}${css}`;
45+
return `${header}${source}`;
2646
}
2747

2848
export async function cleanCss(css: string): Promise<string> {

packages/nx-infra-plugin/src/executors/scss-build/executor.e2e.spec.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ function createMockModules(workspaceRoot: string, projectRoot: string): void {
1616
'}',
1717
'module.exports = {',
1818
' SassString,',
19-
" compile: () => ({ css: '@charset \"UTF-8\"; .a{display:flex}' })",
19+
' compile: () => ({ css: \'@charset "UTF-8"; .a{display:flex}\' })',
2020
'};',
2121
'',
2222
].join('\n'),
@@ -102,8 +102,14 @@ async function setupProjectStructure(workspaceRoot: string): Promise<string> {
102102
].join('\n'),
103103
);
104104

105-
await writeFileText(path.join(buildDir, 'bundle-template.common.scss'), '.common { color: red; }');
106-
await writeFileText(path.join(buildDir, 'bundle-template.generic.scss'), '.generic-$COLOR { color: red; }');
105+
await writeFileText(
106+
path.join(buildDir, 'bundle-template.common.scss'),
107+
'.common { color: red; }',
108+
);
109+
await writeFileText(
110+
path.join(buildDir, 'bundle-template.generic.scss'),
111+
'.generic-$COLOR { color: red; }',
112+
);
107113

108114
createMockModules(workspaceRoot, projectRoot);
109115
return projectRoot;

packages/nx-infra-plugin/src/executors/scss-build/executor.ts

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const EULA_URL = 'https://js.devexpress.com/Licensing/';
2929
interface BuildDependencies {
3030
sass: any;
3131
postcss: any;
32-
autoprefixer: () => any;
32+
autoprefixer: (options?: { overrideBrowserslist?: string[] }) => any;
3333
CleanCss: new (options: unknown) => { minify: (input: string) => { styles: string } };
3434
themeOptions: { getThemes: () => Array<[string, string, string, string?]> };
3535
cleanCssSanitizeOptions: unknown;
@@ -51,9 +51,13 @@ function resolveDataUri(filePath: string, svgEncoding?: string): string {
5151
return `data:image/${ext};base64,${data.toString('base64')}`;
5252
}
5353

54-
function createLicenseHeader(fileName: string, version: string): string {
54+
/**
55+
* Same shape as `packages/devextreme/build/gulp/license-header.txt` with
56+
* `gulp-header` `commentType: '*'` (starLicense) — matches legacy Gulp output.
57+
*/
58+
function createStarLicenseHeader(fileName: string, version: string): string {
5559
return [
56-
'/*!',
60+
'/**',
5761
`* DevExtreme (${fileName.replace(/\\/g, '/')})`,
5862
`* Version: ${version}`,
5963
`* Build date: ${new Date().toDateString()}`,
@@ -65,24 +69,24 @@ function createLicenseHeader(fileName: string, version: string): string {
6569
].join('\n');
6670
}
6771

68-
function moveCharsetToTop(css: string): string {
69-
const match = css.match(/@charset\s+[^;]+;\s*/);
70-
if (!match) {
71-
return css;
72-
}
73-
74-
const charset = match[0];
75-
const withoutCharset = css.replace(charset, '');
76-
return charset + withoutCharset;
72+
/**
73+
* Mirrors `style-compiler.js`: starLicense prepend, then
74+
* `.replace(/([\s\S]*)(@charset.*?;\s)/, '$2$1')` so `@charset` is the first bytes of output.
75+
*/
76+
function prependLicenseAndMoveCharsetFirst(minifiedCss: string, license: string): string {
77+
const withLicense = `${license}${minifiedCss}`;
78+
return withLicense.replace(/([\s\S]*)(@charset[^;]+;\s*)/, '$2$1');
7779
}
7880

7981
function generateBundleName(theme: string, size: string, color: string, mode?: string): string {
80-
return 'dx'
82+
return (
83+
'dx'
8184
+ (theme === 'material' || theme === 'fluent' ? `.${theme}` : '')
8285
+ `.${color}`
8386
+ (mode ? `.${mode}` : '')
8487
+ (size === 'default' ? '' : '.compact')
85-
+ '.scss';
88+
+ '.scss'
89+
);
8690
}
8791

8892
async function generateScssBundles(
@@ -100,7 +104,10 @@ async function generateScssBundles(
100104
const themes = deps.themeOptions.getThemes();
101105
for (const [theme, size, color, mode] of themes) {
102106
const template = await readTemplate(theme);
103-
const content = template.replace('$COLOR', color).replace('$SIZE', size).replace('$MODE', mode || '');
107+
const content = template
108+
.replace('$COLOR', color)
109+
.replace('$SIZE', size)
110+
.replace('$MODE', mode || '');
104111
const fileName = generateBundleName(theme, size, color, mode);
105112
await writeFileText(path.join(resolvedBundlesDir, fileName), content);
106113
}
@@ -121,11 +128,14 @@ function loadDependencies(projectRoot: string): BuildDependencies {
121128
themeOptions: projectRequire(path.resolve(projectRoot, 'build/theme-options.cjs')) as {
122129
getThemes: () => Array<[string, string, string, string?]>;
123130
},
124-
cleanCssSanitizeOptions: projectRequire(path.resolve(projectRoot, 'build/clean-css-options.json')),
131+
cleanCssSanitizeOptions: projectRequire(
132+
path.resolve(projectRoot, 'build/clean-css-options.json'),
133+
),
125134
cleanCssDevOptions: workspaceRequire(
126135
path.resolve(projectRoot, '../devextreme-themebuilder/src/data/clean-css-options.json'),
127136
),
128-
devextremeVersion: workspaceRequire(path.resolve(projectRoot, '../devextreme/package.json')).version,
137+
devextremeVersion: workspaceRequire(path.resolve(projectRoot, '../devextreme/package.json'))
138+
.version,
129139
};
130140
}
131141

@@ -188,15 +198,17 @@ async function compileFile(
188198

189199
const postcssFactory = (deps.postcss as unknown as { default?: any }).default || deps.postcss;
190200
const prefixed = await postcssFactory([deps.autoprefixer()]).process(compiled.css, {
191-
from: undefined,
201+
from: sourceFile,
192202
});
193203

194-
const minifierOptions = minifyProfile === 'ci' ? deps.cleanCssDevOptions : deps.cleanCssSanitizeOptions;
204+
const minifierOptions =
205+
minifyProfile === 'ci' ? deps.cleanCssDevOptions : deps.cleanCssSanitizeOptions;
195206
const minifier = new deps.CleanCss(minifierOptions);
196207
const minified = minifier.minify(prefixed.css).styles;
197208

198209
const outFileName = path.basename(sourceFile, '.scss') + '.css';
199-
const withHeader = createLicenseHeader(outFileName, deps.devextremeVersion) + moveCharsetToTop(minified);
210+
const license = createStarLicenseHeader(outFileName, deps.devextremeVersion);
211+
const withHeader = prependLicenseAndMoveCharsetFirst(minified, license);
200212
await writeFileText(path.join(outputDir, outFileName), withHeader);
201213
}
202214

@@ -319,16 +331,12 @@ async function runWatchBuild(
319331
}, 200);
320332
};
321333

322-
const watcher = fs.watch(
323-
watchDir,
324-
{ recursive: true },
325-
(_eventType, fileName) => {
326-
if (!fileName || !fileName.endsWith('.scss')) {
327-
return;
328-
}
329-
scheduleRebuild();
330-
},
331-
);
334+
const watcher = fs.watch(watchDir, { recursive: true }, (_eventType, fileName) => {
335+
if (!fileName || !fileName.endsWith('.scss')) {
336+
return;
337+
}
338+
scheduleRebuild();
339+
});
332340

333341
const stopWatcher = () => {
334342
watcher.close();

0 commit comments

Comments
 (0)