Skip to content

Commit

Permalink
Feat(exporters): Introduce variables-scss exporter #DS-1412
Browse files Browse the repository at this point in the history
  • Loading branch information
curdaj authored and literat committed Aug 9, 2024
1 parent 2cd0f17 commit b6bf2f8
Show file tree
Hide file tree
Showing 19 changed files with 694 additions and 1 deletion.
2 changes: 2 additions & 0 deletions exporters/variables-scss/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Generated files used by Supernova
generated
46 changes: 46 additions & 0 deletions exporters/variables-scss/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
module.exports = {
extends: [
'../../.eslintrc',
'plugin:@typescript-eslint/recommended',
'prettier',
'plugin:prettier/recommended',
'@lmc-eu/eslint-config-jest',
],

parser: '@typescript-eslint/parser', // the TypeScript parser we installed earlier

parserOptions: {
ecmaVersion: 'latest',
project: './tsconfig.eslint.json',
},

settings: {
'import/resolver': {
node: {
extensions: ['.js', '.ts'],
},
},
},

plugins: ['promise', '@typescript-eslint', 'prettier'],
rules: {
// disable for `scripts` and `config`
'@typescript-eslint/no-var-requires': 'off',
// allow ++ in for loops
'no-plusplus': ['error', { allowForLoopAfterthoughts: true }],
// disabled due to typescript
'no-shadow': 'off',
'@typescript-eslint/no-shadow': ['error', { allow: ['resolve', 'reject', 'done', 'next', 'error'] }],
// disabled due to typescript
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': 'warn',
// We are using typescript, disable jsdoc rules
'jsdoc/require-jsdoc': 'off',
'jsdoc/require-returns': 'off',
'jsdoc/require-param-type': 'off',
// allow reassign in properties
'no-param-reassign': ['warn', { props: false }],
// support monorepos
'import/no-extraneous-dependencies': ['error', { packageDir: ['./', '../../'] }],
},
};
2 changes: 2 additions & 0 deletions exporters/variables-scss/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.build
.coverage
Empty file.
44 changes: 44 additions & 0 deletions exporters/variables-scss/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Exporter Spirit Variables SCSS

[Supernova][supernova-studio] SCSS exporter made for Spirit Design System developed by [Alma Career (formerly LMC)][alma-career].

## Token operations

This exported does several operations with tokens:

- All token groups except [Typography](#typography) are processed using a simple generate function.
- The first step is sorting. Measures are sorted by number in the token name, Generic Tokens (Other) by value, and the rest by its name.
- Next, each token is grouped and its value is prepared to print. Grouping is made using actual groups in Supernova and if these are not present, a common name prefix is used. Separate token values are printed as separate SCSS variables.
- Groups are used for printing SCSS maps with references to separate tokens and pluralized names. Borders groups are skipped and colors have their group name suffixed. These maps are printed into SCSS.
- Shadows are grouped if same name.
- If Gradient names start with `gradients/gradient`, they are not used from Figma, but from Supernova

### Typography

As typography in Figma and Supernova are stored in named text style groups, these groups are used to generate SCSS maps with all the values from Supernova. They are grouped by breakpoints.
⚠️ We do not generate `link` typography tokens (styles that include `-link` in their name).

#### Ebony Font Weight Exception

Font Family Ebony has a different font weight mapping in Figma and in Adobe Fonts. To match these we set its own font weight numeric-name conversion.

### Sorting

Tokens are sorted alphabetically by origin (Figma) name or by name (Supernova). Except Measures - sorted by name number and Other - sorted by value.

## Outputs:

- \_borders.scss
- \_colors.scss
- \_gradients.scss
- \_measures.scss
- \_other.scss
- \_radii.scss
- \_shadows.scss
- \_typography.scss
- index.scss

The index file contains SCSS forwards of all other outputs.

[supernova-studio]: https://github.com/Supernova-Studio
[alma-career]: https://github.com/lmc-eu
9 changes: 9 additions & 0 deletions exporters/variables-scss/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[
{
"key": "generateDisclaimer",
"type": "boolean",
"default": false,
"title": "Show Generated File Disclaimer",
"description": "When enabled, a disclaimer showing the fact that the file was generated automatically and should not be changed manually will appear in all style styles"
}
]
3 changes: 3 additions & 0 deletions exporters/variables-scss/config.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"generateDisclaimer": true
}
9 changes: 9 additions & 0 deletions exporters/variables-scss/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Main configuration of the exporter - type interface.
* Default values for it can be set through `config.json`
* Users can override the behavior when creating the pipelines or by creating `config.local.json` file specifying actual values.
*/
export type ExporterConfiguration = {
/** When enabled, generate a disclaimer showing the fact that the file was generated automatically and should not be changed manually will appear in all files */
generateDisclaimer: boolean;
};
13 changes: 13 additions & 0 deletions exporters/variables-scss/exporter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"id": "almacareer.exporter-spirit-variables-scss",
"name": "Spirit Varibles SCSS Exporter",
"description": "Spirit SCSS Exporter",
"author": "Spirit Team",
"organization": "Alma Career",
"version": "1.0.0",
"usesBrands": true,
"usesThemes": true,
"executable": "/generated/exporter.js",
"engine": "latest",
"tags": ["spirit", "scss", "variables", "exporter"]
}
3 changes: 3 additions & 0 deletions exporters/variables-scss/generated/exporter.js

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions exporters/variables-scss/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const config = {
// The root directory that Jest should scan for tests and modules within.
// https://jestjs.io/docs/configuration#rootdir-string
rootDir: './',

// This option tells Jest that all imported modules in your tests should be mocked automatically.
// https://jestjs.io/docs/configuration#automock-boolean
automock: false,

// Indicates whether each individual test should be reported during the run.
// https://jestjs.io/docs/configuration#verbose-boolean
verbose: false,

// A map from regular expressions to paths to transformers
// https://jestjs.io/docs/configuration#transform-objectstring-pathtotransformer--pathtotransformer-object
transform: {
'^.+\\.(t|j)s?$': ['<rootDir>/../../node_modules/@swc/jest'],
},

// The test environment that will be used for testing.
// https://jestjs.io/docs/configuration#testenvironment-string
// testEnvironment: 'jsdom',

// An array of regexp pattern strings that are matched against all test paths before executing the test
// https://jestjs.io/docs/configuration#testpathignorepatterns-arraystring
testPathIgnorePatterns: ['<rootDir>/dist/', '<rootDir>/node_modules/', '.*__tests__/.*DataProvider.ts'],

// The directory where Jest should output its coverage files.
// https://jestjs.io/docs/configuration#coveragedirectory-string
coverageDirectory: './.coverage',

// An array of glob patterns indicating a set of files for which coverage information should be collected.
// https://jestjs.io/docs/configuration#collectcoveragefrom-array
collectCoverageFrom: ['<rootDir>/src/**/*.{js,ts}', '!<rootDir>/src/**/*.d.ts'],

// An array of regexp pattern strings that are matched against all file paths before executing the test.
// https://jestjs.io/docs/configuration#coveragepathignorepatterns-arraystring
coveragePathIgnorePatterns: ['__fixtures__'],

// A list of reporter names that Jest uses when writing coverage reports. Any istanbul reporter can be used.
// https://jestjs.io/docs/configuration#coveragereporters-arraystring--string-options
coverageReporters: ['text', 'text-summary', ['lcov', { projectRoot: '../../' }]],

// A list of paths to modules that run some code to configure or set up the testing framework before each test.
// https://jestjs.io/docs/configuration#setupfilesafterenv-array
// setupFilesAfterEnv: ['@testing-library/jest-dom'],
};

