From 2ea59de3778f41ff971eaf1d2ab709770ae58b69 Mon Sep 17 00:00:00 2001 From: Kilian Panot Date: Tue, 16 Jan 2024 23:05:44 +0900 Subject: [PATCH 1/2] feat: migration of minimal Design Token Package to v9.6 --- packages/@o3r/design/.compodocrc.json | 22 + packages/@o3r/design/.eslintrc.js | 20 + packages/@o3r/design/.gitignore | 11 + packages/@o3r/design/.npmignore | 0 packages/@o3r/design/README.md | 51 ++ packages/@o3r/design/builders.json | 10 + .../design/builders/generate-css/index.js | 13 + .../design/builders/generate-css/index.ts | 131 +++++ .../design/builders/generate-css/schema.json | 68 +++ .../design/builders/generate-css/schema.ts | 41 ++ .../generate-css-from-design-token.cli.cts | 39 ++ packages/@o3r/design/collection.json | 24 + packages/@o3r/design/jest.config.js | 8 + packages/@o3r/design/migration.json | 5 + packages/@o3r/design/package.json | 141 +++++ packages/@o3r/design/project.json | 90 +++ .../design/schemas/design-token.schema.json | 546 ++++++++++++++++++ .../schematics/ extract-token/index.spec.ts | 106 ++++ .../design/schematics/ extract-token/index.ts | 102 ++++ .../schematics/ extract-token/schema.json | 43 ++ .../schematics/ extract-token/schema.ts | 12 + .../design/schematics/generate-css/index.ts | 58 ++ .../schematics/generate-css/schema.json | 43 ++ .../design/schematics/generate-css/schema.ts | 20 + .../@o3r/design/schematics/index.it.spec.ts | 27 + .../@o3r/design/schematics/ng-add/index.ts | 15 + .../ng-add/register-generate-css/index.ts | 1 + .../register-generate-css/register-task.ts | 73 +++ .../design-token.custom.json.template | 3 + .../templates/theme.scss.template | 6 + .../@o3r/design/schematics/ng-add/schema.json | 18 + .../@o3r/design/schematics/ng-add/schema.ts | 6 + .../design-token-specification.interface.ts | 264 +++++++++ .../core/design-token/design-token.spec.ts | 204 +++++++ .../design/src/core/design-token/index.ts | 3 + .../parsers/design-token-parser.interface.ts | 77 +++ .../parsers/design-token.parser.spec.ts | 70 +++ .../parsers/design-token.parser.ts | 183 ++++++ .../src/core/design-token/parsers/index.ts | 2 + .../design-token-definition.renderers.spec.ts | 50 ++ .../css/design-token-definition.renderers.ts | 74 +++ .../design-token-updater.renderers.spec.ts | 48 ++ .../css/design-token-updater.renderers.ts | 47 ++ .../css/design-token-value.renderers.spec.ts | 48 ++ .../css/design-token-value.renderers.ts | 42 ++ .../core/design-token/renderers/css/index.ts | 3 + .../design-token-style.renderer.spec.ts | 81 +++ .../renderers/design-token-style.renderer.ts | 60 ++ .../design-token.renderer.helpers.spec.ts | 32 + .../design-token.renderer.helpers.ts | 8 + .../design-token.renderer.interface.ts | 63 ++ .../src/core/design-token/renderers/index.ts | 6 + .../design-token-definition.renderers.spec.ts | 40 ++ .../design-token-definition.renderers.ts | 52 ++ .../design-token-updater.renderers.spec.ts | 31 + .../design-token-updater.renderers.ts | 10 + .../design-token-value.renderers.spec.ts | 40 ++ .../metadata/design-token-value.renderers.ts | 40 ++ .../design-token/renderers/metadata/index.ts | 3 + .../design-token-definition.renderers.spec.ts | 44 ++ .../sass/design-token-definition.renderers.ts | 39 ++ .../core/design-token/renderers/sass/index.ts | 1 + packages/@o3r/design/src/core/index.ts | 1 + packages/@o3r/design/src/public_api.ts | 1 + .../testing/mocks/design-token-theme.json | 56 ++ packages/@o3r/design/testing/setup-jest.ts | 0 packages/@o3r/design/tsconfig.build.json | 16 + packages/@o3r/design/tsconfig.builders.json | 25 + packages/@o3r/design/tsconfig.doc.json | 11 + packages/@o3r/design/tsconfig.eslint.json | 10 + packages/@o3r/design/tsconfig.json | 15 + packages/@o3r/design/tsconfig.spec.json | 21 + packages/@o3r/styling/package.json | 4 + tsconfig.base.json | 9 + tsconfig.build.json | 2 + yarn.lock | 86 +++ 76 files changed, 3675 insertions(+) create mode 100644 packages/@o3r/design/.compodocrc.json create mode 100644 packages/@o3r/design/.eslintrc.js create mode 100644 packages/@o3r/design/.gitignore create mode 100644 packages/@o3r/design/.npmignore create mode 100644 packages/@o3r/design/README.md create mode 100644 packages/@o3r/design/builders.json create mode 100644 packages/@o3r/design/builders/generate-css/index.js create mode 100644 packages/@o3r/design/builders/generate-css/index.ts create mode 100644 packages/@o3r/design/builders/generate-css/schema.json create mode 100644 packages/@o3r/design/builders/generate-css/schema.ts create mode 100644 packages/@o3r/design/cli/generate-css-from-design-token.cli.cts create mode 100644 packages/@o3r/design/collection.json create mode 100644 packages/@o3r/design/jest.config.js create mode 100644 packages/@o3r/design/migration.json create mode 100644 packages/@o3r/design/package.json create mode 100644 packages/@o3r/design/project.json create mode 100644 packages/@o3r/design/schemas/design-token.schema.json create mode 100644 packages/@o3r/design/schematics/ extract-token/index.spec.ts create mode 100644 packages/@o3r/design/schematics/ extract-token/index.ts create mode 100644 packages/@o3r/design/schematics/ extract-token/schema.json create mode 100644 packages/@o3r/design/schematics/ extract-token/schema.ts create mode 100644 packages/@o3r/design/schematics/generate-css/index.ts create mode 100644 packages/@o3r/design/schematics/generate-css/schema.json create mode 100644 packages/@o3r/design/schematics/generate-css/schema.ts create mode 100644 packages/@o3r/design/schematics/index.it.spec.ts create mode 100644 packages/@o3r/design/schematics/ng-add/index.ts create mode 100644 packages/@o3r/design/schematics/ng-add/register-generate-css/index.ts create mode 100644 packages/@o3r/design/schematics/ng-add/register-generate-css/register-task.ts create mode 100644 packages/@o3r/design/schematics/ng-add/register-generate-css/templates/design-token.custom.json.template create mode 100644 packages/@o3r/design/schematics/ng-add/register-generate-css/templates/theme.scss.template create mode 100644 packages/@o3r/design/schematics/ng-add/schema.json create mode 100644 packages/@o3r/design/schematics/ng-add/schema.ts create mode 100644 packages/@o3r/design/src/core/design-token/design-token-specification.interface.ts create mode 100644 packages/@o3r/design/src/core/design-token/design-token.spec.ts create mode 100644 packages/@o3r/design/src/core/design-token/index.ts create mode 100644 packages/@o3r/design/src/core/design-token/parsers/design-token-parser.interface.ts create mode 100644 packages/@o3r/design/src/core/design-token/parsers/design-token.parser.spec.ts create mode 100644 packages/@o3r/design/src/core/design-token/parsers/design-token.parser.ts create mode 100644 packages/@o3r/design/src/core/design-token/parsers/index.ts create mode 100644 packages/@o3r/design/src/core/design-token/renderers/css/design-token-definition.renderers.spec.ts create mode 100644 packages/@o3r/design/src/core/design-token/renderers/css/design-token-definition.renderers.ts create mode 100644 packages/@o3r/design/src/core/design-token/renderers/css/design-token-updater.renderers.spec.ts create mode 100644 packages/@o3r/design/src/core/design-token/renderers/css/design-token-updater.renderers.ts create mode 100644 packages/@o3r/design/src/core/design-token/renderers/css/design-token-value.renderers.spec.ts create mode 100644 packages/@o3r/design/src/core/design-token/renderers/css/design-token-value.renderers.ts create mode 100644 packages/@o3r/design/src/core/design-token/renderers/css/index.ts create mode 100644 packages/@o3r/design/src/core/design-token/renderers/design-token-style.renderer.spec.ts create mode 100644 packages/@o3r/design/src/core/design-token/renderers/design-token-style.renderer.ts create mode 100644 packages/@o3r/design/src/core/design-token/renderers/design-token.renderer.helpers.spec.ts create mode 100644 packages/@o3r/design/src/core/design-token/renderers/design-token.renderer.helpers.ts create mode 100644 packages/@o3r/design/src/core/design-token/renderers/design-token.renderer.interface.ts create mode 100644 packages/@o3r/design/src/core/design-token/renderers/index.ts create mode 100644 packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-definition.renderers.spec.ts create mode 100644 packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-definition.renderers.ts create mode 100644 packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-updater.renderers.spec.ts create mode 100644 packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-updater.renderers.ts create mode 100644 packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-value.renderers.spec.ts create mode 100644 packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-value.renderers.ts create mode 100644 packages/@o3r/design/src/core/design-token/renderers/metadata/index.ts create mode 100644 packages/@o3r/design/src/core/design-token/renderers/sass/design-token-definition.renderers.spec.ts create mode 100644 packages/@o3r/design/src/core/design-token/renderers/sass/design-token-definition.renderers.ts create mode 100644 packages/@o3r/design/src/core/design-token/renderers/sass/index.ts create mode 100644 packages/@o3r/design/src/core/index.ts create mode 100644 packages/@o3r/design/src/public_api.ts create mode 100644 packages/@o3r/design/testing/mocks/design-token-theme.json create mode 100644 packages/@o3r/design/testing/setup-jest.ts create mode 100644 packages/@o3r/design/tsconfig.build.json create mode 100644 packages/@o3r/design/tsconfig.builders.json create mode 100644 packages/@o3r/design/tsconfig.doc.json create mode 100644 packages/@o3r/design/tsconfig.eslint.json create mode 100644 packages/@o3r/design/tsconfig.json create mode 100644 packages/@o3r/design/tsconfig.spec.json diff --git a/packages/@o3r/design/.compodocrc.json b/packages/@o3r/design/.compodocrc.json new file mode 100644 index 0000000000..754bd43da8 --- /dev/null +++ b/packages/@o3r/design/.compodocrc.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://raw.githubusercontent.com/compodoc/compodoc/develop/src/config/schema.json", + "name": "Styling", + "output": "../../../generated-doc/styling", + "tsconfig": "./tsconfig.doc.json", + "assetsFolder": "../../../.attachments", + "disableSourceCode": true, + "disableDomTree": true, + "disableTemplateTab": true, + "disableStyleTab": true, + "disableGraph": true, + "disableCoverage": true, + "disablePrivate": true, + "disableProtected": true, + "disableInternal": true, + "disableLifeCycleHooks": true, + "disableRoutesGraph": true, + "disableSearch": false, + "hideGenerator": true, + "customFavicon": "../../../assets/logo/flavors/otter-128x128.png", + "templates": "../../../compodoc-templates/package" +} diff --git a/packages/@o3r/design/.eslintrc.js b/packages/@o3r/design/.eslintrc.js new file mode 100644 index 0000000000..5d890a0275 --- /dev/null +++ b/packages/@o3r/design/.eslintrc.js @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable quote-props */ + +module.exports = { + 'root': true, + 'parserOptions': { + 'EXPERIMENTAL_useSourceOfProjectReferenceRedirect': true, + 'tsconfigRootDir': __dirname, + 'project': [ + 'tsconfig.build.json', + 'tsconfig.builders.json', + 'tsconfig.spec.json', + 'tsconfig.eslint.json' + ], + 'sourceType': 'module' + }, + 'extends': [ + '../../../.eslintrc.js' + ] +}; diff --git a/packages/@o3r/design/.gitignore b/packages/@o3r/design/.gitignore new file mode 100644 index 0000000000..9bc7b5104b --- /dev/null +++ b/packages/@o3r/design/.gitignore @@ -0,0 +1,11 @@ +/index.* + +/common +/component +/configuration +/dist* +/errors +/store +/types +/validation +/utils diff --git a/packages/@o3r/design/.npmignore b/packages/@o3r/design/.npmignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@o3r/design/README.md b/packages/@o3r/design/README.md new file mode 100644 index 0000000000..637b6404db --- /dev/null +++ b/packages/@o3r/design/README.md @@ -0,0 +1,51 @@ +

Otter Design

+

+ Super cute Otter! +

