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.

59 changes: 59 additions & 0 deletions packages/devextreme-scss/tests/accent-custom.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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 readPrimaryDeclarations = (filePath: string): string[] =>
fs.readFileSync(filePath, 'utf8').match(/--dxds-primary-\d+\s*:[^;]+;/g) ?? [];

const readStepColor = (filePath: string, step: string): string => {
const content = fs.readFileSync(filePath, 'utf8');
const match = new RegExp(`--dxds-primary-${step}\\s*:\\s*(#[0-9a-f]{3,8})\\b`, 'i').exec(content);

if (!match) {
throw new Error(`No literal color for step ${step} in ${filePath}`);
}

return match[1];
};

const defaultPalettePath = path.join(designedAccentsDir, 'blue.scss');

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-source instead of a literal color', () => {
const declarations = readPrimaryDeclarations(customAccentPath);

expect(declarations).toHaveLength(readSteps(defaultPalettePath).length);
declarations.forEach((declaration) => {
expect(declaration).toContain('oklch(from var(--dx-accent-color-source)');
expect(declaration).not.toMatch(/#[0-9a-f]{3,8}\b/i);
});
});

it('resolves --dx-accent-color-source to the default blue accent when --dx-accent-color is unset', () => {
const defaultAccentColor = readStepColor(defaultPalettePath, '100');

expect(fs.readFileSync(customAccentPath, 'utf8'))
.toContain(`--dx-accent-color-source: var(--dx-accent-color, ${defaultAccentColor});`);
});
});
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": [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

configurations.internal inherits the same exclude, so devextreme-internal also ships without the fluent-next sources. Intended? If internal consumers should keep them, the internal configuration needs exclude: []; if not, fine - just confirming it's a decision, not an accident.

"widgets/fluent-next/**",
"_design-system/**",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] This exclude keeps the token-derived sources out of devextreme/devextreme-internal, but the same sources still ship through a second channel: themebuilder's generate-metadata walks the whole devextreme-scss/scss tree into src/data/scss, and its only filter is filePath.includes('fluent-next') - _design-system/** doesn't match it.

all:build: devextreme-themebuilder-26.2.0.tgz contains 19 data/scss/_design-system/** files (all 11 palettes, semantic tiers, and variables

The fluent-next bundles are only saved by the name-substring accident, so the collector filter needs extending - ideally sharing one exclude list with this target. Related hygiene: saveScssFiles never cleans the destination, so stale files from earlier checkouts ship too when packing from a dev machine (my local tgz also had 8 leftover widgets/dxdsfluent/ files from before the rename)**

"bundles/dx.fluent-next.*.scss"
]
},
"configurations": {
"internal": {
"outputDir": "./artifacts/npm/devextreme-internal/scss"
}
},
"dependsOn": [
{
"projects": ["devextreme-scss"],
"target": "build:tokens"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dependency generates _design-system, which this very target then excludes from the copy - I assume it's for input-hash determinism? A short comment would help. Meanwhile scss/bundles/*.scss, which IS shipped (.npmignore un-ignores it), isn't guaranteed by any dependency: a standalone nx run devextreme:build:npm:scss on a fresh clone silently packs without bundles. Pre-existing, but since we're adding dependencies here - worth considering.

}
],
"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