module.exports = config;
31 changes: 31 additions & 0 deletions exporters/variables-scss/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "@almacareer/spirit-exporters-variables-scss",
"version": "1.0.0",
"description": "Spirit SCSS Exporter for Supernova",
"author": "Spirit Team",
"license": "MIT",
"scripts": {
"dev": "vite watch",
"build": "vite build",
"lint": "eslint ./",
"lint:fix": "yarn lint --fix",
"test": "npm-run-all lint test:unit:coverage types",
"test:unit": "jest || true",
"test:unit:watch": "yarn test:unit --watchAll",
"test:unit:coverage": "yarn test:unit --coverage",
"types": "true"
},
"dependencies": {
"@supernovaio/export-helpers": "^1.0.9",
"@supernovaio/sdk-exporters": "^2.0.17"
},
"devDependencies": {
"@swc/core": "1.6.5",
"@swc/jest": "0.2.36",
"eslint": "8.57.0",
"jest": "29.7.0",
"npm-run-all": "4.1.5",
"typescript": "5.5.2",
"vite": "5.3.2"
}
}
29 changes: 29 additions & 0 deletions exporters/variables-scss/src/content/token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ColorFormat, CSSHelper, NamingHelper, StringCase } from '@supernovaio/export-helpers';
import { ColorToken, Token, TokenGroup } from '@supernovaio/sdk-exporters';

export function colorTokenToCSS(
token: ColorToken,
mappedTokens: Map<string, Token>,
tokenGroups: Array<TokenGroup>,
): string {
// First creating the name of the token, using helper function which turns any token name / path into a valid variable name
const name = tokenVariableName(token, tokenGroups);

// Then creating the value of the token, using another helper function
const value = CSSHelper.colorTokenValueToCSS(token.value, mappedTokens, {
allowReferences: true,
decimals: 3,
colorFormat: ColorFormat.smartHashHex,
tokenToVariableRef: (t) => {
return `var(--${tokenVariableName(t, tokenGroups)})`;
},
});

return `$${name}: ${value};`;
}

function tokenVariableName(token: Token, tokenGroups: Array<TokenGroup>): string {
const parent = tokenGroups.find((group) => group.id === token.parentGroupId)!;

return NamingHelper.codeSafeVariableNameForToken(token, StringCase.paramCase, parent, ``);
}
7 changes: 7 additions & 0 deletions exporters/variables-scss/src/generators/simple.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Token } from '@supernovaio/sdk-exporters';