+ +This package is an [Otter Framework Module](https://github.com/AmadeusITGroup/otter/tree/main/docs/core/MODULE.md). +
+
+ +## Description + +Set of tools to generate CSS themes and [Metadata](https://github.com/AmadeusITGroup/otter/tree/main/docs/cms-adapters/CMS_ADAPTERS.md) based on the [Design Token Specifications](https://design-tokens.github.io/community-group/format/). + +## How to install + +```shell +ng add @o3r/design +``` + +## Generators + +Otter Design module provides a set of code generators based on [angular schematics](https://angular.io/guide/schematics). + +| Schematics | Description | How to use | +| ------------ | ------------------------------------------------------- | -------------------- | +| add | Include Otter design module in a library / application. | `ng add @o3r/design` | +| generate-css | Generate CSS Theme based on Design Token Files | `ng g generate-css` | + +## Builders + +Otter Design module provides a set of builders based on [angular builders](https://angular.io/guide/cli-builder). + +### generate-css + +The `generate-css` builder can generate CSS and CMS Metadata based on given Design Token Json files. +The following configurations are available: + +| Options | Default Value | Description | +| --------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| **designTokenFilePatterns** | [] *Require* | Path patterns to the Design Token JSON files.
Files in dependencies are supported and resolved with Node Resolver. | +| **output** | *null* | Output file where the CSS will be generated.
The path specified in `o3rTargetFile` will be ignore if this option is specified | +| **defaultStyleFile** | src/theme.scss | File path to generate the variable if not determined by the specifications | +| **metadataOutput** | *null* | Path to generate the metadata for the CMS.
The metadata will be generated only if the file path is specified. | +| **rootPath** | *null* | Root path of files where the CSS will be generated. | +| **failOnDuplicate** | false | Determine if the process should stop in case of Token duplication. | +| **prefix** | *null* | Prefix to append to generated variables. | +| **watch** | false | Enable Watch mode. | + +## Technical documentation + +Documentation providing explanations on the use and customization of the `Design Token` parser and renderers is available in the [technical documentation](https://github.com/AmadeusITGroup/otter/blob/main/docs/design/TECHNICAL_DOCUMENTATION.md). diff --git a/packages/@o3r/design/builders.json b/packages/@o3r/design/builders.json new file mode 100644 index 0000000000..956efba83e --- /dev/null +++ b/packages/@o3r/design/builders.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://raw.githubusercontent.com/angular/angular-cli/master/packages/angular_devkit/architect/src/builders-schema.json", + "builders": { + "generate-css": { + "implementation": "./builders/generate-css/", + "schema": "./builders/generate-css/schema.json", + "description": "Generate CSS from Design Token files" + } + } +} diff --git a/packages/@o3r/design/builders/generate-css/index.js b/packages/@o3r/design/builders/generate-css/index.js new file mode 100644 index 0000000000..5f2ed38442 --- /dev/null +++ b/packages/@o3r/design/builders/generate-css/index.js @@ -0,0 +1,13 @@ +/* + +This files is used to allow the usage of the builder within @o3r/framework mono-repository. +It should not be part of the package. + +*/ + +const {resolve} = require('node:path'); + +require('ts-node').register({ project: resolve(__dirname, '..', '..', 'tsconfig.builders.json') }); +require('ts-node').register = () => { }; + +module.exports = require('./index.ts'); diff --git a/packages/@o3r/design/builders/generate-css/index.ts b/packages/@o3r/design/builders/generate-css/index.ts new file mode 100644 index 0000000000..f9ed1824e1 --- /dev/null +++ b/packages/@o3r/design/builders/generate-css/index.ts @@ -0,0 +1,131 @@ +import type { GenerateCssSchematicsSchema } from './schema'; +import { BuilderOutput, createBuilder } from '@angular-devkit/architect'; +import { + getCssTokenDefinitionRenderer, + getMetadataStyleContentUpdater, + getMetadataTokenDefinitionRenderer, + getSassTokenDefinitionRenderer, + parseDesignTokenFile, + renderDesignTokens, + tokenVariableNameSassRenderer +} from '../../src/public_api'; +import type { DesignTokenRendererOptions, DesignTokenVariableSet, DesignTokenVariableStructure, TokenKeyRenderer } from '../../src/public_api'; +import { resolve } from 'node:path'; +import * as globby from 'globby'; + +/** + * Generate CSS from Design Token files + * @param options + */ +export default createBuilder(async (options, context): Promise => { + const designTokenFilePatterns = Array.isArray(options.designTokenFilePatterns) ? options.designTokenFilePatterns : [options.designTokenFilePatterns]; + const determineCssFileToUpdate = options.output ? () => resolve(context.workspaceRoot, options.output!) : + (token: DesignTokenVariableStructure) => { + if (token.extensions.o3rTargetFile) { + return token.context?.basePath && !options.rootPath ? + resolve(token.context.basePath, token.extensions.o3rTargetFile) : + resolve(context.workspaceRoot, options.rootPath || '', token.extensions.o3rTargetFile); + } + + return resolve(context.workspaceRoot, options.defaultStyleFile); + }; + const tokenVariableNameRenderer: TokenKeyRenderer | undefined = options.prefix ? (variable) => options.prefix! + variable.getKey() : undefined; + const sassRenderer = getSassTokenDefinitionRenderer({ tokenVariableNameRenderer: (v) => (options?.prefixPrivate || '') + tokenVariableNameSassRenderer(v) }); + const renderDesignTokenOptionsCss: DesignTokenRendererOptions = { + determineFileToUpdate: determineCssFileToUpdate, + tokenDefinitionRenderer: getCssTokenDefinitionRenderer({ + tokenVariableNameRenderer: options.prefix ? (variable) => options.prefix! + variable.getKey() : undefined, + privateDefinitionRenderer: options.renderPrivateVariableTo === 'sass' ? sassRenderer : undefined + }), + logger: context.logger + }; + + const renderDesignTokenOptionsMetadata: DesignTokenRendererOptions = { + determineFileToUpdate: () => resolve(context.workspaceRoot, options.metadataOutput!), + styleContentUpdater: getMetadataStyleContentUpdater(), + tokenDefinitionRenderer: getMetadataTokenDefinitionRenderer({ tokenVariableNameRenderer }), + logger: context.logger + }; + + const execute = async (renderDesignTokenOptions: DesignTokenRendererOptions): Promise => { + const files = (await globby(designTokenFilePatterns, { cwd: context.workspaceRoot, absolute: true })); + + const inDependencies = designTokenFilePatterns + .filter((pathName) => !pathName.startsWith('.') && !pathName.startsWith('*') && !pathName.startsWith('/')) + .map((pathName) => { + try { + return require.resolve(pathName); + } catch { + return undefined; + } + }) + .filter((pathName): pathName is string => !!pathName); + files.push(...inDependencies); + + try { + const duplicatedToken: DesignTokenVariableStructure[] = []; + const tokens = (await Promise.all(files.map(async (file) => ({file, parsed: await parseDesignTokenFile(file)})))) + .reduce((acc, {file, parsed}) => { + parsed.forEach((variable, key) => { + if (acc.has(key)) { + context.logger[options.failOnDuplicate ? 'error' : 'warn'](`A duplication of the variable ${key} is found in ${file}`); + duplicatedToken.push(variable); + } + acc.set(key, variable); + }); + return acc; + }, new Map()); + if (options.failOnDuplicate && duplicatedToken.length > 0) { + throw new Error(`Found ${duplicatedToken.length} duplicated Design Token keys`); + } + await renderDesignTokens(tokens, renderDesignTokenOptions); + return { success: true }; + } catch (err) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return { success: false, error: `${err as any}` }; + } + }; + + const executeMultiRenderer = async (): Promise => { + return (await Promise.allSettled[]>([ + execute(renderDesignTokenOptionsCss), + ...(options.metadataOutput ? [execute(renderDesignTokenOptionsMetadata)] : []) + ])).reduce((acc, res) => { + if (res.status === 'fulfilled') { + acc.success &&= res.value.success; + if (!res.value.error) { + acc.error ||= ''; + acc.error += '\n' + res.value.error!; + } + } else { + acc.success = false; + if (res.reason) { + acc.error ||= ''; + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + acc.error += '\n' + res.reason; + } + } + return acc; + }, { success: true } as BuilderOutput); + }; + + if (!options.watch) { + return await executeMultiRenderer(); + } else { + try { + await import('chokidar') + .then((chokidar) => chokidar.watch(designTokenFilePatterns.map((p) => resolve(context.workspaceRoot, p)))) + .then((watcher) => watcher.on('all', async () => { + const res = await executeMultiRenderer(); + + if (res.error) { + context.logger.error(res.error); + } + })); + return { success: true }; + } catch (err) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return { success: false, error: `${err as any}` }; + } + } +}); diff --git a/packages/@o3r/design/builders/generate-css/schema.json b/packages/@o3r/design/builders/generate-css/schema.json new file mode 100644 index 0000000000..a65bf81bc7 --- /dev/null +++ b/packages/@o3r/design/builders/generate-css/schema.json @@ -0,0 +1,68 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "ngAddSchematicsSchema", + "title": "Add Otter Design", + "description": "ngAdd Otter Design", + "properties": { + "designTokenFilePatterns": { + "description": "Path patterns to the Design Token JSON files (it supports Node dependency paths).", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "output": { + "type": "string", + "description": "Path to generate the metadata for CMS. The Metadata will be generated only if the file path is specified" + }, + "defaultStyleFile": { + "type": "string", + "default": "src/theme.scss", + "description": "File path to generate the variable if not determined by the specification" + }, + "metadataOutput": { + "type": "string", + "description": "If specified, all the generated CSS variable will be generated in the given file. Otherwise, the output file will be determined based on the Variable parameters." + }, + "rootPath": { + "type": "string", + "description": "Root path used to determine the CSS file to edit if specified by the o3rTargetFile token extension. It will default to the Design Token file folder." + }, + "watch": { + "type": "boolean", + "default": false, + "description": "Enable Watch mode" + }, + "failOnDuplicate": { + "type": "boolean", + "default": false, + "description": "Determine if the process should stop in case of Token duplication" + }, + "prefix": { + "type": "string", + "description": "Prefix to happen to generated variables" + }, + "prefixPrivate": { + "type": "string", + "description": "Prefix to happen to generated Sass variables if generated" + }, + "renderPrivateVariableTo": { + "type": "string", + "enum": [ + "sass" + ], + "description": "Generate the Private Variable to the given language (the variable is not generated if not specified)" + } + }, + "additionalProperties": true, + "required": [ + "designTokenFilePatterns" + ] +} diff --git a/packages/@o3r/design/builders/generate-css/schema.ts b/packages/@o3r/design/builders/generate-css/schema.ts new file mode 100644 index 0000000000..16b9157b76 --- /dev/null +++ b/packages/@o3r/design/builders/generate-css/schema.ts @@ -0,0 +1,41 @@ +import type { SchematicOptionObject } from '@o3r/schematics'; + +export interface GenerateCssSchematicsSchema extends SchematicOptionObject { + /** Path patterns to the Design Token JSON files */ + designTokenFilePatterns: string | string[]; + + /** + * Path to generate the metadata for CMS + * The Metadata will be generated only if the file path is specified + */ + metadataOutput?: string; + + /** + * Output file where generate the CSS + * + * If specified, all the generated CSS variable will be generated in the given file. + * Otherwise, the output file will be determined based on the Variable parameters + */ + output?: string; + + /** File path to generate the variable if not determined by the specification */ + defaultStyleFile: string; + + /** Enable Watch mode */ + watch?: boolean; + + /** Root path of files where the CSS will be generated */ + rootPath?: string; + + /** Determine if the process should stop in case of Token duplication */ + failOnDuplicate?: boolean; + + /** Prefix to happen to generated variables */ + prefix?: string; + + /** Generate the Private Variable to the given language */ + renderPrivateVariableTo?: 'sass'; + + /** Prefix to happen to generated Sass variables if generated */ + prefixPrivate?: string; +} diff --git a/packages/@o3r/design/cli/generate-css-from-design-token.cli.cts b/packages/@o3r/design/cli/generate-css-from-design-token.cli.cts new file mode 100644 index 0000000000..042344b152 --- /dev/null +++ b/packages/@o3r/design/cli/generate-css-from-design-token.cli.cts @@ -0,0 +1,39 @@ +#!/usr/bin/env node + +import { isAbsolute, normalize, resolve } from 'node:path'; +import { existsSync } from 'node:fs'; +import { parseDesignTokenFile, renderDesignTokens } from '@o3r/design'; +import type { DesignTokenRendererOptions, DesignTokenVariableSet } from '@o3r/design'; +import * as minimist from 'minimist'; + +const args = minimist(process.argv.splice(2)); + +void (async () => { + const renderDesignTokenOptions: DesignTokenRendererOptions = {}; + + const output = args.o || args.output; + if (output) { + renderDesignTokenOptions.determineFileToUpdate = () => resolve(process.cwd(), output); + } + + const tokens = (await Promise.all( + args._ + .map((file) => isAbsolute(file) ? normalize(file) : resolve(process.cwd(), file)) + .filter((file) => { + const res = existsSync(file); + if (!res) { + throw new Error(`The file ${file} does not exist, the process will stop`); + } + return res; + }) + .map(async (file) => ({ file, parsed: await parseDesignTokenFile(file) })) + )).reduce((acc, { file, parsed }) => { + parsed.forEach((variable, key) => { + acc.set(key, variable); + console.warn(`A duplication of the variable ${key} is found in ${file}`); + }); + return acc; + }, new Map()); + + await renderDesignTokens(tokens, renderDesignTokenOptions); +})(); diff --git a/packages/@o3r/design/collection.json b/packages/@o3r/design/collection.json new file mode 100644 index 0000000000..a02090638a --- /dev/null +++ b/packages/@o3r/design/collection.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://raw.githubusercontent.com/angular/angular-cli/master/packages/angular_devkit/schematics/collection-schema.json", + "schematics": { + "ng-add": { + "description": "Add Otter Design to the project.", + "factory": "./schematics/ng-add/index#ngAdd", + "schema": "./schematics/ng-add/schema.json", + "aliases": [ + "install", + "i" + ] + }, + "generate-css": { + "description": "Generate CSS from Design Token files", + "factory": "./schematics/generate-css/index#generateCss", + "schema": "./schematics/generate-css/schema.json" + }, + "extract-token": { + "description": "Extract the Design Token specification from SCSS", + "factory": "./schematics/extract-token/index#extractToken", + "schema": "./schematics/extract-token/schema.json" + } + } +} diff --git a/packages/@o3r/design/jest.config.js b/packages/@o3r/design/jest.config.js new file mode 100644 index 0000000000..affcac71cb --- /dev/null +++ b/packages/@o3r/design/jest.config.js @@ -0,0 +1,8 @@ +const getJestConfig = require('../../../jest.config.ut').getJestConfig; + +/** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */ +module.exports = { + ...getJestConfig(__dirname, false), + displayName: require('./package.json').name, + clearMocks: true +}; diff --git a/packages/@o3r/design/migration.json b/packages/@o3r/design/migration.json new file mode 100644 index 0000000000..0149b023a8 --- /dev/null +++ b/packages/@o3r/design/migration.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://raw.githubusercontent.com/angular/angular-cli/master/packages/angular_devkit/schematics/collection-schema.json", + "schematics": { + } +} diff --git a/packages/@o3r/design/package.json b/packages/@o3r/design/package.json new file mode 100644 index 0000000000..a64b747e8a --- /dev/null +++ b/packages/@o3r/design/package.json @@ -0,0 +1,141 @@ +{ + "name": "@o3r/design", + "version": "0.0.0-placeholder", + "description": "A design framework to generate theme on an Otter application based on Design Tokens.", + "keywords": [ + "design", + "otter", + "otter-module" + ], + "scripts": { + "nx": "nx", + "ng": "yarn nx", + "test": "yarn nx test styling", + "copy:templates": "yarn cpy 'schematics/**/templates/**/*' dist/schematics", + "copy:schemas": "yarn cpy 'schemas/**/*' dist/schemas", + "prepare:build:builders": "yarn cpy 'builders/**/*.json' dist/builders && yarn cpy '{builders,collection,migration}.json' dist && yarn cpy 'schematics/**/*.json' dist/schematics && yarn copy:templates", + "prepare:publish": "prepare-publish ./dist", + "build:source": "tsc -b tsconfig.build.json && yarn cpy package.json dist/", + "build:builders": "tsc -b tsconfig.builders.json --pretty && yarn copy:templates && generate-cjs-manifest", + "build": "yarn nx build styling", + "postbuild": "yarn copy:schemas && patch-package-json-main" + }, + "exports": { + "./package.json": { + "default": "./package.json" + }, + "./schemas/*.json": { + "default": "./schemas/*.json" + }, + ".": { + "es2020": "./dist/src/public_api.js", + "default": "./dist/src/public_api.js", + "typings": "./dist/src/public_api.d.ts", + "node": "./dist/src/public_api.js", + "require": "./dist/src/public_api.js" + }, + "./cli/*": { + "default": "./dist/cli/*" + } + }, + "bin": { + "o3r-css-from-design-token": "./dist/cli/generate-css-from-design-token.cli.cjs" + }, + "dependencies": { + "minimist": "^1.2.6", + "tslib": "^2.5.3" + }, + "peerDependencies": { + "@o3r/core": "workspace:^", + "@o3r/schematics": "workspace:^", + "@o3r/styling": "workspace:^", + "chokidar": "^3.5.2", + "globby": "^11.1.0", + "minimatch": "~9.0.3", + "sass": "~1.69.0" + }, + "peerDependenciesMeta": { + "@o3r/core": { + "optional": true + }, + "@o3r/schematics": { + "optional": true + }, + "@o3r/styling": { + "optional": true + }, + "chokidar": { + "optional": true + }, + "globby": { + "optional": true + }, + "sass": { + "optional": true + } + }, + "devDependencies": { + "@angular-devkit/architect": "~0.1602.0", + "@angular-devkit/build-angular": "~16.2.0", + "@angular-devkit/core": "~16.2.0", + "@angular-devkit/schematics": "~16.2.0", + "@angular-eslint/eslint-plugin": "~16.3.0", + "@angular/cli": "~16.2.0", + "@angular/common": "~16.2.0", + "@angular/compiler": "~16.2.0", + "@angular/compiler-cli": "~16.2.0", + "@angular/core": "~16.2.0", + "@angular/platform-browser": "~16.2.0", + "@angular/platform-browser-dynamic": "~16.2.0", + "@babel/core": "~7.23.0", + "@babel/preset-typescript": "~7.23.0", + "@compodoc/compodoc": "^1.1.19", + "@nx/eslint-plugin": "~16.10.0", + "@nx/jest": "~16.10.0", + "@nx/js": "~16.10.0", + "@nx/linter": "~16.10.0", + "@o3r/build-helpers": "workspace:^", + "@o3r/core": "workspace:^", + "@o3r/eslint-plugin": "workspace:^", + "@o3r/schematics": "workspace:^", + "@o3r/styling": "workspace:^", + "@o3r/test-helpers": "workspace:^", + "@schematics/angular": "~16.2.0", + "@types/jest": "~29.5.2", + "@types/minimist": "^1.2.2", + "@types/node": "^18.0.0", + "@types/semver": "^7.3.13", + "@typescript-eslint/eslint-plugin": "^5.60.1", + "@typescript-eslint/parser": "^5.60.1", + "chokidar": "^3.5.2", + "cpy-cli": "^4.2.0", + "eslint": "^8.42.0", + "eslint-import-resolver-node": "^0.3.4", + "eslint-plugin-jest": "~27.6.0", + "eslint-plugin-jsdoc": "~46.10.0", + "eslint-plugin-prefer-arrow": "~1.2.3", + "eslint-plugin-unicorn": "^47.0.0", + "globby": "^11.1.0", + "jest": "~29.7.0", + "jest-junit": "~16.0.0", + "jsonc-eslint-parser": "~2.4.0", + "jsonschema": "~1.4.1", + "minimatch": "~9.0.3", + "nx": "~16.10.0", + "rxjs": "^7.8.1", + "sass": "~1.69.0", + "ts-jest": "~29.1.1", + "ts-node": "~10.9.1", + "type-fest": "^3.12.0", + "typescript": "~5.1.6", + "zone.js": "~0.13.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "builders": "./builders.json", + "schematics": "./collection.json", + "ng-update": { + "migrations": "./migration.json" + } +} diff --git a/packages/@o3r/design/project.json b/packages/@o3r/design/project.json new file mode 100644 index 0000000000..3ea54d3463 --- /dev/null +++ b/packages/@o3r/design/project.json @@ -0,0 +1,90 @@ +{ + "name": "design", + "$schema": "https://raw.githubusercontent.com/nrwl/nx/master/packages/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "packages/@o3r/design/src", + "prefix": "o3r", + "targets": { + "build": { + "executor": "nx:run-script", + "outputs": [ + "{projectRoot}/dist/package.json" + ], + "options": { + "script": "postbuild" + }, + "dependsOn": [ + "^build", + "build-builders", + "compile" + ] + }, + "compile": { + "executor": "nx:run-script", + "options": { + "script": "build:source" + }, + "inputs": [ + "{projectRoot}/cli/*.cts", + "source", + "^source" + ] + }, + "prepare-build-builders": { + "executor": "nx:run-script", + "options": { + "script": "prepare:build:builders" + } + }, + "build-builders": { + "executor": "nx:run-script", + "options": { + "script": "build:builders" + } + }, + "lint": { + "executor": "@nx/linter:eslint", + "configurations": { + "ci": { + "quiet": true, + "cacheLocation": ".cache/eslint" + } + }, + "options": { + "eslintConfig": "packages/@o3r/design/.eslintrc.js", + "lintFilePatterns": [ + "packages/@o3r/design/src/**/*.ts", + "packages/@o3r/design/schematics/**/*.ts", + "packages/@o3r/design/cli/**/*.ts", + "packages/@o3r/design/builders/**/*.ts", + "packages/@o3r/design/package.json" + ] + } + }, + "test": { + "executor": "@nx/jest:jest", + "options": { + "jestConfig": "packages/@o3r/design/jest.config.js" + } + }, + "prepare-publish": { + "executor": "nx:run-script", + "options": { + "script": "prepare:publish" + } + }, + "publish": { + "executor": "nx:run-commands", + "options": { + "command": "npm publish packages/@o3r/design/dist" + } + }, + "documentation": { + "executor": "nx:run-script", + "options": { + "script": "compodoc" + } + } + }, + "tags": [] +} diff --git a/packages/@o3r/design/schemas/design-token.schema.json b/packages/@o3r/design/schemas/design-token.schema.json new file mode 100644 index 0000000000..4b3731ef84 --- /dev/null +++ b/packages/@o3r/design/schemas/design-token.schema.json @@ -0,0 +1,546 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "DesignTokenSchema", + "description": "Schema Describing the structure of the Design Token, this Schema is a temporary workaround and should be replaced by the one provided by Community-Group https://github.com/design-tokens/community-group", + "allOf": [ + { + "$ref": "#/definitions/tokenNode" + } + ], + "definitions": { + "tokenNode": { + "oneOf": [ + { + "$ref": "#/definitions/tokenGroup" + }, + { + "$ref": "#/definitions/token" + } + ] + }, + "otterExtensionMetadata": { + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { "type": "string" } + }, + "label": { + "type": "string" + }, + "category": { + "type": "string" + } + } + }, + "otterExtension": { + "type": "object", + "properties": { + "o3rTargetFile": { + "type": "string" + }, + "o3rPrivate": { + "type": "boolean" + }, + "o3rImportant": { + "type": "boolean" + }, + "o3rScope": { + "type": "string" + }, + "o3rMetadata": { + "$ref": "#/definitions/otterExtensionMetadata" + } + } + }, + "extensions": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/otterExtension" + } + ] + }, + "tokenGroup": { + "type":"object", + "properties": { + "$schema": { + "type": "string" + }, + "$description": { + "type": "string" + }, + "$extensions": { + "$ref": "#/definitions/extensions" + } + }, + "patternProperties": { + "^[^$].*$": { + "$ref": "#/definitions/tokenNode" + } + }, + "additionalProperties": false + }, + "token": { + "allOf": [ + { + "oneOf": [ + {"$ref": "#/definitions/tokenTypeImplicit"}, + {"$ref": "#/definitions/tokenTypeColor"}, + {"$ref": "#/definitions/tokenTypeDimension"}, + {"$ref": "#/definitions/tokenTypeFontFamily"}, + {"$ref": "#/definitions/tokenTypeDuration"}, + {"$ref": "#/definitions/tokenTypeCubicBezier"}, + {"$ref": "#/definitions/tokenTypeFontWeight"}, + {"$ref": "#/definitions/tokenTypeNumber"}, + + {"$ref": "#/definitions/tokenTypeStrokeStyle"}, + {"$ref": "#/definitions/tokenTypeBorder"}, + {"$ref": "#/definitions/tokenTypeTransition"}, + {"$ref": "#/definitions/tokenTypeShadow"}, + {"$ref": "#/definitions/tokenTypeGradient"}, + {"$ref": "#/definitions/tokenTypeTypography"} + ] + }, + { + "type": "object", + "properties": { + "$extensions": { + "$ref": "#/definitions/extensions" + }, + "$description": { + "type": "string" + } + } + } + ] + }, + + "tokenTypeColor": { + "type": "object", + "required": [ + "$type", + "$value" + ], + "properties": { + "$value": { + "type": "string" + }, + "$type": { + "const": "color", + "type": "string" + } + } + }, + "tokenTypeDimension": { + "type": "object", + "required": [ + "$type", + "$value" + ], + "properties": { + "$value": { + "type": "string" + }, + "$type": { + "const": "dimension", + "type": "string" + } + } + }, + "tokenTypeFontFamily": { + "type": "object", + "required": [ + "$type", + "primary" + ], + "allOf": [ + { + "oneOf": [ + { + "type": "object", + "properties": { + "$value": { + "type": "string" + } + } + }, + { + "type": "object", + "required": ["primary"], + "properties": { + "primary": { + "type": "object", + "required": [ + "$value" + ], + "properties": { + "$value": { + "type": "string" + } + } + }, + "secondary": { + "type": "object", + "required": [ + "$type" + ], + "properties": { + "$value": { + "type": "string" + } + } + } + } + } + ] + }, + { + "type": "object", + "properties": { + "$type": { + "const": "fontFamily", + "type": "string" + } + } + } + ] + }, + "tokenTypeImplicit": { + "type": "object", + "required": [ + "$value" + ], + "properties": { + "$value": { + "type": "string", + "pattern": "^.*\\{.*\\}.*$" + }, + "$type": { + "not": {} + } + } + }, + "tokenTypeDuration": { + "type": "object", + "required": [ + "$type", + "$value" + ], + "properties": { + "$value": { + "type": "number" + }, + "$type": { + "const": "duration", + "type": "string" + } + } + }, + "tokenTypeCubicBezierValue": { + "type": "array", + "minItems": 2, + "maxItems": 4, + "items": { + "type": "number" + } + }, + "tokenTypeCubicBezier": { + "type": "object", + "required": [ + "$type", + "$value" + ], + "properties": { + "$value": { + "$ref": "#/definitions/tokenTypeCubicBezierValue" + }, + "$type": { + "const": "cubicBezier", + "type": "string" + } + } + }, + "tokenTypeFontWeightValue": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + }, + "tokenTypeFontWeight": { + "type": "object", + "required": [ + "$type", + "$value" + ], + "properties": { + "$value": { + "$ref": "#/definitions/tokenTypeFontWeightValue" + }, + "$type": { + "const": "fontWeight", + "type": "string" + } + } + }, + "tokenTypeNumber": { + "type": "object", + "required": [ + "$type", + "$value" + ], + "properties": { + "$value": { + "type": "number" + }, + "$type": { + "const": "number", + "type": "string" + } + } + }, + + "tokenTypeStrokeStyleValue": { + "oneOf": [ + { + "type": "string", + "enum": [ + "solid", + "dashed", + "dotted", + "double", + "groove", + "ridge", + "outset", + "inset" + ] + }, + { + "type": "object", + "properties": { + "dashArray": { + "type": "array", + "minItems": 1, + "maxItems": 4, + "items": { + "type": "string" + } + }, + "lineCap": { + "type": "string", + "enum": [ + "round", + "butt", + "square" + ] + } + }, + "required": [ + "dashArray", + "lineCap" + ] + } + ] + }, + "tokenTypeStrokeStyle": { + "type": "object", + "required": [ + "$type", + "$value" + ], + "properties": { + "$value": { + "$ref": "#/definitions/tokenTypeStrokeStyleValue" + }, + "$type": { + "const": "strokeStyle", + "type": "string" + } + } + }, + "tokenTypeBorder": { + "type": "object", + "required": [ + "$type", + "$value" + ], + "properties": { + "$value": { + "type": "object", + "properties": { + "color": { "type": "string" }, + "width": { "type": "string" }, + "style": { + "allOf": [ + { "$ref": "#/definitions/tokenTypeStrokeStyleValue" }, + { "type": "string" } + ] + } + }, + "required": [ + "color", + "width", + "style" + ] + }, + "$type": { + "const": "border", + "type": "string" + } + } + }, + "tokenTypeTransition": { + "type": "object", + "required": [ + "$type", + "$value" + ], + "properties": { + "$value": { + "type": "object", + "properties": { + "duration": { + "type": "string" + }, + "delay": { + "type": "string" + }, + "timingFunction": { + "allOf": [ + { "$ref": "#/definitions/tokenTypeCubicBezierValue" }, + { "type": "string" } + ] + } + }, + "required": [ + "duration", + "delay", + "timingFunction" + ] + }, + "$type": { + "const": "transition", + "type": "string" + } + } + }, + "tokenTypeShadow": { + "type": "object", + "required": [ + "$type", + "$value" + ], + "properties": { + "$value": { + "type": "object", + "properties": { + "color": { + "type": "string" + }, + "offsetX": { + "type": "string" + }, + "offsetY": { + "type": "string" + }, + "blur": { + "type": "string" + }, + "spread": { + "type": "string" + } + }, + "required": [ + "color", + "offsetX", + "offsetY", + "blur", + "spread" + ] + }, + "$type": { + "const": "shadow", + "type": "string" + } + } + }, + "tokenTypeGradient": { + "type": "object", + "required": [ + "$type", + "$value" + ], + "properties": { + "$value": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "color": { "type": "string" }, + "position": { + "oneOf": [ + { "type": "string" }, + { + "type": "number", + "maximum": 1, + "minimum": 0 + } + ] + } + }, + "required": [ + "color", + "position" + ] + } + }, + "$type": { + "const": "gradient", + "type": "string" + } + } + }, + "tokenTypeTypography": { + "type": "object", + "required": [ + "$type", + "$value" + ], + "properties": { + "$value": { + "type": "object", + "properties": { + "fontFamily": { "type": "string" }, + "fontSize": { "type": "string" }, + "letterSpacing": { "type": "string" }, + "fontWeight": { + "$ref": "#/definitions/tokenTypeFontWeightValue" + }, + "lineHeight": { + "oneOf": [ + { "type": "string" }, + { "type": "number" } + ] + } + }, + "required": [ + "fontFamily", + "fontSize", + "letterSpacing", + "fontWeight", + "lineHeight" + ] + }, + "$type": { + "const": "typography", + "type": "string" + } + } + } + } +} diff --git a/packages/@o3r/design/schematics/ extract-token/index.spec.ts b/packages/@o3r/design/schematics/ extract-token/index.spec.ts new file mode 100644 index 0000000000..209d697bf6 --- /dev/null +++ b/packages/@o3r/design/schematics/ extract-token/index.spec.ts @@ -0,0 +1,106 @@ +import { callRule, Tree } from '@angular-devkit/schematics'; +import { extractToken } from './index'; +import { firstValueFrom } from 'rxjs'; +import type { CssVariable } from '@o3r/styling'; +import { validate } from 'jsonschema'; +import * as fs from 'node:fs'; +import { resolve } from 'node:path'; + +describe('Extract Token schematic', () => { + + let initialTree: Tree; + + const initialSassFile = ` +@use '@o3r/styling' as o3r; + +$breadcrumb-pres-item-icon-size: o3r.variable('breadcrumb-pres-item-icon-size', 3rem); +$breadcrumb-pres-item-icon-color: o3r.variable('breadcrumb-pres-item-icon-color', #fff); +$breadcrumb-pres-item-other-color: o3r.variable('breadcrumb-pres-item-other-color', o3r.var('breadcrumb-pres-item-icon-color')); + +// other CSS +.test { + color: $breadcrumb-pres-item-other-color; +}`; + + beforeEach(() => { + initialTree = Tree.empty(); + initialTree.create('src/component/my-comp.theme.css', 'should be ignored'); + initialTree.create('src/component/my-comp.theme.scss', initialSassFile); + }); + + it('should correctly extract the Design Token', async () => { + const logger = { warn: jest.fn(), debug: jest.fn() }; + jest.mock('@o3r/styling/builders/style-extractor/helpers', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + CssVariableExtractor: class { + public extractFileContent = jest.fn().mockReturnValue([ + { + defaultValue: '3rem', + name: 'breadcrumb-pres-item-icon-size', + label: 'breadcrumb pres item icon size', + type: 'string' + }, + { + defaultValue: '#fff', + name: 'breadcrumb-pres-item-icon-color', + label: 'breadcrumb pres item icon color', + type: 'color' + }, + { + defaultValue: 'var(--breadcrumb-pres-item-icon-color)', + name: 'breadcrumb-pres-item-other-color', + label: 'breadcrumb pres item other color', + type: 'string' + } + ]); + constructor() {} + } + })); + const tree = await firstValueFrom(callRule(extractToken({ + includeTags: false, + componentFilePatterns: ['src/component/**.*scss'] + }), initialTree, { logger } as any)); + + expect(tree.exists('src/component/my-comp.theme.json')).toBe(true); + expect(validate(tree.readText('src/component/my-comp.theme.json'), fs.readFileSync(resolve(__dirname, '../../schemas/design-token.schema.json'))).errors).toHaveLength(0); + expect(tree.readText('src/component/my-comp.theme.scss')).toBe(initialSassFile); + }); + + it('should Update the original file', async () => { + const logger = { warn: jest.fn(), debug: jest.fn() }; + jest.mock('@o3r/styling/builders/style-extractor/helpers', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + CssVariableExtractor: class { + public extractFileContent = jest.fn().mockReturnValue([ + { + defaultValue: '4rem', + name: 'breadcrumb-pres-item-icon-size', + label: 'breadcrumb pres item icon size', + type: 'string' + } + ]); + constructor() { } + } + })); + const tree = await firstValueFrom(callRule(extractToken({ + includeTags: true, + componentFilePatterns: ['src/component/**.*scss'] + }), initialTree, { logger } as any)); + + expect(tree.readText('src/component/my-comp.theme.scss')).not.toBe(initialSassFile); + expect(tree.readText('src/component/my-comp.theme.scss')).toBe(` +@use '@o3r/styling' as o3r; + +/* --- BEGIN THEME Auto-generated --- */ +$breadcrumb-pres-item-icon-size: o3r.variable('breadcrumb-pres-item-icon-size', 3rem); +$breadcrumb-pres-item-icon-color: o3r.variable('breadcrumb-pres-item-icon-color', #fff); +$breadcrumb-pres-item-other-color: o3r.variable('breadcrumb-pres-item-other-color', o3r.var('breadcrumb-pres-item-icon-color')); +/* --- END THEME Auto-generated --- */ + +// other CSS +.test { + color: $breadcrumb-pres-item-other-color; +}` + ); + }); +}); diff --git a/packages/@o3r/design/schematics/ extract-token/index.ts b/packages/@o3r/design/schematics/ extract-token/index.ts new file mode 100644 index 0000000000..e6ce4e184a --- /dev/null +++ b/packages/@o3r/design/schematics/ extract-token/index.ts @@ -0,0 +1,102 @@ +import type { Rule } from '@angular-devkit/schematics'; +import type { ExtractTokenSchematicsSchema } from './schema'; +import { posix, resolve } from 'node:path'; +import { AUTO_GENERATED_END, AUTO_GENERATED_START, DesignToken, DesignTokenGroup, DesignTokenNode } from '../../src/public_api'; + +const patternToDetect = 'o3r.var'; + +/** + * Extract the token from o3r mixin sass file + * @param options + */ +export function extractToken(options: ExtractTokenSchematicsSchema): Rule { + + const updateFileContent = (content: string): string => { + const start = content.indexOf(patternToDetect); + const end = content.lastIndexOf(patternToDetect); + + if (start === -1 || !options.includeTags) { + return content; + } + + const startTag = typeof options.includeTags === 'boolean' ? AUTO_GENERATED_START : options.includeTags.startTag; + const endTag = typeof options.includeTags === 'boolean' ? AUTO_GENERATED_END : options.includeTags.endTag; + const indexToInsertStart = content.substring(0, start).lastIndexOf('\n') + 1; + const indexToInsertEnd = content.substring(end).indexOf('\n') + end + 1; + + return `${content.substring(0, indexToInsertStart)}${startTag}\n` + + content.substring(indexToInsertStart, indexToInsertEnd) + + `${endTag}\n${content.substring(indexToInsertEnd) }`; + }; + + return async (tree, context) => { + try { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { CssVariableExtractor } = await import('@o3r/styling/builders/style-extractor/helpers'); + const { filter } = await import('minimatch'); + const filterFunctions = options.componentFilePatterns.map((pattern) => filter( + '/' + pattern.replace(/[\\/]+/g, '/'), + { dot: true } + )); + const sassParser = new CssVariableExtractor(); + + tree.visit((file) => { + if (!filterFunctions.some((filterFunction) => filterFunction(file))) { + return; + } + const content = tree.readText(file); + const variables = sassParser.extractFileContent(resolve(tree.root.path, file), content); + + if (variables.length > 0 && options.includeTags) { + const newContent = updateFileContent(content); + tree.overwrite(file, newContent); + } + + const isPrivate = file.endsWith('theme.scss'); + const tokenSpecification = variables + .reduce((node, variable) => { + const namePath = variable.name.split('-'); + let targetNode: DesignTokenGroup | DesignToken = node; + namePath.forEach((name) => { + (targetNode as DesignTokenGroup)[name] ||= {}; + targetNode = (targetNode as DesignTokenGroup)[name] as DesignTokenGroup | DesignToken; + }); + + const valueWithVariable = [...variable.defaultValue.matchAll(/var\(--([^)])\)/g)] + .reduce((acc, [variableString, variableName]) => { + return acc.replaceAll(variableString, `{${variableName.replaceAll('-', '.')}}`); + }, variable.defaultValue); + + const targetNodeValue = targetNode as any as DesignToken; + targetNodeValue.$description = variable.description; + targetNodeValue.$type = !variable.type || variable.type === 'string' ? + (isNaN(+variable.defaultValue) ? undefined : 'number') : + variable.type; + targetNodeValue.$value = targetNodeValue.$type === 'number' ? + +variable.defaultValue : + valueWithVariable; + targetNodeValue.$extensions ||= {}; + targetNodeValue.$extensions.o3rMetadata ||= {}; + targetNodeValue.$extensions.o3rMetadata.category = variable.category; + targetNodeValue.$extensions.o3rMetadata.label = variable.label; + targetNodeValue.$extensions.o3rMetadata.tags = variable.tags; + return node; + }, {} as DesignTokenGroup | DesignToken); + + Object.values(tokenSpecification) + .forEach((node) => { + const designTokenNode = (node as DesignTokenNode); + designTokenNode.$extensions ||= {}; + designTokenNode.$extensions.o3rPrivate = isPrivate; + designTokenNode.$extensions.o3rTargetFile = posix.join('.', posix.basename(file)); + }); + tree.create(file.replace(/\.scss$/, '.json'), JSON.stringify(tokenSpecification, null, 2)); + }); + return () => tree; + } catch (e) { + context.logger.warn('The following dependencies should be provided to the extract-token schematics: "@o3r/styling", "minimatch".'); + context.logger.warn('The extraction will stop, it can be re-run with the schematic "@o3r/design:extract-token".'); + context.logger.debug(JSON.stringify(e)); + } + }; +} diff --git a/packages/@o3r/design/schematics/ extract-token/schema.json b/packages/@o3r/design/schematics/ extract-token/schema.json new file mode 100644 index 0000000000..7dd921b1b5 --- /dev/null +++ b/packages/@o3r/design/schematics/ extract-token/schema.json @@ -0,0 +1,43 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "ngAddSchematicsSchema", + "title": "Extract Design Token From Sass", + "description": "Extract Design Token From Sass o3r variable helpers", + "properties": { + "includeTags": { + "description": "Include the tags in the original Sass file", + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "object", + "properties": { + "startTag": { + "type": "string" + }, + "endTag": { + "type": "string" + } + }, + "required": [ + "startTag", + "endTag" + ] + } + ], + "default": true + }, + "componentFilePatterns": { + "description": "List of file pattern of component theme files", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": true, + "required": [ + "componentFilePattern" + ] +} diff --git a/packages/@o3r/design/schematics/ extract-token/schema.ts b/packages/@o3r/design/schematics/ extract-token/schema.ts new file mode 100644 index 0000000000..71ac36ada3 --- /dev/null +++ b/packages/@o3r/design/schematics/ extract-token/schema.ts @@ -0,0 +1,12 @@ +import type { SchematicOptionObject } from '@o3r/schematics'; + +export interface ExtractTokenSchematicsSchema extends SchematicOptionObject { + /** + * Include the tags in the original Sass file + * @default true + */ + includeTags?: boolean | { startTag: string; endTag: string }; + + /** List of file pattern of component theme files */ + componentFilePatterns: string[]; +} diff --git a/packages/@o3r/design/schematics/generate-css/index.ts b/packages/@o3r/design/schematics/generate-css/index.ts new file mode 100644 index 0000000000..8e034d722b --- /dev/null +++ b/packages/@o3r/design/schematics/generate-css/index.ts @@ -0,0 +1,58 @@ +import type { GenerateCssSchematicsSchema } from './schema'; +import type { Rule } from '@angular-devkit/schematics'; +import { parseDesignTokenFile, renderDesignTokens } from '@o3r/design'; +import type { DesignTokenRendererOptions, DesignTokenVariableSet, DesignTokenVariableStructure } from '@o3r/design'; + +/* for v9.6 migration only, it is integrated into @o3r/schematics package in v10 */ +import { getAllFilesInTree } from '@o3r/schematics'; +import type { Tree } from '@angular-devkit/schematics'; +import { minimatch } from 'minimatch'; +function globInTree(tree: Tree, patterns: string[]): string[] { + const files = getAllFilesInTree(tree); + return files.filter((basePath) => patterns.some((p) => minimatch(basePath, p, { dot: true }))); +} + +/** + * Generate CSS from Design Token files + * @param options + */ +export function generateCss(options: GenerateCssSchematicsSchema): Rule { + return async (tree, context) => { + const writeFile = (filePath: string, content: string) => tree.exists(filePath) ? tree.overwrite(filePath, content) : tree.create(filePath, content); + const readFile = tree.readText; + const existsFile = tree.exists; + const determineFileToUpdate = options.output ? () => options.output! : + (token: DesignTokenVariableStructure) => { + if (token.extensions.o3rTargetFile && tree.exists(token.extensions.o3rTargetFile)) { + return token.extensions.o3rTargetFile; + } + + return options.defaultStyleFile; + }; + const renderDesignTokenOptions: DesignTokenRendererOptions = { + readFile, + writeFile, + existsFile, + determineFileToUpdate, + logger: context.logger + }; + + const files = globInTree(tree, Array.isArray(options.designTokenFilePatterns) ? options.designTokenFilePatterns : [options.designTokenFilePatterns]); + + const duplicatedToken: DesignTokenVariableStructure[] = []; + const tokens = (await Promise.all(files.map(async (file) => ({ file, parsed: await parseDesignTokenFile(file) })))) + .reduce((acc, { file, parsed }) => { + parsed.forEach((variable, key) => { + if (acc.has(key)) { + context.logger[options.failOnDuplicate ? 'error' : 'warn'](`A duplication of the variable ${key} is found in ${file}`); + } + acc.set(key, variable); + }); + return acc; + }, new Map()); + if (options.failOnDuplicate && duplicatedToken.length > 0) { + throw new Error(`Found ${duplicatedToken.length} duplicated Design Token keys`); + } + await renderDesignTokens(tokens, renderDesignTokenOptions); + }; +} diff --git a/packages/@o3r/design/schematics/generate-css/schema.json b/packages/@o3r/design/schematics/generate-css/schema.json new file mode 100644 index 0000000000..e2efc36b38 --- /dev/null +++ b/packages/@o3r/design/schematics/generate-css/schema.json @@ -0,0 +1,43 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "ngAddSchematicsSchema", + "title": "Add Otter Design", + "description": "ngAdd Otter Design", + "properties": { + "designTokenFilePatterns": { + "description": "Path patterns to the Design Token JSON files", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], + "$default": { + "$source": "argv" + } + }, + "output": { + "type": "string", + "description": "Output file where generate the CSS" + }, + "defaultStyleFile": { + "type": "string", + "default": "src/theme.scss", + "description": "File path to generate the variable if not determined by the specification" + }, + "failOnDuplicate": { + "type": "boolean", + "default": false, + "description": "Determine if the process should stop in case of Token duplication" + } + }, + "additionalProperties": true, + "required": [ + "designTokenFilePatterns" + ] +} diff --git a/packages/@o3r/design/schematics/generate-css/schema.ts b/packages/@o3r/design/schematics/generate-css/schema.ts new file mode 100644 index 0000000000..7c5eedf755 --- /dev/null +++ b/packages/@o3r/design/schematics/generate-css/schema.ts @@ -0,0 +1,20 @@ +import type { SchematicOptionObject } from '@o3r/schematics'; + +export interface GenerateCssSchematicsSchema extends SchematicOptionObject { + /** Path patterns to the Design Token JSON files */ + designTokenFilePatterns: string | string[]; + + /** File path to generate the variable if not determined by the specification */ + defaultStyleFile: string; + + /** + * Output file where generate the CSS + * + * If specified, all the generated CSS variable will be generated in the given file. + * Otherwise, the output file will be determined based on the Variable parameters + */ + output?: string; + + /** Determine if the process should stop in case of Token duplication */ + failOnDuplicate?: boolean; +} diff --git a/packages/@o3r/design/schematics/index.it.spec.ts b/packages/@o3r/design/schematics/index.it.spec.ts new file mode 100644 index 0000000000..6db7f9f36b --- /dev/null +++ b/packages/@o3r/design/schematics/index.it.spec.ts @@ -0,0 +1,27 @@ +import { + getDefaultExecSyncOptions, + packageManagerExec, + packageManagerInstall, + packageManagerRun, + prepareTestEnv, + setupLocalRegistry +} from '@o3r/test-helpers'; + +const appName = 'test-app-design'; +const o3rVersion = '999.0.0'; +const execAppOptions = getDefaultExecSyncOptions(); +let appFolderPath: string; + +describe.skip('new otter application with Design', () => { + setupLocalRegistry(); + beforeAll(async () => { + appFolderPath = await prepareTestEnv(appName, 'angular-with-o3r-core'); + execAppOptions.cwd = appFolderPath; + }); + test('should add design to existing application', () => { + packageManagerExec(`ng add --skip-confirmation @o3r/design@${o3rVersion}`, execAppOptions); + + expect(() => packageManagerInstall(execAppOptions)).not.toThrow(); + expect(() => packageManagerRun('build', execAppOptions)).not.toThrow(); + }); +}); diff --git a/packages/@o3r/design/schematics/ng-add/index.ts b/packages/@o3r/design/schematics/ng-add/index.ts new file mode 100644 index 0000000000..e0e72368a3 --- /dev/null +++ b/packages/@o3r/design/schematics/ng-add/index.ts @@ -0,0 +1,15 @@ +import { chain, type Rule } from '@angular-devkit/schematics'; +import { registerGenerateCssBuilder } from './register-generate-css'; +import { extractToken } from '../ extract-token'; + +/** + * Add Otter design to an Angular Project + * @param options + */ +export function ngAdd(): Rule { + /* ng add rules */ + return chain([ + registerGenerateCssBuilder(), + extractToken({ componentFilePatterns: ['**/*.scss'], includeTags: true }) + ]); +} diff --git a/packages/@o3r/design/schematics/ng-add/register-generate-css/index.ts b/packages/@o3r/design/schematics/ng-add/register-generate-css/index.ts new file mode 100644 index 0000000000..dbe1b1d9c5 --- /dev/null +++ b/packages/@o3r/design/schematics/ng-add/register-generate-css/index.ts @@ -0,0 +1 @@ +export * from './register-task'; diff --git a/packages/@o3r/design/schematics/ng-add/register-generate-css/register-task.ts b/packages/@o3r/design/schematics/ng-add/register-generate-css/register-task.ts new file mode 100644 index 0000000000..9da6738c67 --- /dev/null +++ b/packages/@o3r/design/schematics/ng-add/register-generate-css/register-task.ts @@ -0,0 +1,73 @@ +import { apply, chain, MergeStrategy, mergeWith, move, renameTemplateFiles, type Rule, template, url } from '@angular-devkit/schematics'; +import { getWorkspaceConfig } from '@o3r/schematics'; +import type { GenerateCssSchematicsSchema } from '../../../builders/generate-css/schema'; +import { posix } from 'node:path'; + +/* for v9.6 migration only, it is integrated into @o3r/schematics package in v10 */ +import type { SchematicOptionObject, WorkspaceProject } from '@o3r/schematics'; +function registerBuilder(workspaceProject: WorkspaceProject, taskName: string, taskParameters: SchematicOptionObject, force = false): WorkspaceProject { + workspaceProject.architect ||= {}; + if (workspaceProject.architect[taskName] && !force) { + throw new Error(`The builder task ${taskName} already exist`); + } + workspaceProject.architect[taskName] = taskParameters; + return workspaceProject; +} + +/** + * Register the Design Token CSS generator + * @param projectName Project name + * @param taskName name of the task to generate + */ +export const registerGenerateCssBuilder = (projectName?: string, taskName = 'generate-css'): Rule => { + const registerBuilderRule: Rule = (tree, {logger}) => { + const workspaceProject = projectName ? getWorkspaceConfig(tree)?.projects[projectName] : undefined; + const srcBasePath = workspaceProject?.sourceRoot || (workspaceProject?.root ? posix.resolve(workspaceProject.root, 'src') : ''); + const themeFile = posix.resolve(srcBasePath, 'style', 'theme.scss'); + const taskParameters: GenerateCssSchematicsSchema = { + defaultStyleFile: themeFile, + renderPrivateVariableTo: 'sass', + designTokenFilePatterns: [ + `${posix.resolve(srcBasePath, 'style', '*.json')}`, + `${posix.resolve(srcBasePath, '**', '*.theme.json')}` + ] + }; + if (!workspaceProject) { + logger.warn(`No angular.json found, the task ${taskName} will not be created`); + return tree; + } + registerBuilder(workspaceProject, taskName, taskParameters); + return tree; + }; + + const generateTemplateRule: Rule = (tree, context) => { + const workspaceProject = projectName ? getWorkspaceConfig(tree)?.projects[projectName] : undefined; + const srcBasePath = workspaceProject?.sourceRoot || (workspaceProject?.root ? posix.resolve(workspaceProject.root, 'src') : ''); + const themeFolder = posix.resolve(srcBasePath, 'style'); + const rule = mergeWith(apply(url('./templates'), [ + template({}), + move(themeFolder), + renameTemplateFiles() + ]), MergeStrategy.Overwrite)(tree, context); + + return rule; + }; + + const importTheme: Rule = (tree, context) => { + const workspaceProject = projectName ? getWorkspaceConfig(tree)?.projects[projectName] : undefined; + const srcBasePath = workspaceProject?.sourceRoot || (workspaceProject?.root ? posix.resolve(workspaceProject.root, 'src') : ''); + const styleFile = posix.resolve(srcBasePath, 'styles.scss'); + if (!tree.exists(styleFile)) { + context.logger.warn(`The theme was not updated as ${styleFile} was not found`); + return tree; + } + + return tree.overwrite(styleFile, '@import "./style/theme.scss";\n' + tree.readText(styleFile)); + }; + + return chain([ + registerBuilderRule, + generateTemplateRule, + importTheme + ]); +}; diff --git a/packages/@o3r/design/schematics/ng-add/register-generate-css/templates/design-token.custom.json.template b/packages/@o3r/design/schematics/ng-add/register-generate-css/templates/design-token.custom.json.template new file mode 100644 index 0000000000..635c8a03c4 --- /dev/null +++ b/packages/@o3r/design/schematics/ng-add/register-generate-css/templates/design-token.custom.json.template @@ -0,0 +1,3 @@ +{ + "$schema": "https://raw.githubusercontent.com/AmadeusITGroup/otter/main/packages/@o3r/design/schemas/design-token.schema.json" +} diff --git a/packages/@o3r/design/schematics/ng-add/register-generate-css/templates/theme.scss.template b/packages/@o3r/design/schematics/ng-add/register-generate-css/templates/theme.scss.template new file mode 100644 index 0000000000..5c9365f8e6 --- /dev/null +++ b/packages/@o3r/design/schematics/ng-add/register-generate-css/templates/theme.scss.template @@ -0,0 +1,6 @@ + +:root { +/* --- BEGIN THEME Auto-generated --- */ + +/* --- END THEME Auto-generated --- */ +} diff --git a/packages/@o3r/design/schematics/ng-add/schema.json b/packages/@o3r/design/schematics/ng-add/schema.json new file mode 100644 index 0000000000..b9b344baf6 --- /dev/null +++ b/packages/@o3r/design/schematics/ng-add/schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "ngAddSchematicsSchema", + "title": "Add Otter Design", + "description": "ngAdd Otter Design", + "properties": { + "projectName": { + "type": "string", + "description": "Project name", + "$default": { + "$source": "projectName" + } + } + }, + "additionalProperties": true, + "required": [ + ] +} diff --git a/packages/@o3r/design/schematics/ng-add/schema.ts b/packages/@o3r/design/schematics/ng-add/schema.ts new file mode 100644 index 0000000000..76d3853e73 --- /dev/null +++ b/packages/@o3r/design/schematics/ng-add/schema.ts @@ -0,0 +1,6 @@ +import type { SchematicOptionObject } from '@o3r/schematics'; + +export interface NgAddSchematicsSchema extends SchematicOptionObject { + /** Project name */ + projectName?: string | undefined; +} diff --git a/packages/@o3r/design/src/core/design-token/design-token-specification.interface.ts b/packages/@o3r/design/src/core/design-token/design-token-specification.interface.ts new file mode 100644 index 0000000000..524fea5204 --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/design-token-specification.interface.ts @@ -0,0 +1,264 @@ +/** Metadata information added in the design token extension for Metadata extraction */ +export interface DesignTokenMetadata { + tags?: string[]; + /** Description of the variable */ + label?: string; + /** Name of a group of variables */ + category?: string; +} + +/** Design Token Group Extension fields supported by the default renderer */ +export interface DesignTokenGroupExtensions { + /** Indicate the file where to generate the token */ + o3rTargetFile?: string; + /** + * Indicate that the variable does not need to be generated. + * It is up to the generator to describe how to render private variables. + * It can choose to ignore the private extension, it can provide a dedicated renderer (for example to prefix it with '_') or it can decide to skip the generation straight to its referenced value. + */ + o3rPrivate?: boolean; + /** Indicate that the value of this token is flagged as important */ + o3rImportant?: boolean; + /** Metadata specific information */ + o3rMetadata?: DesignTokenMetadata; + /** Scope of the Design Token value */ + o3rScope?: string; +} + +/** Design Token Extension fields supported by the default renderer */ +export interface DesignTokenExtensions extends DesignTokenGroupExtensions { +} + + +interface DesignTokenBase { + /** Value of the Token */ + $value: T; + + /** Type of the Design Token */ + $type: string; +} + +/** Design Token without explicit type (mainly alias) */ +export interface DesignTokenTypeImplicit { + /** Value of the Token */ + $value: string; + + /** @inheritdoc */ + $type?: undefined; +} + +/** Design Token Color */ +export interface DesignTokenTypeColor extends DesignTokenBase { + /** @inheritdoc */ + $type: 'color'; +} + +/** Design Token Dimension */ +export interface DesignTokenTypeDimension extends DesignTokenBase { + /** @inheritdoc */ + $type: 'dimension'; +} + +/** Design Token Font Family */ +export interface DesignTokenTypeFontFamily extends DesignTokenBase { + /** @inheritdoc */ + $type: 'fontFamily'; +} + +/** Design Token Duration */ +export interface DesignTokenTypeDuration extends DesignTokenBase { + /** @inheritdoc */ + $type: 'duration'; +} + +type DesignTokenTypeCubicBezierValue = (number | string)[]; + +/** Design Token Cubic Bezier */ +export interface DesignTokenTypeCubicBezier extends DesignTokenBase { + /** @inheritdoc */ + $type: 'cubicBezier'; +} + +type DesignTokenTypeFontWeightValue = number | string; + +/** Design Token Font Weight */ +export interface DesignTokenTypeFontWeight extends DesignTokenBase { + /** @inheritdoc */ + $type: 'fontWeight'; +} + +/** Design Token Number */ +export interface DesignTokenTypeNumber extends DesignTokenBase { + /** @inheritdoc */ + $type: 'number'; +} + +type DesignTokenTypeStrokeStyleDetailsValue = { + dashArray: string[]; + lineCap: 'round' | 'butt' | 'square'; +}; + +/** Value of the Design Token Stroke Style */ +export type DesignTokenTypeStrokeStyleValue = DesignTokenTypeStrokeStyleDetailsValue | + 'solid' | 'dashed' | 'dotted' | 'double'| 'groove' | 'ridge' | 'outset' | 'inset'; + +/** Design Token Stroke Style */ +export interface DesignTokenTypeStrokeStyle extends DesignTokenBase { + /** @inheritdoc */ + $type: 'strokeStyle'; +} + +type DesignTokenTypeBorderValue = { + color: string; + width: string; + // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents + style: string | DesignTokenTypeStrokeStyleValue; + +}; + +/** Design Token Border */ +export interface DesignTokenTypeBorder extends DesignTokenBase { + /** @inheritdoc */ + $type: 'border'; +} + +type DesignTokenTypeTransitionValue = { + duration: string; + delay: string; + timingFunction: string | DesignTokenTypeCubicBezierValue; + +}; + +/** Design Token Transition */ +export interface DesignTokenTypeTransition extends DesignTokenBase { + /** @inheritdoc */ + $type: 'transition'; +} + +type DesignTokenTypeShadowValue = { + color: string; + offsetX: string; + offsetY: string; + blur: string; + spread: string; +}; + +/** Design Token Shadow */ +export interface DesignTokenTypeShadow extends DesignTokenBase { + /** @inheritdoc */ + $type: 'shadow'; +} + +type DesignTokenTypeGradientValue = { + color: string; + position: string | number; +}[]; + +/** Design Token Gradient */ +export interface DesignTokenTypeGradient extends DesignTokenBase { + /** @inheritdoc */ + $type: 'gradient'; +} + +type DesignTokenTypeTypographyValue = { + fontFamily: string; + fontSize: string; + letterSpacing: string; + fontWeight: DesignTokenTypeFontWeightValue; + lineHeight: string | number; +}; + + +/** Design Token Typography */ +export interface DesignTokenTypeTypography extends DesignTokenBase { + /** @inheritdoc */ + $type: 'typography'; +} + +/** Common field for the Design Token Groups */ +export interface DesignTokenGroupCommonFields { + /** Description of the Group */ + $description?: string; + /** Design Token Extension */ + $extensions?: G; +} + +/** Common field for the Design Token */ +export type DesignTokenCommonFields = DesignTokenGroupCommonFields; + +/** Available Design Token types */ +export type DesignToken = DesignTokenCommonFields & ( + DesignTokenTypeColor | + DesignTokenTypeDimension | + DesignTokenTypeFontFamily | + DesignTokenTypeDuration | + DesignTokenTypeCubicBezier | + DesignTokenTypeFontWeight | + DesignTokenTypeNumber | + + DesignTokenTypeStrokeStyle | + DesignTokenTypeBorder | + DesignTokenTypeTransition | + DesignTokenTypeShadow | + DesignTokenTypeGradient | + DesignTokenTypeTypography | + + DesignTokenTypeImplicit +); + +/** Design Token Node (Design Token Group or Item) */ +// eslint-disable-next-line no-use-before-define +export type DesignTokenNode = DesignTokenGroup | DesignToken; + +/** Design Token Group */ +export type DesignTokenGroup = + DesignTokenGroupCommonFields & { [x: string]: DesignTokenNode | E | string | boolean | undefined }; + +/** Context of the Design Token specification document */ +export type DesignTokenContext = { + /** Base path used to compute the path of the file to render the Tokens into */ + basePath?: string; +}; + +/** Design Token specification */ +export type DesignTokenSpecification = { + /** Specification as described on {@link https://design-tokens.github.io/community-group/format/} */ + document: DesignTokenGroup; + /** Specification document context information */ + context?: C; +}; + +/** + * Determine if the Design Token Node is a Token (not a Group) + * @param node Design Token Node + */ +export const isDesignToken = (node?: any): node is DesignToken => { + return !!node && (typeof node.$type !== 'undefined' || typeof node.$value === 'string'); +}; + +/** + * Determine if the Design Token Node is a Group (not a Token) + * @param node Design Token Node + */ +export const isDesignTokenGroup = (node?: any): node is DesignTokenGroup => { + return typeof node === 'object' && Object.keys(node).some((k) => !k.startsWith('$')); +}; + +/** + * Determine if the Stroke Style value is defined or is a reference + * @param value Stroke Style value + * @returns true if it is a defined value + */ +// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents +export const isTokenTypeStrokeStyleValueComplex = (value?: DesignTokenTypeStrokeStyleValue | string): value is DesignTokenTypeStrokeStyleDetailsValue => { + return !!value && typeof value !== 'string'; +}; + +/** + * Determine if the Stroke Style Token has a value defined or is a reference + * @param node Stroke Style Token + * @returns true if it is a token with defined value + */ +export const isTokenTypeStrokeStyleComplex = (node?: DesignTokenTypeStrokeStyle): node is DesignTokenTypeStrokeStyle => { + return !!node && isTokenTypeStrokeStyleValueComplex(node.$value); +}; diff --git a/packages/@o3r/design/src/core/design-token/design-token.spec.ts b/packages/@o3r/design/src/core/design-token/design-token.spec.ts new file mode 100644 index 0000000000..590a5c8229 --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/design-token.spec.ts @@ -0,0 +1,204 @@ +import { + computeFileToUpdatePath, + DesignTokenRendererOptions, + getCssStyleContentUpdater, + getCssTokenDefinitionRenderer, + getMetadataStyleContentUpdater, + getMetadataTokenDefinitionRenderer, + getSassTokenDefinitionRenderer, + renderDesignTokens +} from './renderers/index'; +import { parseDesignToken, TokenKeyRenderer } from './parsers/index'; +import { promises as fs } from 'node:fs'; +import { resolve } from 'node:path'; +import type { DesignTokenSpecification } from './design-token-specification.interface'; +import { validate } from 'jsonschema'; + +describe('Design Token generator', () => { + const AUTO_GENERATED_START = '/* --- BEGIN THEME Test --- */'; + const AUTO_GENERATED_END = '/* --- END THEME Test --- */'; + let exampleVariable!: DesignTokenSpecification; + + beforeAll(async () => { + exampleVariable = {document: JSON.parse(await fs.readFile(resolve(__dirname, '../../../testing/mocks/design-token-theme.json'), {encoding: 'utf-8'}))}; + }); + + describe('CSS renderer', () => { + + const renderDesignTokensOptions = { + styleContentUpdater: getCssStyleContentUpdater({startTag: AUTO_GENERATED_START, endTag: AUTO_GENERATED_END}) + }; + + test('should render variable in CSS', async () => { + let result: string | undefined; + const writeFile = jest.fn().mockImplementation((_, content) => result = content); + const readFile = jest.fn().mockReturnValue(''); + const existsFile = jest.fn().mockReturnValue(true); + const determineFileToUpdate = computeFileToUpdatePath('.'); + const designToken = parseDesignToken(exampleVariable); + + // eslint-disable-next-line @typescript-eslint/await-thenable + await renderDesignTokens(designToken, { + ...renderDesignTokensOptions, + determineFileToUpdate, + existsFile, + writeFile, + readFile + }); + + expect(writeFile).toHaveBeenCalledTimes(1); + expect(result).toContain('--example-var1: #000;'); + expect(result).toContain('--example-color: var(--example-var1);'); + expect(result).not.toContain('--example-test-height: 2.3;'); + expect(result).toContain('--example-test-width: var(--example-test-height, 2.3);'); + }); + + test('should render variable with important flag', async () => { + let result: string | undefined; + const writeFile = jest.fn().mockImplementation((_, content) => result = content); + const readFile = jest.fn().mockReturnValue(''); + const existsFile = jest.fn().mockReturnValue(true); + const determineFileToUpdate = computeFileToUpdatePath('.'); + const designToken = parseDesignToken(exampleVariable); + + // eslint-disable-next-line @typescript-eslint/await-thenable + await renderDesignTokens(designToken, { + ...renderDesignTokensOptions, + determineFileToUpdate, + existsFile, + writeFile, + readFile + }); + + expect(result).toContain('--example-var-important: #000 !important;'); + }); + + test('should render variable with prefix', async () => { + let result: string | undefined; + const prefix = 'prefix-'; + const writeFile = jest.fn().mockImplementation((_, content) => result = content); + const readFile = jest.fn().mockReturnValue(''); + const existsFile = jest.fn().mockReturnValue(true); + const determineFileToUpdate = computeFileToUpdatePath('.'); + const designToken = parseDesignToken(exampleVariable); + const tokenVariableNameRenderer: TokenKeyRenderer = (variable) => prefix + variable.tokenReferenceName.replace(/\./g, '-'); + const tokenDefinitionRenderer = getCssTokenDefinitionRenderer({tokenVariableNameRenderer}); + + // eslint-disable-next-line @typescript-eslint/await-thenable + await renderDesignTokens(designToken, { + ...renderDesignTokensOptions, + tokenDefinitionRenderer, + determineFileToUpdate, + existsFile, + writeFile, + readFile + }); + + expect(result).not.toContain('--example-var1'); + expect(result).toContain('--prefix-example-var1'); + }); + + test('should render variable in existing CSS', async () => { + let result: string | undefined; + const writeFile = jest.fn().mockImplementation((_, content) => result = content); + const existsFile = jest.fn().mockReturnValue(true); + const readFile = jest.fn().mockReturnValue(` + // CSS + :root { + ${AUTO_GENERATED_START} + --some-var: #fff; + ${AUTO_GENERATED_END} + } + `); + const determineFileToUpdate = computeFileToUpdatePath('.'); + const designToken = parseDesignToken(exampleVariable); + + // eslint-disable-next-line @typescript-eslint/await-thenable + await renderDesignTokens(designToken, { + ...renderDesignTokensOptions, + determineFileToUpdate, + existsFile, + writeFile, + readFile + }); + + expect(writeFile).toHaveBeenCalledTimes(1); + expect(result).not.toContain('--some-var: #fff;'); + expect(result).toContain('--example-var1: #000;'); + }); + + test('should render private variable to sass if requested', async () => { + let result: string | undefined; + const expectedSassVar = '$exampleTestHeight: 2.3;'; + const writeFile = jest.fn().mockImplementation((_, content) => result = content); + const readFile = jest.fn().mockReturnValue(''); + const existsFile = jest.fn().mockReturnValue(true); + const determineFileToUpdate = computeFileToUpdatePath('.'); + const designToken = parseDesignToken(exampleVariable); + + const tokenDefinitionRendererWithoutSass = getCssTokenDefinitionRenderer({ + privateDefinitionRenderer: getSassTokenDefinitionRenderer() + }); + + // eslint-disable-next-line @typescript-eslint/await-thenable + await renderDesignTokens(designToken, { + ...renderDesignTokensOptions, + determineFileToUpdate, + writeFile, + existsFile, + readFile + }); + + expect(result).not.toContain(expectedSassVar); + + // eslint-disable-next-line @typescript-eslint/await-thenable + await renderDesignTokens(designToken, { + ...renderDesignTokensOptions, + determineFileToUpdate, + tokenDefinitionRenderer: tokenDefinitionRendererWithoutSass, + writeFile, + existsFile, + readFile + }); + + expect(writeFile).toHaveBeenCalledTimes(2); + expect(result).toContain(expectedSassVar); + }); + }); + + describe('Metadata Renderer', () => { + let metadataSchema: any; + const renderDesignTokensOptions: DesignTokenRendererOptions = { + styleContentUpdater: getMetadataStyleContentUpdater(), + tokenDefinitionRenderer: getMetadataTokenDefinitionRenderer() + }; + + beforeAll(async () => { + metadataSchema = JSON.parse(await fs.readFile(resolve(__dirname, '../../../../styling/schemas/style.metadata.schema.json'), { encoding: 'utf-8' })); + }); + + test('should render valid metadata', async () => { + let result: string | undefined; + const writeFile = jest.fn().mockImplementation((_, content) => result = content); + const readFile = jest.fn().mockReturnValue(''); + const existsFile = jest.fn().mockReturnValue(true); + const determineFileToUpdate = computeFileToUpdatePath('.'); + const designToken = parseDesignToken(exampleVariable); + + // eslint-disable-next-line @typescript-eslint/await-thenable + await renderDesignTokens(designToken, { + ...renderDesignTokensOptions, + determineFileToUpdate, + existsFile, + writeFile, + readFile + }); + + expect(writeFile).toHaveBeenCalledTimes(1); + expect(() => JSON.parse(result)).not.toThrow(); + expect(metadataSchema).toBeDefined(); + expect(validate).toBeDefined(); + expect(validate(JSON.parse(result), metadataSchema).errors).toHaveLength(0); + }); + }); +}); diff --git a/packages/@o3r/design/src/core/design-token/index.ts b/packages/@o3r/design/src/core/design-token/index.ts new file mode 100644 index 0000000000..3d283d48a7 --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/index.ts @@ -0,0 +1,3 @@ +export * from './renderers/index'; +export * from './parsers/index'; +export * from './design-token-specification.interface'; diff --git a/packages/@o3r/design/src/core/design-token/parsers/design-token-parser.interface.ts b/packages/@o3r/design/src/core/design-token/parsers/design-token-parser.interface.ts new file mode 100644 index 0000000000..86bca74d45 --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/parsers/design-token-parser.interface.ts @@ -0,0 +1,77 @@ +import type { DesignToken, DesignTokenContext, DesignTokenExtensions, DesignTokenGroup, DesignTokenGroupExtensions } from '../design-token-specification.interface'; + +/** Reference to a parent node*/ +export interface ParentReference { + /** Design Token name */ + name: string; + /** Design Token Group node */ + tokenNode: DesignTokenGroup; +} + +/** + * Function rendering the Design Token Value + * @param tokenStructure Parsed Design Token + * @param variableSet Complete list of the parsed Design Token + */ +// eslint-disable-next-line no-use-before-define +export type TokenValueRenderer = (tokenStructure: DesignTokenVariableStructure, variableSet: Map) => string; + +/** + * Function rendering the Design Token Key + * @param tokenStructure Parsed Design Token + */ +// eslint-disable-next-line no-use-before-define +export type TokenKeyRenderer = (tokenStructure: DesignTokenVariableStructure) => string; + +/** Complete list of the parsed Design Token */ +// eslint-disable-next-line no-use-before-define +export type DesignTokenVariableSet = Map; + +/** Parsed Design Token variable */ +export interface DesignTokenVariableStructure { + /** Context of the Token determined or provided during the parsing process */ + context?: DesignTokenContext; + /** Design Token Extension */ + extensions: DesignTokenGroupExtensions & DesignTokenExtensions; + /** Reference to the Design Token node */ + node: DesignToken; + /** Name of the token in references */ + tokenReferenceName: string; + /** Description of the Token */ + description?: string; + /** List of the Ancestors references */ + ancestors: ParentReference[]; + /** Reference to the direct parent node */ + parent?: ParentReference; + /** + * Retrieve the list of the references of the Design Token + * @param variableSet Complete list of the parsed Design Token + */ + getReferences: (variableSet?: DesignTokenVariableSet) => string[]; + /** + * Raw CSS value of the Token + * @param variableSet Complete list of the parsed Design Token + */ + getCssRawValue: (variableSet?: DesignTokenVariableSet) => string; + /** + * Determine if the Token is an alias + * @param variableSet Complete list of the parsed Design Token + */ + getIsAlias: (variableSet?: DesignTokenVariableSet) => boolean; + /** + * Retrieve the type calculated for the Token + * @param followReference Determine if the references should be follow to calculate the type + * @param variableSet Complete list of the parsed Design Token + */ + getType: (variableSet?: DesignTokenVariableSet, followReference?: boolean) => DesignToken['$type']; + /** + * Retrieve the list of the references of the Design Token node + * @param followReference Determine if the references should be follow to calculate the type + */ + getReferencesNode: (variableSet?: DesignTokenVariableSet) => DesignTokenVariableStructure[]; + /** + * Retrieve the Design Token Key as rendered by the provided renderer + * @param keyRenderer Renderer for the Design Token key + */ + getKey: (keyRenderer?: TokenKeyRenderer) => string; +} diff --git a/packages/@o3r/design/src/core/design-token/parsers/design-token.parser.spec.ts b/packages/@o3r/design/src/core/design-token/parsers/design-token.parser.spec.ts new file mode 100644 index 0000000000..2ad8bc086a --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/parsers/design-token.parser.spec.ts @@ -0,0 +1,70 @@ +import * as parser from './design-token.parser'; +import { promises as fs } from 'node:fs'; +import { resolve } from 'node:path'; +import type { DesignTokenSpecification } from '../design-token-specification.interface'; + +describe('Design Token Parser', () => { + + let exampleVariable!: DesignTokenSpecification; + + beforeAll(async () => { + const file = await fs.readFile(resolve(__dirname, '../../../../testing/mocks/design-token-theme.json'), { encoding: 'utf-8' }); + exampleVariable = {document: JSON.parse(file)}; + }); + + describe('parseDesignToken', () => { + + test('generate a simple type variable', () => { + const result = parser.parseDesignToken(exampleVariable); + const var1 = result.get('example.var1'); + const var2 = result.get('example.test.var2'); + + expect(result.size).toBeGreaterThan(0); + expect(var2).toBeDefined(); + expect(var1).toBeDefined(); + expect(var2.getKey()).toBe('example-test-var2'); + expect(var1.getKey()).toBe('example-var1'); + expect(var2.description).toBe('my var2'); + expect(var2.getType()).toBe('color'); + expect(var1.getType()).toBe('color'); + }); + + test('generate an alias variable', () => { + const result = parser.parseDesignToken(exampleVariable); + const color = result.get('example.color'); + + expect(color).toBeDefined(); + expect(color.getType()).toBe('color'); + }); + + test('generate a complex variable', () => { + const result = parser.parseDesignToken(exampleVariable); + const border = result.get('example.test.border'); + + expect(border).toBeDefined(); + expect(border.getType()).toBe('border'); + }); + }); + + describe('parseDesignTokenFile', () => { + test('should read the file according to the reader', async () => { + const readFile = jest.fn().mockResolvedValue('{"test": { "$value": "#000", "$type": "color" }}'); + const parseDesignToken = jest.spyOn(parser, 'parseDesignToken').mockImplementation(() => (new Map())); + const result = await parser.parseDesignTokenFile('fakeFile.json', {readFile}); + + expect(result.size).toBe(0); + expect(parseDesignToken).toHaveBeenCalledTimes(1); + expect(readFile).toHaveBeenCalledTimes(1); + expect(parseDesignToken).toHaveBeenCalledWith({context: { basePath: '.' }, document: { test: { $value: '#000', $type: 'color' } } }); + }); + + test('should throw if invalid JSON Token', async () => { + const readFile = jest.fn().mockResolvedValue('{"test": { "$value": "#000", '); + const parseDesignToken = jest.spyOn(parser, 'parseDesignToken').mockImplementation(() => (new Map())); + + await expect(() => parser.parseDesignTokenFile('fakeFile.json', {readFile})).rejects.toThrow(); + expect(parseDesignToken).toHaveBeenCalledTimes(0); + expect(readFile).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/packages/@o3r/design/src/core/design-token/parsers/design-token.parser.ts b/packages/@o3r/design/src/core/design-token/parsers/design-token.parser.ts new file mode 100644 index 0000000000..59ae861da6 --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/parsers/design-token.parser.ts @@ -0,0 +1,183 @@ +import { promises as fs } from 'node:fs'; +import type { DesignTokenVariableSet, DesignTokenVariableStructure, ParentReference } from './design-token-parser.interface'; +import type { + DesignToken, + DesignTokenContext, + DesignTokenExtensions, + DesignTokenGroup, + DesignTokenGroupExtensions, + DesignTokenNode, + DesignTokenSpecification +} from '../design-token-specification.interface'; +import { + DesignTokenTypeStrokeStyleValue, + isDesignToken, + isDesignTokenGroup, + isTokenTypeStrokeStyleValueComplex +} from '../design-token-specification.interface'; +import { dirname } from 'node:path'; + +const tokenReferenceRegExp = /\{([^}]+)\}/g; + +const getTokenReferenceName = (tokenName: string, parents: string[]) => (`${parents.join('.')}.${tokenName}`); +const getExtensions = (parentNode: DesignTokenNode[]) => parentNode.reduce((acc, node) => ({...acc, ...node.$extensions}), {} as DesignTokenGroupExtensions & DesignTokenExtensions); +const getReferences = (cssRawValue: string) => Array.from(cssRawValue.matchAll(tokenReferenceRegExp)).map(([,tokenRef]) => tokenRef); +// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents +const renderCssTypeStrokeStyleValue = (value: DesignTokenTypeStrokeStyleValue | string) => isTokenTypeStrokeStyleValueComplex(value) ? `${value.lineCap} ${value.dashArray.join(' ')}` : value; + +const getCssRawValue = (variableSet: DesignTokenVariableSet, {node, getType}: DesignTokenVariableStructure) => { + const nodeType = getType(variableSet, false); + if (!nodeType && node.$value) { + return typeof node.$value.toString !== undefined ? (node.$value as any).toString() : JSON.stringify(node.$value); + } + const checkNode = { + ...node, + $type: node.$type || nodeType + } as typeof node; + + // TODO in the following code, `typeof checkNode.$value === 'string' ? checkNode.$value :` is defined to please Jest TS compilation. It should be removed when supported + switch (checkNode.$type) { + case 'color': + case 'number': + case 'duration': + case 'fontWeight': + case 'fontFamily': + case 'dimension': { + return checkNode.$value.toString(); + } + case 'strokeStyle': { + return renderCssTypeStrokeStyleValue(checkNode.$value); + } + case 'cubicBezier': { + return typeof checkNode.$value === 'string' ? checkNode.$value : + checkNode.$value.join(', '); + } + case 'border': { + return typeof checkNode.$value === 'string' ? checkNode.$value : + `${checkNode.$value.width} ${renderCssTypeStrokeStyleValue(checkNode.$value.style)} ${checkNode.$value.color}`; + } + case 'gradient': { + return typeof checkNode.$value === 'string' ? checkNode.$value : + // TODO: add support of different gradient type when design-tokens/community-group#101 is fixed. + `linear-gradient(0deg, ${checkNode.$value.map(({color, position}) => `${color} ${position}`).join(', ')})`; + } + case 'shadow': { + return typeof checkNode.$value === 'string' ? checkNode.$value : + `${checkNode.$value.offsetX} ${checkNode.$value.offsetY} ${checkNode.$value.blur} ${checkNode.$value.spread} ${checkNode.$value.color}`; + } + case 'transition': { + return typeof checkNode.$value === 'string' ? checkNode.$value : + typeof checkNode.$value.timingFunction === 'string' ? checkNode.$value.timingFunction : checkNode.$value.timingFunction.join(' ') + + ` ${checkNode.$value.duration} ${checkNode.$value.delay}`; + } + case 'typography': { + return typeof checkNode.$value === 'string' ? checkNode.$value : + `${checkNode.$value.fontWeight} ${checkNode.$value.fontFamily} ${checkNode.$value.fontSize} ${checkNode.$value.letterSpacing} ${checkNode.$value.lineHeight}`; + } + default: { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new Error(`Not supported type ${(checkNode as any).$type || 'unknown'} (value: ${(checkNode as any).$value || 'unknown'})`); + } + } +}; + +const walkThroughDesignTokenNodes = ( + node: DesignTokenNode, + context: DesignTokenContext | undefined, + ancestors: ParentReference[], + mem: DesignTokenVariableSet, + nodeName?: string): DesignTokenVariableSet => { + + if (isDesignTokenGroup(node)) { + Object.entries(node) + .filter(([tokenName, tokenNode]) => !tokenName.startsWith('$') && (isDesignToken(tokenNode) || isDesignTokenGroup(tokenNode))) + .forEach(([tokenName, tokenNode]) => walkThroughDesignTokenNodes( + tokenNode as DesignTokenGroup | DesignToken, context, nodeName ? [...ancestors, { name: nodeName, tokenNode: node }] : ancestors, mem, tokenName + )); + } + + if (isDesignToken(node)) { + if (!nodeName) { + throw new Error('The first node of the Design Specification can not be a token'); + } + const parentNames = ancestors.map(({ name }) => name); + const tokenReferenceName = getTokenReferenceName(nodeName, parentNames); + + const tokenVariable: DesignTokenVariableStructure = { + context, + extensions: getExtensions([...ancestors.map(({ tokenNode }) => tokenNode), node]), + node, + tokenReferenceName, + ancestors, + parent: ancestors.slice(-1)[0], + description: node.$description, + getCssRawValue: function (variableSet = mem) { + return getCssRawValue(variableSet, this); + }, + getReferences: function (variableSet = mem) { + return getReferences(this.getCssRawValue(variableSet)); + }, + getIsAlias: function (variableSet = mem) { + return this.getReferences(variableSet).length === 1 && typeof node.$value === 'string' && !!node.$value?.toString().match(/^\{[^}]*\}$/); + }, + getReferencesNode: function (variableSet = mem) { + return this.getReferences(variableSet) + .map((ref) => { + if (!variableSet.has(ref)) { + throw new Error (`Reference to ${ref} not found`); + } + return variableSet.get(ref)!; + }); + }, + getType: function (variableSet = mem, followReference = true) { + return node.$type || + followReference && this.getIsAlias(variableSet) && this.getReferencesNode(variableSet)[0]?.getType(variableSet, followReference) || + followReference && this.parent?.name && variableSet.get(this.parent.name)?.getType(variableSet, followReference) || + undefined; + }, + getKey: function (keyRenderer) { + return keyRenderer ? keyRenderer(this) : this.tokenReferenceName.replace(/[ .]+/g, '-'); + } + }; + + mem.set(tokenReferenceName, tokenVariable); + } + else if (!isDesignTokenGroup(node)) { + throw new Error('Fail to determine the Design Token Node type'); + } + + return mem; +}; + +/** + * Parse a Design Token Object to provide the map of Token with helpers to generate the different output + * @param specification Design Token content as specified on https://design-tokens.github.io/community-group/format/ + */ +export const parseDesignToken = (specification: DesignTokenSpecification): DesignTokenVariableSet => { + return walkThroughDesignTokenNodes(specification.document, specification.context, [], new Map()); +}; + +interface ParseDesignTokenFileOptions { + /** + * Custom function to read a file required by the token renderer + * @default {@see fs.promises.readFile} + * @param filePath Path to the file to read + */ + readFile?: (filePath: string) => string | Promise; + + /** Custom context to provide to the parser and override the information determined by the specification parse process */ + specificationContext?: DesignTokenContext; +} + +/** + * Parse a Design Token File to provide the map of Token with helpers to generate the different output + * @param specificationFilePath Path to the a Design Token file following the specification on https://design-tokens.github.io/community-group/format/ + * @param options + */ +export const parseDesignTokenFile = async (specificationFilePath: string, options?: ParseDesignTokenFileOptions) => { + const readFile = options?.readFile || ((filePath: string) => fs.readFile(filePath, { encoding: 'utf-8' })); + const context: DesignTokenContext = { + basePath: dirname(specificationFilePath) + }; + return parseDesignToken({ document: JSON.parse(await readFile(specificationFilePath)), context }); +}; diff --git a/packages/@o3r/design/src/core/design-token/parsers/index.ts b/packages/@o3r/design/src/core/design-token/parsers/index.ts new file mode 100644 index 0000000000..618a3de3a7 --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/parsers/index.ts @@ -0,0 +1,2 @@ +export * from './design-token-parser.interface'; +export * from './design-token.parser'; diff --git a/packages/@o3r/design/src/core/design-token/renderers/css/design-token-definition.renderers.spec.ts b/packages/@o3r/design/src/core/design-token/renderers/css/design-token-definition.renderers.spec.ts new file mode 100644 index 0000000000..d9b0311e35 --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/renderers/css/design-token-definition.renderers.spec.ts @@ -0,0 +1,50 @@ +import * as parser from '../../parsers/design-token.parser'; +import { promises as fs } from 'node:fs'; +import { resolve } from 'node:path'; +import type { DesignTokenSpecification } from '../../design-token-specification.interface'; +import type { DesignTokenVariableSet } from '../../parsers'; +import { getCssTokenDefinitionRenderer } from './design-token-definition.renderers'; + +describe('getMetadataTokenDefinitionRenderer', () => { + let exampleVariable!: DesignTokenSpecification; + let designTokens!: DesignTokenVariableSet; + + beforeAll(async () => { + const file = await fs.readFile(resolve(__dirname, '../../../../../testing/mocks/design-token-theme.json'), { encoding: 'utf-8' }); + exampleVariable = { document: JSON.parse(file) }; + designTokens = parser.parseDesignToken(exampleVariable); + }); + + test('should rely on given tokenValueRenderer', () => { + const tokenValueRenderer = jest.fn().mockReturnValue(JSON.stringify({name: 'test-var', value: 'test-value'})); + const renderer = getCssTokenDefinitionRenderer({ tokenValueRenderer }); + const variable = designTokens.get('example.var1'); + + const result = renderer(variable, designTokens); + expect(variable).toBeDefined(); + expect(tokenValueRenderer).toHaveBeenCalledTimes(1); + expect(result).toBeDefined(); + expect(result).toContain('test-var'); + expect(result).toContain('test-value'); + }); + + test('should use private renderer for private variable', () => { + const variable = designTokens.get('example.var3'); + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + const privateDefinitionRenderer = jest.fn().mockImplementation((v: any) => `$test: ${v.getCssRawValue()}`); + + const renderer1 = getCssTokenDefinitionRenderer(); + const renderer2 = getCssTokenDefinitionRenderer({ privateDefinitionRenderer }); + const result1 = renderer1(variable, designTokens); + + expect(variable).toBeDefined(); + expect(result1).not.toBeDefined(); + expect(privateDefinitionRenderer).toHaveBeenCalledTimes(0); + + const result2 = renderer2(variable, designTokens); + + expect(result2).toContain('$test'); + expect(privateDefinitionRenderer).toHaveBeenCalledTimes(1); + }); + +}); diff --git a/packages/@o3r/design/src/core/design-token/renderers/css/design-token-definition.renderers.ts b/packages/@o3r/design/src/core/design-token/renderers/css/design-token-definition.renderers.ts new file mode 100644 index 0000000000..428d7fd6d8 --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/renderers/css/design-token-definition.renderers.ts @@ -0,0 +1,74 @@ +import type { DesignTokenVariableStructure, TokenKeyRenderer, TokenValueRenderer } from '../../parsers/design-token-parser.interface'; +import { isO3rPrivateVariable } from '../design-token.renderer.helpers'; +import { TokenDefinitionRenderer } from '../design-token.renderer.interface'; +import { getCssTokenValueRenderer } from './design-token-value.renderers'; + +/** Options for {@link CssTokenDefinitionRendererOptions} */ +export interface CssTokenDefinitionRendererOptions { + /** + * Determine if the variable is private and should not be rendered + * @default {@see isO3rPrivateVariable} + */ + isPrivateVariable?: (variable: DesignTokenVariableStructure) => boolean; + + /** Custom Design Token value renderer */ + tokenValueRenderer?: TokenValueRenderer; + + /** + * Renderer the name of the CSS Variable (with the initial --) + */ + tokenVariableNameRenderer?: TokenKeyRenderer; + + /** + * Private Design Token definition renderer + * The private variable will not be rendered if not provided + */ + privateDefinitionRenderer?: TokenDefinitionRenderer; +} + +/** + * Retrieve the Design Token variable renderer for CSS + * @param options + * @returns + * @example CSS renderer with Sass fallback + * ```typescript + * import { getSassTokenDefinitionRenderer } from '@o3r/design'; + * + * // List of parsed Design Token items + * const parsedTokenDesign = await parseDesignTokenFile('./path/to/spec.json'); + * + * // Sass variable renderer + * const sassTokenDefinitionRenderer = getSassTokenDefinitionRenderer(); + * + * // CSS variable renderer + * const cssTokenDefinitionRenderer = getCssTokenDefinitionRenderer({ + * // Specify that the private variable should be rendered in Sass variable + * privateDefinitionRenderer: sassTokenDefinitionRenderer + * }); + * + * // Render the CSS variables + * await renderDesignTokens(parsedTokenDesign, { tokenDefinitionRenderer: cssTokenDefinitionRenderer }); + * ``` + */ +export const getCssTokenDefinitionRenderer = (options?: CssTokenDefinitionRendererOptions): TokenDefinitionRenderer => { + const isPrivateVariable = options?.isPrivateVariable || isO3rPrivateVariable; + const tokenVariableNameRenderer = options?.tokenVariableNameRenderer; + const tokenValueRenderer = options?.tokenValueRenderer || getCssTokenValueRenderer({ isPrivateVariable, tokenVariableNameRenderer }); + + const renderer = (variable: DesignTokenVariableStructure, variableSet: Map) => { + let variableString: string | undefined; + if (!isPrivateVariable(variable)) { + variableString = `--${variable.getKey(tokenVariableNameRenderer)}: ${tokenValueRenderer(variable, variableSet)};`; + if (variable.extensions.o3rScope) { + variableString = `${variable.extensions.o3rScope} { ${variableString} }`; + } + } else if (options?.privateDefinitionRenderer && variable.extensions.o3rPrivate) { + variableString = options.privateDefinitionRenderer(variable, variableSet); + } + if (variableString && variable.description) { + variableString = `/* ${variable.description} */\n${variableString}`; + } + return variableString; + }; + return renderer; +}; diff --git a/packages/@o3r/design/src/core/design-token/renderers/css/design-token-updater.renderers.spec.ts b/packages/@o3r/design/src/core/design-token/renderers/css/design-token-updater.renderers.spec.ts new file mode 100644 index 0000000000..c3d2d2dbbc --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/renderers/css/design-token-updater.renderers.spec.ts @@ -0,0 +1,48 @@ +import { getCssStyleContentUpdater } from './design-token-updater.renderers'; + +describe('getCssStyleContentUpdater', () => { + const startTag = '/* test start */'; + const endTag = '/* end start */'; + const cssUpdaterOptions = { startTag, endTag}; + + test('should render CSS Values', () => { + const renderer = getCssStyleContentUpdater(cssUpdaterOptions); + + const variables = ['--var1: #000', '--var2: #fff']; + const result = renderer(variables, '/'); + + expect(result).toBeDefined(); + expect(result).toContain(variables[0]); + expect(result).toContain(variables[1]); + }); + + test('should add tags to new file', () => { + const renderer = getCssStyleContentUpdater(cssUpdaterOptions); + + const variables = ['--var1: #000', '--var2: #fff']; + const result = renderer(variables, '/'); + + expect(result).toBeDefined(); + expect(result.replace(/[\r\n]*/g, '').indexOf(':root {' + startTag)).toEqual(0); + expect(result.replace(/[\r\n]*/g, '').indexOf(endTag) + (endTag + '}').length).toBe(result.replace(/[\r\n]*/g, '').length); + }); + + test('should only update within tag part', () => { + const renderer = getCssStyleContentUpdater(cssUpdaterOptions); + const content = `.my-component { + ${startTag} + --my-comp: red; + ${endTag} + } + `; + + const variables = ['--var1: #000', '--var2: #fff']; + const result = renderer(variables, '/', content); + + expect(result).toBeDefined(); + expect(result).not.toContain(':root'); + expect(result).toContain('.my-component {'); + expect(result).not.toContain('--my-comp: red;'); + expect(result).toContain('--var1: #000'); + }); +}); diff --git a/packages/@o3r/design/src/core/design-token/renderers/css/design-token-updater.renderers.ts b/packages/@o3r/design/src/core/design-token/renderers/css/design-token-updater.renderers.ts new file mode 100644 index 0000000000..00533e0ee0 --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/renderers/css/design-token-updater.renderers.ts @@ -0,0 +1,47 @@ +import type { DesignContentFileUpdater } from '../design-token.renderer.interface'; + +const SANITIZE_TAG_INPUTS_REGEXP = /[.*+?^${}()|[\]\\]/g; + +const generateVars = (variables: string[], startTag: string, endTag: string, addCssScope = false) => + `${addCssScope ? ':root {\n' : ''}${startTag}\n${variables.join('\n')}\n${endTag}${addCssScope ? '\n}' : ''}`; + +/** Default CSS starting tag */ +export const AUTO_GENERATED_START = '/* --- BEGIN THEME Auto-generated --- */'; + +/** Default CSS ending tag */ +export const AUTO_GENERATED_END = '/* --- END THEME Auto-generated --- */'; + +/** Options for {@link getCssStyleContentUpdater} */ +export interface CssStyleContentUpdaterOptions { + /** + * Opening tag marking the content edition part + * @default {@see AUTO_GENERATED_START} + */ + startTag?: string; + + /** + * Closing tag marking the content edition part + * @default {@see AUTO_GENERATED_END} + */ + endTag?: string; +} + +/** + * Retrieve a Content Updater function for CSS generator + * @param options + */ +export const getCssStyleContentUpdater = (options?: CssStyleContentUpdaterOptions): DesignContentFileUpdater => { + const startTag = options?.startTag || AUTO_GENERATED_START; + const endTag = options?.endTag || AUTO_GENERATED_END; + + /** Regexp to replace the content between the detected tags. It also handle possible inputted special character sanitization */ + const regexToReplace = new RegExp(`${startTag.replace(SANITIZE_TAG_INPUTS_REGEXP, '\\$&')}(:?(.|[\n\r])*)${endTag.replace(SANITIZE_TAG_INPUTS_REGEXP, '\\$&')}`); + + return (variables, _file, styleContent = '') => { + if (styleContent.indexOf(startTag) >= 0 && styleContent.indexOf(endTag) >= 0) { + return styleContent.replace(regexToReplace, generateVars(variables, startTag, endTag)); + } else { + return styleContent + '\n' + generateVars(variables, startTag, endTag, true); + } + }; +}; diff --git a/packages/@o3r/design/src/core/design-token/renderers/css/design-token-value.renderers.spec.ts b/packages/@o3r/design/src/core/design-token/renderers/css/design-token-value.renderers.spec.ts new file mode 100644 index 0000000000..4518b7b4f4 --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/renderers/css/design-token-value.renderers.spec.ts @@ -0,0 +1,48 @@ +import * as parser from '../../parsers/design-token.parser'; +import { promises as fs } from 'node:fs'; +import { resolve } from 'node:path'; +import type { DesignTokenSpecification } from '../../design-token-specification.interface'; +import type { DesignTokenVariableSet } from '../../parsers'; +import { getCssTokenValueRenderer } from './design-token-value.renderers'; + +describe('getCssTokenValueRenderer', () => { + let exampleVariable!: DesignTokenSpecification; + let designTokens!: DesignTokenVariableSet; + + beforeAll(async () => { + const file = await fs.readFile(resolve(__dirname, '../../../../../testing/mocks/design-token-theme.json'), { encoding: 'utf-8' }); + exampleVariable = { document: JSON.parse(file) }; + designTokens = parser.parseDesignToken(exampleVariable); + }); + + test('should render valid CSS value', () => { + const renderer = getCssTokenValueRenderer(); + const variable = designTokens.get('example.var1'); + + const result = renderer(variable, designTokens); + expect(variable).toBeDefined(); + expect(result).toBeDefined(); + expect(result).toBe((exampleVariable.document as any).example.var1.$value); + }); + + test('should render valid CSS var', () => { + const renderer = getCssTokenValueRenderer(); + const variable = designTokens.get('example.color'); + + const result = renderer(variable, designTokens); + expect(variable).toBeDefined(); + expect(result).toBeDefined(); + expect(result).toBe('var(--example-var1)'); + }); + + test('should render valid CSS var of not print value', () => { + const renderer = getCssTokenValueRenderer(); + const variable = designTokens.get('example.color2'); + + const result = renderer(variable, designTokens); + expect(variable).toBeDefined(); + expect(result).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + expect(result).toBe(`var(--example-var3, ${(exampleVariable.document as any).example.var3.$value})`); + }); +}); diff --git a/packages/@o3r/design/src/core/design-token/renderers/css/design-token-value.renderers.ts b/packages/@o3r/design/src/core/design-token/renderers/css/design-token-value.renderers.ts new file mode 100644 index 0000000000..ccdbbb8790 --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/renderers/css/design-token-value.renderers.ts @@ -0,0 +1,42 @@ +import type { DesignTokenVariableStructure, TokenKeyRenderer, TokenValueRenderer } from '../../parsers/design-token-parser.interface'; +import { isO3rPrivateVariable } from '../design-token.renderer.helpers'; + +/** Options for {@link getCssTokenValueRenderer} */ +export interface CssTokenValueRendererOptions { + /** + * Determine if the variable is private and should not be rendered + * @default {@see isO3rPrivateVariable} + */ + isPrivateVariable?: (variable: DesignTokenVariableStructure) => boolean; + + /** + * Renderer the name of the CSS Variable (without initial --) + */ + tokenVariableNameRenderer?: TokenKeyRenderer; +} + +/** + * Retrieve the Design Token value renderer + * @param options + */ +export const getCssTokenValueRenderer = (options?: CssTokenValueRendererOptions): TokenValueRenderer => { + const isPrivateVariable = options?.isPrivateVariable || isO3rPrivateVariable; + const tokenVariableNameRenderer = options?.tokenVariableNameRenderer; + + const referenceRenderer = (variable: DesignTokenVariableStructure, variableSet: Map): string => { + if (!isPrivateVariable(variable)) { + return `var(--${variable.getKey(tokenVariableNameRenderer)})`; + } else { + // eslint-disable-next-line no-use-before-define + return `var(--${variable.getKey(tokenVariableNameRenderer)}, ${renderer(variable, variableSet)})`; + } + }; + const renderer = (variable: DesignTokenVariableStructure, variableSet: Map) => { + let variableValue = variable.getCssRawValue(variableSet).replaceAll(/\{([^}]*)\}/g, (defaultValue, matcher) => + (variableSet.has(matcher) ? referenceRenderer(variableSet.get(matcher)!, variableSet) : defaultValue) + ); + variableValue += variableValue && variable.extensions.o3rImportant ? ' !important' : ''; + return variableValue; + }; + return renderer; +}; diff --git a/packages/@o3r/design/src/core/design-token/renderers/css/index.ts b/packages/@o3r/design/src/core/design-token/renderers/css/index.ts new file mode 100644 index 0000000000..026f171acd --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/renderers/css/index.ts @@ -0,0 +1,3 @@ +export * from './design-token-definition.renderers'; +export * from './design-token-value.renderers'; +export * from './design-token-updater.renderers'; diff --git a/packages/@o3r/design/src/core/design-token/renderers/design-token-style.renderer.spec.ts b/packages/@o3r/design/src/core/design-token/renderers/design-token-style.renderer.spec.ts new file mode 100644 index 0000000000..8d5e7d61e6 --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/renderers/design-token-style.renderer.spec.ts @@ -0,0 +1,81 @@ +import * as parser from '../parsers/design-token.parser'; +import { promises as fs } from 'node:fs'; +import { resolve } from 'node:path'; +import type { DesignTokenSpecification } from '../design-token-specification.interface'; +import type { DesignTokenVariableSet } from '../parsers'; +import { computeFileToUpdatePath, renderDesignTokens } from './design-token-style.renderer'; + +describe('Design Token Renderer', () => { + let exampleVariable!: DesignTokenSpecification; + let designTokens!: DesignTokenVariableSet; + + beforeAll(async () => { + const file = await fs.readFile(resolve(__dirname, '../../../../testing/mocks/design-token-theme.json'), { encoding: 'utf-8' }); + exampleVariable = { document: JSON.parse(file) }; + // Add different target file + (exampleVariable.document.example as any)['test.var2'].$extensions = { o3rTargetFile: 'file.scss'}; + designTokens = parser.parseDesignToken(exampleVariable); + }); + + describe('computeFileToUpdatePath', () => { + const DEFAULT_FILE = 'test-result.json'; + const fileToUpdate = computeFileToUpdatePath('/', DEFAULT_FILE); + + test('should return default file if not specified', () => { + const variable = designTokens.get('example.var1'); + const result = fileToUpdate(variable); + + expect(variable.extensions.o3rTargetFile).not.toBeDefined(); + expect(result).toBe(DEFAULT_FILE); + }); + + test('should return file specified by the token', () => { + const variable = designTokens.get('example.test.var2'); + const result = fileToUpdate(variable); + + expect(variable.extensions.o3rTargetFile).toBeDefined(); + expect(result).toBe(resolve('/', variable.extensions.o3rTargetFile)); + }); + }); + + describe('renderDesignTokens', () => { + test('should call the process for all variables', async () => { + const writeFile = jest.fn(); + const readFile = jest.fn().mockReturnValue(''); + const existsFile = jest.fn().mockReturnValue(true); + const determineFileToUpdate = jest.fn().mockReturnValue(computeFileToUpdatePath('.')); + const tokenDefinitionRenderer = jest.fn().mockReturnValue('--test: #000;'); + + await renderDesignTokens(designTokens, { + writeFile, + readFile, + existsFile, + determineFileToUpdate, + tokenDefinitionRenderer + }); + + expect(designTokens.size).toBeGreaterThan(0); + expect(determineFileToUpdate).toHaveBeenCalledTimes(designTokens.size); + expect(tokenDefinitionRenderer).toHaveBeenCalledTimes(designTokens.size); + expect(writeFile).toHaveBeenCalledTimes(1); + }); + + test('should update all the files', async () => { + const writeFile = jest.fn(); + const readFile = jest.fn().mockReturnValue(''); + const existsFile = jest.fn().mockReturnValue(true); + const determineFileToUpdate = jest.fn().mockImplementation(computeFileToUpdatePath('.')); + + await renderDesignTokens(designTokens, { + writeFile, + readFile, + existsFile, + determineFileToUpdate + }); + + expect(designTokens.size).toBeGreaterThan(0); + expect(determineFileToUpdate).toHaveBeenCalledTimes(designTokens.size); + expect(writeFile).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/packages/@o3r/design/src/core/design-token/renderers/design-token-style.renderer.ts b/packages/@o3r/design/src/core/design-token/renderers/design-token-style.renderer.ts new file mode 100644 index 0000000000..a10c69b291 --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/renderers/design-token-style.renderer.ts @@ -0,0 +1,60 @@ +import type { DesignTokenVariableSet, DesignTokenVariableStructure } from '../parsers/design-token-parser.interface'; +import { getCssTokenDefinitionRenderer } from './css/design-token-definition.renderers'; +import { getCssStyleContentUpdater } from './css/design-token-updater.renderers'; +import { existsSync, promises as fs } from 'node:fs'; +import { isAbsolute, resolve } from 'node:path'; +import type { DesignTokenRendererOptions } from './design-token.renderer.interface'; + +/** + * Retrieve the function that determines which file to update for a given token + * @param root Root path used if no base path + * @param defaultFile Default file if not requested by the Token + */ +export const computeFileToUpdatePath = (root = process.cwd(), defaultFile = 'styles.scss') => (token: DesignTokenVariableStructure) => { + if (token.extensions.o3rTargetFile) { + return isAbsolute(token.extensions.o3rTargetFile) ? token.extensions.o3rTargetFile : resolve(token.context?.basePath || root, token.extensions.o3rTargetFile); + } + + return defaultFile; +}; + +/** + * Process the parsed Design Token variables and render them according to the given options and renderers + * @param variableSet Complete list of the parsed Design Token + * @param options Parameters of the Design Token renderer + * @example Basic renderer usage + * ```typescript + * import { parseDesignTokenFile, renderDesignTokens } from '@o3r/design'; + * + * // List of parsed Design Token items + * const parsedTokenDesign = await parseDesignTokenFile('./path/to/spec.json'); + * + * // Render the CSS variables + * await renderDesignTokens(parsedTokenDesign, { logger: console }); + * ``` + */ +export const renderDesignTokens = async (variableSet: DesignTokenVariableSet, options?: DesignTokenRendererOptions) => { + const readFile = options?.readFile || ((filePath: string) => fs.readFile(filePath, {encoding: 'utf-8'})); + const writeFile = options?.writeFile || fs.writeFile; + const existsFile = options?.existsFile || existsSync; + const determineFileToUpdate = options?.determineFileToUpdate || computeFileToUpdatePath(); + const tokenDefinitionRenderer = options?.tokenDefinitionRenderer || getCssTokenDefinitionRenderer(); + const styleContentUpdater = options?.styleContentUpdater || getCssStyleContentUpdater(); + const updates = Array.from(variableSet.values()).reduce((acc, designToken) => { + const filePath = determineFileToUpdate(designToken); + const variable = tokenDefinitionRenderer(designToken, variableSet); + if (variable) { + acc[filePath] ||= []; + acc[filePath].push(variable); + } + return acc; + }, {} as Record); + + await Promise.all( + Object.entries(updates).map(async ([file, vars]) => { + const styleContent = existsFile(file) ? await readFile(file) : ''; + const newStyleContent = styleContentUpdater(vars, file, styleContent); + await writeFile(file, newStyleContent); + }) + ); +}; diff --git a/packages/@o3r/design/src/core/design-token/renderers/design-token.renderer.helpers.spec.ts b/packages/@o3r/design/src/core/design-token/renderers/design-token.renderer.helpers.spec.ts new file mode 100644 index 0000000000..2acff6ba8f --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/renderers/design-token.renderer.helpers.spec.ts @@ -0,0 +1,32 @@ +import * as parser from '../parsers/design-token.parser'; +import { promises as fs } from 'node:fs'; +import { resolve } from 'node:path'; +import type { DesignTokenSpecification } from '../design-token-specification.interface'; +import { isO3rPrivateVariable } from './design-token.renderer.helpers'; +import type { DesignTokenVariableSet } from '../parsers'; + +describe('isO3rPrivateVariable' , () => { + let exampleVariable!: DesignTokenSpecification; + let designTokens!: DesignTokenVariableSet; + + beforeAll(async () => { + const file = await fs.readFile(resolve(__dirname, '../../../../testing/mocks/design-token-theme.json'), { encoding: 'utf-8' }); + exampleVariable = { document: JSON.parse(file) }; + designTokens = parser.parseDesignToken(exampleVariable); + }); + + test('should determine the variable as ignored', () => { + const token = designTokens.get('example.var3'); + expect(isO3rPrivateVariable(token)).toBe(true); + }); + + test('should determine the variable as not ignored', () => { + const token = designTokens.get('example.var1'); + expect(isO3rPrivateVariable(token)).toBe(false); + }); + + test('should determine the alias variable as not ignored', () => { + const token = designTokens.get('example.color'); + expect(isO3rPrivateVariable(token)).toBe(false); + }); +}); diff --git a/packages/@o3r/design/src/core/design-token/renderers/design-token.renderer.helpers.ts b/packages/@o3r/design/src/core/design-token/renderers/design-token.renderer.helpers.ts new file mode 100644 index 0000000000..74cdd56d4e --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/renderers/design-token.renderer.helpers.ts @@ -0,0 +1,8 @@ +import type { DesignTokenVariableStructure } from '../parsers'; + +/** + * Indicate that the variable is private based on the Otter extension + * @param variable Parsed Design Token + * @returns true if private variable + */ +export const isO3rPrivateVariable = (variable: DesignTokenVariableStructure) => !!variable.extensions.o3rPrivate; diff --git a/packages/@o3r/design/src/core/design-token/renderers/design-token.renderer.interface.ts b/packages/@o3r/design/src/core/design-token/renderers/design-token.renderer.interface.ts new file mode 100644 index 0000000000..4ae2f63f59 --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/renderers/design-token.renderer.interface.ts @@ -0,0 +1,63 @@ +import type { DesignTokenVariableStructure } from '../parsers'; +import type { Logger } from '@o3r/core'; + +/** + * Updater function to append the rendered variable into a file content + * @param variables List of rendered variables to append + * @param file Path to the file to edit + * @param styleContent current content where to append the variables + * @returns new content + */ +export type DesignContentFileUpdater = (variables: string[], file: string, styleContent?: string) => string; + +/** + * Render the Design Token variable + * @param tokenStructure Parsed Design Token + * @param variableSet Complete list of the parsed Design Token + */ +export type TokenDefinitionRenderer = (tokenStructure: DesignTokenVariableStructure, variableSet: Map) => string | undefined; + +/** + * Options of the Design Token Renderer value + */ +export interface DesignTokenRendererOptions { + /** Custom Style Content updated function */ + styleContentUpdater?: DesignContentFileUpdater; + + /** + * Custom function to determine the file to update for a given Design Token + * @param token Design Token Variable + */ + determineFileToUpdate?: (token: DesignTokenVariableStructure) => string; + + /** Custom function to render the Design Token variable */ + tokenDefinitionRenderer?: TokenDefinitionRenderer; + + /** + * Custom function to read a file required by the token renderer + * @default {@see fs.promises.readFile} + * @param filePath Path to the file to read + */ + readFile?: (filePath: string) => string | Promise; + + /** + * Custom function to determine if file required by the token renderer exists + * @default {@see fs.existsSync} + * @param filePath Path to the file to check + * @returns + */ + existsFile?: (filePath: string) => boolean; + + /** + * Custom function to write a file required by the token renderer + * @default {@see fs.promise.writeFile} + * @param filePath Path to the file to write + * @param content Content to write + */ + writeFile?: (filePath: string, content: string) => void | Promise; + /** + * Custom logger + * Nothing will be logged if not provided + */ + logger?: Logger; +} diff --git a/packages/@o3r/design/src/core/design-token/renderers/index.ts b/packages/@o3r/design/src/core/design-token/renderers/index.ts new file mode 100644 index 0000000000..5a7f222047 --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/renderers/index.ts @@ -0,0 +1,6 @@ +export * from './design-token-style.renderer'; +export * from './design-token.renderer.interface'; +export * from './design-token-style.renderer'; +export * from './metadata/index'; +export * from './css/index'; +export * from './sass/index'; diff --git a/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-definition.renderers.spec.ts b/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-definition.renderers.spec.ts new file mode 100644 index 0000000000..8b84f6a20f --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-definition.renderers.spec.ts @@ -0,0 +1,40 @@ +import * as parser from '../../parsers/design-token.parser'; +import { promises as fs } from 'node:fs'; +import { resolve } from 'node:path'; +import type { DesignTokenSpecification } from '../../design-token-specification.interface'; +import type { DesignTokenVariableSet } from '../../parsers'; +import { getMetadataTokenDefinitionRenderer } from './design-token-definition.renderers'; + +describe('getMetadataTokenDefinitionRenderer', () => { + let exampleVariable!: DesignTokenSpecification; + let designTokens!: DesignTokenVariableSet; + + beforeAll(async () => { + const file = await fs.readFile(resolve(__dirname, '../../../../../testing/mocks/design-token-theme.json'), { encoding: 'utf-8' }); + exampleVariable = { document: JSON.parse(file) }; + designTokens = parser.parseDesignToken(exampleVariable); + }); + + test('should rely on given tokenValueRenderer', () => { + const tokenValueRenderer = jest.fn().mockReturnValue(JSON.stringify({name: 'test-var', value: 'test-value'})); + const renderer = getMetadataTokenDefinitionRenderer({ tokenValueRenderer }); + const variable = designTokens.get('example.var1'); + + const result = renderer(variable, designTokens); + expect(variable).toBeDefined(); + expect(tokenValueRenderer).toHaveBeenCalledTimes(1); + expect(result).toBeDefined(); + expect(result).toContain('test-var'); + expect(result).toContain('test-value'); + }); + + test('should render valid JSON object', () => { + const renderer = getMetadataTokenDefinitionRenderer(); + const variable = designTokens.get('example.var1'); + + const result = renderer(variable, designTokens); + expect(variable).toBeDefined(); + expect(result).toBeDefined(); + expect(() => JSON.parse(`{${result}}`)).not.toThrow(); + }); +}); diff --git a/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-definition.renderers.ts b/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-definition.renderers.ts new file mode 100644 index 0000000000..fdbbf64252 --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-definition.renderers.ts @@ -0,0 +1,52 @@ +import type { DesignTokenVariableStructure, TokenKeyRenderer, TokenValueRenderer } from '../../parsers/design-token-parser.interface'; +import type { TokenDefinitionRenderer } from '../design-token.renderer.interface'; +import { getMetadataTokenValueRenderer } from './design-token-value.renderers'; +import type { CssVariable } from '@o3r/styling'; + +/** Options for {@link getMetadataTokenDefinitionRenderer} */ +export interface MetadataTokenDefinitionRendererOptions { + /** Custom Design Token value renderer */ + tokenValueRenderer?: TokenValueRenderer; + + /** + * Renderer the name of the CSS Variable (without initial --) + */ + tokenVariableNameRenderer?: TokenKeyRenderer; +} + +/** + * Retrieve the Design Token Variable renderer for Metadata + * @param options + * @example Customize metadata renderer + * ```typescript + * const getCustomMetadataTokenValueRenderer = (options?: MetadataTokenValueRendererOptions): TokenValueRenderer => { + * const defaultMetadataRender = getMetadataTokenValueRenderer(options); + * return (variable, variableSet) => { + * const defaultMetadataObj = JSON.parse(defaultMetadataRender(variable, variableSet)); + * // Add custom field + * defaultMetadataObj.myField = 'custom value'; + * return JSON.stringify(defaultMetadataObj); + * }; + * }; + * + * // List of Design Token item parsed + * // List of parsed Design Token items + * + * const tokenValueRenderer = getCustomMetadataTokenValueRenderer(); + * + * const metadataTokenDefinitionRenderer = getMetadataTokenDefinitionRenderer({ tokenValueRenderer }); + * + * // Render the Metadata file + * await renderDesignTokens(parsedTokenDesign, { tokenDefinitionRenderer: lessTokenDefinitionRenderer }); + * ``` + */ +export const getMetadataTokenDefinitionRenderer = (options?: MetadataTokenDefinitionRendererOptions): TokenDefinitionRenderer => { + const tokenVariableNameRenderer = options?.tokenVariableNameRenderer; + const tokenValueRenderer = options?.tokenValueRenderer || getMetadataTokenValueRenderer({ tokenVariableNameRenderer }); + + const renderer = (variable: DesignTokenVariableStructure, variableSet: Map) => { + const variableValue = tokenValueRenderer(variable, variableSet); + return `"${(JSON.parse(variableValue) as CssVariable).name}": ${variableValue}`; + }; + return renderer; +}; diff --git a/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-updater.renderers.spec.ts b/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-updater.renderers.spec.ts new file mode 100644 index 0000000000..c7cfe602ed --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-updater.renderers.spec.ts @@ -0,0 +1,31 @@ +import * as parser from '../../parsers/design-token.parser'; +import { promises as fs } from 'node:fs'; +import { resolve } from 'node:path'; +import type { DesignTokenSpecification } from '../../design-token-specification.interface'; +import type { DesignTokenVariableSet } from '../../parsers'; +import { getMetadataStyleContentUpdater } from './design-token-updater.renderers'; + +describe('getMetadataStyleContentUpdater', () => { + let exampleVariable!: DesignTokenSpecification; + let designTokens!: DesignTokenVariableSet; + + beforeAll(async () => { + const file = await fs.readFile(resolve(__dirname, '../../../../../testing/mocks/design-token-theme.json'), { encoding: 'utf-8' }); + exampleVariable = { document: JSON.parse(file) }; + designTokens = parser.parseDesignToken(exampleVariable); + }); + + test('should render valid JSON object', () => { + const renderer = getMetadataStyleContentUpdater(); + const variable = designTokens.get('example.var1'); + + const variables = ['"var1": {"value": "#000"}', '"var2": {"value": "#fff"}']; + const result = renderer(variables, '/'); + + expect(variable).toBeDefined(); + expect(result).toBeDefined(); + expect(() => JSON.parse(result)).not.toThrow(); + expect(result.replace(/[\n\r ]*/g, '')).toContain(variables[0].replace(/[\n\r ]*/g, '')); + expect(result.replace(/[\n\r ]*/g, '')).toContain(variables[1].replace(/[\n\r ]*/g, '')); + }); +}); diff --git a/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-updater.renderers.ts b/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-updater.renderers.ts new file mode 100644 index 0000000000..c749162ea9 --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-updater.renderers.ts @@ -0,0 +1,10 @@ +import type { DesignContentFileUpdater } from '../design-token.renderer.interface'; + +/** + * Retrieve a Content Updater function for Metadata generator + */ +export const getMetadataStyleContentUpdater = (): DesignContentFileUpdater => { + return (variables: string[]) => { + return JSON.stringify(JSON.parse(`{"variables":{${variables.join(',')}}}`), null, 2); + }; +}; diff --git a/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-value.renderers.spec.ts b/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-value.renderers.spec.ts new file mode 100644 index 0000000000..7e54fa5036 --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-value.renderers.spec.ts @@ -0,0 +1,40 @@ +import * as parser from '../../parsers/design-token.parser'; +import { promises as fs } from 'node:fs'; +import { resolve } from 'node:path'; +import type { DesignTokenSpecification } from '../../design-token-specification.interface'; +import type { DesignTokenVariableSet } from '../../parsers'; +import { getMetadataTokenValueRenderer } from './design-token-value.renderers'; + +describe('getMetadataTokenValueRenderer', () => { + let exampleVariable!: DesignTokenSpecification; + let designTokens!: DesignTokenVariableSet; + + beforeAll(async () => { + const file = await fs.readFile(resolve(__dirname, '../../../../../testing/mocks/design-token-theme.json'), { encoding: 'utf-8' }); + exampleVariable = { document: JSON.parse(file) }; + designTokens = parser.parseDesignToken(exampleVariable); + }); + + test('should rely on given cssValueRenderer', () => { + const cssValueRenderer = jest.fn().mockReturnValue(JSON.stringify({ name: 'test-var', value: 'test-value' })); + const renderer = getMetadataTokenValueRenderer({ cssValueRenderer }); + const variable = designTokens.get('example.var1'); + + const result = renderer(variable, designTokens); + expect(variable).toBeDefined(); + expect(cssValueRenderer).toHaveBeenCalledTimes(1); + expect(result).toBeDefined(); + expect(result).toContain('test-var'); + expect(result).toContain('test-value'); + }); + + test('should render valid JSON object', () => { + const renderer = getMetadataTokenValueRenderer(); + const variable = designTokens.get('example.var1'); + + const result = renderer(variable, designTokens); + expect(variable).toBeDefined(); + expect(result).toBeDefined(); + expect(() => JSON.parse(result)).not.toThrow(); + }); +}); diff --git a/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-value.renderers.ts b/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-value.renderers.ts new file mode 100644 index 0000000000..3e80c98cc1 --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-value.renderers.ts @@ -0,0 +1,40 @@ +import type { DesignTokenVariableStructure, TokenKeyRenderer, TokenValueRenderer } from '../../parsers/design-token-parser.interface'; +import type { CssVariable } from '@o3r/styling'; +import { getCssTokenValueRenderer } from '../css'; + +/** Options for {@link getMetadataTokenValueRenderer} */ +export interface MetadataTokenValueRendererOptions { + /** + * Custom CSS Design Token value renderer + */ + cssValueRenderer?: TokenValueRenderer; + + /** + * Renderer the name of the CSS Variable (without initial --) + */ + tokenVariableNameRenderer?: TokenKeyRenderer; +} + +/** + * Retrieve the Design Token value renderer + * @param options + */ +export const getMetadataTokenValueRenderer = (options?: MetadataTokenValueRendererOptions): TokenValueRenderer => { + const tokenVariableNameRenderer = options?.tokenVariableNameRenderer; + const cssValueRenderer = options?.cssValueRenderer || getCssTokenValueRenderer({tokenVariableNameRenderer}); + + const renderer = (variable: DesignTokenVariableStructure, variableSet: Map) => { + const cssType = variable.getType(variableSet); + const variableValue: CssVariable = { + name: variable.getKey(tokenVariableNameRenderer), + defaultValue: cssValueRenderer(variable, variableSet), + description: variable.description, + references: variable.getReferencesNode(variableSet).map((node) => JSON.parse(renderer(node, variableSet))), + type: cssType !== 'color' ? 'string' : 'color', + ...variable.extensions.o3rMetadata + }; + + return JSON.stringify(variableValue); + }; + return renderer; +}; diff --git a/packages/@o3r/design/src/core/design-token/renderers/metadata/index.ts b/packages/@o3r/design/src/core/design-token/renderers/metadata/index.ts new file mode 100644 index 0000000000..026f171acd --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/renderers/metadata/index.ts @@ -0,0 +1,3 @@ +export * from './design-token-definition.renderers'; +export * from './design-token-value.renderers'; +export * from './design-token-updater.renderers'; diff --git a/packages/@o3r/design/src/core/design-token/renderers/sass/design-token-definition.renderers.spec.ts b/packages/@o3r/design/src/core/design-token/renderers/sass/design-token-definition.renderers.spec.ts new file mode 100644 index 0000000000..b42691cadf --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/renderers/sass/design-token-definition.renderers.spec.ts @@ -0,0 +1,44 @@ +import * as parser from '../../parsers/design-token.parser'; +import { promises as fs } from 'node:fs'; +import { resolve } from 'node:path'; +import type { DesignTokenSpecification } from '../../design-token-specification.interface'; +import type { DesignTokenVariableSet, TokenKeyRenderer } from '../../parsers'; +import { getSassTokenDefinitionRenderer, tokenVariableNameSassRenderer } from './design-token-definition.renderers'; + +describe('getSassTokenDefinitionRenderer', () => { + let exampleVariable!: DesignTokenSpecification; + let designTokens!: DesignTokenVariableSet; + + beforeAll(async () => { + const file = await fs.readFile(resolve(__dirname, '../../../../../testing/mocks/design-token-theme.json'), { encoding: 'utf-8' }); + exampleVariable = { document: JSON.parse(file) }; + designTokens = parser.parseDesignToken(exampleVariable); + }); + + test('should rely on given tokenValueRenderer', () => { + const tokenValueRenderer = jest.fn().mockReturnValue('test-value'); + const renderer = getSassTokenDefinitionRenderer({ tokenValueRenderer }); + const variable = designTokens.get('example.var1'); + + const result = renderer(variable, designTokens); + expect(variable).toBeDefined(); + expect(tokenValueRenderer).toHaveBeenCalledTimes(1); + expect(result).toBeDefined(); + expect(result).toBe('$exampleVar1: test-value;'); + }); + + test('should prefix private variable', () => { + const tokenVariableNameRenderer: TokenKeyRenderer = (v) => '_' + tokenVariableNameSassRenderer(v); + + const options = { tokenVariableNameRenderer }; + const tokenValueRenderer = jest.spyOn(options, 'tokenVariableNameRenderer'); + const renderer = getSassTokenDefinitionRenderer(options); + const variable = designTokens.get('example.var1'); + + const result = renderer(variable, designTokens); + expect(variable).toBeDefined(); + expect(tokenValueRenderer).toHaveBeenCalledTimes(1); + expect(result).toBeDefined(); + expect(result).toBe('$_exampleVar1: #000;'); + }); +}); diff --git a/packages/@o3r/design/src/core/design-token/renderers/sass/design-token-definition.renderers.ts b/packages/@o3r/design/src/core/design-token/renderers/sass/design-token-definition.renderers.ts new file mode 100644 index 0000000000..cbeb19ee6f --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/renderers/sass/design-token-definition.renderers.ts @@ -0,0 +1,39 @@ +import type { DesignTokenVariableStructure, TokenKeyRenderer, TokenValueRenderer } from '../../parsers/design-token-parser.interface'; +import type { TokenDefinitionRenderer } from '../design-token.renderer.interface'; +import { getCssTokenValueRenderer } from '../css/design-token-value.renderers'; + +export interface SassTokenDefinitionRendererOptions { + + /** Custom Design Token value renderer */ + tokenValueRenderer?: TokenValueRenderer; + + /** + * Renderer the name of the Sass Variable (without initial $) + * @default {@see tokenVariableNameSassRenderer} + */ + tokenVariableNameRenderer?: TokenKeyRenderer; +} + +/** + * Default Sass variable name renderer + * @param variable + */ +export const tokenVariableNameSassRenderer: TokenKeyRenderer = (variable) => { + const tokens = variable.getKey().split('-'); + return tokens[0] + tokens.slice(1).map((token) => token.charAt(0).toUpperCase() + token.slice(1)).join(''); +}; + +/** + * Retrieve the Design Token Variable renderer for Sass + * @param options + * @returns + */ +export const getSassTokenDefinitionRenderer = (options?: SassTokenDefinitionRendererOptions): TokenDefinitionRenderer => { + const tokenValueRenderer = options?.tokenValueRenderer || getCssTokenValueRenderer(); + const keyRenderer = options?.tokenVariableNameRenderer || tokenVariableNameSassRenderer; + + const renderer = (variable: DesignTokenVariableStructure, variableSet: Map) => { + return `$${variable.getKey(keyRenderer)}: ${ tokenValueRenderer(variable, variableSet) };`; + }; + return renderer; +}; diff --git a/packages/@o3r/design/src/core/design-token/renderers/sass/index.ts b/packages/@o3r/design/src/core/design-token/renderers/sass/index.ts new file mode 100644 index 0000000000..d58497e4e1 --- /dev/null +++ b/packages/@o3r/design/src/core/design-token/renderers/sass/index.ts @@ -0,0 +1 @@ +export * from './design-token-definition.renderers'; diff --git a/packages/@o3r/design/src/core/index.ts b/packages/@o3r/design/src/core/index.ts new file mode 100644 index 0000000000..bcbb902d40 --- /dev/null +++ b/packages/@o3r/design/src/core/index.ts @@ -0,0 +1 @@ +export * from './design-token/index'; diff --git a/packages/@o3r/design/src/public_api.ts b/packages/@o3r/design/src/public_api.ts new file mode 100644 index 0000000000..65c514e925 --- /dev/null +++ b/packages/@o3r/design/src/public_api.ts @@ -0,0 +1 @@ +export * from './core/index'; diff --git a/packages/@o3r/design/testing/mocks/design-token-theme.json b/packages/@o3r/design/testing/mocks/design-token-theme.json new file mode 100644 index 0000000000..adae773868 --- /dev/null +++ b/packages/@o3r/design/testing/mocks/design-token-theme.json @@ -0,0 +1,56 @@ +{ + "$schema": "../../schemas/design-token.schema.json", + "example": { + "var1": { + "$type": "color", + "$value": "#000" + }, + "var-important": { + "$extensions": { + "o3rImportant": true + }, + "$type": "color", + "$value": "#000" + }, + "var3": { + "$extensions": { + "o3rPrivate": true + }, + "$type": "color", + "$value": "#000" + }, + "color": { + "$description": "test color", + "$value": "{example.var1}" + }, + "color2": { + "$description": "test color with default value", + "$value": "{example.var3}" + }, + "test.var2": { + "$description": "my var2", + "$type": "color", + "$value": "#fff" + }, + "test": { + "height": { + "$extensions": { + "o3rPrivate": true + }, + "$value": 2.3, + "$type": "number" + }, + "width": { + "$value": "{example.test.height}" + }, + "border": { + "$type": "border", + "$value": { + "color": "{example.color}", + "style": "dashed", + "width": "{example.test.width}" + } + } + } + } +} diff --git a/packages/@o3r/design/testing/setup-jest.ts b/packages/@o3r/design/testing/setup-jest.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@o3r/design/tsconfig.build.json b/packages/@o3r/design/tsconfig.build.json new file mode 100644 index 0000000000..17d0978680 --- /dev/null +++ b/packages/@o3r/design/tsconfig.build.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.build", + "compilerOptions": { + "incremental": true, + "composite": true, + "module": "CommonJS", + "outDir": "./dist", + "rootDir": ".", + "tsBuildInfoFile": "build/.tsbuildinfo" + }, + "include": [ + "src/**/*.ts", + "cli/**/*.cts" + ], + "exclude": ["**/*.spec.ts"] +} diff --git a/packages/@o3r/design/tsconfig.builders.json b/packages/@o3r/design/tsconfig.builders.json new file mode 100644 index 0000000000..196d2a59a1 --- /dev/null +++ b/packages/@o3r/design/tsconfig.builders.json @@ -0,0 +1,25 @@ +{ + "extends": "../../../tsconfig.build", + "compilerOptions": { + "incremental": true, + "composite": true, + "outDir": "./dist", + "module": "CommonJS", + "rootDir": ".", + "tsBuildInfoFile": "build/.tsbuildinfo.builders" + }, + "include": [ + "builders/**/*.ts", + "schematics/**/*.ts" + ], + "exclude": [ + "**/*.spec.ts", + "builders/**/templates/**", + "schematics/**/templates/**" + ], + "references": [ + { + "path": "./tsconfig.build.json" + } + ] +} diff --git a/packages/@o3r/design/tsconfig.doc.json b/packages/@o3r/design/tsconfig.doc.json new file mode 100644 index 0000000000..6ee270fcd3 --- /dev/null +++ b/packages/@o3r/design/tsconfig.doc.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.doc", + "exclude": [ + "**/*.spec.ts", + "**/*reducer.ts", + "**/*.fixture.ts" + ], + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/@o3r/design/tsconfig.eslint.json b/packages/@o3r/design/tsconfig.eslint.json new file mode 100644 index 0000000000..ec5acad834 --- /dev/null +++ b/packages/@o3r/design/tsconfig.eslint.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.build", + "include": [ + ".eslintrc.js", + "jest.config.js", + "testing/*", + "tooling/**/*.js", + "builders/**/index.js" + ] +} diff --git a/packages/@o3r/design/tsconfig.json b/packages/@o3r/design/tsconfig.json new file mode 100644 index 0000000000..d5cee07d89 --- /dev/null +++ b/packages/@o3r/design/tsconfig.json @@ -0,0 +1,15 @@ +/* For IDE usage only */ +{ + "extends": "../../../tsconfig.base", + "references": [ + { + "path": "./tsconfig.build.json" + }, + { + "path": "./tsconfig.builders.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/@o3r/design/tsconfig.spec.json b/packages/@o3r/design/tsconfig.spec.json new file mode 100644 index 0000000000..ffc1f54a1d --- /dev/null +++ b/packages/@o3r/design/tsconfig.spec.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.jest", + "compilerOptions": { + "resolveJsonModule": true, + "composite": true, + "rootDir": ".", + }, + "include": [ + "./src/**/*.spec.ts", + "./schematics/**/*.spec.ts" + ], + "exclude": [], + "references": [ + { + "path": "./tsconfig.build.json" + }, + { + "path": "./tsconfig.builders.json" + } + ] +} diff --git a/packages/@o3r/styling/package.json b/packages/@o3r/styling/package.json index 7c76386550..a574b8bebd 100644 --- a/packages/@o3r/styling/package.json +++ b/packages/@o3r/styling/package.json @@ -26,6 +26,10 @@ }, "./schemas/*.json": { "default": "./schemas/*.json" + }, + "./builders/*/helpers": { + "default": "./builders/*/helpers/index.js", + "types": "./builders/*/helpers/index.d.ts" } }, "peerDependencies": { diff --git a/tsconfig.base.json b/tsconfig.base.json index af52a8ea17..64e0e92da1 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -95,6 +95,9 @@ "@o3r/core": [ "packages/@o3r/core/src/public_api" ], + "@o3r/design": [ + "packages/@o3r/design/src/public_api" + ], "@o3r/dev-tools": [ "packages/@o3r/dev-tools/src/public_api" ], @@ -143,6 +146,12 @@ "@o3r/styling": [ "packages/@o3r/styling/src/public_api" ], + "@o3r/styling/schemas/*": [ + "packages/@o3r/styling/schemas/*" + ], + "@o3r/styling/builders/*/helpers": [ + "packages/@o3r/styling/builders/*/helpers/index" + ], "@o3r/test-helpers": [ "packages/@o3r/test-helpers/src/public_api" ], diff --git a/tsconfig.build.json b/tsconfig.build.json index f226ce6938..2723c224d0 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -23,6 +23,7 @@ "@o3r/components": ["packages/@o3r/components/dist", "packages/@o3r/components/src/public_api"], "@o3r/configuration": ["packages/@o3r/configuration/dist", "packages/@o3r/configuration/src/public_api"], "@o3r/core": ["packages/@o3r/core/dist", "packages/@o3r/core/src/public_api"], + "@o3r/design": ["packages/@o3r/design/dist", "packages/@o3r/design/src/public_api"], "@o3r/dev-tools": ["packages/@o3r/dev-tools/dist", "packages/@o3r/dev-tools/src/public_api"], "@o3r/dynamic-content": ["packages/@o3r/dynamic-content/dist", "packages/@o3r/dynamic-content/src/public_api"], "@o3r/eslint-config-otter": ["packages/@o3r/eslint-config-otter"], @@ -40,6 +41,7 @@ "@o3r/storybook": ["packages/@o3r/storybook/dist", "packages/@o3r/storybook/src/public_api"], "@o3r/stylelint-plugin": ["packages/@o3r/stylelint-plugin/dist", "packages/@o3r/stylelint-plugin/src/public_api"], "@o3r/styling": ["packages/@o3r/styling/dist", "packages/@o3r/styling/src/public_api"], + "@o3r/styling/builders/*/helpers": ["packages/@o3r/styling/dist/builders/*/helpers/index", "packages/@o3r/styling/builders/*/helpers"], "@o3r/test-helpers": ["packages/@o3r/test-helpers/dist", "packages/@o3r/test-helpers/src/public_api"], "@o3r/test-helpers/setup-jest": ["packages/@o3r/test-helpers/dist/src/setup-jest", "packages/@o3r/test-helpers/src/setup-jest"], "@o3r/testing": ["packages/@o3r/testing/dist", "packages/@o3r/testing/src/public_api"], diff --git a/yarn.lock b/yarn.lock index 0f1f9920e2..3a4f3611be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7292,6 +7292,92 @@ __metadata: languageName: unknown linkType: soft +"@o3r/design@workspace:packages/@o3r/design": + version: 0.0.0-use.local + resolution: "@o3r/design@workspace:packages/@o3r/design" + dependencies: + "@angular-devkit/architect": "npm:~0.1602.0" + "@angular-devkit/build-angular": "npm:~16.2.0" + "@angular-devkit/core": "npm:~16.2.0" + "@angular-devkit/schematics": "npm:~16.2.0" + "@angular-eslint/eslint-plugin": "npm:~16.3.0" + "@angular/cli": "npm:~16.2.0" + "@angular/common": "npm:~16.2.0" + "@angular/compiler": "npm:~16.2.0" + "@angular/compiler-cli": "npm:~16.2.0" + "@angular/core": "npm:~16.2.0" + "@angular/platform-browser": "npm:~16.2.0" + "@angular/platform-browser-dynamic": "npm:~16.2.0" + "@babel/core": "npm:~7.23.0" + "@babel/preset-typescript": "npm:~7.23.0" + "@compodoc/compodoc": "npm:^1.1.19" + "@nx/eslint-plugin": "npm:~16.10.0" + "@nx/jest": "npm:~16.10.0" + "@nx/js": "npm:~16.10.0" + "@nx/linter": "npm:~16.10.0" + "@o3r/build-helpers": "workspace:^" + "@o3r/core": "workspace:^" + "@o3r/eslint-plugin": "workspace:^" + "@o3r/schematics": "workspace:^" + "@o3r/styling": "workspace:^" + "@o3r/test-helpers": "workspace:^" + "@schematics/angular": "npm:~16.2.0" + "@types/jest": "npm:~29.5.2" + "@types/minimist": "npm:^1.2.2" + "@types/node": "npm:^18.0.0" + "@types/semver": "npm:^7.3.13" + "@typescript-eslint/eslint-plugin": "npm:^5.60.1" + "@typescript-eslint/parser": "npm:^5.60.1" + chokidar: "npm:^3.5.2" + cpy-cli: "npm:^4.2.0" + eslint: "npm:^8.42.0" + eslint-import-resolver-node: "npm:^0.3.4" + eslint-plugin-jest: "npm:~27.6.0" + eslint-plugin-jsdoc: "npm:~46.10.0" + eslint-plugin-prefer-arrow: "npm:~1.2.3" + eslint-plugin-unicorn: "npm:^47.0.0" + globby: "npm:^11.1.0" + jest: "npm:~29.7.0" + jest-junit: "npm:~16.0.0" + jsonc-eslint-parser: "npm:~2.4.0" + jsonschema: "npm:~1.4.1" + minimatch: "npm:~9.0.3" + minimist: "npm:^1.2.6" + nx: "npm:~16.10.0" + rxjs: "npm:^7.8.1" + sass: "npm:~1.69.0" + ts-jest: "npm:~29.1.1" + ts-node: "npm:~10.9.1" + tslib: "npm:^2.5.3" + type-fest: "npm:^3.12.0" + typescript: "npm:~5.1.6" + zone.js: "npm:~0.13.1" + peerDependencies: + "@o3r/core": "workspace:^" + "@o3r/schematics": "workspace:^" + "@o3r/styling": "workspace:^" + chokidar: ^3.5.2 + globby: ^11.1.0 + minimatch: ~9.0.3 + sass: ~1.69.0 + peerDependenciesMeta: + "@o3r/core": + optional: true + "@o3r/schematics": + optional: true + "@o3r/styling": + optional: true + chokidar: + optional: true + globby: + optional: true + sass: + optional: true + bin: + o3r-css-from-design-token: ./dist/cli/generate-css-from-design-token.cli.cjs + languageName: unknown + linkType: soft + "@o3r/dev-tools@workspace:^, @o3r/dev-tools@workspace:packages/@o3r/dev-tools": version: 0.0.0-use.local resolution: "@o3r/dev-tools@workspace:packages/@o3r/dev-tools" From d2662c47547c6634b11a36c05209a8d866e72af3 Mon Sep 17 00:00:00 2001 From: Kilian Panot Date: Wed, 17 Jan 2024 00:10:09 +0900 Subject: [PATCH 2/2] Revert "feat: migration of minimal Design Token Package to v9.6" This reverts commit 2ea59de3778f41ff971eaf1d2ab709770ae58b69. --- packages/@o3r/design/.compodocrc.json | 22 - packages/@o3r/design/.eslintrc.js | 20 - packages/@o3r/design/.gitignore | 11 - packages/@o3r/design/.npmignore | 0 packages/@o3r/design/README.md | 51 -- packages/@o3r/design/builders.json | 10 - .../design/builders/generate-css/index.js | 13 - .../design/builders/generate-css/index.ts | 131 ----- .../design/builders/generate-css/schema.json | 68 --- .../design/builders/generate-css/schema.ts | 41 -- .../generate-css-from-design-token.cli.cts | 39 -- packages/@o3r/design/collection.json | 24 - packages/@o3r/design/jest.config.js | 8 - packages/@o3r/design/migration.json | 5 - packages/@o3r/design/package.json | 141 ----- packages/@o3r/design/project.json | 90 --- .../design/schemas/design-token.schema.json | 546 ------------------ .../schematics/ extract-token/index.spec.ts | 106 ---- .../design/schematics/ extract-token/index.ts | 102 ---- .../schematics/ extract-token/schema.json | 43 -- .../schematics/ extract-token/schema.ts | 12 - .../design/schematics/generate-css/index.ts | 58 -- .../schematics/generate-css/schema.json | 43 -- .../design/schematics/generate-css/schema.ts | 20 - .../@o3r/design/schematics/index.it.spec.ts | 27 - .../@o3r/design/schematics/ng-add/index.ts | 15 - .../ng-add/register-generate-css/index.ts | 1 - .../register-generate-css/register-task.ts | 73 --- .../design-token.custom.json.template | 3 - .../templates/theme.scss.template | 6 - .../@o3r/design/schematics/ng-add/schema.json | 18 - .../@o3r/design/schematics/ng-add/schema.ts | 6 - .../design-token-specification.interface.ts | 264 --------- .../core/design-token/design-token.spec.ts | 204 ------- .../design/src/core/design-token/index.ts | 3 - .../parsers/design-token-parser.interface.ts | 77 --- .../parsers/design-token.parser.spec.ts | 70 --- .../parsers/design-token.parser.ts | 183 ------ .../src/core/design-token/parsers/index.ts | 2 - .../design-token-definition.renderers.spec.ts | 50 -- .../css/design-token-definition.renderers.ts | 74 --- .../design-token-updater.renderers.spec.ts | 48 -- .../css/design-token-updater.renderers.ts | 47 -- .../css/design-token-value.renderers.spec.ts | 48 -- .../css/design-token-value.renderers.ts | 42 -- .../core/design-token/renderers/css/index.ts | 3 - .../design-token-style.renderer.spec.ts | 81 --- .../renderers/design-token-style.renderer.ts | 60 -- .../design-token.renderer.helpers.spec.ts | 32 - .../design-token.renderer.helpers.ts | 8 - .../design-token.renderer.interface.ts | 63 -- .../src/core/design-token/renderers/index.ts | 6 - .../design-token-definition.renderers.spec.ts | 40 -- .../design-token-definition.renderers.ts | 52 -- .../design-token-updater.renderers.spec.ts | 31 - .../design-token-updater.renderers.ts | 10 - .../design-token-value.renderers.spec.ts | 40 -- .../metadata/design-token-value.renderers.ts | 40 -- .../design-token/renderers/metadata/index.ts | 3 - .../design-token-definition.renderers.spec.ts | 44 -- .../sass/design-token-definition.renderers.ts | 39 -- .../core/design-token/renderers/sass/index.ts | 1 - packages/@o3r/design/src/core/index.ts | 1 - packages/@o3r/design/src/public_api.ts | 1 - .../testing/mocks/design-token-theme.json | 56 -- packages/@o3r/design/testing/setup-jest.ts | 0 packages/@o3r/design/tsconfig.build.json | 16 - packages/@o3r/design/tsconfig.builders.json | 25 - packages/@o3r/design/tsconfig.doc.json | 11 - packages/@o3r/design/tsconfig.eslint.json | 10 - packages/@o3r/design/tsconfig.json | 15 - packages/@o3r/design/tsconfig.spec.json | 21 - packages/@o3r/styling/package.json | 4 - tsconfig.base.json | 9 - tsconfig.build.json | 2 - yarn.lock | 86 --- 76 files changed, 3675 deletions(-) delete mode 100644 packages/@o3r/design/.compodocrc.json delete mode 100644 packages/@o3r/design/.eslintrc.js delete mode 100644 packages/@o3r/design/.gitignore delete mode 100644 packages/@o3r/design/.npmignore delete mode 100644 packages/@o3r/design/README.md delete mode 100644 packages/@o3r/design/builders.json delete mode 100644 packages/@o3r/design/builders/generate-css/index.js delete mode 100644 packages/@o3r/design/builders/generate-css/index.ts delete mode 100644 packages/@o3r/design/builders/generate-css/schema.json delete mode 100644 packages/@o3r/design/builders/generate-css/schema.ts delete mode 100644 packages/@o3r/design/cli/generate-css-from-design-token.cli.cts delete mode 100644 packages/@o3r/design/collection.json delete mode 100644 packages/@o3r/design/jest.config.js delete mode 100644 packages/@o3r/design/migration.json delete mode 100644 packages/@o3r/design/package.json delete mode 100644 packages/@o3r/design/project.json delete mode 100644 packages/@o3r/design/schemas/design-token.schema.json delete mode 100644 packages/@o3r/design/schematics/ extract-token/index.spec.ts delete mode 100644 packages/@o3r/design/schematics/ extract-token/index.ts delete mode 100644 packages/@o3r/design/schematics/ extract-token/schema.json delete mode 100644 packages/@o3r/design/schematics/ extract-token/schema.ts delete mode 100644 packages/@o3r/design/schematics/generate-css/index.ts delete mode 100644 packages/@o3r/design/schematics/generate-css/schema.json delete mode 100644 packages/@o3r/design/schematics/generate-css/schema.ts delete mode 100644 packages/@o3r/design/schematics/index.it.spec.ts delete mode 100644 packages/@o3r/design/schematics/ng-add/index.ts delete mode 100644 packages/@o3r/design/schematics/ng-add/register-generate-css/index.ts delete mode 100644 packages/@o3r/design/schematics/ng-add/register-generate-css/register-task.ts delete mode 100644 packages/@o3r/design/schematics/ng-add/register-generate-css/templates/design-token.custom.json.template delete mode 100644 packages/@o3r/design/schematics/ng-add/register-generate-css/templates/theme.scss.template delete mode 100644 packages/@o3r/design/schematics/ng-add/schema.json delete mode 100644 packages/@o3r/design/schematics/ng-add/schema.ts delete mode 100644 packages/@o3r/design/src/core/design-token/design-token-specification.interface.ts delete mode 100644 packages/@o3r/design/src/core/design-token/design-token.spec.ts delete mode 100644 packages/@o3r/design/src/core/design-token/index.ts delete mode 100644 packages/@o3r/design/src/core/design-token/parsers/design-token-parser.interface.ts delete mode 100644 packages/@o3r/design/src/core/design-token/parsers/design-token.parser.spec.ts delete mode 100644 packages/@o3r/design/src/core/design-token/parsers/design-token.parser.ts delete mode 100644 packages/@o3r/design/src/core/design-token/parsers/index.ts delete mode 100644 packages/@o3r/design/src/core/design-token/renderers/css/design-token-definition.renderers.spec.ts delete mode 100644 packages/@o3r/design/src/core/design-token/renderers/css/design-token-definition.renderers.ts delete mode 100644 packages/@o3r/design/src/core/design-token/renderers/css/design-token-updater.renderers.spec.ts delete mode 100644 packages/@o3r/design/src/core/design-token/renderers/css/design-token-updater.renderers.ts delete mode 100644 packages/@o3r/design/src/core/design-token/renderers/css/design-token-value.renderers.spec.ts delete mode 100644 packages/@o3r/design/src/core/design-token/renderers/css/design-token-value.renderers.ts delete mode 100644 packages/@o3r/design/src/core/design-token/renderers/css/index.ts delete mode 100644 packages/@o3r/design/src/core/design-token/renderers/design-token-style.renderer.spec.ts delete mode 100644 packages/@o3r/design/src/core/design-token/renderers/design-token-style.renderer.ts delete mode 100644 packages/@o3r/design/src/core/design-token/renderers/design-token.renderer.helpers.spec.ts delete mode 100644 packages/@o3r/design/src/core/design-token/renderers/design-token.renderer.helpers.ts delete mode 100644 packages/@o3r/design/src/core/design-token/renderers/design-token.renderer.interface.ts delete mode 100644 packages/@o3r/design/src/core/design-token/renderers/index.ts delete mode 100644 packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-definition.renderers.spec.ts delete mode 100644 packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-definition.renderers.ts delete mode 100644 packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-updater.renderers.spec.ts delete mode 100644 packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-updater.renderers.ts delete mode 100644 packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-value.renderers.spec.ts delete mode 100644 packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-value.renderers.ts delete mode 100644 packages/@o3r/design/src/core/design-token/renderers/metadata/index.ts delete mode 100644 packages/@o3r/design/src/core/design-token/renderers/sass/design-token-definition.renderers.spec.ts delete mode 100644 packages/@o3r/design/src/core/design-token/renderers/sass/design-token-definition.renderers.ts delete mode 100644 packages/@o3r/design/src/core/design-token/renderers/sass/index.ts delete mode 100644 packages/@o3r/design/src/core/index.ts delete mode 100644 packages/@o3r/design/src/public_api.ts delete mode 100644 packages/@o3r/design/testing/mocks/design-token-theme.json delete mode 100644 packages/@o3r/design/testing/setup-jest.ts delete mode 100644 packages/@o3r/design/tsconfig.build.json delete mode 100644 packages/@o3r/design/tsconfig.builders.json delete mode 100644 packages/@o3r/design/tsconfig.doc.json delete mode 100644 packages/@o3r/design/tsconfig.eslint.json delete mode 100644 packages/@o3r/design/tsconfig.json delete mode 100644 packages/@o3r/design/tsconfig.spec.json diff --git a/packages/@o3r/design/.compodocrc.json b/packages/@o3r/design/.compodocrc.json deleted file mode 100644 index 754bd43da8..0000000000 --- a/packages/@o3r/design/.compodocrc.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/compodoc/compodoc/develop/src/config/schema.json", - "name": "Styling", - "output": "../../../generated-doc/styling", - "tsconfig": "./tsconfig.doc.json", - "assetsFolder": "../../../.attachments", - "disableSourceCode": true, - "disableDomTree": true, - "disableTemplateTab": true, - "disableStyleTab": true, - "disableGraph": true, - "disableCoverage": true, - "disablePrivate": true, - "disableProtected": true, - "disableInternal": true, - "disableLifeCycleHooks": true, - "disableRoutesGraph": true, - "disableSearch": false, - "hideGenerator": true, - "customFavicon": "../../../assets/logo/flavors/otter-128x128.png", - "templates": "../../../compodoc-templates/package" -} diff --git a/packages/@o3r/design/.eslintrc.js b/packages/@o3r/design/.eslintrc.js deleted file mode 100644 index 5d890a0275..0000000000 --- a/packages/@o3r/design/.eslintrc.js +++ /dev/null @@ -1,20 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -/* eslint-disable quote-props */ - -module.exports = { - 'root': true, - 'parserOptions': { - 'EXPERIMENTAL_useSourceOfProjectReferenceRedirect': true, - 'tsconfigRootDir': __dirname, - 'project': [ - 'tsconfig.build.json', - 'tsconfig.builders.json', - 'tsconfig.spec.json', - 'tsconfig.eslint.json' - ], - 'sourceType': 'module' - }, - 'extends': [ - '../../../.eslintrc.js' - ] -}; diff --git a/packages/@o3r/design/.gitignore b/packages/@o3r/design/.gitignore deleted file mode 100644 index 9bc7b5104b..0000000000 --- a/packages/@o3r/design/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -/index.* - -/common -/component -/configuration -/dist* -/errors -/store -/types -/validation -/utils diff --git a/packages/@o3r/design/.npmignore b/packages/@o3r/design/.npmignore deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/@o3r/design/README.md b/packages/@o3r/design/README.md deleted file mode 100644 index 637b6404db..0000000000 --- a/packages/@o3r/design/README.md +++ /dev/null @@ -1,51 +0,0 @@ -

Otter Design

-

- Super cute Otter! -

- -This package is an [Otter Framework Module](https://github.com/AmadeusITGroup/otter/tree/main/docs/core/MODULE.md). -
-
- -## Description - -Set of tools to generate CSS themes and [Metadata](https://github.com/AmadeusITGroup/otter/tree/main/docs/cms-adapters/CMS_ADAPTERS.md) based on the [Design Token Specifications](https://design-tokens.github.io/community-group/format/). - -## How to install - -```shell -ng add @o3r/design -``` - -## Generators - -Otter Design module provides a set of code generators based on [angular schematics](https://angular.io/guide/schematics). - -| Schematics | Description | How to use | -| ------------ | ------------------------------------------------------- | -------------------- | -| add | Include Otter design module in a library / application. | `ng add @o3r/design` | -| generate-css | Generate CSS Theme based on Design Token Files | `ng g generate-css` | - -## Builders - -Otter Design module provides a set of builders based on [angular builders](https://angular.io/guide/cli-builder). - -### generate-css - -The `generate-css` builder can generate CSS and CMS Metadata based on given Design Token Json files. -The following configurations are available: - -| Options | Default Value | Description | -| --------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| **designTokenFilePatterns** | [] *Require* | Path patterns to the Design Token JSON files.
Files in dependencies are supported and resolved with Node Resolver. | -| **output** | *null* | Output file where the CSS will be generated.
The path specified in `o3rTargetFile` will be ignore if this option is specified | -| **defaultStyleFile** | src/theme.scss | File path to generate the variable if not determined by the specifications | -| **metadataOutput** | *null* | Path to generate the metadata for the CMS.
The metadata will be generated only if the file path is specified. | -| **rootPath** | *null* | Root path of files where the CSS will be generated. | -| **failOnDuplicate** | false | Determine if the process should stop in case of Token duplication. | -| **prefix** | *null* | Prefix to append to generated variables. | -| **watch** | false | Enable Watch mode. | - -## Technical documentation - -Documentation providing explanations on the use and customization of the `Design Token` parser and renderers is available in the [technical documentation](https://github.com/AmadeusITGroup/otter/blob/main/docs/design/TECHNICAL_DOCUMENTATION.md). diff --git a/packages/@o3r/design/builders.json b/packages/@o3r/design/builders.json deleted file mode 100644 index 956efba83e..0000000000 --- a/packages/@o3r/design/builders.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/angular/angular-cli/master/packages/angular_devkit/architect/src/builders-schema.json", - "builders": { - "generate-css": { - "implementation": "./builders/generate-css/", - "schema": "./builders/generate-css/schema.json", - "description": "Generate CSS from Design Token files" - } - } -} diff --git a/packages/@o3r/design/builders/generate-css/index.js b/packages/@o3r/design/builders/generate-css/index.js deleted file mode 100644 index 5f2ed38442..0000000000 --- a/packages/@o3r/design/builders/generate-css/index.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - -This files is used to allow the usage of the builder within @o3r/framework mono-repository. -It should not be part of the package. - -*/ - -const {resolve} = require('node:path'); - -require('ts-node').register({ project: resolve(__dirname, '..', '..', 'tsconfig.builders.json') }); -require('ts-node').register = () => { }; - -module.exports = require('./index.ts'); diff --git a/packages/@o3r/design/builders/generate-css/index.ts b/packages/@o3r/design/builders/generate-css/index.ts deleted file mode 100644 index f9ed1824e1..0000000000 --- a/packages/@o3r/design/builders/generate-css/index.ts +++ /dev/null @@ -1,131 +0,0 @@ -import type { GenerateCssSchematicsSchema } from './schema'; -import { BuilderOutput, createBuilder } from '@angular-devkit/architect'; -import { - getCssTokenDefinitionRenderer, - getMetadataStyleContentUpdater, - getMetadataTokenDefinitionRenderer, - getSassTokenDefinitionRenderer, - parseDesignTokenFile, - renderDesignTokens, - tokenVariableNameSassRenderer -} from '../../src/public_api'; -import type { DesignTokenRendererOptions, DesignTokenVariableSet, DesignTokenVariableStructure, TokenKeyRenderer } from '../../src/public_api'; -import { resolve } from 'node:path'; -import * as globby from 'globby'; - -/** - * Generate CSS from Design Token files - * @param options - */ -export default createBuilder(async (options, context): Promise => { - const designTokenFilePatterns = Array.isArray(options.designTokenFilePatterns) ? options.designTokenFilePatterns : [options.designTokenFilePatterns]; - const determineCssFileToUpdate = options.output ? () => resolve(context.workspaceRoot, options.output!) : - (token: DesignTokenVariableStructure) => { - if (token.extensions.o3rTargetFile) { - return token.context?.basePath && !options.rootPath ? - resolve(token.context.basePath, token.extensions.o3rTargetFile) : - resolve(context.workspaceRoot, options.rootPath || '', token.extensions.o3rTargetFile); - } - - return resolve(context.workspaceRoot, options.defaultStyleFile); - }; - const tokenVariableNameRenderer: TokenKeyRenderer | undefined = options.prefix ? (variable) => options.prefix! + variable.getKey() : undefined; - const sassRenderer = getSassTokenDefinitionRenderer({ tokenVariableNameRenderer: (v) => (options?.prefixPrivate || '') + tokenVariableNameSassRenderer(v) }); - const renderDesignTokenOptionsCss: DesignTokenRendererOptions = { - determineFileToUpdate: determineCssFileToUpdate, - tokenDefinitionRenderer: getCssTokenDefinitionRenderer({ - tokenVariableNameRenderer: options.prefix ? (variable) => options.prefix! + variable.getKey() : undefined, - privateDefinitionRenderer: options.renderPrivateVariableTo === 'sass' ? sassRenderer : undefined - }), - logger: context.logger - }; - - const renderDesignTokenOptionsMetadata: DesignTokenRendererOptions = { - determineFileToUpdate: () => resolve(context.workspaceRoot, options.metadataOutput!), - styleContentUpdater: getMetadataStyleContentUpdater(), - tokenDefinitionRenderer: getMetadataTokenDefinitionRenderer({ tokenVariableNameRenderer }), - logger: context.logger - }; - - const execute = async (renderDesignTokenOptions: DesignTokenRendererOptions): Promise => { - const files = (await globby(designTokenFilePatterns, { cwd: context.workspaceRoot, absolute: true })); - - const inDependencies = designTokenFilePatterns - .filter((pathName) => !pathName.startsWith('.') && !pathName.startsWith('*') && !pathName.startsWith('/')) - .map((pathName) => { - try { - return require.resolve(pathName); - } catch { - return undefined; - } - }) - .filter((pathName): pathName is string => !!pathName); - files.push(...inDependencies); - - try { - const duplicatedToken: DesignTokenVariableStructure[] = []; - const tokens = (await Promise.all(files.map(async (file) => ({file, parsed: await parseDesignTokenFile(file)})))) - .reduce((acc, {file, parsed}) => { - parsed.forEach((variable, key) => { - if (acc.has(key)) { - context.logger[options.failOnDuplicate ? 'error' : 'warn'](`A duplication of the variable ${key} is found in ${file}`); - duplicatedToken.push(variable); - } - acc.set(key, variable); - }); - return acc; - }, new Map()); - if (options.failOnDuplicate && duplicatedToken.length > 0) { - throw new Error(`Found ${duplicatedToken.length} duplicated Design Token keys`); - } - await renderDesignTokens(tokens, renderDesignTokenOptions); - return { success: true }; - } catch (err) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - return { success: false, error: `${err as any}` }; - } - }; - - const executeMultiRenderer = async (): Promise => { - return (await Promise.allSettled[]>([ - execute(renderDesignTokenOptionsCss), - ...(options.metadataOutput ? [execute(renderDesignTokenOptionsMetadata)] : []) - ])).reduce((acc, res) => { - if (res.status === 'fulfilled') { - acc.success &&= res.value.success; - if (!res.value.error) { - acc.error ||= ''; - acc.error += '\n' + res.value.error!; - } - } else { - acc.success = false; - if (res.reason) { - acc.error ||= ''; - // eslint-disable-next-line @typescript-eslint/restrict-plus-operands - acc.error += '\n' + res.reason; - } - } - return acc; - }, { success: true } as BuilderOutput); - }; - - if (!options.watch) { - return await executeMultiRenderer(); - } else { - try { - await import('chokidar') - .then((chokidar) => chokidar.watch(designTokenFilePatterns.map((p) => resolve(context.workspaceRoot, p)))) - .then((watcher) => watcher.on('all', async () => { - const res = await executeMultiRenderer(); - - if (res.error) { - context.logger.error(res.error); - } - })); - return { success: true }; - } catch (err) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - return { success: false, error: `${err as any}` }; - } - } -}); diff --git a/packages/@o3r/design/builders/generate-css/schema.json b/packages/@o3r/design/builders/generate-css/schema.json deleted file mode 100644 index a65bf81bc7..0000000000 --- a/packages/@o3r/design/builders/generate-css/schema.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ngAddSchematicsSchema", - "title": "Add Otter Design", - "description": "ngAdd Otter Design", - "properties": { - "designTokenFilePatterns": { - "description": "Path patterns to the Design Token JSON files (it supports Node dependency paths).", - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ] - }, - "output": { - "type": "string", - "description": "Path to generate the metadata for CMS. The Metadata will be generated only if the file path is specified" - }, - "defaultStyleFile": { - "type": "string", - "default": "src/theme.scss", - "description": "File path to generate the variable if not determined by the specification" - }, - "metadataOutput": { - "type": "string", - "description": "If specified, all the generated CSS variable will be generated in the given file. Otherwise, the output file will be determined based on the Variable parameters." - }, - "rootPath": { - "type": "string", - "description": "Root path used to determine the CSS file to edit if specified by the o3rTargetFile token extension. It will default to the Design Token file folder." - }, - "watch": { - "type": "boolean", - "default": false, - "description": "Enable Watch mode" - }, - "failOnDuplicate": { - "type": "boolean", - "default": false, - "description": "Determine if the process should stop in case of Token duplication" - }, - "prefix": { - "type": "string", - "description": "Prefix to happen to generated variables" - }, - "prefixPrivate": { - "type": "string", - "description": "Prefix to happen to generated Sass variables if generated" - }, - "renderPrivateVariableTo": { - "type": "string", - "enum": [ - "sass" - ], - "description": "Generate the Private Variable to the given language (the variable is not generated if not specified)" - } - }, - "additionalProperties": true, - "required": [ - "designTokenFilePatterns" - ] -} diff --git a/packages/@o3r/design/builders/generate-css/schema.ts b/packages/@o3r/design/builders/generate-css/schema.ts deleted file mode 100644 index 16b9157b76..0000000000 --- a/packages/@o3r/design/builders/generate-css/schema.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { SchematicOptionObject } from '@o3r/schematics'; - -export interface GenerateCssSchematicsSchema extends SchematicOptionObject { - /** Path patterns to the Design Token JSON files */ - designTokenFilePatterns: string | string[]; - - /** - * Path to generate the metadata for CMS - * The Metadata will be generated only if the file path is specified - */ - metadataOutput?: string; - - /** - * Output file where generate the CSS - * - * If specified, all the generated CSS variable will be generated in the given file. - * Otherwise, the output file will be determined based on the Variable parameters - */ - output?: string; - - /** File path to generate the variable if not determined by the specification */ - defaultStyleFile: string; - - /** Enable Watch mode */ - watch?: boolean; - - /** Root path of files where the CSS will be generated */ - rootPath?: string; - - /** Determine if the process should stop in case of Token duplication */ - failOnDuplicate?: boolean; - - /** Prefix to happen to generated variables */ - prefix?: string; - - /** Generate the Private Variable to the given language */ - renderPrivateVariableTo?: 'sass'; - - /** Prefix to happen to generated Sass variables if generated */ - prefixPrivate?: string; -} diff --git a/packages/@o3r/design/cli/generate-css-from-design-token.cli.cts b/packages/@o3r/design/cli/generate-css-from-design-token.cli.cts deleted file mode 100644 index 042344b152..0000000000 --- a/packages/@o3r/design/cli/generate-css-from-design-token.cli.cts +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env node - -import { isAbsolute, normalize, resolve } from 'node:path'; -import { existsSync } from 'node:fs'; -import { parseDesignTokenFile, renderDesignTokens } from '@o3r/design'; -import type { DesignTokenRendererOptions, DesignTokenVariableSet } from '@o3r/design'; -import * as minimist from 'minimist'; - -const args = minimist(process.argv.splice(2)); - -void (async () => { - const renderDesignTokenOptions: DesignTokenRendererOptions = {}; - - const output = args.o || args.output; - if (output) { - renderDesignTokenOptions.determineFileToUpdate = () => resolve(process.cwd(), output); - } - - const tokens = (await Promise.all( - args._ - .map((file) => isAbsolute(file) ? normalize(file) : resolve(process.cwd(), file)) - .filter((file) => { - const res = existsSync(file); - if (!res) { - throw new Error(`The file ${file} does not exist, the process will stop`); - } - return res; - }) - .map(async (file) => ({ file, parsed: await parseDesignTokenFile(file) })) - )).reduce((acc, { file, parsed }) => { - parsed.forEach((variable, key) => { - acc.set(key, variable); - console.warn(`A duplication of the variable ${key} is found in ${file}`); - }); - return acc; - }, new Map()); - - await renderDesignTokens(tokens, renderDesignTokenOptions); -})(); diff --git a/packages/@o3r/design/collection.json b/packages/@o3r/design/collection.json deleted file mode 100644 index a02090638a..0000000000 --- a/packages/@o3r/design/collection.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/angular/angular-cli/master/packages/angular_devkit/schematics/collection-schema.json", - "schematics": { - "ng-add": { - "description": "Add Otter Design to the project.", - "factory": "./schematics/ng-add/index#ngAdd", - "schema": "./schematics/ng-add/schema.json", - "aliases": [ - "install", - "i" - ] - }, - "generate-css": { - "description": "Generate CSS from Design Token files", - "factory": "./schematics/generate-css/index#generateCss", - "schema": "./schematics/generate-css/schema.json" - }, - "extract-token": { - "description": "Extract the Design Token specification from SCSS", - "factory": "./schematics/extract-token/index#extractToken", - "schema": "./schematics/extract-token/schema.json" - } - } -} diff --git a/packages/@o3r/design/jest.config.js b/packages/@o3r/design/jest.config.js deleted file mode 100644 index affcac71cb..0000000000 --- a/packages/@o3r/design/jest.config.js +++ /dev/null @@ -1,8 +0,0 @@ -const getJestConfig = require('../../../jest.config.ut').getJestConfig; - -/** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */ -module.exports = { - ...getJestConfig(__dirname, false), - displayName: require('./package.json').name, - clearMocks: true -}; diff --git a/packages/@o3r/design/migration.json b/packages/@o3r/design/migration.json deleted file mode 100644 index 0149b023a8..0000000000 --- a/packages/@o3r/design/migration.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/angular/angular-cli/master/packages/angular_devkit/schematics/collection-schema.json", - "schematics": { - } -} diff --git a/packages/@o3r/design/package.json b/packages/@o3r/design/package.json deleted file mode 100644 index a64b747e8a..0000000000 --- a/packages/@o3r/design/package.json +++ /dev/null @@ -1,141 +0,0 @@ -{ - "name": "@o3r/design", - "version": "0.0.0-placeholder", - "description": "A design framework to generate theme on an Otter application based on Design Tokens.", - "keywords": [ - "design", - "otter", - "otter-module" - ], - "scripts": { - "nx": "nx", - "ng": "yarn nx", - "test": "yarn nx test styling", - "copy:templates": "yarn cpy 'schematics/**/templates/**/*' dist/schematics", - "copy:schemas": "yarn cpy 'schemas/**/*' dist/schemas", - "prepare:build:builders": "yarn cpy 'builders/**/*.json' dist/builders && yarn cpy '{builders,collection,migration}.json' dist && yarn cpy 'schematics/**/*.json' dist/schematics && yarn copy:templates", - "prepare:publish": "prepare-publish ./dist", - "build:source": "tsc -b tsconfig.build.json && yarn cpy package.json dist/", - "build:builders": "tsc -b tsconfig.builders.json --pretty && yarn copy:templates && generate-cjs-manifest", - "build": "yarn nx build styling", - "postbuild": "yarn copy:schemas && patch-package-json-main" - }, - "exports": { - "./package.json": { - "default": "./package.json" - }, - "./schemas/*.json": { - "default": "./schemas/*.json" - }, - ".": { - "es2020": "./dist/src/public_api.js", - "default": "./dist/src/public_api.js", - "typings": "./dist/src/public_api.d.ts", - "node": "./dist/src/public_api.js", - "require": "./dist/src/public_api.js" - }, - "./cli/*": { - "default": "./dist/cli/*" - } - }, - "bin": { - "o3r-css-from-design-token": "./dist/cli/generate-css-from-design-token.cli.cjs" - }, - "dependencies": { - "minimist": "^1.2.6", - "tslib": "^2.5.3" - }, - "peerDependencies": { - "@o3r/core": "workspace:^", - "@o3r/schematics": "workspace:^", - "@o3r/styling": "workspace:^", - "chokidar": "^3.5.2", - "globby": "^11.1.0", - "minimatch": "~9.0.3", - "sass": "~1.69.0" - }, - "peerDependenciesMeta": { - "@o3r/core": { - "optional": true - }, - "@o3r/schematics": { - "optional": true - }, - "@o3r/styling": { - "optional": true - }, - "chokidar": { - "optional": true - }, - "globby": { - "optional": true - }, - "sass": { - "optional": true - } - }, - "devDependencies": { - "@angular-devkit/architect": "~0.1602.0", - "@angular-devkit/build-angular": "~16.2.0", - "@angular-devkit/core": "~16.2.0", - "@angular-devkit/schematics": "~16.2.0", - "@angular-eslint/eslint-plugin": "~16.3.0", - "@angular/cli": "~16.2.0", - "@angular/common": "~16.2.0", - "@angular/compiler": "~16.2.0", - "@angular/compiler-cli": "~16.2.0", - "@angular/core": "~16.2.0", - "@angular/platform-browser": "~16.2.0", - "@angular/platform-browser-dynamic": "~16.2.0", - "@babel/core": "~7.23.0", - "@babel/preset-typescript": "~7.23.0", - "@compodoc/compodoc": "^1.1.19", - "@nx/eslint-plugin": "~16.10.0", - "@nx/jest": "~16.10.0", - "@nx/js": "~16.10.0", - "@nx/linter": "~16.10.0", - "@o3r/build-helpers": "workspace:^", - "@o3r/core": "workspace:^", - "@o3r/eslint-plugin": "workspace:^", - "@o3r/schematics": "workspace:^", - "@o3r/styling": "workspace:^", - "@o3r/test-helpers": "workspace:^", - "@schematics/angular": "~16.2.0", - "@types/jest": "~29.5.2", - "@types/minimist": "^1.2.2", - "@types/node": "^18.0.0", - "@types/semver": "^7.3.13", - "@typescript-eslint/eslint-plugin": "^5.60.1", - "@typescript-eslint/parser": "^5.60.1", - "chokidar": "^3.5.2", - "cpy-cli": "^4.2.0", - "eslint": "^8.42.0", - "eslint-import-resolver-node": "^0.3.4", - "eslint-plugin-jest": "~27.6.0", - "eslint-plugin-jsdoc": "~46.10.0", - "eslint-plugin-prefer-arrow": "~1.2.3", - "eslint-plugin-unicorn": "^47.0.0", - "globby": "^11.1.0", - "jest": "~29.7.0", - "jest-junit": "~16.0.0", - "jsonc-eslint-parser": "~2.4.0", - "jsonschema": "~1.4.1", - "minimatch": "~9.0.3", - "nx": "~16.10.0", - "rxjs": "^7.8.1", - "sass": "~1.69.0", - "ts-jest": "~29.1.1", - "ts-node": "~10.9.1", - "type-fest": "^3.12.0", - "typescript": "~5.1.6", - "zone.js": "~0.13.1" - }, - "engines": { - "node": ">=18.0.0" - }, - "builders": "./builders.json", - "schematics": "./collection.json", - "ng-update": { - "migrations": "./migration.json" - } -} diff --git a/packages/@o3r/design/project.json b/packages/@o3r/design/project.json deleted file mode 100644 index 3ea54d3463..0000000000 --- a/packages/@o3r/design/project.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "name": "design", - "$schema": "https://raw.githubusercontent.com/nrwl/nx/master/packages/nx/schemas/project-schema.json", - "projectType": "library", - "sourceRoot": "packages/@o3r/design/src", - "prefix": "o3r", - "targets": { - "build": { - "executor": "nx:run-script", - "outputs": [ - "{projectRoot}/dist/package.json" - ], - "options": { - "script": "postbuild" - }, - "dependsOn": [ - "^build", - "build-builders", - "compile" - ] - }, - "compile": { - "executor": "nx:run-script", - "options": { - "script": "build:source" - }, - "inputs": [ - "{projectRoot}/cli/*.cts", - "source", - "^source" - ] - }, - "prepare-build-builders": { - "executor": "nx:run-script", - "options": { - "script": "prepare:build:builders" - } - }, - "build-builders": { - "executor": "nx:run-script", - "options": { - "script": "build:builders" - } - }, - "lint": { - "executor": "@nx/linter:eslint", - "configurations": { - "ci": { - "quiet": true, - "cacheLocation": ".cache/eslint" - } - }, - "options": { - "eslintConfig": "packages/@o3r/design/.eslintrc.js", - "lintFilePatterns": [ - "packages/@o3r/design/src/**/*.ts", - "packages/@o3r/design/schematics/**/*.ts", - "packages/@o3r/design/cli/**/*.ts", - "packages/@o3r/design/builders/**/*.ts", - "packages/@o3r/design/package.json" - ] - } - }, - "test": { - "executor": "@nx/jest:jest", - "options": { - "jestConfig": "packages/@o3r/design/jest.config.js" - } - }, - "prepare-publish": { - "executor": "nx:run-script", - "options": { - "script": "prepare:publish" - } - }, - "publish": { - "executor": "nx:run-commands", - "options": { - "command": "npm publish packages/@o3r/design/dist" - } - }, - "documentation": { - "executor": "nx:run-script", - "options": { - "script": "compodoc" - } - } - }, - "tags": [] -} diff --git a/packages/@o3r/design/schemas/design-token.schema.json b/packages/@o3r/design/schemas/design-token.schema.json deleted file mode 100644 index 4b3731ef84..0000000000 --- a/packages/@o3r/design/schemas/design-token.schema.json +++ /dev/null @@ -1,546 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "DesignTokenSchema", - "description": "Schema Describing the structure of the Design Token, this Schema is a temporary workaround and should be replaced by the one provided by Community-Group https://github.com/design-tokens/community-group", - "allOf": [ - { - "$ref": "#/definitions/tokenNode" - } - ], - "definitions": { - "tokenNode": { - "oneOf": [ - { - "$ref": "#/definitions/tokenGroup" - }, - { - "$ref": "#/definitions/token" - } - ] - }, - "otterExtensionMetadata": { - "type": "object", - "properties": { - "tags": { - "type": "array", - "items": { "type": "string" } - }, - "label": { - "type": "string" - }, - "category": { - "type": "string" - } - } - }, - "otterExtension": { - "type": "object", - "properties": { - "o3rTargetFile": { - "type": "string" - }, - "o3rPrivate": { - "type": "boolean" - }, - "o3rImportant": { - "type": "boolean" - }, - "o3rScope": { - "type": "string" - }, - "o3rMetadata": { - "$ref": "#/definitions/otterExtensionMetadata" - } - } - }, - "extensions": { - "type": "object", - "allOf": [ - { - "$ref": "#/definitions/otterExtension" - } - ] - }, - "tokenGroup": { - "type":"object", - "properties": { - "$schema": { - "type": "string" - }, - "$description": { - "type": "string" - }, - "$extensions": { - "$ref": "#/definitions/extensions" - } - }, - "patternProperties": { - "^[^$].*$": { - "$ref": "#/definitions/tokenNode" - } - }, - "additionalProperties": false - }, - "token": { - "allOf": [ - { - "oneOf": [ - {"$ref": "#/definitions/tokenTypeImplicit"}, - {"$ref": "#/definitions/tokenTypeColor"}, - {"$ref": "#/definitions/tokenTypeDimension"}, - {"$ref": "#/definitions/tokenTypeFontFamily"}, - {"$ref": "#/definitions/tokenTypeDuration"}, - {"$ref": "#/definitions/tokenTypeCubicBezier"}, - {"$ref": "#/definitions/tokenTypeFontWeight"}, - {"$ref": "#/definitions/tokenTypeNumber"}, - - {"$ref": "#/definitions/tokenTypeStrokeStyle"}, - {"$ref": "#/definitions/tokenTypeBorder"}, - {"$ref": "#/definitions/tokenTypeTransition"}, - {"$ref": "#/definitions/tokenTypeShadow"}, - {"$ref": "#/definitions/tokenTypeGradient"}, - {"$ref": "#/definitions/tokenTypeTypography"} - ] - }, - { - "type": "object", - "properties": { - "$extensions": { - "$ref": "#/definitions/extensions" - }, - "$description": { - "type": "string" - } - } - } - ] - }, - - "tokenTypeColor": { - "type": "object", - "required": [ - "$type", - "$value" - ], - "properties": { - "$value": { - "type": "string" - }, - "$type": { - "const": "color", - "type": "string" - } - } - }, - "tokenTypeDimension": { - "type": "object", - "required": [ - "$type", - "$value" - ], - "properties": { - "$value": { - "type": "string" - }, - "$type": { - "const": "dimension", - "type": "string" - } - } - }, - "tokenTypeFontFamily": { - "type": "object", - "required": [ - "$type", - "primary" - ], - "allOf": [ - { - "oneOf": [ - { - "type": "object", - "properties": { - "$value": { - "type": "string" - } - } - }, - { - "type": "object", - "required": ["primary"], - "properties": { - "primary": { - "type": "object", - "required": [ - "$value" - ], - "properties": { - "$value": { - "type": "string" - } - } - }, - "secondary": { - "type": "object", - "required": [ - "$type" - ], - "properties": { - "$value": { - "type": "string" - } - } - } - } - } - ] - }, - { - "type": "object", - "properties": { - "$type": { - "const": "fontFamily", - "type": "string" - } - } - } - ] - }, - "tokenTypeImplicit": { - "type": "object", - "required": [ - "$value" - ], - "properties": { - "$value": { - "type": "string", - "pattern": "^.*\\{.*\\}.*$" - }, - "$type": { - "not": {} - } - } - }, - "tokenTypeDuration": { - "type": "object", - "required": [ - "$type", - "$value" - ], - "properties": { - "$value": { - "type": "number" - }, - "$type": { - "const": "duration", - "type": "string" - } - } - }, - "tokenTypeCubicBezierValue": { - "type": "array", - "minItems": 2, - "maxItems": 4, - "items": { - "type": "number" - } - }, - "tokenTypeCubicBezier": { - "type": "object", - "required": [ - "$type", - "$value" - ], - "properties": { - "$value": { - "$ref": "#/definitions/tokenTypeCubicBezierValue" - }, - "$type": { - "const": "cubicBezier", - "type": "string" - } - } - }, - "tokenTypeFontWeightValue": { - "oneOf": [ - { - "type": "number" - }, - { - "type": "string" - } - ] - }, - "tokenTypeFontWeight": { - "type": "object", - "required": [ - "$type", - "$value" - ], - "properties": { - "$value": { - "$ref": "#/definitions/tokenTypeFontWeightValue" - }, - "$type": { - "const": "fontWeight", - "type": "string" - } - } - }, - "tokenTypeNumber": { - "type": "object", - "required": [ - "$type", - "$value" - ], - "properties": { - "$value": { - "type": "number" - }, - "$type": { - "const": "number", - "type": "string" - } - } - }, - - "tokenTypeStrokeStyleValue": { - "oneOf": [ - { - "type": "string", - "enum": [ - "solid", - "dashed", - "dotted", - "double", - "groove", - "ridge", - "outset", - "inset" - ] - }, - { - "type": "object", - "properties": { - "dashArray": { - "type": "array", - "minItems": 1, - "maxItems": 4, - "items": { - "type": "string" - } - }, - "lineCap": { - "type": "string", - "enum": [ - "round", - "butt", - "square" - ] - } - }, - "required": [ - "dashArray", - "lineCap" - ] - } - ] - }, - "tokenTypeStrokeStyle": { - "type": "object", - "required": [ - "$type", - "$value" - ], - "properties": { - "$value": { - "$ref": "#/definitions/tokenTypeStrokeStyleValue" - }, - "$type": { - "const": "strokeStyle", - "type": "string" - } - } - }, - "tokenTypeBorder": { - "type": "object", - "required": [ - "$type", - "$value" - ], - "properties": { - "$value": { - "type": "object", - "properties": { - "color": { "type": "string" }, - "width": { "type": "string" }, - "style": { - "allOf": [ - { "$ref": "#/definitions/tokenTypeStrokeStyleValue" }, - { "type": "string" } - ] - } - }, - "required": [ - "color", - "width", - "style" - ] - }, - "$type": { - "const": "border", - "type": "string" - } - } - }, - "tokenTypeTransition": { - "type": "object", - "required": [ - "$type", - "$value" - ], - "properties": { - "$value": { - "type": "object", - "properties": { - "duration": { - "type": "string" - }, - "delay": { - "type": "string" - }, - "timingFunction": { - "allOf": [ - { "$ref": "#/definitions/tokenTypeCubicBezierValue" }, - { "type": "string" } - ] - } - }, - "required": [ - "duration", - "delay", - "timingFunction" - ] - }, - "$type": { - "const": "transition", - "type": "string" - } - } - }, - "tokenTypeShadow": { - "type": "object", - "required": [ - "$type", - "$value" - ], - "properties": { - "$value": { - "type": "object", - "properties": { - "color": { - "type": "string" - }, - "offsetX": { - "type": "string" - }, - "offsetY": { - "type": "string" - }, - "blur": { - "type": "string" - }, - "spread": { - "type": "string" - } - }, - "required": [ - "color", - "offsetX", - "offsetY", - "blur", - "spread" - ] - }, - "$type": { - "const": "shadow", - "type": "string" - } - } - }, - "tokenTypeGradient": { - "type": "object", - "required": [ - "$type", - "$value" - ], - "properties": { - "$value": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "properties": { - "color": { "type": "string" }, - "position": { - "oneOf": [ - { "type": "string" }, - { - "type": "number", - "maximum": 1, - "minimum": 0 - } - ] - } - }, - "required": [ - "color", - "position" - ] - } - }, - "$type": { - "const": "gradient", - "type": "string" - } - } - }, - "tokenTypeTypography": { - "type": "object", - "required": [ - "$type", - "$value" - ], - "properties": { - "$value": { - "type": "object", - "properties": { - "fontFamily": { "type": "string" }, - "fontSize": { "type": "string" }, - "letterSpacing": { "type": "string" }, - "fontWeight": { - "$ref": "#/definitions/tokenTypeFontWeightValue" - }, - "lineHeight": { - "oneOf": [ - { "type": "string" }, - { "type": "number" } - ] - } - }, - "required": [ - "fontFamily", - "fontSize", - "letterSpacing", - "fontWeight", - "lineHeight" - ] - }, - "$type": { - "const": "typography", - "type": "string" - } - } - } - } -} diff --git a/packages/@o3r/design/schematics/ extract-token/index.spec.ts b/packages/@o3r/design/schematics/ extract-token/index.spec.ts deleted file mode 100644 index 209d697bf6..0000000000 --- a/packages/@o3r/design/schematics/ extract-token/index.spec.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { callRule, Tree } from '@angular-devkit/schematics'; -import { extractToken } from './index'; -import { firstValueFrom } from 'rxjs'; -import type { CssVariable } from '@o3r/styling'; -import { validate } from 'jsonschema'; -import * as fs from 'node:fs'; -import { resolve } from 'node:path'; - -describe('Extract Token schematic', () => { - - let initialTree: Tree; - - const initialSassFile = ` -@use '@o3r/styling' as o3r; - -$breadcrumb-pres-item-icon-size: o3r.variable('breadcrumb-pres-item-icon-size', 3rem); -$breadcrumb-pres-item-icon-color: o3r.variable('breadcrumb-pres-item-icon-color', #fff); -$breadcrumb-pres-item-other-color: o3r.variable('breadcrumb-pres-item-other-color', o3r.var('breadcrumb-pres-item-icon-color')); - -// other CSS -.test { - color: $breadcrumb-pres-item-other-color; -}`; - - beforeEach(() => { - initialTree = Tree.empty(); - initialTree.create('src/component/my-comp.theme.css', 'should be ignored'); - initialTree.create('src/component/my-comp.theme.scss', initialSassFile); - }); - - it('should correctly extract the Design Token', async () => { - const logger = { warn: jest.fn(), debug: jest.fn() }; - jest.mock('@o3r/styling/builders/style-extractor/helpers', () => ({ - // eslint-disable-next-line @typescript-eslint/naming-convention - CssVariableExtractor: class { - public extractFileContent = jest.fn().mockReturnValue([ - { - defaultValue: '3rem', - name: 'breadcrumb-pres-item-icon-size', - label: 'breadcrumb pres item icon size', - type: 'string' - }, - { - defaultValue: '#fff', - name: 'breadcrumb-pres-item-icon-color', - label: 'breadcrumb pres item icon color', - type: 'color' - }, - { - defaultValue: 'var(--breadcrumb-pres-item-icon-color)', - name: 'breadcrumb-pres-item-other-color', - label: 'breadcrumb pres item other color', - type: 'string' - } - ]); - constructor() {} - } - })); - const tree = await firstValueFrom(callRule(extractToken({ - includeTags: false, - componentFilePatterns: ['src/component/**.*scss'] - }), initialTree, { logger } as any)); - - expect(tree.exists('src/component/my-comp.theme.json')).toBe(true); - expect(validate(tree.readText('src/component/my-comp.theme.json'), fs.readFileSync(resolve(__dirname, '../../schemas/design-token.schema.json'))).errors).toHaveLength(0); - expect(tree.readText('src/component/my-comp.theme.scss')).toBe(initialSassFile); - }); - - it('should Update the original file', async () => { - const logger = { warn: jest.fn(), debug: jest.fn() }; - jest.mock('@o3r/styling/builders/style-extractor/helpers', () => ({ - // eslint-disable-next-line @typescript-eslint/naming-convention - CssVariableExtractor: class { - public extractFileContent = jest.fn().mockReturnValue([ - { - defaultValue: '4rem', - name: 'breadcrumb-pres-item-icon-size', - label: 'breadcrumb pres item icon size', - type: 'string' - } - ]); - constructor() { } - } - })); - const tree = await firstValueFrom(callRule(extractToken({ - includeTags: true, - componentFilePatterns: ['src/component/**.*scss'] - }), initialTree, { logger } as any)); - - expect(tree.readText('src/component/my-comp.theme.scss')).not.toBe(initialSassFile); - expect(tree.readText('src/component/my-comp.theme.scss')).toBe(` -@use '@o3r/styling' as o3r; - -/* --- BEGIN THEME Auto-generated --- */ -$breadcrumb-pres-item-icon-size: o3r.variable('breadcrumb-pres-item-icon-size', 3rem); -$breadcrumb-pres-item-icon-color: o3r.variable('breadcrumb-pres-item-icon-color', #fff); -$breadcrumb-pres-item-other-color: o3r.variable('breadcrumb-pres-item-other-color', o3r.var('breadcrumb-pres-item-icon-color')); -/* --- END THEME Auto-generated --- */ - -// other CSS -.test { - color: $breadcrumb-pres-item-other-color; -}` - ); - }); -}); diff --git a/packages/@o3r/design/schematics/ extract-token/index.ts b/packages/@o3r/design/schematics/ extract-token/index.ts deleted file mode 100644 index e6ce4e184a..0000000000 --- a/packages/@o3r/design/schematics/ extract-token/index.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { Rule } from '@angular-devkit/schematics'; -import type { ExtractTokenSchematicsSchema } from './schema'; -import { posix, resolve } from 'node:path'; -import { AUTO_GENERATED_END, AUTO_GENERATED_START, DesignToken, DesignTokenGroup, DesignTokenNode } from '../../src/public_api'; - -const patternToDetect = 'o3r.var'; - -/** - * Extract the token from o3r mixin sass file - * @param options - */ -export function extractToken(options: ExtractTokenSchematicsSchema): Rule { - - const updateFileContent = (content: string): string => { - const start = content.indexOf(patternToDetect); - const end = content.lastIndexOf(patternToDetect); - - if (start === -1 || !options.includeTags) { - return content; - } - - const startTag = typeof options.includeTags === 'boolean' ? AUTO_GENERATED_START : options.includeTags.startTag; - const endTag = typeof options.includeTags === 'boolean' ? AUTO_GENERATED_END : options.includeTags.endTag; - const indexToInsertStart = content.substring(0, start).lastIndexOf('\n') + 1; - const indexToInsertEnd = content.substring(end).indexOf('\n') + end + 1; - - return `${content.substring(0, indexToInsertStart)}${startTag}\n` + - content.substring(indexToInsertStart, indexToInsertEnd) + - `${endTag}\n${content.substring(indexToInsertEnd) }`; - }; - - return async (tree, context) => { - try { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { CssVariableExtractor } = await import('@o3r/styling/builders/style-extractor/helpers'); - const { filter } = await import('minimatch'); - const filterFunctions = options.componentFilePatterns.map((pattern) => filter( - '/' + pattern.replace(/[\\/]+/g, '/'), - { dot: true } - )); - const sassParser = new CssVariableExtractor(); - - tree.visit((file) => { - if (!filterFunctions.some((filterFunction) => filterFunction(file))) { - return; - } - const content = tree.readText(file); - const variables = sassParser.extractFileContent(resolve(tree.root.path, file), content); - - if (variables.length > 0 && options.includeTags) { - const newContent = updateFileContent(content); - tree.overwrite(file, newContent); - } - - const isPrivate = file.endsWith('theme.scss'); - const tokenSpecification = variables - .reduce((node, variable) => { - const namePath = variable.name.split('-'); - let targetNode: DesignTokenGroup | DesignToken = node; - namePath.forEach((name) => { - (targetNode as DesignTokenGroup)[name] ||= {}; - targetNode = (targetNode as DesignTokenGroup)[name] as DesignTokenGroup | DesignToken; - }); - - const valueWithVariable = [...variable.defaultValue.matchAll(/var\(--([^)])\)/g)] - .reduce((acc, [variableString, variableName]) => { - return acc.replaceAll(variableString, `{${variableName.replaceAll('-', '.')}}`); - }, variable.defaultValue); - - const targetNodeValue = targetNode as any as DesignToken; - targetNodeValue.$description = variable.description; - targetNodeValue.$type = !variable.type || variable.type === 'string' ? - (isNaN(+variable.defaultValue) ? undefined : 'number') : - variable.type; - targetNodeValue.$value = targetNodeValue.$type === 'number' ? - +variable.defaultValue : - valueWithVariable; - targetNodeValue.$extensions ||= {}; - targetNodeValue.$extensions.o3rMetadata ||= {}; - targetNodeValue.$extensions.o3rMetadata.category = variable.category; - targetNodeValue.$extensions.o3rMetadata.label = variable.label; - targetNodeValue.$extensions.o3rMetadata.tags = variable.tags; - return node; - }, {} as DesignTokenGroup | DesignToken); - - Object.values(tokenSpecification) - .forEach((node) => { - const designTokenNode = (node as DesignTokenNode); - designTokenNode.$extensions ||= {}; - designTokenNode.$extensions.o3rPrivate = isPrivate; - designTokenNode.$extensions.o3rTargetFile = posix.join('.', posix.basename(file)); - }); - tree.create(file.replace(/\.scss$/, '.json'), JSON.stringify(tokenSpecification, null, 2)); - }); - return () => tree; - } catch (e) { - context.logger.warn('The following dependencies should be provided to the extract-token schematics: "@o3r/styling", "minimatch".'); - context.logger.warn('The extraction will stop, it can be re-run with the schematic "@o3r/design:extract-token".'); - context.logger.debug(JSON.stringify(e)); - } - }; -} diff --git a/packages/@o3r/design/schematics/ extract-token/schema.json b/packages/@o3r/design/schematics/ extract-token/schema.json deleted file mode 100644 index 7dd921b1b5..0000000000 --- a/packages/@o3r/design/schematics/ extract-token/schema.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ngAddSchematicsSchema", - "title": "Extract Design Token From Sass", - "description": "Extract Design Token From Sass o3r variable helpers", - "properties": { - "includeTags": { - "description": "Include the tags in the original Sass file", - "oneOf": [ - { - "type": "boolean" - }, - { - "type": "object", - "properties": { - "startTag": { - "type": "string" - }, - "endTag": { - "type": "string" - } - }, - "required": [ - "startTag", - "endTag" - ] - } - ], - "default": true - }, - "componentFilePatterns": { - "description": "List of file pattern of component theme files", - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": true, - "required": [ - "componentFilePattern" - ] -} diff --git a/packages/@o3r/design/schematics/ extract-token/schema.ts b/packages/@o3r/design/schematics/ extract-token/schema.ts deleted file mode 100644 index 71ac36ada3..0000000000 --- a/packages/@o3r/design/schematics/ extract-token/schema.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { SchematicOptionObject } from '@o3r/schematics'; - -export interface ExtractTokenSchematicsSchema extends SchematicOptionObject { - /** - * Include the tags in the original Sass file - * @default true - */ - includeTags?: boolean | { startTag: string; endTag: string }; - - /** List of file pattern of component theme files */ - componentFilePatterns: string[]; -} diff --git a/packages/@o3r/design/schematics/generate-css/index.ts b/packages/@o3r/design/schematics/generate-css/index.ts deleted file mode 100644 index 8e034d722b..0000000000 --- a/packages/@o3r/design/schematics/generate-css/index.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { GenerateCssSchematicsSchema } from './schema'; -import type { Rule } from '@angular-devkit/schematics'; -import { parseDesignTokenFile, renderDesignTokens } from '@o3r/design'; -import type { DesignTokenRendererOptions, DesignTokenVariableSet, DesignTokenVariableStructure } from '@o3r/design'; - -/* for v9.6 migration only, it is integrated into @o3r/schematics package in v10 */ -import { getAllFilesInTree } from '@o3r/schematics'; -import type { Tree } from '@angular-devkit/schematics'; -import { minimatch } from 'minimatch'; -function globInTree(tree: Tree, patterns: string[]): string[] { - const files = getAllFilesInTree(tree); - return files.filter((basePath) => patterns.some((p) => minimatch(basePath, p, { dot: true }))); -} - -/** - * Generate CSS from Design Token files - * @param options - */ -export function generateCss(options: GenerateCssSchematicsSchema): Rule { - return async (tree, context) => { - const writeFile = (filePath: string, content: string) => tree.exists(filePath) ? tree.overwrite(filePath, content) : tree.create(filePath, content); - const readFile = tree.readText; - const existsFile = tree.exists; - const determineFileToUpdate = options.output ? () => options.output! : - (token: DesignTokenVariableStructure) => { - if (token.extensions.o3rTargetFile && tree.exists(token.extensions.o3rTargetFile)) { - return token.extensions.o3rTargetFile; - } - - return options.defaultStyleFile; - }; - const renderDesignTokenOptions: DesignTokenRendererOptions = { - readFile, - writeFile, - existsFile, - determineFileToUpdate, - logger: context.logger - }; - - const files = globInTree(tree, Array.isArray(options.designTokenFilePatterns) ? options.designTokenFilePatterns : [options.designTokenFilePatterns]); - - const duplicatedToken: DesignTokenVariableStructure[] = []; - const tokens = (await Promise.all(files.map(async (file) => ({ file, parsed: await parseDesignTokenFile(file) })))) - .reduce((acc, { file, parsed }) => { - parsed.forEach((variable, key) => { - if (acc.has(key)) { - context.logger[options.failOnDuplicate ? 'error' : 'warn'](`A duplication of the variable ${key} is found in ${file}`); - } - acc.set(key, variable); - }); - return acc; - }, new Map()); - if (options.failOnDuplicate && duplicatedToken.length > 0) { - throw new Error(`Found ${duplicatedToken.length} duplicated Design Token keys`); - } - await renderDesignTokens(tokens, renderDesignTokenOptions); - }; -} diff --git a/packages/@o3r/design/schematics/generate-css/schema.json b/packages/@o3r/design/schematics/generate-css/schema.json deleted file mode 100644 index e2efc36b38..0000000000 --- a/packages/@o3r/design/schematics/generate-css/schema.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ngAddSchematicsSchema", - "title": "Add Otter Design", - "description": "ngAdd Otter Design", - "properties": { - "designTokenFilePatterns": { - "description": "Path patterns to the Design Token JSON files", - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ], - "$default": { - "$source": "argv" - } - }, - "output": { - "type": "string", - "description": "Output file where generate the CSS" - }, - "defaultStyleFile": { - "type": "string", - "default": "src/theme.scss", - "description": "File path to generate the variable if not determined by the specification" - }, - "failOnDuplicate": { - "type": "boolean", - "default": false, - "description": "Determine if the process should stop in case of Token duplication" - } - }, - "additionalProperties": true, - "required": [ - "designTokenFilePatterns" - ] -} diff --git a/packages/@o3r/design/schematics/generate-css/schema.ts b/packages/@o3r/design/schematics/generate-css/schema.ts deleted file mode 100644 index 7c5eedf755..0000000000 --- a/packages/@o3r/design/schematics/generate-css/schema.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { SchematicOptionObject } from '@o3r/schematics'; - -export interface GenerateCssSchematicsSchema extends SchematicOptionObject { - /** Path patterns to the Design Token JSON files */ - designTokenFilePatterns: string | string[]; - - /** File path to generate the variable if not determined by the specification */ - defaultStyleFile: string; - - /** - * Output file where generate the CSS - * - * If specified, all the generated CSS variable will be generated in the given file. - * Otherwise, the output file will be determined based on the Variable parameters - */ - output?: string; - - /** Determine if the process should stop in case of Token duplication */ - failOnDuplicate?: boolean; -} diff --git a/packages/@o3r/design/schematics/index.it.spec.ts b/packages/@o3r/design/schematics/index.it.spec.ts deleted file mode 100644 index 6db7f9f36b..0000000000 --- a/packages/@o3r/design/schematics/index.it.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { - getDefaultExecSyncOptions, - packageManagerExec, - packageManagerInstall, - packageManagerRun, - prepareTestEnv, - setupLocalRegistry -} from '@o3r/test-helpers'; - -const appName = 'test-app-design'; -const o3rVersion = '999.0.0'; -const execAppOptions = getDefaultExecSyncOptions(); -let appFolderPath: string; - -describe.skip('new otter application with Design', () => { - setupLocalRegistry(); - beforeAll(async () => { - appFolderPath = await prepareTestEnv(appName, 'angular-with-o3r-core'); - execAppOptions.cwd = appFolderPath; - }); - test('should add design to existing application', () => { - packageManagerExec(`ng add --skip-confirmation @o3r/design@${o3rVersion}`, execAppOptions); - - expect(() => packageManagerInstall(execAppOptions)).not.toThrow(); - expect(() => packageManagerRun('build', execAppOptions)).not.toThrow(); - }); -}); diff --git a/packages/@o3r/design/schematics/ng-add/index.ts b/packages/@o3r/design/schematics/ng-add/index.ts deleted file mode 100644 index e0e72368a3..0000000000 --- a/packages/@o3r/design/schematics/ng-add/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { chain, type Rule } from '@angular-devkit/schematics'; -import { registerGenerateCssBuilder } from './register-generate-css'; -import { extractToken } from '../ extract-token'; - -/** - * Add Otter design to an Angular Project - * @param options - */ -export function ngAdd(): Rule { - /* ng add rules */ - return chain([ - registerGenerateCssBuilder(), - extractToken({ componentFilePatterns: ['**/*.scss'], includeTags: true }) - ]); -} diff --git a/packages/@o3r/design/schematics/ng-add/register-generate-css/index.ts b/packages/@o3r/design/schematics/ng-add/register-generate-css/index.ts deleted file mode 100644 index dbe1b1d9c5..0000000000 --- a/packages/@o3r/design/schematics/ng-add/register-generate-css/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './register-task'; diff --git a/packages/@o3r/design/schematics/ng-add/register-generate-css/register-task.ts b/packages/@o3r/design/schematics/ng-add/register-generate-css/register-task.ts deleted file mode 100644 index 9da6738c67..0000000000 --- a/packages/@o3r/design/schematics/ng-add/register-generate-css/register-task.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { apply, chain, MergeStrategy, mergeWith, move, renameTemplateFiles, type Rule, template, url } from '@angular-devkit/schematics'; -import { getWorkspaceConfig } from '@o3r/schematics'; -import type { GenerateCssSchematicsSchema } from '../../../builders/generate-css/schema'; -import { posix } from 'node:path'; - -/* for v9.6 migration only, it is integrated into @o3r/schematics package in v10 */ -import type { SchematicOptionObject, WorkspaceProject } from '@o3r/schematics'; -function registerBuilder(workspaceProject: WorkspaceProject, taskName: string, taskParameters: SchematicOptionObject, force = false): WorkspaceProject { - workspaceProject.architect ||= {}; - if (workspaceProject.architect[taskName] && !force) { - throw new Error(`The builder task ${taskName} already exist`); - } - workspaceProject.architect[taskName] = taskParameters; - return workspaceProject; -} - -/** - * Register the Design Token CSS generator - * @param projectName Project name - * @param taskName name of the task to generate - */ -export const registerGenerateCssBuilder = (projectName?: string, taskName = 'generate-css'): Rule => { - const registerBuilderRule: Rule = (tree, {logger}) => { - const workspaceProject = projectName ? getWorkspaceConfig(tree)?.projects[projectName] : undefined; - const srcBasePath = workspaceProject?.sourceRoot || (workspaceProject?.root ? posix.resolve(workspaceProject.root, 'src') : ''); - const themeFile = posix.resolve(srcBasePath, 'style', 'theme.scss'); - const taskParameters: GenerateCssSchematicsSchema = { - defaultStyleFile: themeFile, - renderPrivateVariableTo: 'sass', - designTokenFilePatterns: [ - `${posix.resolve(srcBasePath, 'style', '*.json')}`, - `${posix.resolve(srcBasePath, '**', '*.theme.json')}` - ] - }; - if (!workspaceProject) { - logger.warn(`No angular.json found, the task ${taskName} will not be created`); - return tree; - } - registerBuilder(workspaceProject, taskName, taskParameters); - return tree; - }; - - const generateTemplateRule: Rule = (tree, context) => { - const workspaceProject = projectName ? getWorkspaceConfig(tree)?.projects[projectName] : undefined; - const srcBasePath = workspaceProject?.sourceRoot || (workspaceProject?.root ? posix.resolve(workspaceProject.root, 'src') : ''); - const themeFolder = posix.resolve(srcBasePath, 'style'); - const rule = mergeWith(apply(url('./templates'), [ - template({}), - move(themeFolder), - renameTemplateFiles() - ]), MergeStrategy.Overwrite)(tree, context); - - return rule; - }; - - const importTheme: Rule = (tree, context) => { - const workspaceProject = projectName ? getWorkspaceConfig(tree)?.projects[projectName] : undefined; - const srcBasePath = workspaceProject?.sourceRoot || (workspaceProject?.root ? posix.resolve(workspaceProject.root, 'src') : ''); - const styleFile = posix.resolve(srcBasePath, 'styles.scss'); - if (!tree.exists(styleFile)) { - context.logger.warn(`The theme was not updated as ${styleFile} was not found`); - return tree; - } - - return tree.overwrite(styleFile, '@import "./style/theme.scss";\n' + tree.readText(styleFile)); - }; - - return chain([ - registerBuilderRule, - generateTemplateRule, - importTheme - ]); -}; diff --git a/packages/@o3r/design/schematics/ng-add/register-generate-css/templates/design-token.custom.json.template b/packages/@o3r/design/schematics/ng-add/register-generate-css/templates/design-token.custom.json.template deleted file mode 100644 index 635c8a03c4..0000000000 --- a/packages/@o3r/design/schematics/ng-add/register-generate-css/templates/design-token.custom.json.template +++ /dev/null @@ -1,3 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/AmadeusITGroup/otter/main/packages/@o3r/design/schemas/design-token.schema.json" -} diff --git a/packages/@o3r/design/schematics/ng-add/register-generate-css/templates/theme.scss.template b/packages/@o3r/design/schematics/ng-add/register-generate-css/templates/theme.scss.template deleted file mode 100644 index 5c9365f8e6..0000000000 --- a/packages/@o3r/design/schematics/ng-add/register-generate-css/templates/theme.scss.template +++ /dev/null @@ -1,6 +0,0 @@ - -:root { -/* --- BEGIN THEME Auto-generated --- */ - -/* --- END THEME Auto-generated --- */ -} diff --git a/packages/@o3r/design/schematics/ng-add/schema.json b/packages/@o3r/design/schematics/ng-add/schema.json deleted file mode 100644 index b9b344baf6..0000000000 --- a/packages/@o3r/design/schematics/ng-add/schema.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ngAddSchematicsSchema", - "title": "Add Otter Design", - "description": "ngAdd Otter Design", - "properties": { - "projectName": { - "type": "string", - "description": "Project name", - "$default": { - "$source": "projectName" - } - } - }, - "additionalProperties": true, - "required": [ - ] -} diff --git a/packages/@o3r/design/schematics/ng-add/schema.ts b/packages/@o3r/design/schematics/ng-add/schema.ts deleted file mode 100644 index 76d3853e73..0000000000 --- a/packages/@o3r/design/schematics/ng-add/schema.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { SchematicOptionObject } from '@o3r/schematics'; - -export interface NgAddSchematicsSchema extends SchematicOptionObject { - /** Project name */ - projectName?: string | undefined; -} diff --git a/packages/@o3r/design/src/core/design-token/design-token-specification.interface.ts b/packages/@o3r/design/src/core/design-token/design-token-specification.interface.ts deleted file mode 100644 index 524fea5204..0000000000 --- a/packages/@o3r/design/src/core/design-token/design-token-specification.interface.ts +++ /dev/null @@ -1,264 +0,0 @@ -/** Metadata information added in the design token extension for Metadata extraction */ -export interface DesignTokenMetadata { - tags?: string[]; - /** Description of the variable */ - label?: string; - /** Name of a group of variables */ - category?: string; -} - -/** Design Token Group Extension fields supported by the default renderer */ -export interface DesignTokenGroupExtensions { - /** Indicate the file where to generate the token */ - o3rTargetFile?: string; - /** - * Indicate that the variable does not need to be generated. - * It is up to the generator to describe how to render private variables. - * It can choose to ignore the private extension, it can provide a dedicated renderer (for example to prefix it with '_') or it can decide to skip the generation straight to its referenced value. - */ - o3rPrivate?: boolean; - /** Indicate that the value of this token is flagged as important */ - o3rImportant?: boolean; - /** Metadata specific information */ - o3rMetadata?: DesignTokenMetadata; - /** Scope of the Design Token value */ - o3rScope?: string; -} - -/** Design Token Extension fields supported by the default renderer */ -export interface DesignTokenExtensions extends DesignTokenGroupExtensions { -} - - -interface DesignTokenBase { - /** Value of the Token */ - $value: T; - - /** Type of the Design Token */ - $type: string; -} - -/** Design Token without explicit type (mainly alias) */ -export interface DesignTokenTypeImplicit { - /** Value of the Token */ - $value: string; - - /** @inheritdoc */ - $type?: undefined; -} - -/** Design Token Color */ -export interface DesignTokenTypeColor extends DesignTokenBase { - /** @inheritdoc */ - $type: 'color'; -} - -/** Design Token Dimension */ -export interface DesignTokenTypeDimension extends DesignTokenBase { - /** @inheritdoc */ - $type: 'dimension'; -} - -/** Design Token Font Family */ -export interface DesignTokenTypeFontFamily extends DesignTokenBase { - /** @inheritdoc */ - $type: 'fontFamily'; -} - -/** Design Token Duration */ -export interface DesignTokenTypeDuration extends DesignTokenBase { - /** @inheritdoc */ - $type: 'duration'; -} - -type DesignTokenTypeCubicBezierValue = (number | string)[]; - -/** Design Token Cubic Bezier */ -export interface DesignTokenTypeCubicBezier extends DesignTokenBase { - /** @inheritdoc */ - $type: 'cubicBezier'; -} - -type DesignTokenTypeFontWeightValue = number | string; - -/** Design Token Font Weight */ -export interface DesignTokenTypeFontWeight extends DesignTokenBase { - /** @inheritdoc */ - $type: 'fontWeight'; -} - -/** Design Token Number */ -export interface DesignTokenTypeNumber extends DesignTokenBase { - /** @inheritdoc */ - $type: 'number'; -} - -type DesignTokenTypeStrokeStyleDetailsValue = { - dashArray: string[]; - lineCap: 'round' | 'butt' | 'square'; -}; - -/** Value of the Design Token Stroke Style */ -export type DesignTokenTypeStrokeStyleValue = DesignTokenTypeStrokeStyleDetailsValue | - 'solid' | 'dashed' | 'dotted' | 'double'| 'groove' | 'ridge' | 'outset' | 'inset'; - -/** Design Token Stroke Style */ -export interface DesignTokenTypeStrokeStyle extends DesignTokenBase { - /** @inheritdoc */ - $type: 'strokeStyle'; -} - -type DesignTokenTypeBorderValue = { - color: string; - width: string; - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents - style: string | DesignTokenTypeStrokeStyleValue; - -}; - -/** Design Token Border */ -export interface DesignTokenTypeBorder extends DesignTokenBase { - /** @inheritdoc */ - $type: 'border'; -} - -type DesignTokenTypeTransitionValue = { - duration: string; - delay: string; - timingFunction: string | DesignTokenTypeCubicBezierValue; - -}; - -/** Design Token Transition */ -export interface DesignTokenTypeTransition extends DesignTokenBase { - /** @inheritdoc */ - $type: 'transition'; -} - -type DesignTokenTypeShadowValue = { - color: string; - offsetX: string; - offsetY: string; - blur: string; - spread: string; -}; - -/** Design Token Shadow */ -export interface DesignTokenTypeShadow extends DesignTokenBase { - /** @inheritdoc */ - $type: 'shadow'; -} - -type DesignTokenTypeGradientValue = { - color: string; - position: string | number; -}[]; - -/** Design Token Gradient */ -export interface DesignTokenTypeGradient extends DesignTokenBase { - /** @inheritdoc */ - $type: 'gradient'; -} - -type DesignTokenTypeTypographyValue = { - fontFamily: string; - fontSize: string; - letterSpacing: string; - fontWeight: DesignTokenTypeFontWeightValue; - lineHeight: string | number; -}; - - -/** Design Token Typography */ -export interface DesignTokenTypeTypography extends DesignTokenBase { - /** @inheritdoc */ - $type: 'typography'; -} - -/** Common field for the Design Token Groups */ -export interface DesignTokenGroupCommonFields { - /** Description of the Group */ - $description?: string; - /** Design Token Extension */ - $extensions?: G; -} - -/** Common field for the Design Token */ -export type DesignTokenCommonFields = DesignTokenGroupCommonFields; - -/** Available Design Token types */ -export type DesignToken = DesignTokenCommonFields & ( - DesignTokenTypeColor | - DesignTokenTypeDimension | - DesignTokenTypeFontFamily | - DesignTokenTypeDuration | - DesignTokenTypeCubicBezier | - DesignTokenTypeFontWeight | - DesignTokenTypeNumber | - - DesignTokenTypeStrokeStyle | - DesignTokenTypeBorder | - DesignTokenTypeTransition | - DesignTokenTypeShadow | - DesignTokenTypeGradient | - DesignTokenTypeTypography | - - DesignTokenTypeImplicit -); - -/** Design Token Node (Design Token Group or Item) */ -// eslint-disable-next-line no-use-before-define -export type DesignTokenNode = DesignTokenGroup | DesignToken; - -/** Design Token Group */ -export type DesignTokenGroup = - DesignTokenGroupCommonFields & { [x: string]: DesignTokenNode | E | string | boolean | undefined }; - -/** Context of the Design Token specification document */ -export type DesignTokenContext = { - /** Base path used to compute the path of the file to render the Tokens into */ - basePath?: string; -}; - -/** Design Token specification */ -export type DesignTokenSpecification = { - /** Specification as described on {@link https://design-tokens.github.io/community-group/format/} */ - document: DesignTokenGroup; - /** Specification document context information */ - context?: C; -}; - -/** - * Determine if the Design Token Node is a Token (not a Group) - * @param node Design Token Node - */ -export const isDesignToken = (node?: any): node is DesignToken => { - return !!node && (typeof node.$type !== 'undefined' || typeof node.$value === 'string'); -}; - -/** - * Determine if the Design Token Node is a Group (not a Token) - * @param node Design Token Node - */ -export const isDesignTokenGroup = (node?: any): node is DesignTokenGroup => { - return typeof node === 'object' && Object.keys(node).some((k) => !k.startsWith('$')); -}; - -/** - * Determine if the Stroke Style value is defined or is a reference - * @param value Stroke Style value - * @returns true if it is a defined value - */ -// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents -export const isTokenTypeStrokeStyleValueComplex = (value?: DesignTokenTypeStrokeStyleValue | string): value is DesignTokenTypeStrokeStyleDetailsValue => { - return !!value && typeof value !== 'string'; -}; - -/** - * Determine if the Stroke Style Token has a value defined or is a reference - * @param node Stroke Style Token - * @returns true if it is a token with defined value - */ -export const isTokenTypeStrokeStyleComplex = (node?: DesignTokenTypeStrokeStyle): node is DesignTokenTypeStrokeStyle => { - return !!node && isTokenTypeStrokeStyleValueComplex(node.$value); -}; diff --git a/packages/@o3r/design/src/core/design-token/design-token.spec.ts b/packages/@o3r/design/src/core/design-token/design-token.spec.ts deleted file mode 100644 index 590a5c8229..0000000000 --- a/packages/@o3r/design/src/core/design-token/design-token.spec.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { - computeFileToUpdatePath, - DesignTokenRendererOptions, - getCssStyleContentUpdater, - getCssTokenDefinitionRenderer, - getMetadataStyleContentUpdater, - getMetadataTokenDefinitionRenderer, - getSassTokenDefinitionRenderer, - renderDesignTokens -} from './renderers/index'; -import { parseDesignToken, TokenKeyRenderer } from './parsers/index'; -import { promises as fs } from 'node:fs'; -import { resolve } from 'node:path'; -import type { DesignTokenSpecification } from './design-token-specification.interface'; -import { validate } from 'jsonschema'; - -describe('Design Token generator', () => { - const AUTO_GENERATED_START = '/* --- BEGIN THEME Test --- */'; - const AUTO_GENERATED_END = '/* --- END THEME Test --- */'; - let exampleVariable!: DesignTokenSpecification; - - beforeAll(async () => { - exampleVariable = {document: JSON.parse(await fs.readFile(resolve(__dirname, '../../../testing/mocks/design-token-theme.json'), {encoding: 'utf-8'}))}; - }); - - describe('CSS renderer', () => { - - const renderDesignTokensOptions = { - styleContentUpdater: getCssStyleContentUpdater({startTag: AUTO_GENERATED_START, endTag: AUTO_GENERATED_END}) - }; - - test('should render variable in CSS', async () => { - let result: string | undefined; - const writeFile = jest.fn().mockImplementation((_, content) => result = content); - const readFile = jest.fn().mockReturnValue(''); - const existsFile = jest.fn().mockReturnValue(true); - const determineFileToUpdate = computeFileToUpdatePath('.'); - const designToken = parseDesignToken(exampleVariable); - - // eslint-disable-next-line @typescript-eslint/await-thenable - await renderDesignTokens(designToken, { - ...renderDesignTokensOptions, - determineFileToUpdate, - existsFile, - writeFile, - readFile - }); - - expect(writeFile).toHaveBeenCalledTimes(1); - expect(result).toContain('--example-var1: #000;'); - expect(result).toContain('--example-color: var(--example-var1);'); - expect(result).not.toContain('--example-test-height: 2.3;'); - expect(result).toContain('--example-test-width: var(--example-test-height, 2.3);'); - }); - - test('should render variable with important flag', async () => { - let result: string | undefined; - const writeFile = jest.fn().mockImplementation((_, content) => result = content); - const readFile = jest.fn().mockReturnValue(''); - const existsFile = jest.fn().mockReturnValue(true); - const determineFileToUpdate = computeFileToUpdatePath('.'); - const designToken = parseDesignToken(exampleVariable); - - // eslint-disable-next-line @typescript-eslint/await-thenable - await renderDesignTokens(designToken, { - ...renderDesignTokensOptions, - determineFileToUpdate, - existsFile, - writeFile, - readFile - }); - - expect(result).toContain('--example-var-important: #000 !important;'); - }); - - test('should render variable with prefix', async () => { - let result: string | undefined; - const prefix = 'prefix-'; - const writeFile = jest.fn().mockImplementation((_, content) => result = content); - const readFile = jest.fn().mockReturnValue(''); - const existsFile = jest.fn().mockReturnValue(true); - const determineFileToUpdate = computeFileToUpdatePath('.'); - const designToken = parseDesignToken(exampleVariable); - const tokenVariableNameRenderer: TokenKeyRenderer = (variable) => prefix + variable.tokenReferenceName.replace(/\./g, '-'); - const tokenDefinitionRenderer = getCssTokenDefinitionRenderer({tokenVariableNameRenderer}); - - // eslint-disable-next-line @typescript-eslint/await-thenable - await renderDesignTokens(designToken, { - ...renderDesignTokensOptions, - tokenDefinitionRenderer, - determineFileToUpdate, - existsFile, - writeFile, - readFile - }); - - expect(result).not.toContain('--example-var1'); - expect(result).toContain('--prefix-example-var1'); - }); - - test('should render variable in existing CSS', async () => { - let result: string | undefined; - const writeFile = jest.fn().mockImplementation((_, content) => result = content); - const existsFile = jest.fn().mockReturnValue(true); - const readFile = jest.fn().mockReturnValue(` - // CSS - :root { - ${AUTO_GENERATED_START} - --some-var: #fff; - ${AUTO_GENERATED_END} - } - `); - const determineFileToUpdate = computeFileToUpdatePath('.'); - const designToken = parseDesignToken(exampleVariable); - - // eslint-disable-next-line @typescript-eslint/await-thenable - await renderDesignTokens(designToken, { - ...renderDesignTokensOptions, - determineFileToUpdate, - existsFile, - writeFile, - readFile - }); - - expect(writeFile).toHaveBeenCalledTimes(1); - expect(result).not.toContain('--some-var: #fff;'); - expect(result).toContain('--example-var1: #000;'); - }); - - test('should render private variable to sass if requested', async () => { - let result: string | undefined; - const expectedSassVar = '$exampleTestHeight: 2.3;'; - const writeFile = jest.fn().mockImplementation((_, content) => result = content); - const readFile = jest.fn().mockReturnValue(''); - const existsFile = jest.fn().mockReturnValue(true); - const determineFileToUpdate = computeFileToUpdatePath('.'); - const designToken = parseDesignToken(exampleVariable); - - const tokenDefinitionRendererWithoutSass = getCssTokenDefinitionRenderer({ - privateDefinitionRenderer: getSassTokenDefinitionRenderer() - }); - - // eslint-disable-next-line @typescript-eslint/await-thenable - await renderDesignTokens(designToken, { - ...renderDesignTokensOptions, - determineFileToUpdate, - writeFile, - existsFile, - readFile - }); - - expect(result).not.toContain(expectedSassVar); - - // eslint-disable-next-line @typescript-eslint/await-thenable - await renderDesignTokens(designToken, { - ...renderDesignTokensOptions, - determineFileToUpdate, - tokenDefinitionRenderer: tokenDefinitionRendererWithoutSass, - writeFile, - existsFile, - readFile - }); - - expect(writeFile).toHaveBeenCalledTimes(2); - expect(result).toContain(expectedSassVar); - }); - }); - - describe('Metadata Renderer', () => { - let metadataSchema: any; - const renderDesignTokensOptions: DesignTokenRendererOptions = { - styleContentUpdater: getMetadataStyleContentUpdater(), - tokenDefinitionRenderer: getMetadataTokenDefinitionRenderer() - }; - - beforeAll(async () => { - metadataSchema = JSON.parse(await fs.readFile(resolve(__dirname, '../../../../styling/schemas/style.metadata.schema.json'), { encoding: 'utf-8' })); - }); - - test('should render valid metadata', async () => { - let result: string | undefined; - const writeFile = jest.fn().mockImplementation((_, content) => result = content); - const readFile = jest.fn().mockReturnValue(''); - const existsFile = jest.fn().mockReturnValue(true); - const determineFileToUpdate = computeFileToUpdatePath('.'); - const designToken = parseDesignToken(exampleVariable); - - // eslint-disable-next-line @typescript-eslint/await-thenable - await renderDesignTokens(designToken, { - ...renderDesignTokensOptions, - determineFileToUpdate, - existsFile, - writeFile, - readFile - }); - - expect(writeFile).toHaveBeenCalledTimes(1); - expect(() => JSON.parse(result)).not.toThrow(); - expect(metadataSchema).toBeDefined(); - expect(validate).toBeDefined(); - expect(validate(JSON.parse(result), metadataSchema).errors).toHaveLength(0); - }); - }); -}); diff --git a/packages/@o3r/design/src/core/design-token/index.ts b/packages/@o3r/design/src/core/design-token/index.ts deleted file mode 100644 index 3d283d48a7..0000000000 --- a/packages/@o3r/design/src/core/design-token/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './renderers/index'; -export * from './parsers/index'; -export * from './design-token-specification.interface'; diff --git a/packages/@o3r/design/src/core/design-token/parsers/design-token-parser.interface.ts b/packages/@o3r/design/src/core/design-token/parsers/design-token-parser.interface.ts deleted file mode 100644 index 86bca74d45..0000000000 --- a/packages/@o3r/design/src/core/design-token/parsers/design-token-parser.interface.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { DesignToken, DesignTokenContext, DesignTokenExtensions, DesignTokenGroup, DesignTokenGroupExtensions } from '../design-token-specification.interface'; - -/** Reference to a parent node*/ -export interface ParentReference { - /** Design Token name */ - name: string; - /** Design Token Group node */ - tokenNode: DesignTokenGroup; -} - -/** - * Function rendering the Design Token Value - * @param tokenStructure Parsed Design Token - * @param variableSet Complete list of the parsed Design Token - */ -// eslint-disable-next-line no-use-before-define -export type TokenValueRenderer = (tokenStructure: DesignTokenVariableStructure, variableSet: Map) => string; - -/** - * Function rendering the Design Token Key - * @param tokenStructure Parsed Design Token - */ -// eslint-disable-next-line no-use-before-define -export type TokenKeyRenderer = (tokenStructure: DesignTokenVariableStructure) => string; - -/** Complete list of the parsed Design Token */ -// eslint-disable-next-line no-use-before-define -export type DesignTokenVariableSet = Map; - -/** Parsed Design Token variable */ -export interface DesignTokenVariableStructure { - /** Context of the Token determined or provided during the parsing process */ - context?: DesignTokenContext; - /** Design Token Extension */ - extensions: DesignTokenGroupExtensions & DesignTokenExtensions; - /** Reference to the Design Token node */ - node: DesignToken; - /** Name of the token in references */ - tokenReferenceName: string; - /** Description of the Token */ - description?: string; - /** List of the Ancestors references */ - ancestors: ParentReference[]; - /** Reference to the direct parent node */ - parent?: ParentReference; - /** - * Retrieve the list of the references of the Design Token - * @param variableSet Complete list of the parsed Design Token - */ - getReferences: (variableSet?: DesignTokenVariableSet) => string[]; - /** - * Raw CSS value of the Token - * @param variableSet Complete list of the parsed Design Token - */ - getCssRawValue: (variableSet?: DesignTokenVariableSet) => string; - /** - * Determine if the Token is an alias - * @param variableSet Complete list of the parsed Design Token - */ - getIsAlias: (variableSet?: DesignTokenVariableSet) => boolean; - /** - * Retrieve the type calculated for the Token - * @param followReference Determine if the references should be follow to calculate the type - * @param variableSet Complete list of the parsed Design Token - */ - getType: (variableSet?: DesignTokenVariableSet, followReference?: boolean) => DesignToken['$type']; - /** - * Retrieve the list of the references of the Design Token node - * @param followReference Determine if the references should be follow to calculate the type - */ - getReferencesNode: (variableSet?: DesignTokenVariableSet) => DesignTokenVariableStructure[]; - /** - * Retrieve the Design Token Key as rendered by the provided renderer - * @param keyRenderer Renderer for the Design Token key - */ - getKey: (keyRenderer?: TokenKeyRenderer) => string; -} diff --git a/packages/@o3r/design/src/core/design-token/parsers/design-token.parser.spec.ts b/packages/@o3r/design/src/core/design-token/parsers/design-token.parser.spec.ts deleted file mode 100644 index 2ad8bc086a..0000000000 --- a/packages/@o3r/design/src/core/design-token/parsers/design-token.parser.spec.ts +++ /dev/null @@ -1,70 +0,0 @@ -import * as parser from './design-token.parser'; -import { promises as fs } from 'node:fs'; -import { resolve } from 'node:path'; -import type { DesignTokenSpecification } from '../design-token-specification.interface'; - -describe('Design Token Parser', () => { - - let exampleVariable!: DesignTokenSpecification; - - beforeAll(async () => { - const file = await fs.readFile(resolve(__dirname, '../../../../testing/mocks/design-token-theme.json'), { encoding: 'utf-8' }); - exampleVariable = {document: JSON.parse(file)}; - }); - - describe('parseDesignToken', () => { - - test('generate a simple type variable', () => { - const result = parser.parseDesignToken(exampleVariable); - const var1 = result.get('example.var1'); - const var2 = result.get('example.test.var2'); - - expect(result.size).toBeGreaterThan(0); - expect(var2).toBeDefined(); - expect(var1).toBeDefined(); - expect(var2.getKey()).toBe('example-test-var2'); - expect(var1.getKey()).toBe('example-var1'); - expect(var2.description).toBe('my var2'); - expect(var2.getType()).toBe('color'); - expect(var1.getType()).toBe('color'); - }); - - test('generate an alias variable', () => { - const result = parser.parseDesignToken(exampleVariable); - const color = result.get('example.color'); - - expect(color).toBeDefined(); - expect(color.getType()).toBe('color'); - }); - - test('generate a complex variable', () => { - const result = parser.parseDesignToken(exampleVariable); - const border = result.get('example.test.border'); - - expect(border).toBeDefined(); - expect(border.getType()).toBe('border'); - }); - }); - - describe('parseDesignTokenFile', () => { - test('should read the file according to the reader', async () => { - const readFile = jest.fn().mockResolvedValue('{"test": { "$value": "#000", "$type": "color" }}'); - const parseDesignToken = jest.spyOn(parser, 'parseDesignToken').mockImplementation(() => (new Map())); - const result = await parser.parseDesignTokenFile('fakeFile.json', {readFile}); - - expect(result.size).toBe(0); - expect(parseDesignToken).toHaveBeenCalledTimes(1); - expect(readFile).toHaveBeenCalledTimes(1); - expect(parseDesignToken).toHaveBeenCalledWith({context: { basePath: '.' }, document: { test: { $value: '#000', $type: 'color' } } }); - }); - - test('should throw if invalid JSON Token', async () => { - const readFile = jest.fn().mockResolvedValue('{"test": { "$value": "#000", '); - const parseDesignToken = jest.spyOn(parser, 'parseDesignToken').mockImplementation(() => (new Map())); - - await expect(() => parser.parseDesignTokenFile('fakeFile.json', {readFile})).rejects.toThrow(); - expect(parseDesignToken).toHaveBeenCalledTimes(0); - expect(readFile).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/packages/@o3r/design/src/core/design-token/parsers/design-token.parser.ts b/packages/@o3r/design/src/core/design-token/parsers/design-token.parser.ts deleted file mode 100644 index 59ae861da6..0000000000 --- a/packages/@o3r/design/src/core/design-token/parsers/design-token.parser.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { promises as fs } from 'node:fs'; -import type { DesignTokenVariableSet, DesignTokenVariableStructure, ParentReference } from './design-token-parser.interface'; -import type { - DesignToken, - DesignTokenContext, - DesignTokenExtensions, - DesignTokenGroup, - DesignTokenGroupExtensions, - DesignTokenNode, - DesignTokenSpecification -} from '../design-token-specification.interface'; -import { - DesignTokenTypeStrokeStyleValue, - isDesignToken, - isDesignTokenGroup, - isTokenTypeStrokeStyleValueComplex -} from '../design-token-specification.interface'; -import { dirname } from 'node:path'; - -const tokenReferenceRegExp = /\{([^}]+)\}/g; - -const getTokenReferenceName = (tokenName: string, parents: string[]) => (`${parents.join('.')}.${tokenName}`); -const getExtensions = (parentNode: DesignTokenNode[]) => parentNode.reduce((acc, node) => ({...acc, ...node.$extensions}), {} as DesignTokenGroupExtensions & DesignTokenExtensions); -const getReferences = (cssRawValue: string) => Array.from(cssRawValue.matchAll(tokenReferenceRegExp)).map(([,tokenRef]) => tokenRef); -// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents -const renderCssTypeStrokeStyleValue = (value: DesignTokenTypeStrokeStyleValue | string) => isTokenTypeStrokeStyleValueComplex(value) ? `${value.lineCap} ${value.dashArray.join(' ')}` : value; - -const getCssRawValue = (variableSet: DesignTokenVariableSet, {node, getType}: DesignTokenVariableStructure) => { - const nodeType = getType(variableSet, false); - if (!nodeType && node.$value) { - return typeof node.$value.toString !== undefined ? (node.$value as any).toString() : JSON.stringify(node.$value); - } - const checkNode = { - ...node, - $type: node.$type || nodeType - } as typeof node; - - // TODO in the following code, `typeof checkNode.$value === 'string' ? checkNode.$value :` is defined to please Jest TS compilation. It should be removed when supported - switch (checkNode.$type) { - case 'color': - case 'number': - case 'duration': - case 'fontWeight': - case 'fontFamily': - case 'dimension': { - return checkNode.$value.toString(); - } - case 'strokeStyle': { - return renderCssTypeStrokeStyleValue(checkNode.$value); - } - case 'cubicBezier': { - return typeof checkNode.$value === 'string' ? checkNode.$value : - checkNode.$value.join(', '); - } - case 'border': { - return typeof checkNode.$value === 'string' ? checkNode.$value : - `${checkNode.$value.width} ${renderCssTypeStrokeStyleValue(checkNode.$value.style)} ${checkNode.$value.color}`; - } - case 'gradient': { - return typeof checkNode.$value === 'string' ? checkNode.$value : - // TODO: add support of different gradient type when design-tokens/community-group#101 is fixed. - `linear-gradient(0deg, ${checkNode.$value.map(({color, position}) => `${color} ${position}`).join(', ')})`; - } - case 'shadow': { - return typeof checkNode.$value === 'string' ? checkNode.$value : - `${checkNode.$value.offsetX} ${checkNode.$value.offsetY} ${checkNode.$value.blur} ${checkNode.$value.spread} ${checkNode.$value.color}`; - } - case 'transition': { - return typeof checkNode.$value === 'string' ? checkNode.$value : - typeof checkNode.$value.timingFunction === 'string' ? checkNode.$value.timingFunction : checkNode.$value.timingFunction.join(' ') + - ` ${checkNode.$value.duration} ${checkNode.$value.delay}`; - } - case 'typography': { - return typeof checkNode.$value === 'string' ? checkNode.$value : - `${checkNode.$value.fontWeight} ${checkNode.$value.fontFamily} ${checkNode.$value.fontSize} ${checkNode.$value.letterSpacing} ${checkNode.$value.lineHeight}`; - } - default: { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - throw new Error(`Not supported type ${(checkNode as any).$type || 'unknown'} (value: ${(checkNode as any).$value || 'unknown'})`); - } - } -}; - -const walkThroughDesignTokenNodes = ( - node: DesignTokenNode, - context: DesignTokenContext | undefined, - ancestors: ParentReference[], - mem: DesignTokenVariableSet, - nodeName?: string): DesignTokenVariableSet => { - - if (isDesignTokenGroup(node)) { - Object.entries(node) - .filter(([tokenName, tokenNode]) => !tokenName.startsWith('$') && (isDesignToken(tokenNode) || isDesignTokenGroup(tokenNode))) - .forEach(([tokenName, tokenNode]) => walkThroughDesignTokenNodes( - tokenNode as DesignTokenGroup | DesignToken, context, nodeName ? [...ancestors, { name: nodeName, tokenNode: node }] : ancestors, mem, tokenName - )); - } - - if (isDesignToken(node)) { - if (!nodeName) { - throw new Error('The first node of the Design Specification can not be a token'); - } - const parentNames = ancestors.map(({ name }) => name); - const tokenReferenceName = getTokenReferenceName(nodeName, parentNames); - - const tokenVariable: DesignTokenVariableStructure = { - context, - extensions: getExtensions([...ancestors.map(({ tokenNode }) => tokenNode), node]), - node, - tokenReferenceName, - ancestors, - parent: ancestors.slice(-1)[0], - description: node.$description, - getCssRawValue: function (variableSet = mem) { - return getCssRawValue(variableSet, this); - }, - getReferences: function (variableSet = mem) { - return getReferences(this.getCssRawValue(variableSet)); - }, - getIsAlias: function (variableSet = mem) { - return this.getReferences(variableSet).length === 1 && typeof node.$value === 'string' && !!node.$value?.toString().match(/^\{[^}]*\}$/); - }, - getReferencesNode: function (variableSet = mem) { - return this.getReferences(variableSet) - .map((ref) => { - if (!variableSet.has(ref)) { - throw new Error (`Reference to ${ref} not found`); - } - return variableSet.get(ref)!; - }); - }, - getType: function (variableSet = mem, followReference = true) { - return node.$type || - followReference && this.getIsAlias(variableSet) && this.getReferencesNode(variableSet)[0]?.getType(variableSet, followReference) || - followReference && this.parent?.name && variableSet.get(this.parent.name)?.getType(variableSet, followReference) || - undefined; - }, - getKey: function (keyRenderer) { - return keyRenderer ? keyRenderer(this) : this.tokenReferenceName.replace(/[ .]+/g, '-'); - } - }; - - mem.set(tokenReferenceName, tokenVariable); - } - else if (!isDesignTokenGroup(node)) { - throw new Error('Fail to determine the Design Token Node type'); - } - - return mem; -}; - -/** - * Parse a Design Token Object to provide the map of Token with helpers to generate the different output - * @param specification Design Token content as specified on https://design-tokens.github.io/community-group/format/ - */ -export const parseDesignToken = (specification: DesignTokenSpecification): DesignTokenVariableSet => { - return walkThroughDesignTokenNodes(specification.document, specification.context, [], new Map()); -}; - -interface ParseDesignTokenFileOptions { - /** - * Custom function to read a file required by the token renderer - * @default {@see fs.promises.readFile} - * @param filePath Path to the file to read - */ - readFile?: (filePath: string) => string | Promise; - - /** Custom context to provide to the parser and override the information determined by the specification parse process */ - specificationContext?: DesignTokenContext; -} - -/** - * Parse a Design Token File to provide the map of Token with helpers to generate the different output - * @param specificationFilePath Path to the a Design Token file following the specification on https://design-tokens.github.io/community-group/format/ - * @param options - */ -export const parseDesignTokenFile = async (specificationFilePath: string, options?: ParseDesignTokenFileOptions) => { - const readFile = options?.readFile || ((filePath: string) => fs.readFile(filePath, { encoding: 'utf-8' })); - const context: DesignTokenContext = { - basePath: dirname(specificationFilePath) - }; - return parseDesignToken({ document: JSON.parse(await readFile(specificationFilePath)), context }); -}; diff --git a/packages/@o3r/design/src/core/design-token/parsers/index.ts b/packages/@o3r/design/src/core/design-token/parsers/index.ts deleted file mode 100644 index 618a3de3a7..0000000000 --- a/packages/@o3r/design/src/core/design-token/parsers/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './design-token-parser.interface'; -export * from './design-token.parser'; diff --git a/packages/@o3r/design/src/core/design-token/renderers/css/design-token-definition.renderers.spec.ts b/packages/@o3r/design/src/core/design-token/renderers/css/design-token-definition.renderers.spec.ts deleted file mode 100644 index d9b0311e35..0000000000 --- a/packages/@o3r/design/src/core/design-token/renderers/css/design-token-definition.renderers.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import * as parser from '../../parsers/design-token.parser'; -import { promises as fs } from 'node:fs'; -import { resolve } from 'node:path'; -import type { DesignTokenSpecification } from '../../design-token-specification.interface'; -import type { DesignTokenVariableSet } from '../../parsers'; -import { getCssTokenDefinitionRenderer } from './design-token-definition.renderers'; - -describe('getMetadataTokenDefinitionRenderer', () => { - let exampleVariable!: DesignTokenSpecification; - let designTokens!: DesignTokenVariableSet; - - beforeAll(async () => { - const file = await fs.readFile(resolve(__dirname, '../../../../../testing/mocks/design-token-theme.json'), { encoding: 'utf-8' }); - exampleVariable = { document: JSON.parse(file) }; - designTokens = parser.parseDesignToken(exampleVariable); - }); - - test('should rely on given tokenValueRenderer', () => { - const tokenValueRenderer = jest.fn().mockReturnValue(JSON.stringify({name: 'test-var', value: 'test-value'})); - const renderer = getCssTokenDefinitionRenderer({ tokenValueRenderer }); - const variable = designTokens.get('example.var1'); - - const result = renderer(variable, designTokens); - expect(variable).toBeDefined(); - expect(tokenValueRenderer).toHaveBeenCalledTimes(1); - expect(result).toBeDefined(); - expect(result).toContain('test-var'); - expect(result).toContain('test-value'); - }); - - test('should use private renderer for private variable', () => { - const variable = designTokens.get('example.var3'); - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - const privateDefinitionRenderer = jest.fn().mockImplementation((v: any) => `$test: ${v.getCssRawValue()}`); - - const renderer1 = getCssTokenDefinitionRenderer(); - const renderer2 = getCssTokenDefinitionRenderer({ privateDefinitionRenderer }); - const result1 = renderer1(variable, designTokens); - - expect(variable).toBeDefined(); - expect(result1).not.toBeDefined(); - expect(privateDefinitionRenderer).toHaveBeenCalledTimes(0); - - const result2 = renderer2(variable, designTokens); - - expect(result2).toContain('$test'); - expect(privateDefinitionRenderer).toHaveBeenCalledTimes(1); - }); - -}); diff --git a/packages/@o3r/design/src/core/design-token/renderers/css/design-token-definition.renderers.ts b/packages/@o3r/design/src/core/design-token/renderers/css/design-token-definition.renderers.ts deleted file mode 100644 index 428d7fd6d8..0000000000 --- a/packages/@o3r/design/src/core/design-token/renderers/css/design-token-definition.renderers.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { DesignTokenVariableStructure, TokenKeyRenderer, TokenValueRenderer } from '../../parsers/design-token-parser.interface'; -import { isO3rPrivateVariable } from '../design-token.renderer.helpers'; -import { TokenDefinitionRenderer } from '../design-token.renderer.interface'; -import { getCssTokenValueRenderer } from './design-token-value.renderers'; - -/** Options for {@link CssTokenDefinitionRendererOptions} */ -export interface CssTokenDefinitionRendererOptions { - /** - * Determine if the variable is private and should not be rendered - * @default {@see isO3rPrivateVariable} - */ - isPrivateVariable?: (variable: DesignTokenVariableStructure) => boolean; - - /** Custom Design Token value renderer */ - tokenValueRenderer?: TokenValueRenderer; - - /** - * Renderer the name of the CSS Variable (with the initial --) - */ - tokenVariableNameRenderer?: TokenKeyRenderer; - - /** - * Private Design Token definition renderer - * The private variable will not be rendered if not provided - */ - privateDefinitionRenderer?: TokenDefinitionRenderer; -} - -/** - * Retrieve the Design Token variable renderer for CSS - * @param options - * @returns - * @example CSS renderer with Sass fallback - * ```typescript - * import { getSassTokenDefinitionRenderer } from '@o3r/design'; - * - * // List of parsed Design Token items - * const parsedTokenDesign = await parseDesignTokenFile('./path/to/spec.json'); - * - * // Sass variable renderer - * const sassTokenDefinitionRenderer = getSassTokenDefinitionRenderer(); - * - * // CSS variable renderer - * const cssTokenDefinitionRenderer = getCssTokenDefinitionRenderer({ - * // Specify that the private variable should be rendered in Sass variable - * privateDefinitionRenderer: sassTokenDefinitionRenderer - * }); - * - * // Render the CSS variables - * await renderDesignTokens(parsedTokenDesign, { tokenDefinitionRenderer: cssTokenDefinitionRenderer }); - * ``` - */ -export const getCssTokenDefinitionRenderer = (options?: CssTokenDefinitionRendererOptions): TokenDefinitionRenderer => { - const isPrivateVariable = options?.isPrivateVariable || isO3rPrivateVariable; - const tokenVariableNameRenderer = options?.tokenVariableNameRenderer; - const tokenValueRenderer = options?.tokenValueRenderer || getCssTokenValueRenderer({ isPrivateVariable, tokenVariableNameRenderer }); - - const renderer = (variable: DesignTokenVariableStructure, variableSet: Map) => { - let variableString: string | undefined; - if (!isPrivateVariable(variable)) { - variableString = `--${variable.getKey(tokenVariableNameRenderer)}: ${tokenValueRenderer(variable, variableSet)};`; - if (variable.extensions.o3rScope) { - variableString = `${variable.extensions.o3rScope} { ${variableString} }`; - } - } else if (options?.privateDefinitionRenderer && variable.extensions.o3rPrivate) { - variableString = options.privateDefinitionRenderer(variable, variableSet); - } - if (variableString && variable.description) { - variableString = `/* ${variable.description} */\n${variableString}`; - } - return variableString; - }; - return renderer; -}; diff --git a/packages/@o3r/design/src/core/design-token/renderers/css/design-token-updater.renderers.spec.ts b/packages/@o3r/design/src/core/design-token/renderers/css/design-token-updater.renderers.spec.ts deleted file mode 100644 index c3d2d2dbbc..0000000000 --- a/packages/@o3r/design/src/core/design-token/renderers/css/design-token-updater.renderers.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { getCssStyleContentUpdater } from './design-token-updater.renderers'; - -describe('getCssStyleContentUpdater', () => { - const startTag = '/* test start */'; - const endTag = '/* end start */'; - const cssUpdaterOptions = { startTag, endTag}; - - test('should render CSS Values', () => { - const renderer = getCssStyleContentUpdater(cssUpdaterOptions); - - const variables = ['--var1: #000', '--var2: #fff']; - const result = renderer(variables, '/'); - - expect(result).toBeDefined(); - expect(result).toContain(variables[0]); - expect(result).toContain(variables[1]); - }); - - test('should add tags to new file', () => { - const renderer = getCssStyleContentUpdater(cssUpdaterOptions); - - const variables = ['--var1: #000', '--var2: #fff']; - const result = renderer(variables, '/'); - - expect(result).toBeDefined(); - expect(result.replace(/[\r\n]*/g, '').indexOf(':root {' + startTag)).toEqual(0); - expect(result.replace(/[\r\n]*/g, '').indexOf(endTag) + (endTag + '}').length).toBe(result.replace(/[\r\n]*/g, '').length); - }); - - test('should only update within tag part', () => { - const renderer = getCssStyleContentUpdater(cssUpdaterOptions); - const content = `.my-component { - ${startTag} - --my-comp: red; - ${endTag} - } - `; - - const variables = ['--var1: #000', '--var2: #fff']; - const result = renderer(variables, '/', content); - - expect(result).toBeDefined(); - expect(result).not.toContain(':root'); - expect(result).toContain('.my-component {'); - expect(result).not.toContain('--my-comp: red;'); - expect(result).toContain('--var1: #000'); - }); -}); diff --git a/packages/@o3r/design/src/core/design-token/renderers/css/design-token-updater.renderers.ts b/packages/@o3r/design/src/core/design-token/renderers/css/design-token-updater.renderers.ts deleted file mode 100644 index 00533e0ee0..0000000000 --- a/packages/@o3r/design/src/core/design-token/renderers/css/design-token-updater.renderers.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { DesignContentFileUpdater } from '../design-token.renderer.interface'; - -const SANITIZE_TAG_INPUTS_REGEXP = /[.*+?^${}()|[\]\\]/g; - -const generateVars = (variables: string[], startTag: string, endTag: string, addCssScope = false) => - `${addCssScope ? ':root {\n' : ''}${startTag}\n${variables.join('\n')}\n${endTag}${addCssScope ? '\n}' : ''}`; - -/** Default CSS starting tag */ -export const AUTO_GENERATED_START = '/* --- BEGIN THEME Auto-generated --- */'; - -/** Default CSS ending tag */ -export const AUTO_GENERATED_END = '/* --- END THEME Auto-generated --- */'; - -/** Options for {@link getCssStyleContentUpdater} */ -export interface CssStyleContentUpdaterOptions { - /** - * Opening tag marking the content edition part - * @default {@see AUTO_GENERATED_START} - */ - startTag?: string; - - /** - * Closing tag marking the content edition part - * @default {@see AUTO_GENERATED_END} - */ - endTag?: string; -} - -/** - * Retrieve a Content Updater function for CSS generator - * @param options - */ -export const getCssStyleContentUpdater = (options?: CssStyleContentUpdaterOptions): DesignContentFileUpdater => { - const startTag = options?.startTag || AUTO_GENERATED_START; - const endTag = options?.endTag || AUTO_GENERATED_END; - - /** Regexp to replace the content between the detected tags. It also handle possible inputted special character sanitization */ - const regexToReplace = new RegExp(`${startTag.replace(SANITIZE_TAG_INPUTS_REGEXP, '\\$&')}(:?(.|[\n\r])*)${endTag.replace(SANITIZE_TAG_INPUTS_REGEXP, '\\$&')}`); - - return (variables, _file, styleContent = '') => { - if (styleContent.indexOf(startTag) >= 0 && styleContent.indexOf(endTag) >= 0) { - return styleContent.replace(regexToReplace, generateVars(variables, startTag, endTag)); - } else { - return styleContent + '\n' + generateVars(variables, startTag, endTag, true); - } - }; -}; diff --git a/packages/@o3r/design/src/core/design-token/renderers/css/design-token-value.renderers.spec.ts b/packages/@o3r/design/src/core/design-token/renderers/css/design-token-value.renderers.spec.ts deleted file mode 100644 index 4518b7b4f4..0000000000 --- a/packages/@o3r/design/src/core/design-token/renderers/css/design-token-value.renderers.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as parser from '../../parsers/design-token.parser'; -import { promises as fs } from 'node:fs'; -import { resolve } from 'node:path'; -import type { DesignTokenSpecification } from '../../design-token-specification.interface'; -import type { DesignTokenVariableSet } from '../../parsers'; -import { getCssTokenValueRenderer } from './design-token-value.renderers'; - -describe('getCssTokenValueRenderer', () => { - let exampleVariable!: DesignTokenSpecification; - let designTokens!: DesignTokenVariableSet; - - beforeAll(async () => { - const file = await fs.readFile(resolve(__dirname, '../../../../../testing/mocks/design-token-theme.json'), { encoding: 'utf-8' }); - exampleVariable = { document: JSON.parse(file) }; - designTokens = parser.parseDesignToken(exampleVariable); - }); - - test('should render valid CSS value', () => { - const renderer = getCssTokenValueRenderer(); - const variable = designTokens.get('example.var1'); - - const result = renderer(variable, designTokens); - expect(variable).toBeDefined(); - expect(result).toBeDefined(); - expect(result).toBe((exampleVariable.document as any).example.var1.$value); - }); - - test('should render valid CSS var', () => { - const renderer = getCssTokenValueRenderer(); - const variable = designTokens.get('example.color'); - - const result = renderer(variable, designTokens); - expect(variable).toBeDefined(); - expect(result).toBeDefined(); - expect(result).toBe('var(--example-var1)'); - }); - - test('should render valid CSS var of not print value', () => { - const renderer = getCssTokenValueRenderer(); - const variable = designTokens.get('example.color2'); - - const result = renderer(variable, designTokens); - expect(variable).toBeDefined(); - expect(result).toBeDefined(); - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - expect(result).toBe(`var(--example-var3, ${(exampleVariable.document as any).example.var3.$value})`); - }); -}); diff --git a/packages/@o3r/design/src/core/design-token/renderers/css/design-token-value.renderers.ts b/packages/@o3r/design/src/core/design-token/renderers/css/design-token-value.renderers.ts deleted file mode 100644 index ccdbbb8790..0000000000 --- a/packages/@o3r/design/src/core/design-token/renderers/css/design-token-value.renderers.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { DesignTokenVariableStructure, TokenKeyRenderer, TokenValueRenderer } from '../../parsers/design-token-parser.interface'; -import { isO3rPrivateVariable } from '../design-token.renderer.helpers'; - -/** Options for {@link getCssTokenValueRenderer} */ -export interface CssTokenValueRendererOptions { - /** - * Determine if the variable is private and should not be rendered - * @default {@see isO3rPrivateVariable} - */ - isPrivateVariable?: (variable: DesignTokenVariableStructure) => boolean; - - /** - * Renderer the name of the CSS Variable (without initial --) - */ - tokenVariableNameRenderer?: TokenKeyRenderer; -} - -/** - * Retrieve the Design Token value renderer - * @param options - */ -export const getCssTokenValueRenderer = (options?: CssTokenValueRendererOptions): TokenValueRenderer => { - const isPrivateVariable = options?.isPrivateVariable || isO3rPrivateVariable; - const tokenVariableNameRenderer = options?.tokenVariableNameRenderer; - - const referenceRenderer = (variable: DesignTokenVariableStructure, variableSet: Map): string => { - if (!isPrivateVariable(variable)) { - return `var(--${variable.getKey(tokenVariableNameRenderer)})`; - } else { - // eslint-disable-next-line no-use-before-define - return `var(--${variable.getKey(tokenVariableNameRenderer)}, ${renderer(variable, variableSet)})`; - } - }; - const renderer = (variable: DesignTokenVariableStructure, variableSet: Map) => { - let variableValue = variable.getCssRawValue(variableSet).replaceAll(/\{([^}]*)\}/g, (defaultValue, matcher) => - (variableSet.has(matcher) ? referenceRenderer(variableSet.get(matcher)!, variableSet) : defaultValue) - ); - variableValue += variableValue && variable.extensions.o3rImportant ? ' !important' : ''; - return variableValue; - }; - return renderer; -}; diff --git a/packages/@o3r/design/src/core/design-token/renderers/css/index.ts b/packages/@o3r/design/src/core/design-token/renderers/css/index.ts deleted file mode 100644 index 026f171acd..0000000000 --- a/packages/@o3r/design/src/core/design-token/renderers/css/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './design-token-definition.renderers'; -export * from './design-token-value.renderers'; -export * from './design-token-updater.renderers'; diff --git a/packages/@o3r/design/src/core/design-token/renderers/design-token-style.renderer.spec.ts b/packages/@o3r/design/src/core/design-token/renderers/design-token-style.renderer.spec.ts deleted file mode 100644 index 8d5e7d61e6..0000000000 --- a/packages/@o3r/design/src/core/design-token/renderers/design-token-style.renderer.spec.ts +++ /dev/null @@ -1,81 +0,0 @@ -import * as parser from '../parsers/design-token.parser'; -import { promises as fs } from 'node:fs'; -import { resolve } from 'node:path'; -import type { DesignTokenSpecification } from '../design-token-specification.interface'; -import type { DesignTokenVariableSet } from '../parsers'; -import { computeFileToUpdatePath, renderDesignTokens } from './design-token-style.renderer'; - -describe('Design Token Renderer', () => { - let exampleVariable!: DesignTokenSpecification; - let designTokens!: DesignTokenVariableSet; - - beforeAll(async () => { - const file = await fs.readFile(resolve(__dirname, '../../../../testing/mocks/design-token-theme.json'), { encoding: 'utf-8' }); - exampleVariable = { document: JSON.parse(file) }; - // Add different target file - (exampleVariable.document.example as any)['test.var2'].$extensions = { o3rTargetFile: 'file.scss'}; - designTokens = parser.parseDesignToken(exampleVariable); - }); - - describe('computeFileToUpdatePath', () => { - const DEFAULT_FILE = 'test-result.json'; - const fileToUpdate = computeFileToUpdatePath('/', DEFAULT_FILE); - - test('should return default file if not specified', () => { - const variable = designTokens.get('example.var1'); - const result = fileToUpdate(variable); - - expect(variable.extensions.o3rTargetFile).not.toBeDefined(); - expect(result).toBe(DEFAULT_FILE); - }); - - test('should return file specified by the token', () => { - const variable = designTokens.get('example.test.var2'); - const result = fileToUpdate(variable); - - expect(variable.extensions.o3rTargetFile).toBeDefined(); - expect(result).toBe(resolve('/', variable.extensions.o3rTargetFile)); - }); - }); - - describe('renderDesignTokens', () => { - test('should call the process for all variables', async () => { - const writeFile = jest.fn(); - const readFile = jest.fn().mockReturnValue(''); - const existsFile = jest.fn().mockReturnValue(true); - const determineFileToUpdate = jest.fn().mockReturnValue(computeFileToUpdatePath('.')); - const tokenDefinitionRenderer = jest.fn().mockReturnValue('--test: #000;'); - - await renderDesignTokens(designTokens, { - writeFile, - readFile, - existsFile, - determineFileToUpdate, - tokenDefinitionRenderer - }); - - expect(designTokens.size).toBeGreaterThan(0); - expect(determineFileToUpdate).toHaveBeenCalledTimes(designTokens.size); - expect(tokenDefinitionRenderer).toHaveBeenCalledTimes(designTokens.size); - expect(writeFile).toHaveBeenCalledTimes(1); - }); - - test('should update all the files', async () => { - const writeFile = jest.fn(); - const readFile = jest.fn().mockReturnValue(''); - const existsFile = jest.fn().mockReturnValue(true); - const determineFileToUpdate = jest.fn().mockImplementation(computeFileToUpdatePath('.')); - - await renderDesignTokens(designTokens, { - writeFile, - readFile, - existsFile, - determineFileToUpdate - }); - - expect(designTokens.size).toBeGreaterThan(0); - expect(determineFileToUpdate).toHaveBeenCalledTimes(designTokens.size); - expect(writeFile).toHaveBeenCalledTimes(2); - }); - }); -}); diff --git a/packages/@o3r/design/src/core/design-token/renderers/design-token-style.renderer.ts b/packages/@o3r/design/src/core/design-token/renderers/design-token-style.renderer.ts deleted file mode 100644 index a10c69b291..0000000000 --- a/packages/@o3r/design/src/core/design-token/renderers/design-token-style.renderer.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { DesignTokenVariableSet, DesignTokenVariableStructure } from '../parsers/design-token-parser.interface'; -import { getCssTokenDefinitionRenderer } from './css/design-token-definition.renderers'; -import { getCssStyleContentUpdater } from './css/design-token-updater.renderers'; -import { existsSync, promises as fs } from 'node:fs'; -import { isAbsolute, resolve } from 'node:path'; -import type { DesignTokenRendererOptions } from './design-token.renderer.interface'; - -/** - * Retrieve the function that determines which file to update for a given token - * @param root Root path used if no base path - * @param defaultFile Default file if not requested by the Token - */ -export const computeFileToUpdatePath = (root = process.cwd(), defaultFile = 'styles.scss') => (token: DesignTokenVariableStructure) => { - if (token.extensions.o3rTargetFile) { - return isAbsolute(token.extensions.o3rTargetFile) ? token.extensions.o3rTargetFile : resolve(token.context?.basePath || root, token.extensions.o3rTargetFile); - } - - return defaultFile; -}; - -/** - * Process the parsed Design Token variables and render them according to the given options and renderers - * @param variableSet Complete list of the parsed Design Token - * @param options Parameters of the Design Token renderer - * @example Basic renderer usage - * ```typescript - * import { parseDesignTokenFile, renderDesignTokens } from '@o3r/design'; - * - * // List of parsed Design Token items - * const parsedTokenDesign = await parseDesignTokenFile('./path/to/spec.json'); - * - * // Render the CSS variables - * await renderDesignTokens(parsedTokenDesign, { logger: console }); - * ``` - */ -export const renderDesignTokens = async (variableSet: DesignTokenVariableSet, options?: DesignTokenRendererOptions) => { - const readFile = options?.readFile || ((filePath: string) => fs.readFile(filePath, {encoding: 'utf-8'})); - const writeFile = options?.writeFile || fs.writeFile; - const existsFile = options?.existsFile || existsSync; - const determineFileToUpdate = options?.determineFileToUpdate || computeFileToUpdatePath(); - const tokenDefinitionRenderer = options?.tokenDefinitionRenderer || getCssTokenDefinitionRenderer(); - const styleContentUpdater = options?.styleContentUpdater || getCssStyleContentUpdater(); - const updates = Array.from(variableSet.values()).reduce((acc, designToken) => { - const filePath = determineFileToUpdate(designToken); - const variable = tokenDefinitionRenderer(designToken, variableSet); - if (variable) { - acc[filePath] ||= []; - acc[filePath].push(variable); - } - return acc; - }, {} as Record); - - await Promise.all( - Object.entries(updates).map(async ([file, vars]) => { - const styleContent = existsFile(file) ? await readFile(file) : ''; - const newStyleContent = styleContentUpdater(vars, file, styleContent); - await writeFile(file, newStyleContent); - }) - ); -}; diff --git a/packages/@o3r/design/src/core/design-token/renderers/design-token.renderer.helpers.spec.ts b/packages/@o3r/design/src/core/design-token/renderers/design-token.renderer.helpers.spec.ts deleted file mode 100644 index 2acff6ba8f..0000000000 --- a/packages/@o3r/design/src/core/design-token/renderers/design-token.renderer.helpers.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as parser from '../parsers/design-token.parser'; -import { promises as fs } from 'node:fs'; -import { resolve } from 'node:path'; -import type { DesignTokenSpecification } from '../design-token-specification.interface'; -import { isO3rPrivateVariable } from './design-token.renderer.helpers'; -import type { DesignTokenVariableSet } from '../parsers'; - -describe('isO3rPrivateVariable' , () => { - let exampleVariable!: DesignTokenSpecification; - let designTokens!: DesignTokenVariableSet; - - beforeAll(async () => { - const file = await fs.readFile(resolve(__dirname, '../../../../testing/mocks/design-token-theme.json'), { encoding: 'utf-8' }); - exampleVariable = { document: JSON.parse(file) }; - designTokens = parser.parseDesignToken(exampleVariable); - }); - - test('should determine the variable as ignored', () => { - const token = designTokens.get('example.var3'); - expect(isO3rPrivateVariable(token)).toBe(true); - }); - - test('should determine the variable as not ignored', () => { - const token = designTokens.get('example.var1'); - expect(isO3rPrivateVariable(token)).toBe(false); - }); - - test('should determine the alias variable as not ignored', () => { - const token = designTokens.get('example.color'); - expect(isO3rPrivateVariable(token)).toBe(false); - }); -}); diff --git a/packages/@o3r/design/src/core/design-token/renderers/design-token.renderer.helpers.ts b/packages/@o3r/design/src/core/design-token/renderers/design-token.renderer.helpers.ts deleted file mode 100644 index 74cdd56d4e..0000000000 --- a/packages/@o3r/design/src/core/design-token/renderers/design-token.renderer.helpers.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { DesignTokenVariableStructure } from '../parsers'; - -/** - * Indicate that the variable is private based on the Otter extension - * @param variable Parsed Design Token - * @returns true if private variable - */ -export const isO3rPrivateVariable = (variable: DesignTokenVariableStructure) => !!variable.extensions.o3rPrivate; diff --git a/packages/@o3r/design/src/core/design-token/renderers/design-token.renderer.interface.ts b/packages/@o3r/design/src/core/design-token/renderers/design-token.renderer.interface.ts deleted file mode 100644 index 4ae2f63f59..0000000000 --- a/packages/@o3r/design/src/core/design-token/renderers/design-token.renderer.interface.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { DesignTokenVariableStructure } from '../parsers'; -import type { Logger } from '@o3r/core'; - -/** - * Updater function to append the rendered variable into a file content - * @param variables List of rendered variables to append - * @param file Path to the file to edit - * @param styleContent current content where to append the variables - * @returns new content - */ -export type DesignContentFileUpdater = (variables: string[], file: string, styleContent?: string) => string; - -/** - * Render the Design Token variable - * @param tokenStructure Parsed Design Token - * @param variableSet Complete list of the parsed Design Token - */ -export type TokenDefinitionRenderer = (tokenStructure: DesignTokenVariableStructure, variableSet: Map) => string | undefined; - -/** - * Options of the Design Token Renderer value - */ -export interface DesignTokenRendererOptions { - /** Custom Style Content updated function */ - styleContentUpdater?: DesignContentFileUpdater; - - /** - * Custom function to determine the file to update for a given Design Token - * @param token Design Token Variable - */ - determineFileToUpdate?: (token: DesignTokenVariableStructure) => string; - - /** Custom function to render the Design Token variable */ - tokenDefinitionRenderer?: TokenDefinitionRenderer; - - /** - * Custom function to read a file required by the token renderer - * @default {@see fs.promises.readFile} - * @param filePath Path to the file to read - */ - readFile?: (filePath: string) => string | Promise; - - /** - * Custom function to determine if file required by the token renderer exists - * @default {@see fs.existsSync} - * @param filePath Path to the file to check - * @returns - */ - existsFile?: (filePath: string) => boolean; - - /** - * Custom function to write a file required by the token renderer - * @default {@see fs.promise.writeFile} - * @param filePath Path to the file to write - * @param content Content to write - */ - writeFile?: (filePath: string, content: string) => void | Promise; - /** - * Custom logger - * Nothing will be logged if not provided - */ - logger?: Logger; -} diff --git a/packages/@o3r/design/src/core/design-token/renderers/index.ts b/packages/@o3r/design/src/core/design-token/renderers/index.ts deleted file mode 100644 index 5a7f222047..0000000000 --- a/packages/@o3r/design/src/core/design-token/renderers/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './design-token-style.renderer'; -export * from './design-token.renderer.interface'; -export * from './design-token-style.renderer'; -export * from './metadata/index'; -export * from './css/index'; -export * from './sass/index'; diff --git a/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-definition.renderers.spec.ts b/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-definition.renderers.spec.ts deleted file mode 100644 index 8b84f6a20f..0000000000 --- a/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-definition.renderers.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import * as parser from '../../parsers/design-token.parser'; -import { promises as fs } from 'node:fs'; -import { resolve } from 'node:path'; -import type { DesignTokenSpecification } from '../../design-token-specification.interface'; -import type { DesignTokenVariableSet } from '../../parsers'; -import { getMetadataTokenDefinitionRenderer } from './design-token-definition.renderers'; - -describe('getMetadataTokenDefinitionRenderer', () => { - let exampleVariable!: DesignTokenSpecification; - let designTokens!: DesignTokenVariableSet; - - beforeAll(async () => { - const file = await fs.readFile(resolve(__dirname, '../../../../../testing/mocks/design-token-theme.json'), { encoding: 'utf-8' }); - exampleVariable = { document: JSON.parse(file) }; - designTokens = parser.parseDesignToken(exampleVariable); - }); - - test('should rely on given tokenValueRenderer', () => { - const tokenValueRenderer = jest.fn().mockReturnValue(JSON.stringify({name: 'test-var', value: 'test-value'})); - const renderer = getMetadataTokenDefinitionRenderer({ tokenValueRenderer }); - const variable = designTokens.get('example.var1'); - - const result = renderer(variable, designTokens); - expect(variable).toBeDefined(); - expect(tokenValueRenderer).toHaveBeenCalledTimes(1); - expect(result).toBeDefined(); - expect(result).toContain('test-var'); - expect(result).toContain('test-value'); - }); - - test('should render valid JSON object', () => { - const renderer = getMetadataTokenDefinitionRenderer(); - const variable = designTokens.get('example.var1'); - - const result = renderer(variable, designTokens); - expect(variable).toBeDefined(); - expect(result).toBeDefined(); - expect(() => JSON.parse(`{${result}}`)).not.toThrow(); - }); -}); diff --git a/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-definition.renderers.ts b/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-definition.renderers.ts deleted file mode 100644 index fdbbf64252..0000000000 --- a/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-definition.renderers.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { DesignTokenVariableStructure, TokenKeyRenderer, TokenValueRenderer } from '../../parsers/design-token-parser.interface'; -import type { TokenDefinitionRenderer } from '../design-token.renderer.interface'; -import { getMetadataTokenValueRenderer } from './design-token-value.renderers'; -import type { CssVariable } from '@o3r/styling'; - -/** Options for {@link getMetadataTokenDefinitionRenderer} */ -export interface MetadataTokenDefinitionRendererOptions { - /** Custom Design Token value renderer */ - tokenValueRenderer?: TokenValueRenderer; - - /** - * Renderer the name of the CSS Variable (without initial --) - */ - tokenVariableNameRenderer?: TokenKeyRenderer; -} - -/** - * Retrieve the Design Token Variable renderer for Metadata - * @param options - * @example Customize metadata renderer - * ```typescript - * const getCustomMetadataTokenValueRenderer = (options?: MetadataTokenValueRendererOptions): TokenValueRenderer => { - * const defaultMetadataRender = getMetadataTokenValueRenderer(options); - * return (variable, variableSet) => { - * const defaultMetadataObj = JSON.parse(defaultMetadataRender(variable, variableSet)); - * // Add custom field - * defaultMetadataObj.myField = 'custom value'; - * return JSON.stringify(defaultMetadataObj); - * }; - * }; - * - * // List of Design Token item parsed - * // List of parsed Design Token items - * - * const tokenValueRenderer = getCustomMetadataTokenValueRenderer(); - * - * const metadataTokenDefinitionRenderer = getMetadataTokenDefinitionRenderer({ tokenValueRenderer }); - * - * // Render the Metadata file - * await renderDesignTokens(parsedTokenDesign, { tokenDefinitionRenderer: lessTokenDefinitionRenderer }); - * ``` - */ -export const getMetadataTokenDefinitionRenderer = (options?: MetadataTokenDefinitionRendererOptions): TokenDefinitionRenderer => { - const tokenVariableNameRenderer = options?.tokenVariableNameRenderer; - const tokenValueRenderer = options?.tokenValueRenderer || getMetadataTokenValueRenderer({ tokenVariableNameRenderer }); - - const renderer = (variable: DesignTokenVariableStructure, variableSet: Map) => { - const variableValue = tokenValueRenderer(variable, variableSet); - return `"${(JSON.parse(variableValue) as CssVariable).name}": ${variableValue}`; - }; - return renderer; -}; diff --git a/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-updater.renderers.spec.ts b/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-updater.renderers.spec.ts deleted file mode 100644 index c7cfe602ed..0000000000 --- a/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-updater.renderers.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import * as parser from '../../parsers/design-token.parser'; -import { promises as fs } from 'node:fs'; -import { resolve } from 'node:path'; -import type { DesignTokenSpecification } from '../../design-token-specification.interface'; -import type { DesignTokenVariableSet } from '../../parsers'; -import { getMetadataStyleContentUpdater } from './design-token-updater.renderers'; - -describe('getMetadataStyleContentUpdater', () => { - let exampleVariable!: DesignTokenSpecification; - let designTokens!: DesignTokenVariableSet; - - beforeAll(async () => { - const file = await fs.readFile(resolve(__dirname, '../../../../../testing/mocks/design-token-theme.json'), { encoding: 'utf-8' }); - exampleVariable = { document: JSON.parse(file) }; - designTokens = parser.parseDesignToken(exampleVariable); - }); - - test('should render valid JSON object', () => { - const renderer = getMetadataStyleContentUpdater(); - const variable = designTokens.get('example.var1'); - - const variables = ['"var1": {"value": "#000"}', '"var2": {"value": "#fff"}']; - const result = renderer(variables, '/'); - - expect(variable).toBeDefined(); - expect(result).toBeDefined(); - expect(() => JSON.parse(result)).not.toThrow(); - expect(result.replace(/[\n\r ]*/g, '')).toContain(variables[0].replace(/[\n\r ]*/g, '')); - expect(result.replace(/[\n\r ]*/g, '')).toContain(variables[1].replace(/[\n\r ]*/g, '')); - }); -}); diff --git a/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-updater.renderers.ts b/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-updater.renderers.ts deleted file mode 100644 index c749162ea9..0000000000 --- a/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-updater.renderers.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { DesignContentFileUpdater } from '../design-token.renderer.interface'; - -/** - * Retrieve a Content Updater function for Metadata generator - */ -export const getMetadataStyleContentUpdater = (): DesignContentFileUpdater => { - return (variables: string[]) => { - return JSON.stringify(JSON.parse(`{"variables":{${variables.join(',')}}}`), null, 2); - }; -}; diff --git a/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-value.renderers.spec.ts b/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-value.renderers.spec.ts deleted file mode 100644 index 7e54fa5036..0000000000 --- a/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-value.renderers.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import * as parser from '../../parsers/design-token.parser'; -import { promises as fs } from 'node:fs'; -import { resolve } from 'node:path'; -import type { DesignTokenSpecification } from '../../design-token-specification.interface'; -import type { DesignTokenVariableSet } from '../../parsers'; -import { getMetadataTokenValueRenderer } from './design-token-value.renderers'; - -describe('getMetadataTokenValueRenderer', () => { - let exampleVariable!: DesignTokenSpecification; - let designTokens!: DesignTokenVariableSet; - - beforeAll(async () => { - const file = await fs.readFile(resolve(__dirname, '../../../../../testing/mocks/design-token-theme.json'), { encoding: 'utf-8' }); - exampleVariable = { document: JSON.parse(file) }; - designTokens = parser.parseDesignToken(exampleVariable); - }); - - test('should rely on given cssValueRenderer', () => { - const cssValueRenderer = jest.fn().mockReturnValue(JSON.stringify({ name: 'test-var', value: 'test-value' })); - const renderer = getMetadataTokenValueRenderer({ cssValueRenderer }); - const variable = designTokens.get('example.var1'); - - const result = renderer(variable, designTokens); - expect(variable).toBeDefined(); - expect(cssValueRenderer).toHaveBeenCalledTimes(1); - expect(result).toBeDefined(); - expect(result).toContain('test-var'); - expect(result).toContain('test-value'); - }); - - test('should render valid JSON object', () => { - const renderer = getMetadataTokenValueRenderer(); - const variable = designTokens.get('example.var1'); - - const result = renderer(variable, designTokens); - expect(variable).toBeDefined(); - expect(result).toBeDefined(); - expect(() => JSON.parse(result)).not.toThrow(); - }); -}); diff --git a/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-value.renderers.ts b/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-value.renderers.ts deleted file mode 100644 index 3e80c98cc1..0000000000 --- a/packages/@o3r/design/src/core/design-token/renderers/metadata/design-token-value.renderers.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { DesignTokenVariableStructure, TokenKeyRenderer, TokenValueRenderer } from '../../parsers/design-token-parser.interface'; -import type { CssVariable } from '@o3r/styling'; -import { getCssTokenValueRenderer } from '../css'; - -/** Options for {@link getMetadataTokenValueRenderer} */ -export interface MetadataTokenValueRendererOptions { - /** - * Custom CSS Design Token value renderer - */ - cssValueRenderer?: TokenValueRenderer; - - /** - * Renderer the name of the CSS Variable (without initial --) - */ - tokenVariableNameRenderer?: TokenKeyRenderer; -} - -/** - * Retrieve the Design Token value renderer - * @param options - */ -export const getMetadataTokenValueRenderer = (options?: MetadataTokenValueRendererOptions): TokenValueRenderer => { - const tokenVariableNameRenderer = options?.tokenVariableNameRenderer; - const cssValueRenderer = options?.cssValueRenderer || getCssTokenValueRenderer({tokenVariableNameRenderer}); - - const renderer = (variable: DesignTokenVariableStructure, variableSet: Map) => { - const cssType = variable.getType(variableSet); - const variableValue: CssVariable = { - name: variable.getKey(tokenVariableNameRenderer), - defaultValue: cssValueRenderer(variable, variableSet), - description: variable.description, - references: variable.getReferencesNode(variableSet).map((node) => JSON.parse(renderer(node, variableSet))), - type: cssType !== 'color' ? 'string' : 'color', - ...variable.extensions.o3rMetadata - }; - - return JSON.stringify(variableValue); - }; - return renderer; -}; diff --git a/packages/@o3r/design/src/core/design-token/renderers/metadata/index.ts b/packages/@o3r/design/src/core/design-token/renderers/metadata/index.ts deleted file mode 100644 index 026f171acd..0000000000 --- a/packages/@o3r/design/src/core/design-token/renderers/metadata/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './design-token-definition.renderers'; -export * from './design-token-value.renderers'; -export * from './design-token-updater.renderers'; diff --git a/packages/@o3r/design/src/core/design-token/renderers/sass/design-token-definition.renderers.spec.ts b/packages/@o3r/design/src/core/design-token/renderers/sass/design-token-definition.renderers.spec.ts deleted file mode 100644 index b42691cadf..0000000000 --- a/packages/@o3r/design/src/core/design-token/renderers/sass/design-token-definition.renderers.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import * as parser from '../../parsers/design-token.parser'; -import { promises as fs } from 'node:fs'; -import { resolve } from 'node:path'; -import type { DesignTokenSpecification } from '../../design-token-specification.interface'; -import type { DesignTokenVariableSet, TokenKeyRenderer } from '../../parsers'; -import { getSassTokenDefinitionRenderer, tokenVariableNameSassRenderer } from './design-token-definition.renderers'; - -describe('getSassTokenDefinitionRenderer', () => { - let exampleVariable!: DesignTokenSpecification; - let designTokens!: DesignTokenVariableSet; - - beforeAll(async () => { - const file = await fs.readFile(resolve(__dirname, '../../../../../testing/mocks/design-token-theme.json'), { encoding: 'utf-8' }); - exampleVariable = { document: JSON.parse(file) }; - designTokens = parser.parseDesignToken(exampleVariable); - }); - - test('should rely on given tokenValueRenderer', () => { - const tokenValueRenderer = jest.fn().mockReturnValue('test-value'); - const renderer = getSassTokenDefinitionRenderer({ tokenValueRenderer }); - const variable = designTokens.get('example.var1'); - - const result = renderer(variable, designTokens); - expect(variable).toBeDefined(); - expect(tokenValueRenderer).toHaveBeenCalledTimes(1); - expect(result).toBeDefined(); - expect(result).toBe('$exampleVar1: test-value;'); - }); - - test('should prefix private variable', () => { - const tokenVariableNameRenderer: TokenKeyRenderer = (v) => '_' + tokenVariableNameSassRenderer(v); - - const options = { tokenVariableNameRenderer }; - const tokenValueRenderer = jest.spyOn(options, 'tokenVariableNameRenderer'); - const renderer = getSassTokenDefinitionRenderer(options); - const variable = designTokens.get('example.var1'); - - const result = renderer(variable, designTokens); - expect(variable).toBeDefined(); - expect(tokenValueRenderer).toHaveBeenCalledTimes(1); - expect(result).toBeDefined(); - expect(result).toBe('$_exampleVar1: #000;'); - }); -}); diff --git a/packages/@o3r/design/src/core/design-token/renderers/sass/design-token-definition.renderers.ts b/packages/@o3r/design/src/core/design-token/renderers/sass/design-token-definition.renderers.ts deleted file mode 100644 index cbeb19ee6f..0000000000 --- a/packages/@o3r/design/src/core/design-token/renderers/sass/design-token-definition.renderers.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { DesignTokenVariableStructure, TokenKeyRenderer, TokenValueRenderer } from '../../parsers/design-token-parser.interface'; -import type { TokenDefinitionRenderer } from '../design-token.renderer.interface'; -import { getCssTokenValueRenderer } from '../css/design-token-value.renderers'; - -export interface SassTokenDefinitionRendererOptions { - - /** Custom Design Token value renderer */ - tokenValueRenderer?: TokenValueRenderer; - - /** - * Renderer the name of the Sass Variable (without initial $) - * @default {@see tokenVariableNameSassRenderer} - */ - tokenVariableNameRenderer?: TokenKeyRenderer; -} - -/** - * Default Sass variable name renderer - * @param variable - */ -export const tokenVariableNameSassRenderer: TokenKeyRenderer = (variable) => { - const tokens = variable.getKey().split('-'); - return tokens[0] + tokens.slice(1).map((token) => token.charAt(0).toUpperCase() + token.slice(1)).join(''); -}; - -/** - * Retrieve the Design Token Variable renderer for Sass - * @param options - * @returns - */ -export const getSassTokenDefinitionRenderer = (options?: SassTokenDefinitionRendererOptions): TokenDefinitionRenderer => { - const tokenValueRenderer = options?.tokenValueRenderer || getCssTokenValueRenderer(); - const keyRenderer = options?.tokenVariableNameRenderer || tokenVariableNameSassRenderer; - - const renderer = (variable: DesignTokenVariableStructure, variableSet: Map) => { - return `$${variable.getKey(keyRenderer)}: ${ tokenValueRenderer(variable, variableSet) };`; - }; - return renderer; -}; diff --git a/packages/@o3r/design/src/core/design-token/renderers/sass/index.ts b/packages/@o3r/design/src/core/design-token/renderers/sass/index.ts deleted file mode 100644 index d58497e4e1..0000000000 --- a/packages/@o3r/design/src/core/design-token/renderers/sass/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './design-token-definition.renderers'; diff --git a/packages/@o3r/design/src/core/index.ts b/packages/@o3r/design/src/core/index.ts deleted file mode 100644 index bcbb902d40..0000000000 --- a/packages/@o3r/design/src/core/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './design-token/index'; diff --git a/packages/@o3r/design/src/public_api.ts b/packages/@o3r/design/src/public_api.ts deleted file mode 100644 index 65c514e925..0000000000 --- a/packages/@o3r/design/src/public_api.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './core/index'; diff --git a/packages/@o3r/design/testing/mocks/design-token-theme.json b/packages/@o3r/design/testing/mocks/design-token-theme.json deleted file mode 100644 index adae773868..0000000000 --- a/packages/@o3r/design/testing/mocks/design-token-theme.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "$schema": "../../schemas/design-token.schema.json", - "example": { - "var1": { - "$type": "color", - "$value": "#000" - }, - "var-important": { - "$extensions": { - "o3rImportant": true - }, - "$type": "color", - "$value": "#000" - }, - "var3": { - "$extensions": { - "o3rPrivate": true - }, - "$type": "color", - "$value": "#000" - }, - "color": { - "$description": "test color", - "$value": "{example.var1}" - }, - "color2": { - "$description": "test color with default value", - "$value": "{example.var3}" - }, - "test.var2": { - "$description": "my var2", - "$type": "color", - "$value": "#fff" - }, - "test": { - "height": { - "$extensions": { - "o3rPrivate": true - }, - "$value": 2.3, - "$type": "number" - }, - "width": { - "$value": "{example.test.height}" - }, - "border": { - "$type": "border", - "$value": { - "color": "{example.color}", - "style": "dashed", - "width": "{example.test.width}" - } - } - } - } -} diff --git a/packages/@o3r/design/testing/setup-jest.ts b/packages/@o3r/design/testing/setup-jest.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/@o3r/design/tsconfig.build.json b/packages/@o3r/design/tsconfig.build.json deleted file mode 100644 index 17d0978680..0000000000 --- a/packages/@o3r/design/tsconfig.build.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "../../../tsconfig.build", - "compilerOptions": { - "incremental": true, - "composite": true, - "module": "CommonJS", - "outDir": "./dist", - "rootDir": ".", - "tsBuildInfoFile": "build/.tsbuildinfo" - }, - "include": [ - "src/**/*.ts", - "cli/**/*.cts" - ], - "exclude": ["**/*.spec.ts"] -} diff --git a/packages/@o3r/design/tsconfig.builders.json b/packages/@o3r/design/tsconfig.builders.json deleted file mode 100644 index 196d2a59a1..0000000000 --- a/packages/@o3r/design/tsconfig.builders.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "extends": "../../../tsconfig.build", - "compilerOptions": { - "incremental": true, - "composite": true, - "outDir": "./dist", - "module": "CommonJS", - "rootDir": ".", - "tsBuildInfoFile": "build/.tsbuildinfo.builders" - }, - "include": [ - "builders/**/*.ts", - "schematics/**/*.ts" - ], - "exclude": [ - "**/*.spec.ts", - "builders/**/templates/**", - "schematics/**/templates/**" - ], - "references": [ - { - "path": "./tsconfig.build.json" - } - ] -} diff --git a/packages/@o3r/design/tsconfig.doc.json b/packages/@o3r/design/tsconfig.doc.json deleted file mode 100644 index 6ee270fcd3..0000000000 --- a/packages/@o3r/design/tsconfig.doc.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../tsconfig.doc", - "exclude": [ - "**/*.spec.ts", - "**/*reducer.ts", - "**/*.fixture.ts" - ], - "include": [ - "src/**/*.ts" - ] -} diff --git a/packages/@o3r/design/tsconfig.eslint.json b/packages/@o3r/design/tsconfig.eslint.json deleted file mode 100644 index ec5acad834..0000000000 --- a/packages/@o3r/design/tsconfig.eslint.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.build", - "include": [ - ".eslintrc.js", - "jest.config.js", - "testing/*", - "tooling/**/*.js", - "builders/**/index.js" - ] -} diff --git a/packages/@o3r/design/tsconfig.json b/packages/@o3r/design/tsconfig.json deleted file mode 100644 index d5cee07d89..0000000000 --- a/packages/@o3r/design/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -/* For IDE usage only */ -{ - "extends": "../../../tsconfig.base", - "references": [ - { - "path": "./tsconfig.build.json" - }, - { - "path": "./tsconfig.builders.json" - }, - { - "path": "./tsconfig.spec.json" - } - ] -} diff --git a/packages/@o3r/design/tsconfig.spec.json b/packages/@o3r/design/tsconfig.spec.json deleted file mode 100644 index ffc1f54a1d..0000000000 --- a/packages/@o3r/design/tsconfig.spec.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "extends": "../../../tsconfig.jest", - "compilerOptions": { - "resolveJsonModule": true, - "composite": true, - "rootDir": ".", - }, - "include": [ - "./src/**/*.spec.ts", - "./schematics/**/*.spec.ts" - ], - "exclude": [], - "references": [ - { - "path": "./tsconfig.build.json" - }, - { - "path": "./tsconfig.builders.json" - } - ] -} diff --git a/packages/@o3r/styling/package.json b/packages/@o3r/styling/package.json index a574b8bebd..7c76386550 100644 --- a/packages/@o3r/styling/package.json +++ b/packages/@o3r/styling/package.json @@ -26,10 +26,6 @@ }, "./schemas/*.json": { "default": "./schemas/*.json" - }, - "./builders/*/helpers": { - "default": "./builders/*/helpers/index.js", - "types": "./builders/*/helpers/index.d.ts" } }, "peerDependencies": { diff --git a/tsconfig.base.json b/tsconfig.base.json index 64e0e92da1..af52a8ea17 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -95,9 +95,6 @@ "@o3r/core": [ "packages/@o3r/core/src/public_api" ], - "@o3r/design": [ - "packages/@o3r/design/src/public_api" - ], "@o3r/dev-tools": [ "packages/@o3r/dev-tools/src/public_api" ], @@ -146,12 +143,6 @@ "@o3r/styling": [ "packages/@o3r/styling/src/public_api" ], - "@o3r/styling/schemas/*": [ - "packages/@o3r/styling/schemas/*" - ], - "@o3r/styling/builders/*/helpers": [ - "packages/@o3r/styling/builders/*/helpers/index" - ], "@o3r/test-helpers": [ "packages/@o3r/test-helpers/src/public_api" ], diff --git a/tsconfig.build.json b/tsconfig.build.json index 2723c224d0..f226ce6938 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -23,7 +23,6 @@ "@o3r/components": ["packages/@o3r/components/dist", "packages/@o3r/components/src/public_api"], "@o3r/configuration": ["packages/@o3r/configuration/dist", "packages/@o3r/configuration/src/public_api"], "@o3r/core": ["packages/@o3r/core/dist", "packages/@o3r/core/src/public_api"], - "@o3r/design": ["packages/@o3r/design/dist", "packages/@o3r/design/src/public_api"], "@o3r/dev-tools": ["packages/@o3r/dev-tools/dist", "packages/@o3r/dev-tools/src/public_api"], "@o3r/dynamic-content": ["packages/@o3r/dynamic-content/dist", "packages/@o3r/dynamic-content/src/public_api"], "@o3r/eslint-config-otter": ["packages/@o3r/eslint-config-otter"], @@ -41,7 +40,6 @@ "@o3r/storybook": ["packages/@o3r/storybook/dist", "packages/@o3r/storybook/src/public_api"], "@o3r/stylelint-plugin": ["packages/@o3r/stylelint-plugin/dist", "packages/@o3r/stylelint-plugin/src/public_api"], "@o3r/styling": ["packages/@o3r/styling/dist", "packages/@o3r/styling/src/public_api"], - "@o3r/styling/builders/*/helpers": ["packages/@o3r/styling/dist/builders/*/helpers/index", "packages/@o3r/styling/builders/*/helpers"], "@o3r/test-helpers": ["packages/@o3r/test-helpers/dist", "packages/@o3r/test-helpers/src/public_api"], "@o3r/test-helpers/setup-jest": ["packages/@o3r/test-helpers/dist/src/setup-jest", "packages/@o3r/test-helpers/src/setup-jest"], "@o3r/testing": ["packages/@o3r/testing/dist", "packages/@o3r/testing/src/public_api"], diff --git a/yarn.lock b/yarn.lock index 3a4f3611be..0f1f9920e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7292,92 +7292,6 @@ __metadata: languageName: unknown linkType: soft -"@o3r/design@workspace:packages/@o3r/design": - version: 0.0.0-use.local - resolution: "@o3r/design@workspace:packages/@o3r/design" - dependencies: - "@angular-devkit/architect": "npm:~0.1602.0" - "@angular-devkit/build-angular": "npm:~16.2.0" - "@angular-devkit/core": "npm:~16.2.0" - "@angular-devkit/schematics": "npm:~16.2.0" - "@angular-eslint/eslint-plugin": "npm:~16.3.0" - "@angular/cli": "npm:~16.2.0" - "@angular/common": "npm:~16.2.0" - "@angular/compiler": "npm:~16.2.0" - "@angular/compiler-cli": "npm:~16.2.0" - "@angular/core": "npm:~16.2.0" - "@angular/platform-browser": "npm:~16.2.0" - "@angular/platform-browser-dynamic": "npm:~16.2.0" - "@babel/core": "npm:~7.23.0" - "@babel/preset-typescript": "npm:~7.23.0" - "@compodoc/compodoc": "npm:^1.1.19" - "@nx/eslint-plugin": "npm:~16.10.0" - "@nx/jest": "npm:~16.10.0" - "@nx/js": "npm:~16.10.0" - "@nx/linter": "npm:~16.10.0" - "@o3r/build-helpers": "workspace:^" - "@o3r/core": "workspace:^" - "@o3r/eslint-plugin": "workspace:^" - "@o3r/schematics": "workspace:^" - "@o3r/styling": "workspace:^" - "@o3r/test-helpers": "workspace:^" - "@schematics/angular": "npm:~16.2.0" - "@types/jest": "npm:~29.5.2" - "@types/minimist": "npm:^1.2.2" - "@types/node": "npm:^18.0.0" - "@types/semver": "npm:^7.3.13" - "@typescript-eslint/eslint-plugin": "npm:^5.60.1" - "@typescript-eslint/parser": "npm:^5.60.1" - chokidar: "npm:^3.5.2" - cpy-cli: "npm:^4.2.0" - eslint: "npm:^8.42.0" - eslint-import-resolver-node: "npm:^0.3.4" - eslint-plugin-jest: "npm:~27.6.0" - eslint-plugin-jsdoc: "npm:~46.10.0" - eslint-plugin-prefer-arrow: "npm:~1.2.3" - eslint-plugin-unicorn: "npm:^47.0.0" - globby: "npm:^11.1.0" - jest: "npm:~29.7.0" - jest-junit: "npm:~16.0.0" - jsonc-eslint-parser: "npm:~2.4.0" - jsonschema: "npm:~1.4.1" - minimatch: "npm:~9.0.3" - minimist: "npm:^1.2.6" - nx: "npm:~16.10.0" - rxjs: "npm:^7.8.1" - sass: "npm:~1.69.0" - ts-jest: "npm:~29.1.1" - ts-node: "npm:~10.9.1" - tslib: "npm:^2.5.3" - type-fest: "npm:^3.12.0" - typescript: "npm:~5.1.6" - zone.js: "npm:~0.13.1" - peerDependencies: - "@o3r/core": "workspace:^" - "@o3r/schematics": "workspace:^" - "@o3r/styling": "workspace:^" - chokidar: ^3.5.2 - globby: ^11.1.0 - minimatch: ~9.0.3 - sass: ~1.69.0 - peerDependenciesMeta: - "@o3r/core": - optional: true - "@o3r/schematics": - optional: true - "@o3r/styling": - optional: true - chokidar: - optional: true - globby: - optional: true - sass: - optional: true - bin: - o3r-css-from-design-token: ./dist/cli/generate-css-from-design-token.cli.cjs - languageName: unknown - linkType: soft - "@o3r/dev-tools@workspace:^, @o3r/dev-tools@workspace:packages/@o3r/dev-tools": version: 0.0.0-use.local resolution: "@o3r/dev-tools@workspace:packages/@o3r/dev-tools"