From dbaca1279f0afdf88d745451e74300a3ca803c2f Mon Sep 17 00:00:00 2001 From: matthieu-crouzet Date: Thu, 20 Jul 2023 15:10:51 +0200 Subject: [PATCH] feat: add metadata for styling --- .../helpers/css-variable.extractor.ts | 107 +++++++++++++----- .../styling/builders/style-extractor/index.ts | 38 ++++++- .../builders/style-extractor/schema.json | 4 + .../builders/style-extractor/schema.ts | 3 + .../templates/__name__.style.theme.scss | 3 +- .../@o3r/styling/src/core/styles.interface.ts | 12 ++ 6 files changed, 132 insertions(+), 35 deletions(-) diff --git a/packages/@o3r/styling/builders/style-extractor/helpers/css-variable.extractor.ts b/packages/@o3r/styling/builders/style-extractor/helpers/css-variable.extractor.ts index 20b3f44fd4..e22290570a 100644 --- a/packages/@o3r/styling/builders/style-extractor/helpers/css-variable.extractor.ts +++ b/packages/@o3r/styling/builders/style-extractor/helpers/css-variable.extractor.ts @@ -1,5 +1,5 @@ import { getLibraryCmsMetadata } from '@o3r/extractors'; -import type { CssMetadata, CssVariable } from '@o3r/styling'; +import type { CssMetadata, CssVariable, CssVariableType } from '@o3r/styling'; import * as fs from 'node:fs'; import { pathToFileURL } from 'node:url'; import * as path from 'node:path'; @@ -8,6 +8,7 @@ import { SassBoolean, SassColor, SassList, + SassMap, SassNumber, SassString, Value @@ -90,6 +91,22 @@ export class CssVariableExtractor { return url.split('/').slice(0, 2).join('/'); } + private static extractTags(tags: Value) { + let contextTags: string[] | undefined; + if (tags instanceof SassString) { + contextTags = [tags.text]; + } else if (tags instanceof SassList) { + contextTags = []; + for (let i = 0; i < tags.asList.size; i++) { + const item = tags.get(i); + if (item instanceof SassString) { + contextTags.push(item.text); + } + } + } + return contextTags; + } + /** * Extract metadata from Sass file * @@ -124,36 +141,66 @@ export class CssVariableExtractor { // eslint-disable-next-line @typescript-eslint/naming-convention 'metadata-report($name, $value, $tags: null)': (args: Value[]) => { let contextTags: string[] | undefined; - const name = args[0]; - const value = args[1]; + const varName = args[0]; + const varValue = args[1]; const tags = args[2]; + let description: string | undefined; + let label: string | undefined; + let category: string | undefined; + let type: CssVariableType | undefined; if (tags) { - if (tags instanceof SassString) { - contextTags = [tags.text]; - } else if (tags instanceof SassList) { - contextTags = []; - for (let i = 0; i < tags.asList.size; i++) { - const item = tags.get(i); - if (item instanceof SassString) { - contextTags.push(item.text); + if (tags instanceof SassMap) { + for (const [key, value] of tags.contents.toArray()) { + if (key instanceof SassString) { + switch (key.text) { + case 'description': + if (value instanceof SassString) { + description = value.text; + } + break; + case 'label': + if (value instanceof SassString) { + label = value.text; + } + break; + case 'type': + if (value instanceof SassString) { + type = value.text as CssVariableType; + } + break; + case 'category': + if (value instanceof SassString) { + category = value.text; + } + break; + case 'tags': + contextTags = CssVariableExtractor.extractTags(value); + break; + default: + console.warn(`Unsupported property: ${key.text}`); + break; + } } } } + if (!contextTags) { + contextTags = CssVariableExtractor.extractTags(tags); + } } - if (!(name instanceof SassString)) { + if (!(varName instanceof SassString)) { throw new Error('invalid variable name'); } let parsedValue: string; - if (value instanceof SassString || value instanceof SassNumber || value instanceof SassBoolean) { - parsedValue = value.toString(); - } else if (value instanceof SassColor) { - parsedValue = CssVariableExtractor.getColorString(value); - } else if (value instanceof SassList) { + if (varValue instanceof SassString || varValue instanceof SassNumber || varValue instanceof SassBoolean) { + parsedValue = varValue.toString(); + } else if (varValue instanceof SassColor) { + parsedValue = CssVariableExtractor.getColorString(varValue); + } else if (varValue instanceof SassList) { const invalidIndexes: number[] = []; const parsedValueItems: string[] = []; - for (let i = 0; i < value.asList.size; i++) { - const item = value.get(i); + for (let i = 0; i < varValue.asList.size; i++) { + const item = varValue.get(i); if (item instanceof SassString || item instanceof SassNumber || item instanceof SassBoolean) { parsedValueItems.push(item.toString()); } else if (item instanceof SassColor) { @@ -166,20 +213,22 @@ export class CssVariableExtractor { } parsedValue = parsedValueItems.join(' '); if (invalidIndexes.length) { - console.warn(`invalid value in the list (indexes: ${invalidIndexes.join(', ')}) for variable ${name.text}, it will be ignored`); + console.warn(`invalid value in the list (indexes: ${invalidIndexes.join(', ')}) for variable ${varName.text}, it will be ignored`); } - } else if (CssVariableExtractor.isSassCalculation(value)) { - parsedValue = `calc(${value.$arguments[0]})`; + } else if (CssVariableExtractor.isSassCalculation(varValue)) { + parsedValue = `calc(${varValue.$arguments[0]})`; } else { - console.warn(`invalid value for variable ${name.text}, it will be ignored`); - return new SassString(`[METADATA:VARIABLE] ${name.text} : invalid value`); + console.warn(`invalid value for variable ${varName.text}, it will be ignored`); + return new SassString(`[METADATA:VARIABLE] ${varName.text} : invalid value`); } - const cssVariableObj = this.parseCssVariable(name.text, parsedValue); + const cssVariableObj = this.parseCssVariable(varName.text, parsedValue); cssVariableObj.tags = contextTags; - if (cssVariableObj) { - cssVariables.push(cssVariableObj); - } - return new SassString(`[METADATA:VARIABLE] ${name.text} : ${parsedValue}` + (contextTags ? ` (tags: ${contextTags.join(', ')})` : '')); + cssVariableObj.description = description; + cssVariableObj.label = label; + cssVariableObj.type = type || 'string'; + cssVariableObj.category = category; + cssVariables.push(cssVariableObj); + return new SassString(`[METADATA:VARIABLE] ${varName.text} : ${parsedValue}` + (contextTags ? ` (tags: ${contextTags.join(', ')})` : '')); } } }; diff --git a/packages/@o3r/styling/builders/style-extractor/index.ts b/packages/@o3r/styling/builders/style-extractor/index.ts index 6929ee217b..a480669197 100644 --- a/packages/@o3r/styling/builders/style-extractor/index.ts +++ b/packages/@o3r/styling/builders/style-extractor/index.ts @@ -1,17 +1,31 @@ import { BuilderOutput, createBuilder } from '@angular-devkit/architect'; import { CmsMedataData, getLibraryCmsMetadata, validateJson } from '@o3r/extractors'; -import type { CssMetadata, CssVariable } from '@o3r/styling'; +import { isO3rClassComponent } from '@o3r/schematics'; +import type { CssMetadata } from '@o3r/styling'; import * as chokidar from 'chokidar'; import * as fs from 'node:fs'; import { sync as globbySync } from 'globby'; import * as path from 'node:path'; +import * as ts from 'typescript'; import { CssVariableExtractor } from './helpers/index'; import { StyleExtractorBuilderSchema } from './schema'; export * from './schema'; + +/** + * Get the library name from package.json + * + * @param currentDir + */ +const defaultLibraryName = (currentDir: string = process.cwd()) => { + const packageJsonPath = path.resolve(currentDir, 'package.json'); + return JSON.parse(fs.readFileSync(packageJsonPath, { encoding: 'utf-8'})).name as string; +}; + export default createBuilder(async (options, context): Promise => { context.reportRunning(); + const libraryName = options.name || defaultLibraryName(context.currentDirectory); const cssVariableExtractor = new CssVariableExtractor(); @@ -30,7 +44,23 @@ export default createBuilder(async (options, contex await Promise.all(files.map((file, idx) => { try { context.reportProgress(idx, STEP_NUMBER, `Extracting ${file}`); - return cssVariableExtractor.extractFile(file); + const variables = cssVariableExtractor.extractFile(file); + const themeFileSuffix = '.style.theme.scss'; + if (file.endsWith(themeFileSuffix)) { + const componentPath = path.join(path.dirname(file), `${path.basename(file, themeFileSuffix)}.component.ts`); + const componentSourceFile = ts.createSourceFile( + componentPath, + fs.readFileSync(componentPath).toString(), + ts.ScriptTarget.ES2020, + true + ); + const componentDeclaration = componentSourceFile.statements.find((s): s is ts.ClassDeclaration => ts.isClassDeclaration(s) && isO3rClassComponent(s)); + const componentName: string | undefined = componentDeclaration?.name?.escapedText.toString(); + if (componentName) { + return variables.map((variable) => ({ ...variable, component: { library: libraryName, name: componentName }})); + } + } + return variables; } catch (error: any) { hasFailedFiles.push({file, error}); return []; @@ -40,9 +70,9 @@ export default createBuilder(async (options, contex // Check duplicate CSS variable cssVarList .filter((cssVar) => !!acc.variables[cssVar.name]) - .filter((cssVar) => !initialPreviousMetadata.variables[cssVar.name] && (acc.variables[cssVar.name] as CssVariable).defaultValue !== cssVar.defaultValue) + .filter((cssVar) => !initialPreviousMetadata.variables[cssVar.name] && acc.variables[cssVar.name].defaultValue !== cssVar.defaultValue) .forEach((cssVar) => - context.logger.warn(`Duplicate "${cssVar.name}" (${(acc.variables[cssVar.name] as CssVariable).defaultValue} will be replaced by ${cssVar.defaultValue})`) + context.logger.warn(`Duplicate "${cssVar.name}" (${acc.variables[cssVar.name].defaultValue} will be replaced by ${cssVar.defaultValue})`) ); // merge all variables form all the files diff --git a/packages/@o3r/styling/builders/style-extractor/schema.json b/packages/@o3r/styling/builders/style-extractor/schema.json index 9652fcfecd..786a0cfb25 100644 --- a/packages/@o3r/styling/builders/style-extractor/schema.json +++ b/packages/@o3r/styling/builders/style-extractor/schema.json @@ -13,6 +13,10 @@ }, "default": ["src/**/*.style.scss"] }, + "name": { + "type": "string", + "description": "Library/Application name to be assigned into metadata" + }, "outputFile": { "type": "string", "description": "Path to the output file", diff --git a/packages/@o3r/styling/builders/style-extractor/schema.ts b/packages/@o3r/styling/builders/style-extractor/schema.ts index 7b054d5341..f3c77190d6 100644 --- a/packages/@o3r/styling/builders/style-extractor/schema.ts +++ b/packages/@o3r/styling/builders/style-extractor/schema.ts @@ -15,4 +15,7 @@ export interface StyleExtractorBuilderSchema extends JsonObject { /** List of libraries imported */ libraries: string[]; + + /** Library/Application name to be assigned into metadata */ + name: string | null; } diff --git a/packages/@o3r/styling/schematics/theming-to-component/templates/__name__.style.theme.scss b/packages/@o3r/styling/schematics/theming-to-component/templates/__name__.style.theme.scss index 3ed1a6251d..7b96753424 100644 --- a/packages/@o3r/styling/schematics/theming-to-component/templates/__name__.style.theme.scss +++ b/packages/@o3r/styling/schematics/theming-to-component/templates/__name__.style.theme.scss @@ -4,5 +4,4 @@ $palette-primary: o3r.get-mandatory(o3r.$default-theme, 'primary'); // list of variables used by the component // The variables will be exposed by the CMS and should default on the theme -// Example of variable -$<%=name%>-color: o3r.variable('<%=name%>-color', o3r.color($palette-primary, 700)) !default; +$<%=name%>-color: o3r.variable('<%=name%>-color', o3r.color($palette-primary, 700), (description: 'Example of variable', type: 'color', category: 'examples', label: 'Color example')) !default; diff --git a/packages/@o3r/styling/src/core/styles.interface.ts b/packages/@o3r/styling/src/core/styles.interface.ts index 1cc9fb8334..f4cdb3276a 100644 --- a/packages/@o3r/styling/src/core/styles.interface.ts +++ b/packages/@o3r/styling/src/core/styles.interface.ts @@ -1,3 +1,5 @@ +export type CssVariableType = 'string' | 'color'; + /** Metadata for a CSS Variable */ export interface CssVariable { /** Name of the variable */ @@ -8,6 +10,16 @@ export interface CssVariable { references?: CssVariable[]; /** Tags of the variable */ tags?: string[]; + /** Description of the variable */ + description?: string; + /** Description of the variable */ + label?: string; + /** Type of the variable */ + type?: CssVariableType; + /** Name of a group of variables */ + category?: string; + /** component reference if the variable is linked to one */ + component?: { library: string; name: string }; } /** Style Metadata map */