export const generateSimple = (allTokens: Token[]) => {
const sortedTokens = allTokens.sort((a, b) => a.name.localeCompare(b.name));

return sortedTokens.map((token) => token);
};
80 changes: 80 additions & 0 deletions exporters/variables-scss/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { FileHelper } from '@supernovaio/export-helpers';
import {
AnyOutputFile,
ColorToken,
PulsarContext,
RemoteVersionIdentifier,
Supernova,
TokenType,
} from '@supernovaio/sdk-exporters';
import { ExporterConfiguration } from '../config';
import { colorTokenToCSS } from './content/token';
import { generateSimple } from './generators/simple';

/**
* Export entrypoint.
* When running `export` through extensions or pipelines, this function will be called.
* Context contains information about the design system and version that is currently being exported.
*/
Pulsar.export(async (sdk: Supernova, context: PulsarContext): Promise<Array<AnyOutputFile>> => {
// Fetch data from design system that is currently being exported (context)
const remoteVersionIdentifier: RemoteVersionIdentifier = {
designSystemId: context.dsId,
versionId: context.versionId,
};

// Fetch the necessary data
let tokens = await sdk.tokens.getTokens(remoteVersionIdentifier);
let tokenGroups = await sdk.tokens.getTokenGroups(remoteVersionIdentifier);

// Filter by brand, if specified by the VSCode extension or pipeline configuration
if (context.brandId) {
tokens = tokens.filter((token) => token.brandId === context.brandId);
tokenGroups = tokenGroups.filter((tokenGroup) => tokenGroup.brandId === context.brandId);
}

// Apply theme, if specified by the VSCode extension or pipeline configuration
if (context.themeId) {
const themes = await sdk.tokens.getTokenThemes(remoteVersionIdentifier);
const currentTheme = themes.find((theme) => theme.id === context.themeId);
if (currentTheme) {
tokens = await sdk.tokens.computeTokensByApplyingThemes(tokens, [currentTheme]);
} else {
// Don't allow applying theme which doesn't exist in the system
throw new Error("Unable to apply theme which doesn't exist in the system.");
}
}

// Convert all color tokens to CSS variables
const simpleTokens = generateSimple(tokens);
const mappedTokens = new Map(simpleTokens.map((token) => [token.id, token]));

const cssVariables = tokens
.filter((t) => t.tokenType === TokenType.color)
.map((token) => colorTokenToCSS(token as ColorToken, mappedTokens, tokenGroups))
.join('\n');

// Create CSS file content
let content = cssVariables;
if (exportConfiguration.generateDisclaimer) {
// Add disclaimer to every file if enabled
content = `/* This file was generated by Supernova, don't change by hand */\n${content}`;
}

// Create output file and return it
return [
FileHelper.createTextFile({
relativePath: './',
fileName: '_colors.scss',
content,
}),
FileHelper.createTextFile({
relativePath: './original-data/',
fileName: '_colors.json',
content: JSON.stringify(tokens, null, 2),
}),
];
});

/** Exporter configuration. Adheres to the `ExporterConfiguration` interface and its content comes from the resolved default configuration + user overrides of various configuration keys */
export const exportConfiguration = Pulsar.exportConfig<ExporterConfiguration>();
4 changes: 4 additions & 0 deletions exporters/variables-scss/tsconfig.eslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"include": ["./", "./.eslintrc.js"]
}
33 changes: 33 additions & 0 deletions exporters/variables-scss/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"compileOnSave": true,
"compilerOptions": {
"baseUrl": ".",
"outDir": "./dist",
"sourceMap": true,
"declaration": true,
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"module": "es2015",
"target": "es2015",
"noEmit": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"resolveJsonModule": true,
"noImplicitReturns": true,
"allowUnreachableCode": false,
"allowSyntheticDefaultImports": true,
"alwaysStrict": true,
"forceConsistentCasingInFileNames": true,
"noUnusedParameters": false,
"noUnusedLocals": true,
"strictFunctionTypes": true,
"noImplicitAny": true,
"esModuleInterop": true,
"typeRoots": ["../../node_modules/@types"],
"lib": ["es2015", "dom", "dom.iterable"],
"types": ["node", "jest", "@testing-library/jest-dom"]
},
"include": ["./src/**/*"],
"exclude": ["./node_modules", "./dist/**/*"]
}
20 changes: 20 additions & 0 deletions exporters/variables-scss/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { resolve } from 'path';
import { defineConfig } from 'vite';

export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'Exporter',
fileName: 'exporter',
formats: ['cjs'],
},
rollupOptions: {
output: {
dir: resolve(__dirname, './generated'),
banner:
'/**\n * THIS FILE IS GENERATED USING `build` SCRIPT\n * DO NOT EDIT MANUALLY\n * SEE CONTRIBUTING.md\n*/',
},
},
},
});
Loading

0 comments on commit b6bf2f8

Please sign in to comment.