figma-json-tree 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +172 -0
  2. package/dist/cli/index.js +1138 -0
  3. package/dist/cli/index.js.map +1 -0
  4. package/dist/figma-html.js +95 -0
  5. package/dist/figma-html.js.map +1 -0
  6. package/dist/figma-json-fetch.js +53 -0
  7. package/dist/figma-json-fetch.js.map +1 -0
  8. package/dist/figma-tree.js +808 -0
  9. package/dist/figma-tree.js.map +1 -0
  10. package/dist/types/cli/args.d.ts +27 -0
  11. package/dist/types/cli/args.d.ts.map +1 -0
  12. package/dist/types/cli/commands/download.d.ts +4 -0
  13. package/dist/types/cli/commands/download.d.ts.map +1 -0
  14. package/dist/types/cli/commands/export.d.ts +3 -0
  15. package/dist/types/cli/commands/export.d.ts.map +1 -0
  16. package/dist/types/cli/commands/query.d.ts +3 -0
  17. package/dist/types/cli/commands/query.d.ts.map +1 -0
  18. package/dist/types/cli/env.d.ts +2 -0
  19. package/dist/types/cli/env.d.ts.map +1 -0
  20. package/dist/types/cli/html/input.d.ts +5 -0
  21. package/dist/types/cli/html/input.d.ts.map +1 -0
  22. package/dist/types/cli/index.d.ts +3 -0
  23. package/dist/types/cli/index.d.ts.map +1 -0
  24. package/dist/types/cli/output.d.ts +4 -0
  25. package/dist/types/cli/output.d.ts.map +1 -0
  26. package/dist/types/cli/run.d.ts +8 -0
  27. package/dist/types/cli/run.d.ts.map +1 -0
  28. package/dist/types/figma-html/attributes.d.ts +4 -0
  29. package/dist/types/figma-html/attributes.d.ts.map +1 -0
  30. package/dist/types/figma-html/classes.d.ts +3 -0
  31. package/dist/types/figma-html/classes.d.ts.map +1 -0
  32. package/dist/types/figma-html/escape.d.ts +2 -0
  33. package/dist/types/figma-html/escape.d.ts.map +1 -0
  34. package/dist/types/figma-html/index.d.ts +3 -0
  35. package/dist/types/figma-html/index.d.ts.map +1 -0
  36. package/dist/types/figma-html/renderer.d.ts +5 -0
  37. package/dist/types/figma-html/renderer.d.ts.map +1 -0
  38. package/dist/types/figma-html/types.d.ts +4 -0
  39. package/dist/types/figma-html/types.d.ts.map +1 -0
  40. package/dist/types/figma-html/validate.d.ts +3 -0
  41. package/dist/types/figma-html/validate.d.ts.map +1 -0
  42. package/dist/types/figma-json-fetch/client.d.ts +8 -0
  43. package/dist/types/figma-json-fetch/client.d.ts.map +1 -0
  44. package/dist/types/figma-json-fetch/endpoints.d.ts +3 -0
  45. package/dist/types/figma-json-fetch/endpoints.d.ts.map +1 -0
  46. package/dist/types/figma-json-fetch/errors.d.ts +7 -0
  47. package/dist/types/figma-json-fetch/errors.d.ts.map +1 -0
  48. package/dist/types/figma-json-fetch/index.d.ts +4 -0
  49. package/dist/types/figma-json-fetch/index.d.ts.map +1 -0
  50. package/dist/types/figma-json-fetch/request.d.ts +3 -0
  51. package/dist/types/figma-json-fetch/request.d.ts.map +1 -0
  52. package/dist/types/figma-json-fetch/types.d.ts +9 -0
  53. package/dist/types/figma-json-fetch/types.d.ts.map +1 -0
  54. package/dist/types/figma-tree/index.d.ts +10 -0
  55. package/dist/types/figma-tree/index.d.ts.map +1 -0
  56. package/dist/types/figma-tree/input/normalize.d.ts +11 -0
  57. package/dist/types/figma-tree/input/normalize.d.ts.map +1 -0
  58. package/dist/types/figma-tree/ir/converters/diagnostics.d.ts +3 -0
  59. package/dist/types/figma-tree/ir/converters/diagnostics.d.ts.map +1 -0
  60. package/dist/types/figma-tree/ir/converters/layout.d.ts +4 -0
  61. package/dist/types/figma-tree/ir/converters/layout.d.ts.map +1 -0
  62. package/dist/types/figma-tree/ir/converters/style.d.ts +6 -0
  63. package/dist/types/figma-tree/ir/converters/style.d.ts.map +1 -0
  64. package/dist/types/figma-tree/ir/converters/text.d.ts +4 -0
  65. package/dist/types/figma-tree/ir/converters/text.d.ts.map +1 -0
  66. package/dist/types/figma-tree/ir/model/document.d.ts +12 -0
  67. package/dist/types/figma-tree/ir/model/document.d.ts.map +1 -0
  68. package/dist/types/figma-tree/ir/model/types.d.ts +61 -0
  69. package/dist/types/figma-tree/ir/model/types.d.ts.map +1 -0
  70. package/dist/types/figma-tree/ir/pipeline.d.ts +5 -0
  71. package/dist/types/figma-tree/ir/pipeline.d.ts.map +1 -0
  72. package/dist/types/figma-tree/ir/plugins/run.d.ts +11 -0
  73. package/dist/types/figma-tree/ir/plugins/run.d.ts.map +1 -0
  74. package/dist/types/figma-tree/model/types.d.ts +30 -0
  75. package/dist/types/figma-tree/model/types.d.ts.map +1 -0
  76. package/dist/types/figma-tree/model/value.d.ts +4 -0
  77. package/dist/types/figma-tree/model/value.d.ts.map +1 -0
  78. package/dist/types/figma-tree/selector/match.d.ts +4 -0
  79. package/dist/types/figma-tree/selector/match.d.ts.map +1 -0
  80. package/dist/types/figma-tree/selector/parser.d.ts +3 -0
  81. package/dist/types/figma-tree/selector/parser.d.ts.map +1 -0
  82. package/dist/types/figma-tree/selector/types.d.ts +24 -0
  83. package/dist/types/figma-tree/selector/types.d.ts.map +1 -0
  84. package/dist/types/figma-tree/tailwind/convert.d.ts +3 -0
  85. package/dist/types/figma-tree/tailwind/convert.d.ts.map +1 -0
  86. package/dist/types/figma-tree/tailwind/style.d.ts +6 -0
  87. package/dist/types/figma-tree/tailwind/style.d.ts.map +1 -0
  88. package/dist/types/figma-tree/tree/figma-tree.d.ts +39 -0
  89. package/dist/types/figma-tree/tree/figma-tree.d.ts.map +1 -0
  90. package/dist/types/figma-tree/tree/records.d.ts +5 -0
  91. package/dist/types/figma-tree/tree/records.d.ts.map +1 -0
  92. package/docs/api.md +89 -0
  93. package/docs/architecture.md +70 -0
  94. package/docs/html.md +90 -0
  95. package/docs/ir.md +84 -0
  96. package/docs/verification.md +33 -0
  97. package/package.json +57 -0
