Skip to content

Commit

Permalink
Merge pull request #18 from pinterest/protodave/GESTALT-7668-variable…
Browse files Browse the repository at this point in the history
…-support

GESTALT-7668: Variable support
  • Loading branch information
protodave authored Aug 20, 2024
2 parents 9156552 + 4255844 commit f5e0967
Show file tree
Hide file tree
Showing 27 changed files with 1,398 additions and 3,738 deletions.
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,5 @@ dist/*
tests/token.ts
tests/token.js
src/examples/token.ts
comp
comps.json
styles
styles.json
3 changes: 3 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
.github
jest.config.ts
tsconfig.json
tsconfig.build.json
tests/*
src/*
dist/examples/*
comps.json
styles.json
8 changes: 4 additions & 4 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { JestConfigWithTsJest } from 'ts-jest'
import type { JestConfigWithTsJest } from "ts-jest";

const jestConfig: JestConfigWithTsJest = {
preset: 'ts-jest',
}
preset: "ts-jest",
};

export default jestConfig
export default jestConfig;
4,086 changes: 463 additions & 3,623 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"build": "rm -rf dist && tsc --project tsconfig.build.json",
"watch": "tsc --project tsconfig.build.json -w",
"test": "jest src",
"test-stats": "jest tests/stats.test.ts",
"perf": "jest perf.test.ts",
Expand All @@ -15,7 +15,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"axios": "1.5.1",
"axios": "1.6.0",
"dayjs": "^1.11.10",
"jsonpath": "^1.1.1"
},
Expand Down
209 changes: 192 additions & 17 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {
FigmaFile,
FigmaLocalVariable,
FigmaLocalVariableCollection,
FigmaLocalVariableCollections,
FigmaLocalVariables,
FigmaPublishedVariable,
FigmaPublishedVariableCollection,
FigmaTeamComponent,
Expand All @@ -11,6 +13,7 @@ import {
import {
AdoptionCalculationOptions,
AggregateCounts,
AggregateCountsCompliance,
LintCheck,
LintCheckPercent,
ProcessedNodeTree,
Expand Down Expand Up @@ -38,16 +41,29 @@ import {
ProcessedNodeOptions,
} from "./utils/process";
import { getFigmaPagesForTeam } from "./utils/teams";
import {
createHexColorToVariableMap,
getCollectionVariables,
HexColorToFigmaVariableMap,
} from "./utils/variables";
import { FigmaAPIHelper } from "./webapi";

// exporting the types to reuse
export * from "./models/stats";
export * from "./models/figma";
export * from "./rules/cleaners";
export * from "./utils/variables";

export class FigmaCalculator extends FigmaDocumentParser {
components: FigmaTeamComponent[] = [];
allStyles: FigmaTeamStyle[] = [];
localVariables: FigmaLocalVariables = {};
localVariableCollections: FigmaLocalVariableCollections = {};
publishedVariables: FigmaLocalVariables = {};
publishedVariableCollections: FigmaLocalVariableCollections = {};

// :TODO: Also stash loaded local and published variables when loaded

apiToken: string = "";

constructor() {
Expand Down Expand Up @@ -169,7 +185,16 @@ export class FigmaCalculator extends FigmaDocumentParser {
variables: Record<string, FigmaLocalVariable>;
variableCollections: Record<string, FigmaLocalVariableCollection>;
}> {
return await FigmaAPIHelper.getFileLocalVariables(fileKey);
const localVariables = await FigmaAPIHelper.getFileLocalVariables(fileKey);

// Merge the variables and variable collections with any previously loaded ones
Object.assign(this.localVariables, localVariables.variables);
Object.assign(
this.localVariableCollections,
localVariables.variableCollections
);

return localVariables;
}

/**
Expand All @@ -180,7 +205,18 @@ export class FigmaCalculator extends FigmaDocumentParser {
variables: Record<string, FigmaPublishedVariable>;
variableCollections: Record<string, FigmaPublishedVariableCollection>;
}> {
return await FigmaAPIHelper.getFilePublishedVariables(fileKey);
const publishedVariables = await FigmaAPIHelper.getFilePublishedVariables(
fileKey
);

// Merge the variables and variable collections with any previously loaded ones
Object.assign(this.publishedVariables, publishedVariables.variables);
Object.assign(
this.publishedVariableCollections,
publishedVariables.variableCollections
);

return publishedVariables;
}

static generateStyleBucket = generateStyleBucket;
Expand All @@ -199,9 +235,16 @@ export class FigmaCalculator extends FigmaDocumentParser {
opts?: {
styles?: FigmaTeamStyle[];
styleBucket?: StyleBucket;
colorVariableCollectionIds?: string[];
variables?: FigmaLocalVariables;
variableCollections?: FigmaLocalVariableCollections;
} & LintCheckOptions
): LintCheck[] {
let allStyles = this.allStyles || opts?.styles;
let allStyles = opts?.styles || this.allStyles;
let colorVariableCollectionIds = opts?.colorVariableCollectionIds || [];
let variables = opts?.variables || this.localVariables;
let variableCollections =
opts?.variableCollections || this.localVariableCollections;

let styleBucket =
opts?.styleBucket || FigmaCalculator.generateStyleBucket(allStyles);
Expand All @@ -211,7 +254,31 @@ export class FigmaCalculator extends FigmaDocumentParser {
"No style bucket, or array of styles provided to generate lint results"
);

return runSimilarityChecks(styleBucket, node, opts);
// Just grab the variables from specific color variable collection(s)
let colorVariableIds: string[] = [];
let hexColorToVariableMap: HexColorToFigmaVariableMap = {};
if (
colorVariableCollectionIds.length > 0 &&
variables &&
Object.keys(variables).length > 0 &&
variableCollections &&
Object.keys(variableCollections).length > 0
) {
colorVariableIds = getCollectionVariables(
colorVariableCollectionIds,
variableCollections
);
hexColorToVariableMap = createHexColorToVariableMap(
colorVariableIds,
variables,
variableCollections
);
}

return runSimilarityChecks(styleBucket, variables, node, {
...opts,
hexColorToVariableMap,
});
}

static filterHiddenNodes(nodes: BaseNode[]): {
Expand Down Expand Up @@ -283,7 +350,7 @@ export class FigmaCalculator extends FigmaDocumentParser {
// get the component's real name
// check if a component has a mainComponent?
const isLibraryComponent = (instanceNode: any) => {
// if it's a web file, then check the componentId else the mainCompponent property to get the key
// if it's a web file, then check the componentId else the mainComponent property to get the key
const componentKey =
instanceNode.componentId || instanceNode.mainComponent.key;

Expand Down Expand Up @@ -334,60 +401,168 @@ export class FigmaCalculator extends FigmaDocumentParser {
opts?: {
components?: FigmaTeamComponent[];
allStyles?: FigmaTeamStyle[];
colorVariableCollectionIds?: string[];
variables?: FigmaLocalVariables;
variableCollections?: FigmaLocalVariableCollections;
} & ProcessedNodeOptions
): ProcessedNodeTree {
const { components, allStyles } = opts || {};
const {
components,
allStyles,
colorVariableCollectionIds,
variables,
variableCollections,
} = opts || {};
const processedNodeOpts: ProcessedNodeOptions = {
onProcessNode: opts?.onProcessNode,
hexStyleMap: opts?.hexStyleMap,
styleLookupMap: opts?.styleLookupMap,
};

const { allHiddenNodes, libraryNodes, totalNodes, processedNodes } =
getProcessedNodes(
rootNode,
components || this.components,
allStyles || this.allStyles,
opts
colorVariableCollectionIds || [],
variables || this.localVariables,
variableCollections || this.localVariableCollections,
processedNodeOpts
);

const compliance: AggregateCountsCompliance = {
fills: {
attached: 0,
detached: 0,
none: 0,
},
strokes: {
attached: 0,
detached: 0,
none: 0,
},
text: {
attached: 0,
detached: 0,
none: 0,
},
};

const aggregates: AggregateCounts = {
totalNodes,
hiddenNodes: allHiddenNodes,
libraryNodes,
checks: {},
compliance,
};

// loop through the array and calculate the lint check totals

for (const node of processedNodes) {
// Init/reset the Style/Variable bitwise boolean counter helpers
const counters = {
fills: {
full: 0,
partial: 0,
none: 0,
},
strokes: {
full: 0,
partial: 0,
none: 0,
},
text: {
full: 0,
partial: 0,
none: 0,
},
};

for (const check of node.lintChecks) {
if (!aggregates.checks[check.checkName]) {
aggregates.checks[check.checkName] = {
const { checkName, matchLevel } = check;

if (!aggregates.checks[checkName]) {
aggregates.checks[checkName] = {
full: 0,
partial: 0,
skip: 0,
none: 0,
};
}

switch (check.matchLevel) {
switch (matchLevel) {
case "Full":
{
aggregates.checks[check.checkName]!.full += 1;
aggregates.checks[checkName]!.full += 1;
if (checkName === "Fill-Style" || checkName === "Fill-Variable")
counters.fills.full++;
else if (
checkName === "Stroke-Fill-Style" ||
checkName === "Stroke-Fill-Variable"
)
counters.strokes.full++;
else if (checkName === "Text-Style") counters.text.full++;
}
break;
case "None":

case "Partial":
{
aggregates.checks[check.checkName]!.none += 1;
aggregates.checks[checkName]!.partial += 1;
if (checkName === "Fill-Style" || checkName === "Fill-Variable")
counters.fills.partial++;
else if (
checkName === "Stroke-Fill-Style" ||
checkName === "Stroke-Fill-Variable"
)
counters.strokes.partial++;
else if (checkName === "Text-Style") counters.text.partial++;
}
break;
case "Partial":

case "None":
{
aggregates.checks[check.checkName]!.partial += 1;
aggregates.checks[checkName]!.none += 1;
if (checkName === "Fill-Style" || checkName === "Fill-Variable")
counters.fills.none++;
else if (
checkName === "Stroke-Fill-Style" ||
checkName === "Stroke-Fill-Variable"
)
counters.strokes.none++;
else if (checkName === "Text-Style") counters.text.none++;
}
break;

case "Skip": {
aggregates.checks[check.checkName]!.skip += 1;
aggregates.checks[checkName]!.skip += 1;
}
}
}

// Attached means either using a style or a variable exact match (full)
// Bitwise boolean OR to increment if either is true
compliance.fills.attached += counters.fills.full > 0 ? 1 : 0;
compliance.strokes.attached += counters.strokes.full > 0 ? 1 : 0;

// Detached means either a matching style or variable was found, but not used (partial)
// Relies on the fact that we don't try to find a variable suggestion if an exact style match was found, and vice versa
// Bitwise boolean OR
compliance.fills.detached += counters.fills.partial > 0 ? 1 : 0;
compliance.strokes.detached += counters.strokes.partial > 0 ? 1 : 0;

// None (outside of system) means neither a matching style or variable was found (none)
// Bitwise boolean AND to increment if both are true
compliance.fills.none += counters.fills.none === 2 ? 1 : 0;
compliance.strokes.none += counters.strokes.none === 2 ? 1 : 0;

// We don't have Text variable support (yet), so pass thru
compliance.text.attached += counters.text.full;
compliance.text.detached += counters.text.partial;
compliance.text.none += counters.text.none;
}

aggregates.compliance = compliance;

return {
parentNode: {
id: rootNode.id,
Expand Down Expand Up @@ -424,7 +599,7 @@ export class FigmaCalculator extends FigmaDocumentParser {

const adoptionPercent = makePercent(
(allTotals.totalNodesInLibrary + allTotals.totalMatchingText) /
allTotals.totalNodesOnPage
allTotals.totalNodesOnPage
);

return adoptionPercent;
Expand Down
Loading

0 comments on commit f5e0967

Please sign in to comment.