diff --git a/packages/@o3r/design/builders/generate-css/index.ts b/packages/@o3r/design/builders/generate-css/index.ts index c69b59352a..631c5ecdd0 100644 --- a/packages/@o3r/design/builders/generate-css/index.ts +++ b/packages/@o3r/design/builders/generate-css/index.ts @@ -6,13 +6,14 @@ import { getMetadataStyleContentUpdater, getMetadataTokenDefinitionRenderer, getSassTokenDefinitionRenderer, + mergeDesignTokenTemplates, parseDesignTokenFile, renderDesignTokens, tokenVariableNameSassRenderer } from '../../src/public_api'; import type { DesignTokenGroupTemplate, DesignTokenRendererOptions, DesignTokenVariableSet, DesignTokenVariableStructure, TokenKeyRenderer } from '../../src/public_api'; import { resolve } from 'node:path'; -import * as globby from 'globby'; +import { sync } from 'globby'; import { EOL } from 'node:os'; import { readFile } from 'node:fs/promises'; @@ -21,6 +22,9 @@ import { readFile } from 'node:fs/promises'; * @param options */ export default createBuilder(async (options, context): Promise => { + const templateFilePaths = options.templateFile + && (typeof options.templateFile === 'string' ? [options.templateFile] : options.templateFile).map((templateFile) => resolve(context.workspaceRoot, templateFile)) + || undefined; const designTokenFilePatterns = Array.isArray(options.designTokenFilePatterns) ? options.designTokenFilePatterns : [options.designTokenFilePatterns]; const determineFileToUpdate = options.output ? () => resolve(context.workspaceRoot, options.output!) : (token: DesignTokenVariableStructure) => { @@ -69,8 +73,15 @@ export default createBuilder(async (options, contex }; const execute = async (renderDesignTokenOptions: DesignTokenRendererOptions): Promise => { - const template = options.templateFile ? JSON.parse(await readFile(resolve(context.workspaceRoot, options.templateFile), { encoding: 'utf-8' })) as DesignTokenGroupTemplate : undefined; - const files = (await globby(designTokenFilePatterns, { cwd: context.workspaceRoot, absolute: true })); + let template: DesignTokenGroupTemplate | undefined; + if (templateFilePaths) { + const templateFiles = await Promise.all( + templateFilePaths + .map(async (templateFile) => JSON.parse(await readFile(templateFile, { encoding: 'utf-8' })) as DesignTokenGroupTemplate) + ); + template = templateFiles.reduce((acc, cur) => mergeDesignTokenTemplates(acc, cur), {}); + } + const files = sync(designTokenFilePatterns, { cwd: context.workspaceRoot, absolute: true }); const inDependencies = designTokenFilePatterns .filter((pathName) => !pathName.startsWith('.') && !pathName.startsWith('*') && !pathName.startsWith('/')) @@ -137,7 +148,7 @@ export default createBuilder(async (options, contex await import('chokidar') .then((chokidar) => chokidar.watch([ ...designTokenFilePatterns.map((p) => resolve(context.workspaceRoot, p)), - ...(options.templateFile ? [resolve(context.workspaceRoot, options.templateFile)] : []) + ...(templateFilePaths || []) ])) .then((watcher) => watcher.on('all', async () => { const res = await executeMultiRenderer(); diff --git a/packages/@o3r/design/builders/generate-css/schema.json b/packages/@o3r/design/builders/generate-css/schema.json index c9dabe6d9b..8efc5f54c1 100644 --- a/packages/@o3r/design/builders/generate-css/schema.json +++ b/packages/@o3r/design/builders/generate-css/schema.json @@ -60,8 +60,18 @@ "description": "Determine if the process should stop in case of Token duplication" }, "templateFile": { - "type": "string", - "description": "Path to a template file to apply as default configuration to a Design Token extension" + "description": "Path to a template file(s) to apply as default configuration to a Design Token extension. In case of multiple files, the JSON will be deeply merged in the order defined by the list.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] }, "prefix": { "type": "string", diff --git a/packages/@o3r/design/builders/generate-css/schema.ts b/packages/@o3r/design/builders/generate-css/schema.ts index 26e89bd0f6..a3b8a15c05 100644 --- a/packages/@o3r/design/builders/generate-css/schema.ts +++ b/packages/@o3r/design/builders/generate-css/schema.ts @@ -51,5 +51,5 @@ export interface GenerateCssSchematicsSchema extends SchematicOptionObject { variableType?: 'css' | 'sass'; /** Path to a template file to apply as default configuration to a Design Token extension */ - templateFile?: string; + templateFile?: string | string[]; } diff --git a/packages/@o3r/design/src/core/design-token/parsers/design-token-template.helper.spec.ts b/packages/@o3r/design/src/core/design-token/parsers/design-token-template.helper.spec.ts new file mode 100644 index 0000000000..01b4b57632 --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/parsers/design-token-template.helper.spec.ts @@ -0,0 +1,60 @@ +import { mergeDesignTokenTemplates } from './design-token-template.helpers'; +import type { DesignTokenGroupTemplate } from '../..'; + +describe('mergeDesignTokenTemplates function', () => { + + it('should merge object', () => { + const templateA: DesignTokenGroupTemplate = { + field1: { + field2: { + $extensions: { + o3rPrivate: true + } + } + } as DesignTokenGroupTemplate + }; + + const templateB: DesignTokenGroupTemplate = { + field1: { + field3: { + $extensions: { + o3rPrivate: false + } + } + } as DesignTokenGroupTemplate + }; + + const result: any = mergeDesignTokenTemplates(templateA, templateB); + expect(result.field1.field2).toBeDefined(); + expect(result.field1.field3).toBeDefined(); + expect(result.field1.field2.$extensions.o3rPrivate).toBe(true); + expect(result.field1.field3.$extensions.o3rPrivate).toBe(false); + }); + + it('should override data in case on config', () => { + const templateA: DesignTokenGroupTemplate = { + field1: { + field2: { + $extensions: { + o3rPrivate: true + } + } + } as DesignTokenGroupTemplate + }; + + const templateB: DesignTokenGroupTemplate = { + field1: { + field2: { + $extensions: { + o3rPrivate: false + } + } + } as DesignTokenGroupTemplate + }; + + const result: any = mergeDesignTokenTemplates(templateA, templateB); + expect(result.field1.field2).toBeDefined(); + expect(result.field1.field2.$extensions.o3rPrivate).toBe(false); + }); + +}); diff --git a/packages/@o3r/design/src/core/design-token/parsers/design-token-template.helpers.ts b/packages/@o3r/design/src/core/design-token/parsers/design-token-template.helpers.ts new file mode 100644 index 0000000000..faadae8205 --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/parsers/design-token-template.helpers.ts @@ -0,0 +1,33 @@ +import type { DesignTokenGroupExtensions, DesignTokenGroupTemplate } from '../design-token-specification.interface'; + +/** + * Merge TemplateB into TemplateA + * @param templateA + * @param templateB + */ +export const mergeDesignTokenTemplates = < + A extends DesignTokenGroupExtensions = DesignTokenGroupExtensions, + B extends DesignTokenGroupExtensions = DesignTokenGroupExtensions> + (templateA: DesignTokenGroupTemplate, templateB: DesignTokenGroupTemplate): DesignTokenGroupTemplate => { + + const entries = Object.entries(templateB); + const template = { + ...templateA, + ...Object.fromEntries( + entries + .filter(([key]) => key.startsWith('$')) + ) + }; + + return entries + .filter(([key]) => !key.startsWith('$')) + .reduce((acc, [key, value]) => { + const node = acc[key]; + if (node && typeof node === 'object' && typeof value === 'object') { + acc[key] = mergeDesignTokenTemplates(node as DesignTokenGroupTemplate, value as DesignTokenGroupTemplate); + } else { + acc[key] = value; + } + return acc; + }, template as DesignTokenGroupTemplate); +}; diff --git a/packages/@o3r/design/src/core/design-token/parsers/index.ts b/packages/@o3r/design/src/core/design-token/parsers/index.ts index 618a3de3a7..b4e610ba69 100644 --- a/packages/@o3r/design/src/core/design-token/parsers/index.ts +++ b/packages/@o3r/design/src/core/design-token/parsers/index.ts @@ -1,2 +1,3 @@ export * from './design-token-parser.interface'; export * from './design-token.parser'; +export * from './design-token-template.helpers'; diff --git a/packages/@o3r/design/tsconfig.spec.json b/packages/@o3r/design/tsconfig.spec.json index ffc1f54a1d..fa32740a8b 100644 --- a/packages/@o3r/design/tsconfig.spec.json +++ b/packages/@o3r/design/tsconfig.spec.json @@ -7,7 +7,8 @@ }, "include": [ "./src/**/*.spec.ts", - "./schematics/**/*.spec.ts" + "./schematics/**/*.spec.ts", + "./builders/**/*.spec.ts" ], "exclude": [], "references": [