Skip to content
Open
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
8 changes: 6 additions & 2 deletions packages/devextreme-scss/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@
],
"outputs": [
"{projectRoot}/scss/bundles",
"{workspaceRoot}/packages/devextreme/artifacts/css/dx.*.css"
"{workspaceRoot}/packages/devextreme/artifacts/css/dx.*.css",
"{workspaceRoot}/packages/devextreme/artifacts/css/accents"
],
"cache": true
},
Expand All @@ -113,7 +114,8 @@
],
"outputs": [
"{projectRoot}/scss/bundles",
"{workspaceRoot}/packages/devextreme/artifacts/css/dx.*.css"
"{workspaceRoot}/packages/devextreme/artifacts/css/dx.*.css",
"{workspaceRoot}/packages/devextreme/artifacts/css/accents"
],
"cache": true
},
Expand All @@ -139,6 +141,7 @@
"outputs": [
"{projectRoot}/scss/bundles",
"{workspaceRoot}/packages/devextreme/artifacts/css/dx.*.css",
"{workspaceRoot}/packages/devextreme/artifacts/css/accents",
"{workspaceRoot}/packages/devextreme/artifacts/css/fonts",
"{workspaceRoot}/packages/devextreme/artifacts/css/icons"
],
Expand Down Expand Up @@ -167,6 +170,7 @@
"outputs": [
"{projectRoot}/scss/bundles",
"{workspaceRoot}/packages/devextreme/artifacts/css/dx.*.css",
"{workspaceRoot}/packages/devextreme/artifacts/css/accents",
"{workspaceRoot}/packages/devextreme/artifacts/css/fonts",
"{workspaceRoot}/packages/devextreme/artifacts/css/icons"
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,9 @@
$color: null !default;
$mode: null !default;

$theme-marker-color: null !default;
$theme-marker-color: $color !default;
$theme-marker-mode: null !default;

@if $color == "blue" {
$theme-marker-color: "blue" !default;
}

@if $mode == "light" {
$theme-marker-mode: "light" !default;
}
Expand Down

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

37 changes: 37 additions & 0 deletions packages/devextreme-scss/tests/accent-custom.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as fs from 'fs';
import * as path from 'path';

const scssRoot = path.resolve(__dirname, '..', 'scss');
const designedAccentsDir = path.join(scssRoot, '_design-system', 'fluent', 'accents');
const customAccentPath = path.join(scssRoot, 'widgets', 'fluent-next', 'accents', 'custom.css');

const readSteps = (filePath: string): string[] => {
const content = fs.readFileSync(filePath, 'utf8');
return [...content.matchAll(/--dxds-primary-(\d+)\s*:/g)].map((match) => match[1]);
};

const designedPalettes = fs.readdirSync(designedAccentsDir)
.filter((name) => name.endsWith('.scss'))
.map((name) => ({ name, steps: readSteps(path.join(designedAccentsDir, name)) }));

describe('accents/custom.css', () => {
it('covers exactly the steps the designed palettes declare', () => {
const customAccentSteps = readSteps(customAccentPath);

expect(designedPalettes.length).toBeGreaterThan(0);
designedPalettes.forEach(({ name, steps }) => {
expect([name, customAccentSteps]).toEqual([name, steps]);
});
});

it('derives every step from --dx-accent-color instead of a literal color', () => {
const declarations = fs.readFileSync(customAccentPath, 'utf8')
.match(/--dxds-primary-\d+\s*:[^;]+;/g) ?? [];

expect(declarations).toHaveLength(readSteps(customAccentPath).length);
declarations.forEach((declaration) => {
expect(declaration).toContain('oklch(from var(--dx-accent-color)');
expect(declaration).not.toMatch(/#[0-9a-f]{3,8}\b/i);
});
});
});
6 changes: 5 additions & 1 deletion packages/devextreme-scss/tools/naming/derive-registries.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,12 @@ const OVERRIDES = {
* `typography` is a registered concern — the common/ criterion, satisfied without moving anything.
* Moving the declarations into common/ was tried and reverted: it pulled common/ earlier in the
* load order and shifted `--dx-line-height` inside its :root block, i.e. it changed the emitted CSS.
*
* `accents/` holds no SCSS at all: it is the hand-written companion of the generated palettes in
* `_design-system/fluent/accents/`, shipped as a standalone stylesheet next to them in
* `dist/css/accents/`. It declares design-system custom properties, never theme variables.
*/
systemFolders: ['common', 'typography'],
systemFolders: ['accents', 'common', 'typography'],

// component -> folder that is allowed to declare it (O2: exactly one declaration home).
// Only needed where more than one folder currently declares the component's variables.
Expand Down
3 changes: 2 additions & 1 deletion packages/devextreme-scss/tools/naming/registries.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"derivedFrom": {
"componentTokens": "@devexpress/design-tokens-internal → components/core/theme/fluent",
"componentTokenCount": 601,
"themeFolders": 86
"themeFolders": 87
},
"components": {
"accordion": "accordion",
Expand Down Expand Up @@ -178,6 +178,7 @@
"widget": "widget"
},
"systemFolders": [
"accents",
"common",
"typography"
],
Expand Down
13 changes: 12 additions & 1 deletion packages/devextreme/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -1226,13 +1226,24 @@
"executor": "devextreme-nx-infra-plugin:scss-assemble",
"options": {
"scssPackagePath": "../devextreme-scss",
"outputDir": "./artifacts/npm/devextreme/scss"
"outputDir": "./artifacts/npm/devextreme/scss",
"exclude": [
"widgets/fluent-next/**",
"_design-system/**",
"bundles/dx.fluent-next.*.scss"
]
},
"configurations": {
"internal": {
"outputDir": "./artifacts/npm/devextreme-internal/scss"
}
},
"dependsOn": [
{
"projects": ["devextreme-scss"],
"target": "build:tokens"
}
],
"inputs": [
"{workspaceRoot}/packages/devextreme-scss/scss/**/*",
"{workspaceRoot}/packages/devextreme-scss/fonts/**/*",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,29 @@ describe('ScssAssembleExecutor E2E', () => {
expect(content).toContain(expectedSvg);
expect(content).toContain(expectedPng);
});

it('should keep excluded sources out of the package', async () => {
await writeFileText(
path.join(scssPackageDir, 'scss', 'widgets', 'kept', '_index.scss'),
'.a {}',
);
await writeFileText(
path.join(scssPackageDir, 'scss', 'widgets', 'dropped', '_index.scss'),
'.b {}',
);
await writeFileText(path.join(scssPackageDir, 'scss', 'bundles', 'dx.kept.scss'), '.c {}');
await writeFileText(path.join(scssPackageDir, 'scss', 'bundles', 'dx.dropped.scss'), '.d {}');

const context = createMockContext({ root: tempDir });
const result = await executor(
{ ...OPTIONS, exclude: ['widgets/dropped/**', 'bundles/dx.dropped.scss'] },
context,
);

expect(result.success).toBe(true);
expect(fs.existsSync(path.join(outputDir, 'widgets', 'kept', '_index.scss'))).toBe(true);
expect(fs.existsSync(path.join(outputDir, 'bundles', 'dx.kept.scss'))).toBe(true);
expect(fs.existsSync(path.join(outputDir, 'widgets', 'dropped'))).toBe(false);
expect(fs.existsSync(path.join(outputDir, 'bundles', 'dx.dropped.scss'))).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@
"outputDir": {
"type": "string",
"description": "Output directory for assembled SCSS files (relative to project root)."
},
"exclude": {
"type": "array",
"description": "Glob patterns, relative to the scss directory, kept out of the package.",
"items": {
"type": "string"
}
}
},
"required": ["scssPackagePath", "outputDir"]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export interface ScssAssembleExecutorSchema {
scssPackagePath: string;
outputDir: string;
exclude?: string[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ async function inlineDataUri(content: string, scssRoot: string): Promise<string>
async function copyScssWithInlineDataUri(
scssPackagePath: string,
outputDir: string,
exclude: string[],
): Promise<void> {
const scssSourceDir = path.join(scssPackagePath, 'scss');
const cwd = toPosixPath(scssSourceDir);
const relPaths = await glob('**/*', { cwd, nodir: true });
const relPaths = await glob('**/*', { cwd, nodir: true, ignore: exclude });

await Promise.all(
relPaths.map(async (relPath) => {
Expand Down Expand Up @@ -77,18 +78,19 @@ async function copyIcons(scssPackagePath: string, outputDir: string): Promise<vo
interface ResolvedScssAssemble {
scssPackagePath: string;
outputDir: string;
exclude: string[];
}

export default createExecutor<ScssAssembleExecutorSchema, ResolvedScssAssemble>({
name: 'ScssAssemble',
resolve: (options, { projectRoot }) => {
const scssPackagePath = path.resolve(projectRoot, options.scssPackagePath);
const outputDir = path.resolve(projectRoot, options.outputDir);
return { scssPackagePath, outputDir };
return { scssPackagePath, outputDir, exclude: options.exclude ?? [] };
},
run: async ({ scssPackagePath, outputDir }) => {
run: async ({ scssPackagePath, outputDir, exclude }) => {
await Promise.all([
copyScssWithInlineDataUri(scssPackagePath, outputDir),
copyScssWithInlineDataUri(scssPackagePath, outputDir, exclude),
copyFonts(scssPackagePath, outputDir),
copyIcons(scssPackagePath, outputDir),
]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as fs from 'fs';
import * as path from 'path';
import executor from './executor';
import executor, { findMissingThemeCss } from './executor';
import { ScssBuildExecutorSchema } from './schema';
import { createMockContext, createTempDir, cleanupTempDir } from '../../utils/test-utils';
import { writeFileText, writeJson, readFileText } from '../../utils';
Expand Down Expand Up @@ -29,7 +29,7 @@ function createMockModules(projectRoot: string): void {
'}',
'module.exports = {',
' SassString,',
' compile: () => ({ css: \'@charset "UTF-8"; .a{display:flex}\' })',
' compile: () => ({ css: \'/**\\n * Do not edit directly, this file was auto-generated.\\n */\\n@charset "UTF-8"; .a{display:flex}\' })',
'};',
'',
].join('\n'),
Expand Down Expand Up @@ -144,6 +144,11 @@ async function setupProjectStructure(workspaceRoot: string): Promise<string> {
'.generic-$COLOR { color: red; }',
);

await writeFileText(
path.join(projectRoot, 'scss', '_design-system', 'fluent', 'accents', 'blue.scss'),
':root { --dxds-primary-100: #0f6cbd; }',
);

createMockModules(projectRoot);
return projectRoot;
}
Expand Down Expand Up @@ -189,6 +194,42 @@ describe('ScssBuildExecutor E2E', () => {
expect(commonCss).toContain('DevExtreme (dx.common.css)');
});

it('compiles design-system accent sources into the accents subfolder without minification', async () => {
const projectRoot = await setupProjectStructure(tempDir);
await writeFileText(
path.join(projectRoot, 'scss', '_design-system', 'fluent', 'accents', 'storm.scss'),
':root { --dxds-primary-100: #6d6a68; }',
);
await writeFileText(
path.join(projectRoot, 'scss', 'widgets', 'fluent-next', 'accents', 'custom.css'),
':root { --dxds-primary-100: oklch(from var(--dx-accent-color) l c h); }',
);

const context = createMockContext({
root: tempDir,
projectName: 'devextreme-scss',
projectRoot: 'packages/devextreme-scss',
});

const options: ScssBuildExecutorSchema = { mode: 'all', cssOutputDir: './artifacts/css' };
const result = await executor(options, context);

expect(result.success).toBe(true);

const stormCss = await readFileText(
path.join(projectRoot, 'artifacts', 'css', 'accents', 'storm.css'),
);
expect(stormCss).toContain('DevExtreme (storm.css)');
expect(stormCss).not.toContain('/*min:');
expect(stormCss).not.toContain('/*prefixed*/');
expect(stormCss).not.toContain('auto-generated');

const customAccentCss = await readFileText(
path.join(projectRoot, 'artifacts', 'css', 'accents', 'custom.css'),
);
expect(customAccentCss).toContain('DevExtreme (custom.css)');
});

it('builds ci mode only for selected dev bundles and uses ci profile', async () => {
const projectRoot = await setupProjectStructure(tempDir);
const context = createMockContext({
Expand Down Expand Up @@ -219,6 +260,41 @@ describe('ScssBuildExecutor E2E', () => {
expect(fs.existsSync(path.join(projectRoot, 'scss', 'bundles', 'dx.common.scss'))).toBe(true);
});

it('fails when the design-system produced no accent palettes', async () => {
const projectRoot = await setupProjectStructure(tempDir);
fs.rmSync(path.join(projectRoot, 'scss', '_design-system'), { recursive: true });

const context = createMockContext({
root: tempDir,
projectName: 'devextreme-scss',
projectRoot: 'packages/devextreme-scss',
});

const options: ScssBuildExecutorSchema = { mode: 'all', cssOutputDir: './artifacts/css' };
const result = await executor(options, context);

expect(result.success).toBe(false);
});

it('reports declared themes that left no CSS behind', async () => {
const projectRoot = await setupProjectStructure(tempDir);
const context = createMockContext({
root: tempDir,
projectName: 'devextreme-scss',
projectRoot: 'packages/devextreme-scss',
});

await executor({ mode: 'all', cssOutputDir: './artifacts/css' }, context);

const cssDir = path.join(projectRoot, 'artifacts', 'css');
const deps = { themeOptions: { getThemes: () => [['generic', 'default', 'light']] } };

expect(findMissingThemeCss(cssDir, deps as never)).toEqual([]);

fs.rmSync(path.join(cssDir, 'dx.light.css'));
expect(findMissingThemeCss(cssDir, deps as never)).toEqual(['dx.light.css']);
});

it('fails in ci mode when a configured bundle source is missing', async () => {
await setupProjectStructure(tempDir);
const context = createMockContext({
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default } from './scss-build.impl';
export { findMissingThemeCss } from './scss-build.impl';
Loading
Loading