@@ -0,0 +1,11 @@
1
+ import type { NodeRecord } from '../../model/types.js';
2
+ import type { IRNode, IRPlugin } from '../model/types.js';
3
+ export declare class IRPluginError extends Error {
4
+ readonly pluginName: string;
5
+ readonly nodeId: string;
6
+ name: string;
7
+ constructor(pluginName: string, nodeId: string, cause: unknown);
8
+ }
9
+ export declare function validateIRNode(node: unknown): asserts node is IRNode;
10
+ export declare function runPlugins(initial: IRNode, record: NodeRecord, plugins: readonly IRPlugin[]): IRNode;
11
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../../../src/figma-tree/ir/plugins/run.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgB,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAEpE,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAmB,MAAM,mBAAmB,CAAA;AAE1E,qBAAa,aAAc,SAAQ,KAAK;IAGpC,QAAQ,CAAC,UAAU,EAAE,MAAM;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM;IAHhB,IAAI,SAAkB;gBAEpB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACvB,KAAK,EAAE,OAAO;CAIjB;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,IAAI,MAAM,CA+BpE;AA+BD,wBAAgB,UAAU,CACxB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,SAAS,QAAQ,EAAE,GAC3B,MAAM,CAkBR"}
@@ -0,0 +1,30 @@
1
+ export type { GetFileNodesResponse, GetFileResponse, Node as FigmaRESTNode, } from '@figma/rest-api-spec';
2
+ /** Minimal structural input: unknown Figma properties survive round-tripping. */
3
+ export interface FigmaNodeData {
4
+ id: string;
5
+ name: string;
6
+ type: string;
7
+ children?: FigmaNodeData[];
8
+ [key: string]: unknown;
9
+ }
10
+ export type DeepReadonly<T> = T extends (...args: never[]) => unknown ? T : T extends readonly (infer Item)[] ? readonly DeepReadonly<Item>[] : T extends object ? {
11
+ readonly [K in keyof T]: DeepReadonly<T[K]>;
12
+ } : T;
13
+ export interface ComponentMetadata {
14
+ readonly name?: string;
15
+ readonly [key: string]: unknown;
16
+ }
17
+ export interface Diagnostic {
18
+ readonly code: string;
19
+ readonly message: string;
20
+ readonly nodeId?: string;
21
+ readonly property?: string;
22
+ }
23
+ export interface NodeRecord {
24
+ readonly source: DeepReadonly<FigmaNodeData>;
25
+ readonly parent?: NodeRecord;
26
+ readonly children: NodeRecord[];
27
+ readonly componentName?: string;
28
+ readonly component?: ComponentMetadata;
29
+ }
30
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/figma-tree/model/types.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,oBAAoB,EACpB,eAAe,EACf,IAAI,IAAI,aAAa,GACtB,MAAM,sBAAsB,CAAA;AAE7B,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAA;IAC1B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,GACjE,CAAC,GACD,CAAC,SAAS,SAAS,CAAC,MAAM,IAAI,CAAC,EAAE,GAC/B,SAAS,YAAY,CAAC,IAAI,CAAC,EAAE,GAC7B,CAAC,SAAS,MAAM,GACd;IAAE,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,GAC/C,CAAC,CAAA;AAET,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAChC;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC,aAAa,CAAC,CAAA;IAC5C,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAA;IAC5B,QAAQ,CAAC,QAAQ,EAAE,UAAU,EAAE,CAAA;IAC/B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;IAC/B,QAAQ,CAAC,SAAS,CAAC,EAAE,iBAAiB,CAAA;CACvC"}
@@ -0,0 +1,4 @@
1
+ export declare function isObject(value: unknown): value is Record<string, unknown>;
2
+ export declare function deepFreeze<T>(value: T): T;
3
+ export declare function numberValue(value: unknown): number | undefined;
4
+ //# sourceMappingURL=value.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"value.d.ts","sourceRoot":"","sources":["../../../../src/figma-tree/model/value.ts"],"names":[],"mappings":"AAAA,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzE;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAWzC;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAE9D"}
@@ -0,0 +1,4 @@
1
+ import type { NodeRecord } from '../model/types.js';
2
+ import type { SelectorPart } from './types.js';
3
+ export declare function matchesSelector(node: NodeRecord, parts: readonly SelectorPart[], boundary?: NodeRecord): boolean;
4
+ //# sourceMappingURL=match.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"match.d.ts","sourceRoot":"","sources":["../../../../src/figma-tree/selector/match.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,KAAK,EAAiB,YAAY,EAAE,MAAM,YAAY,CAAA;AA8B7D,wBAAgB,eAAe,CAC7B,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,SAAS,YAAY,EAAE,EAC9B,QAAQ,CAAC,EAAE,UAAU,GACpB,OAAO,CAeT"}
@@ -0,0 +1,3 @@
1
+ import type { Selector, SelectorPart } from './types.js';
2
+ export declare function parseSelector(selector: Selector): SelectorPart[];
3
+ //# sourceMappingURL=parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../../src/figma-tree/selector/parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA4B,QAAQ,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAuGlF,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,YAAY,EAAE,CAsBhE"}
@@ -0,0 +1,24 @@
1
+ export declare const attributes: readonly ["id", "name", "type", "visible", "componentName"];
2
+ export type Attribute = (typeof attributes)[number];
3
+ export interface QueryCondition {
4
+ id?: string | RegExp;
5
+ name?: string | RegExp;
6
+ type?: string | RegExp;
7
+ visible?: boolean;
8
+ componentName?: string | RegExp;
9
+ }
10
+ export type Selector = string | QueryCondition;
11
+ export interface AttributeTest {
12
+ key: Attribute;
13
+ operator: 'exists' | '=' | '*=' | '^=' | '$=' | 'regex';
14
+ value?: string | boolean | RegExp;
15
+ }
16
+ export interface SelectorPart {
17
+ type?: string;
18
+ tests: AttributeTest[];
19
+ relation?: 'descendant' | 'child';
20
+ }
21
+ export declare class SelectorSyntaxError extends Error {
22
+ name: string;
23
+ }
24
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/figma-tree/selector/types.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU,6DAA8D,CAAA;AACrF,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAA;AACnD,MAAM,WAAW,cAAc;IAC7B,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;CAChC;AACD,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,cAAc,CAAA;AAC9C,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,SAAS,CAAA;IACd,QAAQ,EAAE,QAAQ,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,OAAO,CAAA;IACvD,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAA;CAClC;AACD,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,aAAa,EAAE,CAAA;IACtB,QAAQ,CAAC,EAAE,YAAY,GAAG,OAAO,CAAA;CAClC;AACD,qBAAa,mBAAoB,SAAQ,KAAK;IACnC,IAAI,SAAwB;CACtC"}
@@ -0,0 +1,3 @@
1
+ import type { IRData, TailwindIR } from '../ir/model/types.js';
2
+ export declare function convertToTailwind(data: IRData): TailwindIR;
3
+ //# sourceMappingURL=convert.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"convert.d.ts","sourceRoot":"","sources":["../../../../src/figma-tree/tailwind/convert.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAU,UAAU,EAAgB,MAAM,sBAAsB,CAAA;AAGpF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAuB1D"}
@@ -0,0 +1,6 @@
1
+ import type { CSSStyle } from '../ir/model/types.js';
2
+ export declare function convertCSS(style: CSSStyle): {
3
+ className: string;
4
+ style: CSSStyle;
5
+ };
6
+ //# sourceMappingURL=style.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"style.d.ts","sourceRoot":"","sources":["../../../../src/figma-tree/tailwind/style.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAsFpD,wBAAgB,UAAU,CAAC,KAAK,EAAE,QAAQ,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,QAAQ,CAAA;CAAE,CASlF"}
@@ -0,0 +1,39 @@
1
+ import type { IRDocument } from '../ir/model/document.js';
2
+ import type { IROptions, IRPlugin } from '../ir/model/types.js';
3
+ import type { DeepReadonly, Diagnostic, FigmaNodeData, NodeRecord } from '../model/types.js';
4
+ import type { Selector } from '../selector/types.js';
5
+ export interface FigmaTreeOptions {
6
+ irPlugins?: readonly IRPlugin[];
7
+ }
8
+ declare class TreeContext {
9
+ #private;
10
+ readonly plugins: readonly IRPlugin[];
11
+ constructor(plugins: readonly IRPlugin[]);
12
+ wrap(record: NodeRecord): FigmaNode;
13
+ search(roots: readonly NodeRecord[], selector: Selector, boundary?: NodeRecord): Generator<FigmaNode>;
14
+ }
15
+ export declare class FigmaNode {
16
+ #private;
17
+ /** @internal Obtain nodes through FigmaTree queries. */
18
+ constructor(record: NodeRecord, context: TreeContext);
19
+ get id(): string;
20
+ get name(): string;
21
+ get type(): string;
22
+ get componentName(): string | undefined;
23
+ get children(): readonly FigmaNode[];
24
+ query(selector: Selector): FigmaNode | undefined;
25
+ queryAll(selector: Selector): FigmaNode[];
26
+ toJSON(): FigmaNodeData;
27
+ toIR(options?: IROptions): IRDocument;
28
+ }
29
+ export declare class FigmaTree {
30
+ #private;
31
+ readonly diagnostics: DeepReadonly<Diagnostic[]>;
32
+ private constructor();
33
+ static fromJson(input: unknown, options?: FigmaTreeOptions): FigmaTree;
34
+ get roots(): readonly FigmaNode[];
35
+ query(selector: Selector): FigmaNode | undefined;
36
+ queryAll(selector: Selector): FigmaNode[];
37
+ }
38
+ export {};
39
+ //# sourceMappingURL=figma-tree.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"figma-tree.d.ts","sourceRoot":"","sources":["../../../../src/figma-tree/tree/figma-tree.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AACzD,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAE/D,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAI5F,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAGpD,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,SAAS,QAAQ,EAAE,CAAA;CAChC;AAED,cAAM,WAAW;;IAEH,QAAQ,CAAC,OAAO,EAAE,SAAS,QAAQ,EAAE;gBAA5B,OAAO,EAAE,SAAS,QAAQ,EAAE;IACjD,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS;IAQlC,MAAM,CACL,KAAK,EAAE,SAAS,UAAU,EAAE,EAC5B,QAAQ,EAAE,QAAQ,EAClB,QAAQ,CAAC,EAAE,UAAU,GACpB,SAAS,CAAC,SAAS,CAAC;CAUxB;AAED,qBAAa,SAAS;;IAGpB,wDAAwD;gBAC5C,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW;IAIpD,IAAI,EAAE,IAAI,MAAM,CAEf;IACD,IAAI,IAAI,IAAI,MAAM,CAEjB;IACD,IAAI,IAAI,IAAI,MAAM,CAEjB;IACD,IAAI,aAAa,IAAI,MAAM,GAAG,SAAS,CAEtC;IACD,IAAI,QAAQ,IAAI,SAAS,SAAS,EAAE,CAEnC;IACD,KAAK,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,GAAG,SAAS;IAGhD,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,EAAE;IAGzC,MAAM,IAAI,aAAa;IAGvB,IAAI,CAAC,OAAO,GAAE,SAAc,GAAG,UAAU;CAG1C;AAED,qBAAa,SAAS;;IAGpB,QAAQ,CAAC,WAAW,EAAE,YAAY,CAAC,UAAU,EAAE,CAAC,CAAA;IAChD,OAAO;IAMP,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAG1E,IAAI,KAAK,IAAI,SAAS,SAAS,EAAE,CAEhC;IACD,KAAK,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,GAAG,SAAS;IAGhD,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,EAAE;CAG1C"}
@@ -0,0 +1,5 @@
1
+ import type { NormalizedInput } from '../input/normalize.js';
2
+ import type { NodeRecord } from '../model/types.js';
3
+ export declare function walk(roots: readonly NodeRecord[]): Generator<NodeRecord>;
4
+ export declare function buildRecords(input: NormalizedInput): NodeRecord[];
5
+ //# sourceMappingURL=records.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"records.d.ts","sourceRoot":"","sources":["../../../../src/figma-tree/tree/records.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAC5D,OAAO,KAAK,EAA+B,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAEhF,wBAAiB,IAAI,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,SAAS,CAAC,UAAU,CAAC,CAOzE;AAaD,wBAAgB,YAAY,CAAC,KAAK,EAAE,eAAe,GAAG,UAAU,EAAE,CAsBjE"}
package/docs/api.md ADDED
@@ -0,0 +1,89 @@
1
+ # API와 선택자
2
+
3
+ ## FigmaTree
4
+
5
+ ```ts
6
+ FigmaTree.fromJson(input: unknown, options?: FigmaTreeOptions): FigmaTree
7
+ tree.query(selector: Selector): FigmaNode | undefined
8
+ tree.queryAll(selector: Selector): FigmaNode[]
9
+ tree.roots: readonly FigmaNode[]
10
+ tree.diagnostics: readonly Diagnostic[]
11
+ ```
12
+
13
+ 입력은 파싱된 REST 파일 응답, REST 노드 응답, 단일 노드입니다. JSON 문자열은 먼저 `JSON.parse`합니다. 노드는 문자열 `id`, `name`, `type`을 가져야 하며 `children`이 있다면 노드 배열이어야 합니다. 원본의 추가 속성은 보존합니다. 순환·공유 노드 객체는 JSON 트리 입력이 아니므로 거부합니다.
14
+
15
+ 노드 응답의 `null` 항목은 `missing-node` 진단을 남기고 건너뜁니다. `null`만 있는 응답도 빈 트리로 사용할 수 있습니다. 메타데이터를 검색 노드로 취급하지 않습니다.
16
+
17
+ ## FigmaNode
18
+
19
+ ```ts
20
+ node.id: string
21
+ node.name: string
22
+ node.type: string
23
+ node.componentName: string | undefined
24
+ node.children: readonly FigmaNode[]
25
+ node.query(selector: Selector): FigmaNode | undefined
26
+ node.queryAll(selector: Selector): FigmaNode[]
27
+ node.toJSON(): FigmaNodeData
28
+ node.toIR(options?: IROptions): IRDocument
29
+ ```
30
+
31
+ 노드 wrapper는 트리에서 얻습니다. 생성자를 직접 호출하지 않습니다. 동일 레코드는 동일 wrapper를 반환합니다. 출력 배열은 호출자가 변경해도 트리에 영향을 주지 않으며, 읽기 전용 `children`과 `roots` 배열은 동결됩니다.
32
+
33
+ `toJSON`에는 전체 자식과 원본 속성이 포함되며 부모 참조·파생 `componentName`은 추가되지 않습니다. IR 변환에 필요한 메타데이터는 wrapper가 보관합니다. 원본 subtree만 저장해 다시 읽으면 외부 컴포넌트 메타데이터는 없으므로 컴포넌트 이름을 해석하지 못할 수 있습니다. 메타데이터까지 유지하려면 다운로드한 응답을 저장합니다.
34
+
35
+ ## 선택자
36
+
37
+ | 문법 | 예제 |
38
+ | --- | --- |
39
+ | 타입·전체 | `FRAME`, `*` |
40
+ | 속성 존재 | `[componentName]` |
41
+ | 일치 | `[name="SearchFilter"]` |
42
+ | 포함 | `[name*="Filter"]` |
43
+ | 접두 | `[name^="ToBe"]` |
44
+ | 접미 | `[name$="Table"]` |
45
+ | 불리언 | `[visible=true]` |
46
+ | AND | `INSTANCE[componentName="Button"][visible=true]` |
47
+ | 자손 | `FRAME TEXT` |
48
+ | 직계 자식 | `FRAME > INSTANCE` |
49
+
50
+ 타입은 대문자이며 문자열 비교는 대소문자를 구분합니다. 문자열 속성은 작은따옴표 또는 큰따옴표로 감쌉니다. 따옴표와 역슬래시는 역슬래시로 escape할 수 있습니다. `visible`은 따옴표 없는 `true` 또는 `false`만 받습니다. 문자열 안의 공백·`>`·대괄호는 값으로 처리합니다.
51
+
52
+ 지원 속성은 `id`, `name`, `type`, `visible`, `componentName`입니다. 쉼표 그룹, 형제 선택자, 가상 클래스, CSS ID 문법, 정규식 문자열 DSL은 지원하지 않으며 `SelectorSyntaxError`를 발생시킵니다.
53
+
54
+ ```ts
55
+ type QueryCondition = {
56
+ id?: string | RegExp
57
+ name?: string | RegExp
58
+ type?: string | RegExp
59
+ visible?: boolean
60
+ componentName?: string | RegExp
61
+ }
62
+ ```
63
+
64
+ 객체 조건의 속성은 AND로 결합합니다. 빈 조건 `{}`는 전체 일치입니다. 정규식은 `g`·`y` 플래그와 무관하게 각 노드에서 `lastIndex=0`으로 검사하며 원래 정규식의 상태는 변경하지 않습니다.
65
+
66
+ 트리 검색은 모든 입력 루트를 포함하고 노드 검색은 자신을 제외한 자손만 포함합니다. scope 밖 조상은 계층 선택자에도 사용하지 않습니다. 결과는 입력 순서의 깊이 우선 전위 순서이며, 중첩된 부모·자식이 함께 일치하면 둘 다 반환합니다. 중첩된 `/nodes` 응답에서 같은 ID가 여러 번 나타나면 검색 결과의 ID는 한 번만 반환합니다.
67
+
68
+ `visible` 생략은 `true`이고 부모의 가시성과 별개인 노드 자신의 속성을 검색합니다. 숨김 노드는 기본 탐색에서 제외하지 않습니다. `componentName`은 컴포넌트 메타데이터 이름을 우선하며, 없으면 입력 내 동일 `componentId`의 컴포넌트 이름을 사용합니다. Variant의 메타데이터 이름은 `State=On`처럼 컴포넌트 세트 이름과 다를 수 있습니다.
69
+
70
+ ## 다운로드
71
+
72
+ ```ts
73
+ new FigmaClient({ token, fetch? })
74
+ client.getFile(fileKey, { signal? }): Promise<GetFileResponse>
75
+ client.getNodes(fileKey, nodeIds, { signal? }): Promise<GetFileNodesResponse>
76
+ ```
77
+
78
+ 클라이언트는 원본 응답을 반환하며 캐시·재시도·파일 저장을 수행하지 않습니다. 숫자 노드 ID와 `I49:7403;38:5243` 같은 인스턴스 내부 ID를 지원합니다. `AbortSignal`로 요청을 취소할 수 있습니다.
79
+
80
+ HTTP 실패는 `FigmaHTTPError`의 `status`, `retryAfter`로 확인합니다. 서버 응답 본문과 토큰은 오류 메시지에 넣지 않습니다. 네트워크·JSON 해석·취소 오류는 원래 오류를 전달합니다. 인증 헤더가 다른 호스트로 전달되지 않도록 redirect를 거부합니다.
81
+
82
+ ## 오류
83
+
84
+ - `FigmaInputError`: 잘못된 입력 형태·노드·순환 구조.
85
+ - `SelectorSyntaxError`: 잘못되거나 지원하지 않는 선택자.
86
+ - `IRPluginError`: 플러그인 실행·반환 계약 오류. `pluginName`, `nodeId`, `cause` 제공.
87
+ - `FigmaHTTPError`: API HTTP 실패. 다운로드 서브패스에서 export.
88
+
89
+ 실제 Figma의 파일 응답과 노드 응답은 같은 버전이어도 일부 메타데이터가 다를 수 있습니다. 라이브러리는 각각의 입력을 보존하며 서로 일치하도록 원본을 수정하지 않습니다.
@@ -0,0 +1,70 @@
1
+ # 설계
2
+
3
+ ## 모듈 경계
4
+
5
+ ```text
6
+ src/
7
+ cli/ # 명령·인자·인증 환경·파일 입출력
8
+ figma-json-fetch/ # API 요청·URL 구성·HTTP 오류
9
+ figma-html/ # IR → body 내부 HTML 조각, 브라우저에서도 사용 가능한 순수함수
10
+ figma-tree/ # JSON 정규화·탐색·IR·Tailwind
11
+ model/
12
+ input/
13
+ tree/
14
+ selector/
15
+ ir/
16
+ model/
17
+ converters/
18
+ plugins/
19
+ pipeline.ts
20
+ tailwind/
21
+ ```
22
+
23
+ 하나의 패키지에서 코어는 루트 export, 다운로드는 `figma-json-tree/figma-json-fetch`, HTML은 `figma-json-tree/figma-html` export로 제공합니다. CLI는 별도 실행 엔트리입니다.
24
+
25
+ CLI의 다운로드 명령은 `figma-json-fetch`, 검색 명령은 `figma-tree`에 의존합니다. CLI가 로컬 JSON 파일을 읽고 원본 subtree 출력을 저장하며 선택자 파싱·검색은 기존 코어 API에 위임합니다. 두 라이브러리 모듈은 서로 import하지 않습니다. 공식 `@figma/rest-api-spec`은 타입 전용으로 사용하며 런타임 번들에 포함되지 않습니다.
26
+
27
+ HTML 내보내기는 CLI가 `figma-tree`로 입력을 준비하고 `figma-html`로 렌더링합니다. `figma-html`은 IR 타입만 참조하며 코어 객체나 Tailwind 컴파일러에 런타임 의존하지 않습니다. `figma-tree`도 HTML 렌더러를 import하지 않습니다. 입력 준비와 파일 입출력은 `cli/html`과 `cli/commands/export`가 담당합니다. 출력은 body 내부 마크업이며 문서 wrapper 생성과 CSS 컴파일은 소비 프로젝트가 담당합니다.
28
+
29
+ 코어 내부 데이터 흐름은 다음과 같습니다.
30
+
31
+ ```text
32
+ 입력 JSON → 검증·복제 → 노드 레코드·컴포넌트 인덱스
33
+ 선택자 → 조건 AST → 순회·매칭 → 노드 wrapper
34
+ 선택한 subtree → 기본 속성 변환 → 사용자 플러그인 → IRDocument
35
+ IRDocument → CSS 클래스 매핑 → Tailwind IR
36
+ IR 또는 Tailwind IR → HTML 렌더러 → HTML 조각
37
+ CLI → HTML 조각 → body 내부 마크업 파일
38
+ ```
39
+
40
+ ## SOLID
41
+
42
+ - **SRP:** 클라이언트는 요청, 파서는 문법, 매처는 조건 판정, 변환기는 속성 매핑, CLI는 실행 환경을 담당합니다.
43
+ - **OCP:** 사용자 변환은 `IRPlugin`으로 추가합니다. 변환 엔진 수정 없이 표준 IR 필드와 사용자 확장을 변경할 수 있습니다.
44
+ - **LSP:** 플러그인은 동일한 표준 IR 계약을 지켜야 합니다. 엔진은 잘못된 결과를 플러그인 오류로 보고합니다.
45
+ - **ISP:** 네트워크·트리·IR API를 분리하며 플러그인 계약은 `name`과 `transform`만 요구합니다.
46
+ - **DIP:** 클라이언트는 주입 가능한 `fetch`, CLI 실행부는 주입 가능한 출력과 환경에 의존합니다. 코어는 I/O를 모릅니다.
47
+
48
+ 추상 클래스와 DI 컨테이너는 사용하지 않습니다. 공개 객체 API와 작은 함수 조합으로 경계를 유지합니다.
49
+
50
+ ## SLAP — Single Level of Abstraction Principle
51
+
52
+ 한 함수에서는 같은 추상화 수준의 작업을 다룹니다. `FigmaNode.toIR`은 플러그인 구성을 조합하여 변환 엔진에 전달합니다. 엔진은 노드 변환과 플러그인 실행을 조합하며 색상 채널 계산이나 선택자 문자 처리를 직접 수행하지 않습니다.
53
+
54
+ 저수준의 URL 구성·선택자 파싱·매칭·색상·레이아웃·텍스트·Tailwind 매핑은 같은 입력에 같은 결과를 반환하는 함수로 둡니다. 함수 내부의 임시 배열·Map 변경은 허용하지만 외부 입력은 변경하지 않습니다. 순수함수와 SLAP은 별개의 원칙으로 함께 적용합니다.
55
+
56
+ ## 데이터와 순서
57
+
58
+ 입력은 한 번 복제·동결합니다. 노드 wrapper는 내부 원본을 직접 노출하지 않고 `toJSON`으로 독립 복사본을 반환합니다. 메타데이터와 부모 연결은 JSON 외부의 레코드에 저장하므로 원본 직렬화에 섞이지 않습니다.
59
+
60
+ 탐색은 반복문과 명시적 stack을 사용합니다. 전체 탐색은 노드 수에 비례하며 계층 선택자는 추가 조상 탐색이 필요합니다. 과도하게 복잡한 정규식의 실행 시간은 호출자가 전달한 패턴에 따라 달라집니다.
61
+
62
+ IR은 자식부터 변환하되 결과의 자식 순서는 원본 순서를 유지합니다. 플러그인 결과 검증·복제는 안전한 불변성 경계를 위한 비용이며, 깊은 트리에서 큰 subtree를 매번 교체하는 플러그인은 추가 비용이 발생합니다.
63
+
64
+ ## 빌드와 검증
65
+
66
+ Vite library mode로 두 ESM 엔트리를 만들고 별도 Vite 설정으로 Node CLI를 빌드합니다. `tsc`로 선언을 생성합니다. 공개 배포는 자동으로 수행하지 않습니다.
67
+
68
+ `npm run check`는 Biome lint·포맷·import 정렬 검사, 타입·단위/통합 테스트·빌드·패키지 소비 검증을 실행합니다. 소비 검증은 실제 tarball 설치, 외부 TypeScript 코드 컴파일, Node 실행, CLI 실행 권한, 브라우저 번들을 확인합니다. 실제 Figma 검증은 네트워크와 인증이 필요하므로 기본 테스트와 분리합니다.
69
+
70
+ Biome은 버전을 고정하고 권장 lint 규칙을 사용합니다. 공백 2칸, 작은따옴표, 불필요한 세미콜론 생략, 100자 줄 너비를 적용합니다. `noUncheckedIndexedAccess` 아래에서 길이·범위를 확인한 배열 접근과 테스트의 확정된 fixture 접근에 사용하는 `!`를 허용하도록 `noNonNullAssertion`만 비활성화합니다. `npm run lint:fix`는 안전한 자동 수정을 적용하며 unsafe 수정은 일괄 실행하지 않습니다. [Biome 설정 문서](https://biomejs.dev/reference/configuration/)를 기준으로 설정합니다.
package/docs/html.md ADDED
@@ -0,0 +1,90 @@
1
+ # HTML 출력
2
+
3
+ ## 라이브러리
4
+
5
+ ```ts
6
+ import { renderHTML, collectTailwindClasses } from 'figma-json-tree/figma-html'
7
+
8
+ const ir = selectedFrame.toIR()
9
+ const inlineFragment = renderHTML(ir)
10
+ const tailwind = ir.toTailwind()
11
+ const tailwindFragment = renderHTML(tailwind)
12
+ const candidates = collectTailwindClasses(tailwind)
13
+
14
+ ```
15
+
16
+ `renderHTML(input): string`은 body 내부에 삽입할 HTML 조각만 반환합니다. `doctype`, `html`, `head`, `body`, `style`, `script` 태그를 생성하지 않습니다. `input`은 `IRDocument`, 직렬화된 표준 IR, Tailwind v4 IR을 받습니다. `HTMLInput` 타입은 읽기 전용입니다.
17
+
18
+ 표준 IR은 CSS 속성을 inline `style`로 직렬화합니다. Tailwind IR은 `className`을 `class`로, 잔여 스타일을 `style`로 직렬화합니다. `collectTailwindClasses(input)`은 중복 제거·정렬한 클래스 목록을 반환하고 표준 IR에서는 빈 배열을 반환합니다.
19
+
20
+ 라이브러리 렌더러는 동기식 순수함수이며 CSS 빌드·네트워크·파일 입출력을 하지 않습니다. Tailwind 결과를 삽입할 프로젝트에서 클래스에 맞는 CSS를 빌드해야 합니다.
21
+
22
+ HTML 렌더링 시 Tailwind의 숫자 arbitrary value는 소수점 두 자리까지 반올림하고 불필요한 끝자리 0을 제거합니다. 예: `leading-[14.522727012634277px]` → `leading-[14.52px]`. `collectTailwindClasses`도 동일한 클래스명을 반환합니다. 원본 IR과 inline 스타일 값은 변경하지 않습니다.
23
+
24
+ ## HTML 구조와 안전한 직렬화
25
+
26
+ - IR 노드 하나를 `div` 하나로 표현하고 자식 순서·텍스트·노드 자체의 가시성을 유지합니다. 이름에서 버튼·링크 동작을 추측하지 않습니다.
27
+ - 원본 노드 추적용 `data-figma-id`, `data-figma-name`, `data-figma-type`을 출력합니다.
28
+ - 텍스트와 속성값의 `&`, `<`, `>`, 따옴표를 escape합니다. 노드 이름·텍스트를 HTML 코드로 실행하지 않습니다.
29
+ - 텍스트 앞뒤에 포맷용 공백을 삽입하지 않습니다. IR의 `whiteSpace`를 적용해 줄바꿈을 표현합니다.
30
+ - camelCase CSS 속성을 kebab-case로 바꾸며 CSS custom properties와 vendor prefix를 유지합니다. 문자열의 단위는 그대로 사용하고 숫자에 임의로 px를 붙이지 않습니다.
31
+ - 숨김 노드는 `hidden`과 `display:none`으로 유지합니다. 입력 IR과 diagnostics는 변경하지 않습니다.
32
+ - 순환 자식, 잘못된 기본 노드 형태, 잘못된 CSS 속성 이름·값 타입은 오류로 처리합니다.
33
+
34
+ 스타일은 신뢰할 수 있는 변환기/플러그인이 만든 CSS를 전제로 합니다. 이 API는 범용 CSS sanitizer가 아닙니다.
35
+
36
+ ## CLI
37
+
38
+ ```sh
39
+ figma-json-tree export --input file.json --selector 'FRAME[name="Hover Card"]' \
40
+ --format html --styles inline --out hover-card.html
41
+
42
+ figma-json-tree export --input file.json --selector 'FRAME[id="41:5868"]' \
43
+ --format html --styles tailwind --out hover-card.tailwind.html
44
+
45
+ figma-json-tree export --input subtree.json --out subtree.html
46
+ figma-json-tree export --input design.ir.json --styles tailwind --out design.html
47
+ figma-json-tree export --input design.tailwind-ir.json --out design.html
48
+ ```
49
+
50
+ 설치 전에는 `figma-json-tree` 대신 `node dist/cli/index.js`로 실행합니다.
51
+
52
+ | 옵션 | 동작 |
53
+ | --- | --- |
54
+ | `--input` | 필수. 원본 Figma 파일/노드 응답·단일 subtree·표준 IR·Tailwind IR JSON 파일 |
55
+ | `--selector` | 원본 Figma JSON에서 첫 일치 노드 선택. 생략 시 루트가 정확히 하나여야 함 |
56
+ | `--format` | 현재 `html`만 지원, 기본 `html` |
57
+ | `--styles` | `inline` 또는 `tailwind`. 기본 inline, Tailwind IR 입력에서는 tailwind |
58
+ | `--out` | 결과 파일 경로. 생략 시 stdout |
59
+ | `--force` | 기존 출력 파일 덮어쓰기 |
60
+
61
+ IR 입력에 `--selector`를 사용하는 것은 지원하지 않습니다. 이미 선택된 IR 루트 전체를 렌더링합니다. Tailwind IR은 원래 inline 스타일을 갖고 있지 않으므로 `--styles inline`을 지정하면 오류입니다. `queryAll`의 배열 출력은 입력으로 받지 않습니다.
62
+
63
+ 일치 없음·모호한 루트·잘못된 JSON·쓰기 오류는 stderr와 종료 코드 1을 반환합니다. 정상 HTML 출력은 종료 코드 0이며, IR diagnostics가 있으면 stderr에 진단 개수를 알립니다. stdout에는 body 내부 HTML만 나옵니다. 파일 작성이나 렌더링에 실패하면 성공을 보고하지 않습니다.
64
+
65
+ Tailwind 출력에는 클래스와 잔여 inline 스타일만 포함됩니다. HTML 파일을 Tailwind 소스 탐지 대상으로 등록하거나 `collectTailwindClasses` 결과로 소비 프로젝트의 CSS를 빌드합니다. CLI는 CSS를 컴파일하거나 삽입하지 않습니다.
66
+
67
+ ## 표현 범위
68
+
69
+ 현재 IR이 지원하는 스타일을 표현합니다. 벡터 패스·이미지·효과·stroke alignment 등 미지원 속성이 HTML에서 복원되지는 않습니다. 예를 들어 Hover Card의 벡터 로고는 원본 로고 형태로 재현되지 않습니다. 별도 폰트 파일을 다운로드하거나 HTML에 포함하지 않으므로 시스템에 폰트가 없으면 대체 폰트가 사용됩니다. 고정/절대 레이아웃을 반응형으로 재설계하거나 hover 등의 동작을 생성하지 않습니다.
70
+
71
+ ## 실제 데이터 검증
72
+
73
+ ```sh
74
+ npm run test:html:live
75
+ # 다른 다운로드 디렉터리
76
+ npm run test:html:live -- /absolute/path/to/artifacts/live
77
+ ```
78
+
79
+ 기존에 다운로드한 실제 파일을 사용하며 Figma 토큰이나 새로운 API 요청이 필요하지 않습니다. 파일 키 `vHYqaZykgJgAjgUs1mxjp3`, Frame `41:5868`을 대상으로 다음 6개 경로를 검증합니다.
80
+
81
+ 1. 전체 Figma 응답 → 선택 Frame → inline HTML
82
+ 2. 전체 Figma 응답 → 선택 Frame → Tailwind HTML
83
+ 3. 추출한 단일 Frame → inline HTML
84
+ 4. 저장된 표준 IR → inline HTML
85
+ 5. 저장된 표준 IR → Tailwind HTML
86
+ 6. 저장된 Tailwind IR → Tailwind HTML
87
+
88
+ 모든 경로에서 36개 노드와 텍스트·스타일을 보존하며 같은 출력 모드끼리 HTML이 일치하는지 검사합니다. Tailwind 클래스 92개가 모두 마크업에 포함되고 문서 wrapper·style·script 태그가 없는지 검사합니다. 결과 HTML과 `html-report.json`은 지정 디렉터리에 생성됩니다. 고정 이름의 Hover Card 입력/출력 산출물은 검증 실행 시 다시 생성하며, 원본 `file.json`은 변경하지 않습니다.
89
+
90
+ 이전 독립 문서 버전의 브라우저 측정 증거(`html-browser-report.json`, `hover-card.tailwind.png`)는 문서 wrapper와 CSS가 있던 당시의 기록입니다. 현재 출력은 HTML 조각으로 변경되었으며, Tailwind HTML 조각을 단독으로 열면 프로젝트의 CSS가 적용되지 않습니다. IR 미지원 진단 10개는 유지되며 원본 Figma 화면과 픽셀 단위 일치를 의미하지 않습니다.
package/docs/ir.md ADDED
@@ -0,0 +1,84 @@
1
+ # IR·플러그인·Tailwind
2
+
3
+ ## 표준 IR v1
4
+
5
+ `IRDocument`는 읽기 전용 `root`와 `diagnostics`, `schemaVersion: 1`, 복사본을 만드는 `toJSON()`, Tailwind 결과를 만드는 `toTailwind()`를 제공합니다. JSON 직렬화 시 `toJSON()`이 자동 호출됩니다.
6
+
7
+ 각 노드는 다음 필드를 가집니다.
8
+
9
+ | 필드 | 의미 |
10
+ | --- | --- |
11
+ | `source` | `id`, `name`, Figma `type`, 선택적 `componentName` |
12
+ | `kind` | `container`, `text`, `shape`, `instance`, `unknown` |
13
+ | `visible` | 노드 자체의 가시성 |
14
+ | `layout` | `mode: none/horizontal/vertical`, `wrap` |
15
+ | `size` | 측정 `width/height`, `widthMode/heightMode: FIXED/HUG/FILL` |
16
+ | `style` | CSS 속성 이름과 문자열·숫자 값 |
17
+ | `text` | 텍스트 노드의 `characters` |
18
+ | `children` | 원본 순서의 자식 IR |
19
+ | `extensions` | 사용자 JSON 데이터 |
20
+
21
+ `style`이 Tailwind 변환의 입력입니다. 플러그인에서 `size`나 `layout`을 바꿀 때 CSS도 바꾸려면 `style`을 함께 수정합니다. 후속 변환에서 메타데이터만으로 CSS를 다시 계산하지 않습니다.
22
+
23
+ ## 기본 변환
24
+
25
+ - Auto Layout 방향·간격·패딩·정렬·wrap을 flex CSS로 옮깁니다.
26
+ - 고정 크기는 px, HUG는 `fit-content`, FILL은 부모 주축에서 flex grow, 교차축에서 stretch로 표현합니다. 선택 루트의 FILL은 100%로 표현합니다.
27
+ - Auto Layout 외부 자식과 명시적 절대 배치 자식은 부모 기준 좌표로 옮깁니다. 선택 루트의 위치는 제거하고 `relative`로 둡니다.
28
+ - 단일 단색 fill·stroke, 개별 테두리 폭·모서리, opacity, 기본 텍스트 속성을 지원합니다.
29
+ - 숨김 노드는 유지하고 `display: none`으로 표현합니다.
30
+ - font family 등 클래스화하지 않는 속성은 Tailwind 결과의 잔여 `style`에 남깁니다.
31
+
32
+ 일반 IR은 렌더러 독립적인 기본 디자인 표현입니다. 이미지 다운로드·벡터 패스·복잡한 효과·혼합 텍스트·grid·회전·마스크 등의 완전한 시각 재현은 하지 않습니다. 해당 정보가 있으면 `unsupported-property` 진단에 노드 ID와 속성을 기록합니다. 기본 스타일로 근사되는 stroke alignment 등의 차이도 진단합니다. 원본 값이 필요하면 플러그인 context 또는 원본 subtree를 사용합니다.
33
+
34
+ ## 플러그인 계약
35
+
36
+ ```ts
37
+ interface IRPlugin {
38
+ readonly name: string
39
+ transform(node: DeepReadonly<IRNode>, context: IRPluginContext): DeepReadonly<IRNode>
40
+ }
41
+ ```
42
+
43
+ 읽기 전용 입력을 그대로 반환하거나 spread로 교체한 IR을 반환할 수 있습니다. `transform`은 동기 함수입니다. context에는 읽기 전용 `source`, 원래 트리의 `parent`, 인스턴스의 `component` 메타데이터가 있습니다. 선택 루트를 변환할 때도 context의 `parent`는 원래 부모이며, 기본 좌표 변환만 원점 기준으로 처리합니다.
44
+
45
+ 트리 등록 플러그인과 호출별 플러그인을 배열 순서대로 실행하고 마지막 반환값이 다음 플러그인으로 전달됩니다. 자식의 모든 플러그인이 끝난 뒤 부모가 실행됩니다. 형제 실행 순서는 공개 계약이 아니며 플러그인은 외부 가변 상태에 의존하지 않는 것을 권장합니다.
46
+
47
+ ```ts
48
+ const rounded: IRPlugin = {
49
+ name: 'rounded-buttons',
50
+ transform(node, context) {
51
+ if (context.component?.name !== 'Button') return node
52
+ return {
53
+ ...node,
54
+ style: { ...node.style, borderRadius: '10px' },
55
+ extensions: { ...node.extensions, role: 'button' },
56
+ }
57
+ },
58
+ }
59
+
60
+ const ir = selected.toIR({ plugins: [rounded] })
61
+ ```
62
+
63
+ 엔진은 결과의 표준 노드 구조·자식·스타일·JSON extensions를 검증하고 복제합니다. 클래스 인스턴스, 날짜, 함수, 비유한 숫자, 순환값은 extensions에 넣지 않습니다. 필요한 메타데이터는 JSON 값으로 변환해서 넣습니다. 잘못된 결과는 `IRPluginError`로 보고합니다.
64
+
65
+ ## Tailwind v4
66
+
67
+ ```ts
68
+ const tailwind = ir.toTailwind()
69
+ // { schemaVersion: 1, target: 'tailwind-v4', root, diagnostics }
70
+ // 각 root/children: 기존 IR 필드 + className + 잔여 style
71
+ ```
72
+
73
+ `gap-[13px]`, `text-[length:14px]`, `bg-[rgba(255,128,0,0.5)]`처럼 수치를 보존합니다. 디자인 토큰 근사나 반올림 스케일을 강제하지 않습니다. 색상 채널은 CSS 표현을 위해 8비트로 변환하며 alpha는 소수점 6자리까지 표현합니다. `extensions`는 그대로 유지합니다.
74
+
75
+ Tailwind는 빌드 시 클래스 문자열을 탐지해야 합니다. 런타임에 JSON을 읽는 것만으로 CSS가 생성되지는 않습니다. 미리 생성한 결과 파일을 소스로 등록할 수 있습니다.
76
+
77
+ ```css
78
+ @import "tailwindcss";
79
+ @source "./generated/tailwind-ir.json";
80
+ ```
81
+
82
+ 또는 모든 결과 노드의 클래스 목록을 수집하여 `@source inline("flex gap-[13px] ...")`으로 등록합니다. 이 저장소는 실제 Tailwind CLI로 출력된 모든 클래스의 CSS 생성 여부를 테스트합니다. 잔여 `style`은 소비 렌더러가 inline CSS로 적용해야 합니다. 별도 `figma-html` 모듈과 CLI의 `export --styles tailwind`는 클래스와 잔여 inline 스타일이 있는 body 내부 HTML을 생성합니다. CSS 컴파일과 문서 wrapper는 소비 프로젝트가 담당합니다. JSX 생성기는 제공하지 않습니다. [HTML 출력 문서](html.md)를 참고하세요.
83
+
84
+ 공식 자료: [Figma 파일 API](https://developers.figma.com/docs/rest-api/file-endpoints/), [Figma 노드 속성](https://developers.figma.com/docs/rest-api/file-node-types/), [Tailwind 소스 탐지](https://tailwindcss.com/docs/detecting-classes-in-source-files), [Vite library mode](https://vite.dev/guide/build.html#library-mode).
@@ -0,0 +1,33 @@
1
+ # 실제 검증 결과
2
+
3
+ 검증 시각: 2026-09-10T13:39:52.491Z (UTC)
4
+
5
+ - 파일: @shadcn/ui Design System 2024 (Community)
6
+ - 파일 키: `vHYqaZykgJgAjgUs1mxjp3`
7
+ - 파일 버전: `2397592628893201071`
8
+ - 기준 노드: `49:7390` (Cover)
9
+ - 실행 방식: 새 Figma API 응답 다운로드, CLI 실제 실행
10
+ - 결과: 전체 통과
11
+
12
+ ## 검증 범위
13
+
14
+ | 항목 | 결과 |
15
+ | --- | --- |
16
+ | 전체 파일 탐색 | 7,171개 노드, 독립 순회와 ID·순서 일치 |
17
+ | 기준 subtree | 292개 노드, 각 API 원본과 깊은 동등성 일치 |
18
+ | 타입·이름·componentName 검색 | 통과 |
19
+ | ToBe 정규식 | 실제 파일 0건, 별도 fixture 양성 사례 통과 |
20
+ | 기본 IR·플러그인 | 속성·자식 보존·사용자 확장 검증 통과 |
21
+ | Tailwind v4 | 생성 클래스 446개 모두 CSS 생성 확인 |
22
+ | 자동 검사 | 테스트 66개·타입 검사·빌드 통과 |
23
+ | 패키지 소비 | tarball 설치·선언 파일·Node·CLI·브라우저 번들 통과 |
24
+
25
+ ## 관찰된 제한
26
+
27
+ 동일 버전의 파일 전체 응답과 노드 응답에서 `fontPostScriptName`이 45곳 달랐습니다. 파일 응답은 null, 노드 응답은 문자열이었습니다. 입력을 서로 맞추도록 수정하지 않고 각각 그대로 보존하며 API 간 차이를 별도 기록합니다.
28
+
29
+ 기준 subtree의 미지원 속성 진단은 126개입니다. 이미지/복합 fill, 벡터 geometry, 혼합 텍스트, 효과, 회전, stroke alignment 등 기본 IR의 표현 범위를 넘는 정보이며 픽셀 단위 화면 일치를 검증한 것은 아닙니다.
30
+
31
+ 상세 보고서·다운로드 JSON·IR·Tailwind IR·CSS는 로컬 `artifacts/live/`에 있습니다. 이 디렉터리는 Git과 배포 패키지에서 제외합니다. 토큰은 파일이나 보고서에 저장하지 않았습니다.
32
+
33
+ 재실행: 환경에 `FIGMA_TOKEN`을 설정하고 `npm run test:live`를 실행합니다. 파일이 변경되면 검색 건수와 진단 수가 달라질 수 있습니다.
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "figma-json-tree",
3
+ "version": "0.1.0",
4
+ "description": "Query Figma JSON trees and convert subtrees to extensible design and Tailwind IR.",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "engines": {
8
+ "node": ">=22.12.0"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "docs"
14
+ ],
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/types/figma-tree/index.d.ts",
18
+ "import": "./dist/figma-tree.js"
19
+ },
20
+ "./figma-json-fetch": {
21
+ "types": "./dist/types/figma-json-fetch/index.d.ts",
22
+ "import": "./dist/figma-json-fetch.js"
23
+ },
24
+ "./figma-html": {
25
+ "types": "./dist/types/figma-html/index.d.ts",
26
+ "import": "./dist/figma-html.js"
27
+ }
28
+ },
29
+ "bin": {
30
+ "figma-json-tree": "dist/cli/index.js"
31
+ },
32
+ "scripts": {
33
+ "lint": "biome check . --error-on-warnings",
34
+ "lint:fix": "biome check --write . --error-on-warnings",
35
+ "format": "biome format --write .",
36
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
37
+ "build": "vite build && vite build --config vite.cli.config.ts && tsc --emitDeclarationOnly",
38
+ "test": "vitest run",
39
+ "test:live": "npm run build && tsx tests/live/verify.ts",
40
+ "test:html:live": "npm run build && tsx tests/live/html-verify.ts",
41
+ "test:package": "node tests/package-smoke.mjs",
42
+ "check": "npm run lint && npm run typecheck && npm test && npm run build && npm run test:package"
43
+ },
44
+ "dependencies": {
45
+ "@figma/rest-api-spec": "^0.42.0"
46
+ },
47
+ "devDependencies": {
48
+ "@biomejs/biome": "2.5.13",
49
+ "@tailwindcss/cli": "^4.3.3",
50
+ "@types/node": "^24.0.0",
51
+ "tailwindcss": "^4.3.3",
52
+ "tsx": "^4.20.0",
53
+ "typescript": "^5.9.0",
54
+ "vite": "^8.3.0",
55
+ "vitest": "^5.0.0"
56
+ }
57
+ }