Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Architecture refactor #8

Merged
merged 2 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 5 additions & 9 deletions packages/figma-parser/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export { FigmaParser } from './parser';
export { ParserFactory } from './parser-with-plugins';
export { FigmaParser } from './parser/parser';
export { FigmaTypes };

import * as FigmaTypes from './types';
Expand All @@ -11,8 +10,6 @@ import * as FigmaTypes from './types';
// export { universalTextPlugin } from './plugins/markdown/universal-text-plugin'
// export type { FetchContentPlugin } from './plugins/markdown/types'

export { HardCachePlugin } from './plugins/hard-cache/hard-cache.plugin';

export { StylesPlugin } from './plugins/styles/styles.plugin';
export { DesignTokens } from './plugins/styles/transformers/design-tokens/index';
export type {
Expand All @@ -30,9 +27,8 @@ export type {
TypographyTokenValue,
} from './plugins/styles/transformers/design-tokens/index';

export { DocumentPlugin } from './plugins/document/document.plugin';
export { NodeCollection } from './plugins/document/node-collection';
export { SingleNode } from './plugins/document/single-node';
export { NodeCollection } from './parser/node-collection';
export { SingleNode } from './parser/single-node';
export {
hasChildren,
isBooleanOperationNode,
Expand All @@ -55,7 +51,7 @@ export {
isTextNode,
isVectorNode,
nodeTypes,
} from './plugins/document/types';
} from './parser/types';
export type {
SingleBooleanOperationNode,
SingleCanvasNode,
Expand All @@ -73,4 +69,4 @@ export type {
SingleStarNode,
SingleTextNode,
SingleVectorNode,
} from './plugins/document/types';
} from './parser/types';
12 changes: 0 additions & 12 deletions packages/figma-parser/src/parser-with-plugins.ts

This file was deleted.

68 changes: 0 additions & 68 deletions packages/figma-parser/src/parser.ts

This file was deleted.

56 changes: 56 additions & 0 deletions packages/figma-parser/src/parser/hard-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
import { join, resolve } from 'path';
import { hashData } from '../shared/create-hash.util';

export class HardCache {
constructor(
private directory: string = './.cache',
private lifetime: number = 1000 * 60 * 60 * 8
) {
this.directory = resolve(directory);
}

cacheFile(data: object) {
const filename = hashData(JSON.stringify(data));
return join(this.directory, `${filename}.cache`);
}

isValid(data: object) {
const file = this.cacheFile(data);
const cacheFileValid = Date.now() - statSync(file).birthtimeMs < this.lifetime;
if (!cacheFileValid) {
this.invalidate(data);
}
return cacheFileValid;
}

invalidate(data: object) {
const file = this.cacheFile(data);

if (existsSync(file)) {
rmSync(file);
}
}

get(data: object) {
if (!this.isValid(data)) return false;

const file = this.cacheFile(data);

if (existsSync(file)) {
return readFileSync(file, 'utf-8');
}

return false;
}

set(data: object, content: string) {
const file = this.cacheFile(data);

if (!existsSync(this.directory)) {
mkdirSync(this.directory, { recursive: true });
}

writeFileSync(file, content, 'utf-8');
}
}
42 changes: 42 additions & 0 deletions packages/figma-parser/src/parser/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
interface LoggerOptions {
header: string;
separator: string;
}

interface Logger {
log: (...messages: unknown[]) => void;
info: (...messages: unknown[]) => void;
warn: (...messages: unknown[]) => void;
error: (...messages: unknown[]) => void;
}

export function loggerFactory(options?: Partial<LoggerOptions>): Logger;
export function loggerFactory(header: string, options?: Partial<LoggerOptions>): Logger;
export function loggerFactory(headerOrOptions?: Partial<LoggerOptions> | string, options?: Partial<LoggerOptions>): Logger {
let loggerOptions: LoggerOptions = {
separator: ':',
} as LoggerOptions;

if (typeof headerOrOptions === 'string') {
loggerOptions.header = headerOrOptions;
}

if (typeof headerOrOptions === 'object' && headerOrOptions.constructor === Object) {
loggerOptions = { ...loggerOptions, ...headerOrOptions };
}

if (!!options && typeof options === 'object' && options.constructor === Object) {
loggerOptions = { ...loggerOptions, ...options };
}

const header = loggerOptions.header ? loggerOptions.header + loggerOptions.separator : undefined;

return {
log: (...messages: unknown[]) => console.log(header, ...messages),
info: (...messages: unknown[]) => console.info(header, ...messages),
warn: (...messages: unknown[]) => console.warn(header, ...messages),
error: (...messages: unknown[]) => console.error(header, ...messages),
};
}

export const logger = loggerFactory();
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
import { Node } from '../../full-figma-types';
import { Node } from '../full-figma-types';
import { SingleNode } from './single-node';
import { CallbackFunction, FigmaNodeId, PathBreadcrumb } from './types';
import { CallbackFunction, FigmaNodeId, NodeCollectionMixin, NodeMixin, PathBreadcrumb } from './types';

export class NodeCollection {
public readonly length: number = 0;
public readonly parent: SingleNode;

[i: number]: SingleNode;

constructor(nodes: Node[] | ReadonlyArray<Node> | SingleNode[], parent: SingleNode) {
constructor(
nodes: Node[] | ReadonlyArray<Node> | SingleNode[],
parent: SingleNode,
private nodeMixins: NodeMixin[],
private nodeCollectionMixins: NodeCollectionMixin[] = []
) {
let length = 0;

nodes.forEach((node, index) => {
this[index] = node instanceof SingleNode ? node : new SingleNode(node);
const nodeCtor = this.nodeMixins.reduce((wrapped, mixin) => mixin(wrapped), SingleNode);
this[index] = node instanceof SingleNode ? node : new nodeCtor(node, this.nodeMixins, this.nodeCollectionMixins);
length++;
});

this.parent = parent;
this.length = length;
}

hasMixin(mixin: NodeCollectionMixin): this is SingleNode & typeof mixin {
return this.nodeCollectionMixins.includes(mixin);
}

table(): void {
const lines = Array.from(this).map((node) => ({
name: node?.name,
Expand Down Expand Up @@ -79,7 +89,7 @@ export class NodeCollection {
out.push(this[i]);
}
}
return new NodeCollection(out, this.parent);
return new NodeCollection(out, this.parent, this.nodeMixins, this.nodeCollectionMixins);
}

map<Output>(callback: (node: SingleNode, index?: number, collection?: NodeCollection) => Output): Output[] {
Expand Down
108 changes: 108 additions & 0 deletions packages/figma-parser/src/parser/parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { FileResponse } from '../full-figma-types';
import { deepMerge } from '../shared/deep-merge';
import { HardCache } from './hard-cache';
import { loggerFactory } from './logger';
import { SingleNode } from './single-node';
import { FigmaParserPlugin, FigmaParserPluginConstructor, FigmaParserPluginFunction, NodeCollectionMixin, NodeMixin } from './types';

export type FigmaPAT = string;
// export type FigmaPAT = `figd_${string}`

export interface FigmaParserOptions {
plugins: FigmaParserPlugin[];
nodeMixins: NodeMixin[];
nodeCollectionMixins: NodeCollectionMixin[];
hardCache?: boolean;
cacheDir?: string;
cacheLifetime: number;
}

const logger = loggerFactory('Figma Parser');

export class FigmaParser {
plugins: FigmaParserPlugin[] = [];
cache: HardCache;

readonly options: FigmaParserOptions = {
plugins: [],
nodeMixins: [],
nodeCollectionMixins: [],
hardCache: true,
cacheDir: './cache',
cacheLifetime: 1000 * 60 * 60 * 8, // 8 hours
};

constructor(
private token: FigmaPAT,
userOptions: Partial<FigmaParserOptions> = {}
) {
if (!token) throw new Error('You need to provide Personal Access Token for Figma.');

this.options = deepMerge(this.options, userOptions) as FigmaParserOptions;

this.cache = new HardCache(this.options.cacheDir, this.options.cacheLifetime);

this.options.plugins.forEach((plugin) => this.loadPlugin(plugin));
}

async request<Response = object>(path: string, params?: Record<string, string>): Promise<Response> {
const cached = this.cache.get({ path, params });
if (cached && this.options.hardCache) {
logger.info('(Using cache)', `Found cached request. Retrieving from cache.`);
return (await Promise.resolve(JSON.parse(cached))) as Response;
}

let url = `https://api.figma.com/v1/${path}`;
const headers = new Headers({
'X-Figma-Token': this.token,
});

if (params && Object.keys(params).length > 0) {
url += '?' + new URLSearchParams(params).toString();
}

logger.info(`Requesting ${url}...`);
const data = await fetch(url, { headers })
.catch((e) => {
throw new Error(e.message);
})
.then((response) => {
if (!response.ok) throw new Error(response.statusText);
return response.json() as Response;
});

if (this.options.hardCache) {
logger.info('(Using cache)', `Caching request.`);
this.cache.set({ path, params }, JSON.stringify(data, null, 2));
}

return data as Response;
}

private loadPlugin(...plugins: FigmaParserPlugin[]) {
plugins.forEach((plugin) => {
let pluginInstance;

try {
pluginInstance = new (plugin as FigmaParserPluginConstructor)(this);
} catch (e) {
if (typeof plugin !== 'function') throw new Error(`Provided plugin is not a constructor nor a function.`);

pluginInstance = (plugin as FigmaParserPluginFunction)(this);
}

this.plugins.push(pluginInstance as FigmaParserPlugin);
});
}

async document(fileId: string): Promise<SingleNode> {
const file: FileResponse = await this.request(`files/${fileId}`);
const nodeCtor = this.options.nodeMixins.reduce((wrapped, mixin) => mixin(wrapped), SingleNode);
return new nodeCtor(file.document, this.options.nodeMixins, this.options.nodeCollectionMixins);
}
}

export interface FigmaRequestOptions {
path: string;
params: Record<string, string> | object;
}
Loading
Loading