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
5 changes: 5 additions & 0 deletions packages/devextreme-scss/build/internal-scss-paths.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
"widgets/fluent-next/",
"_design-system/",
"bundles/dx.fluent-next."
]
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
13 changes: 10 additions & 3 deletions packages/devextreme-themebuilder/src/metadata/collector.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import { promises as fs } from 'fs';
import {
resolve, relative, join, dirname,
resolve, relative, join, dirname, sep,
} from 'path';
import MetadataGenerator from './generator';
import internalScssPaths from '../../../devextreme-scss/build/internal-scss-paths.json';

const isInternalScssPath = (relativePath: string): boolean => {
const posixPath = relativePath.split(sep).join('/');
return internalScssPaths.some((internalPath) => posixPath.startsWith(internalPath));
};

export default class MetadataCollector {
generator = new MetadataGenerator();

static async saveScssFiles(files: Promise<FileInfo[]>, destination: string): Promise<void> {
await fs.rm(resolve(destination), { recursive: true, force: true });

await Promise.all((await files).map(async (file) => {
const absolutePath = resolve(join(destination, file.path));
const directory = dirname(absolutePath);
Expand Down Expand Up @@ -36,8 +44,7 @@ export default class MetadataCollector {
handler: (content: string) => string,
): Promise<FileInfo[]> {
const fileList = (await this.getFileList(dirName))
// fluent-next is an internal design-tokens theme, not exposed in ThemeBuilder
.filter((filePath) => !filePath.includes('fluent-next'));
.filter((filePath) => !isInternalScssPath(relative(dirName, filePath)));

return Promise.all(fileList.map(async (filePath) => {
const relativePath = relative(dirName, filePath);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:root {
--dxds-primary-100: #0f6cbd;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$primary-100: var(--dxds-primary-100);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@use "../widgets/fluent-next/colors" with ($color: "blue", $mode: "light");
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@use "../../_design-system/variables/ds" as ds;

$base-accent: ds.$primary-100;
39 changes: 32 additions & 7 deletions packages/devextreme-themebuilder/tests/metadata/collector.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { join, resolve, dirname } from 'path';
import {
join, resolve, dirname, relative,
} from 'path';
import { promises } from 'fs';

import MetadataCollector from '../../src/metadata/collector';
Expand All @@ -7,7 +9,7 @@ const rootDir = join(__dirname, '..', '..');
const scssDir = join(rootDir, 'tests', 'data', 'scss');

describe('MetadataCollector', () => {
const expectedFileList: string[] = [
const exposedFileList: string[] = [
join('bundles', 'dx.light.scss'),
join('bundles', 'dx.material.blue.light.scss'),
join('widgets', 'generic', 'accordion', '_colors.scss'),
Expand All @@ -21,8 +23,16 @@ describe('MetadataCollector', () => {
join('widgets', 'material', '_index.scss'),
];

const internalFileList: string[] = [
join('_design-system', 'fluent', 'accents', 'blue.scss'),
join('_design-system', 'variables', '_ds.scss'),
join('widgets', 'fluent-next', '_colors.scss'),
join('bundles', 'dx.fluent-next.blue.light.scss'),
];

promises.mkdir = jest.fn();
promises.writeFile = jest.fn();
promises.rm = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
Expand All @@ -31,22 +41,26 @@ describe('MetadataCollector', () => {
test('getFileList', async () => {
const collector = new MetadataCollector();
const fileList = await collector.getFileList(join(rootDir, 'tests', 'data', 'scss'));
const expectedFullPaths = expectedFileList.map((file) => join(scssDir, file));
const expectedFullPaths = [...exposedFileList, ...internalFileList]
.map((file) => join(scssDir, file));

expect(fileList.length).toBe(expectedFullPaths.length);

fileList.forEach((file) => expect(expectedFullPaths).toContain(file));
});

test('readFiles', async () => {
test('readFiles keeps out the paths the scss package marks as internal', async () => {
const collector = new MetadataCollector();
const handler = (content: string): string => content;
const filesInfo = await collector.readFiles(join(rootDir, 'tests', 'data', 'scss'), handler);
const onDisk = (await collector.getFileList(scssDir)).map((file) => relative(scssDir, file));
const filesInfo = await collector.readFiles(scssDir, handler);
const returnedPaths = filesInfo.map((file) => file.path);

expect(filesInfo.length).toBe(expectedFileList.length);
expect(onDisk.filter((file) => !returnedPaths.includes(file)).sort())
.toEqual([...internalFileList].sort());
expect(returnedPaths.sort()).toEqual([...exposedFileList].sort());

filesInfo.forEach((file) => {
expect(expectedFileList).toContain(file.path);
expect(typeof file.content).toBe('string');
expect(file.content.length).toBeGreaterThan(0);
});
Expand Down Expand Up @@ -79,6 +93,17 @@ describe('MetadataCollector', () => {
expect(promises.writeFile).toHaveBeenCalledWith(expectedDestinationPath, fileContent);
});

test('saveScssFiles wipes the destination so sources deleted upstream stop shipping', async () => {
const destinationPath = './scss';

await MetadataCollector.saveScssFiles(Promise.resolve([]), destinationPath);

expect(promises.rm).toHaveBeenCalledWith(
resolve(destinationPath),
{ recursive: true, force: true },
);
});

test('saveMetadata', async () => {
const collector = new MetadataCollector();
const version = '1.1.1';
Expand Down
9 changes: 8 additions & 1 deletion packages/devextreme/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -1233,10 +1233,17 @@
"outputDir": "./artifacts/npm/devextreme-internal/scss"
}
},
"dependsOn": [
{
"projects": ["devextreme-scss"],
"target": "build:themes"
}
],
"inputs": [
"{workspaceRoot}/packages/devextreme-scss/scss/**/*",
"{workspaceRoot}/packages/devextreme-scss/fonts/**/*",
"{workspaceRoot}/packages/devextreme-scss/icons/**/*"
"{workspaceRoot}/packages/devextreme-scss/icons/**/*",
"{workspaceRoot}/packages/devextreme-scss/build/internal-scss-paths.json"
],
"outputs": [
"{projectRoot}/artifacts/npm/devextreme/scss/**/*",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ describe('ScssAssembleExecutor E2E', () => {
fs.mkdirSync(path.join(scssPackageDir, 'scss'), { recursive: true });
fs.mkdirSync(path.join(scssPackageDir, 'fonts'), { recursive: true });
fs.mkdirSync(path.join(scssPackageDir, 'icons', 'material'), { recursive: true });
fs.mkdirSync(path.join(scssPackageDir, 'build'), { recursive: true });
fs.writeFileSync(
path.join(scssPackageDir, 'build', 'internal-scss-paths.json'),
JSON.stringify(['widgets/dropped/', 'bundles/dx.dropped.']),
);
});

afterEach(() => {
Expand Down Expand Up @@ -83,4 +88,63 @@ describe('ScssAssembleExecutor E2E', () => {
expect(content).toContain(expectedSvg);
expect(content).toContain(expectedPng);
});

it('should keep the paths listed as internal by the scss package out of the package', async () => {
await writeFileText(
path.join(scssPackageDir, 'scss', 'widgets', 'kept', '_index.scss'),
'.a {}',
);
await writeFileText(
path.join(scssPackageDir, 'scss', 'widgets', 'dropped', 'nested', '_index.scss'),
'.b {}',
);
await writeFileText(path.join(scssPackageDir, 'scss', 'bundles', 'dx.kept.scss'), '.c {}');
await writeFileText(
path.join(scssPackageDir, 'scss', 'bundles', 'dx.dropped.blue.light.scss'),
'.d {}',
);

const context = createMockContext({ root: tempDir });
const result = await executor(OPTIONS, 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.blue.light.scss'))).toBe(
false,
);
});

it('should fail instead of publishing everything when the internal path list is missing', async () => {
await writeFileText(path.join(outputDir, 'widgets', 'kept', '_index.scss'), '.a {}');
await writeFileText(path.join(scssPackageDir, 'scss', 'placeholder.scss'), '.a {}');
fs.rmSync(path.join(scssPackageDir, 'build', 'internal-scss-paths.json'));

const context = createMockContext({ root: tempDir });
const result = await executor(OPTIONS, context);

expect(result.success).toBe(false);
expect(fs.existsSync(path.join(outputDir, 'placeholder.scss'))).toBe(false);
expect(await readFileText(path.join(outputDir, 'widgets', 'kept', '_index.scss'))).toBe(
'.a {}',
);
});

it('should drop files an earlier run left in the output directory', async () => {
await writeFileText(path.join(outputDir, 'widgets', 'renamed-away', '_index.scss'), '.old {}');
await writeFileText(
path.join(scssPackageDir, 'scss', 'widgets', 'kept', '_index.scss'),
'.new {}',
);

const context = createMockContext({ root: tempDir });
const result = await executor(OPTIONS, context);

expect(result.success).toBe(true);
expect(fs.existsSync(path.join(outputDir, 'widgets', 'renamed-away'))).toBe(false);
expect(await readFileText(path.join(outputDir, 'widgets', 'kept', '_index.scss'))).toBe(
'.new {}',
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,28 @@ import { glob } from 'glob';
import { logger } from '@nx/devkit';
import { createExecutor } from '../../utils/create-executor';
import { toPosixPath } from '../../utils/path-resolver';
import { readFileText, writeFileText, ensureDir } from '../../utils/file-operations';
import { readFileText, writeFileText, ensureDir, exists } from '../../utils/file-operations';
import { copyDirectory } from '../copy-files/copy-files.impl';
import { DATA_URI_SCSS_REGEX, encodeDataUriForCssUrl } from '../../utils/scss-data-uri';
import { ScssAssembleExecutorSchema } from './schema';

const SCSS_EXTENSIONS = new Set(['.scss', '.css']);
const INTERNAL_SCSS_PATHS_FILE = path.join('build', 'internal-scss-paths.json');

export async function readInternalScssPaths(scssPackagePath: string): Promise<string[]> {
const listPath = path.join(scssPackagePath, INTERNAL_SCSS_PATHS_FILE);

if (!(await exists(listPath))) {
throw new Error(`Internal SCSS path list not found: ${listPath}`);
}

return JSON.parse(await readFileText(listPath)) as string[];
}

export function isInternalScssPath(relativePath: string, internalPaths: string[]): boolean {
const posixPath = relativePath.split('\\').join('/');
return internalPaths.some((internalPath) => posixPath.startsWith(internalPath));
}

async function inlineDataUri(content: string, scssRoot: string): Promise<string> {
const matches = [...content.matchAll(DATA_URI_SCSS_REGEX)];
Expand Down Expand Up @@ -37,10 +53,13 @@ async function inlineDataUri(content: string, scssRoot: string): Promise<string>
async function copyScssWithInlineDataUri(
scssPackagePath: string,
outputDir: string,
internalPaths: 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 })).filter(
(relPath) => !isInternalScssPath(relPath, internalPaths),
);

await Promise.all(
relPaths.map(async (relPath) => {
Expand Down Expand Up @@ -87,8 +106,12 @@ export default createExecutor<ScssAssembleExecutorSchema, ResolvedScssAssemble>(
return { scssPackagePath, outputDir };
},
run: async ({ scssPackagePath, outputDir }) => {
const internalPaths = await readInternalScssPaths(scssPackagePath);

await fs.rm(outputDir, { recursive: true, force: true });

await Promise.all([
copyScssWithInlineDataUri(scssPackagePath, outputDir),
copyScssWithInlineDataUri(scssPackagePath, outputDir, internalPaths),
copyFonts(scssPackagePath, outputDir),
copyIcons(scssPackagePath, outputDir),
]);
Expand Down
Loading
Loading