mini-figma-code-connect 0.1.0 → 0.1.3

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 (61) hide show
  1. package/LICENSE +13 -9
  2. package/README.md +52 -130
  3. package/dist/build.mjs +222 -0
  4. package/dist/figma-mapping.mjs +611 -0
  5. package/dist/generate-registry.mjs +79 -0
  6. package/dist/install-skill.mjs +85 -0
  7. package/dist/main/code.d.ts +1 -0
  8. package/dist/main/code.js +388 -0
  9. package/dist/main/handle.d.ts +55 -0
  10. package/dist/main/handle.js +167 -0
  11. package/dist/main/mapping-table.generated.json +1 -0
  12. package/dist/main/messages.d.ts +71 -0
  13. package/dist/main/messages.js +1 -0
  14. package/dist/main/schema.d.ts +9 -0
  15. package/dist/main/schema.js +39 -0
  16. package/dist/main/validate.d.ts +16 -0
  17. package/dist/main/validate.js +40 -0
  18. package/dist/runtime/define.d.ts +3 -0
  19. package/dist/runtime/define.js +4 -0
  20. package/{src/runtime/index.ts → dist/runtime/index.d.ts} +3 -14
  21. package/dist/runtime/index.js +9 -0
  22. package/dist/runtime/registry.d.ts +5 -0
  23. package/dist/runtime/registry.generated.d.ts +3 -0
  24. package/dist/runtime/registry.generated.js +1 -0
  25. package/{src/runtime/registry.ts → dist/runtime/registry.js} +7 -8
  26. package/dist/runtime/render.d.ts +6 -0
  27. package/dist/runtime/render.js +27 -0
  28. package/dist/runtime/tagged.d.ts +6 -0
  29. package/dist/runtime/tagged.js +45 -0
  30. package/dist/runtime/types.d.ts +107 -0
  31. package/dist/runtime/types.js +7 -0
  32. package/dist/scaffold-manifest.mjs +80 -0
  33. package/dist/scaffold-plugin.mjs +462 -0
  34. package/dist/ui/ui.d.ts +84 -0
  35. package/dist/ui/ui.js +190 -0
  36. package/package.json +23 -18
  37. package/scripts/build-plugin.mjs +0 -132
  38. package/scripts/copy-mapping-table.mjs +0 -19
  39. package/scripts/figma-mapping/ai-generate.mjs +0 -102
  40. package/scripts/figma-mapping/index.mjs +0 -250
  41. package/scripts/figma-mapping/lib.mjs +0 -112
  42. package/scripts/figma-mapping/registry.mjs +0 -12
  43. package/scripts/figma-mapping/scaffold.mjs +0 -153
  44. package/scripts/generate-registry.mjs +0 -94
  45. package/scripts/install-skill.mjs +0 -79
  46. package/scripts/is-cli-entrypoint.mjs +0 -21
  47. package/scripts/scaffold-manifest.mjs +0 -84
  48. package/scripts/scaffold-plugin.mjs +0 -172
  49. package/src/dev/demo.ts +0 -207
  50. package/src/dev/export.ts +0 -28
  51. package/src/main/code.ts +0 -422
  52. package/src/main/handle.ts +0 -207
  53. package/src/main/messages.ts +0 -49
  54. package/src/main/schema.ts +0 -45
  55. package/src/main/validate.ts +0 -49
  56. package/src/runtime/define.ts +0 -6
  57. package/src/runtime/render.ts +0 -26
  58. package/src/runtime/tagged.ts +0 -46
  59. package/src/runtime/types.ts +0 -86
  60. package/src/ui/ui.ts +0 -271
  61. /package/{src → dist}/ui/ui.html +0 -0
@@ -0,0 +1,167 @@
1
+ import { lookup } from '../runtime/registry';
2
+ export class ErrorHandle {
3
+ constructor(message) {
4
+ this.message = message;
5
+ this.type = 'ERROR';
6
+ }
7
+ }
8
+ class TextHandle {
9
+ constructor(node) {
10
+ this.node = node;
11
+ this.type = 'TEXT';
12
+ }
13
+ get name() {
14
+ return this.node.name;
15
+ }
16
+ get textContent() {
17
+ return this.node.characters;
18
+ }
19
+ }
20
+ export class InstanceHandle {
21
+ constructor(node, ctx) {
22
+ this.node = node;
23
+ this.ctx = ctx;
24
+ this.type = 'INSTANCE';
25
+ }
26
+ get name() {
27
+ return this.node.name;
28
+ }
29
+ // ── 属性查找:key 带 "#id" 后缀,按显示名找回来 ──
30
+ entryKey(prop) {
31
+ const props = this.node.componentProperties;
32
+ if (Object.prototype.hasOwnProperty.call(props, prop))
33
+ return prop;
34
+ return Object.keys(props).find((k) => k.split('#')[0] === prop);
35
+ }
36
+ /** 保留 type —— 否则 getString 之类无法察觉自己读错了属性类型 */
37
+ entry(prop) {
38
+ const k = this.entryKey(prop);
39
+ if (k === undefined)
40
+ return undefined;
41
+ return this.node.componentProperties[k];
42
+ }
43
+ log(c) {
44
+ this.ctx.calls.push({ ...c, depth: this.ctx.depth });
45
+ }
46
+ getString(prop) {
47
+ const e = this.entry(prop);
48
+ if (!e) {
49
+ this.log({ prop, method: 'getString', ok: false, note: '属性不存在' });
50
+ return '';
51
+ }
52
+ if (e.type === 'INSTANCE_SWAP') {
53
+ // INSTANCE_SWAP 的 value 是个节点 id,直接当字符串吐进代码里毫无意义
54
+ this.log({
55
+ prop,
56
+ method: 'getString',
57
+ ok: false,
58
+ note: '这是 INSTANCE_SWAP 属性,应该用 getInstanceSwap()',
59
+ });
60
+ return '';
61
+ }
62
+ this.log({ prop, method: 'getString', ok: true });
63
+ return String(e.value);
64
+ }
65
+ getBoolean(prop, mapping) {
66
+ const e = this.entry(prop);
67
+ if (!e) {
68
+ this.log({ prop, method: 'getBoolean', ok: false, note: '属性不存在' });
69
+ return mapping ? mapping.false : false;
70
+ }
71
+ // 布尔常被做成 VARIANT,选项写作 "True"/"False"(Figma UI 的默认命名),
72
+ // 所以必须大小写不敏感,否则 Disabled 永远读成 false
73
+ const v = this.ctx.probe ? true : e.value === true || String(e.value).toLowerCase() === 'true';
74
+ this.log({ prop, method: 'getBoolean', ok: true });
75
+ return mapping ? (v ? mapping.true : mapping.false) : v;
76
+ }
77
+ /** 字典查表。命中不了就返回 undefined —— 跟真实 CC 一样静默,靠 validate() 事后揪出来 */
78
+ getEnum(prop, mapping) {
79
+ const keys = Object.keys(mapping);
80
+ const e = this.entry(prop);
81
+ if (!e) {
82
+ this.log({ prop, method: 'getEnum', mappingKeys: keys, ok: false, note: '属性不存在' });
83
+ return undefined;
84
+ }
85
+ const raw = String(e.value);
86
+ const hit = Object.prototype.hasOwnProperty.call(mapping, raw);
87
+ this.log({
88
+ prop,
89
+ method: 'getEnum',
90
+ mappingKeys: keys,
91
+ ok: hit,
92
+ note: hit ? undefined : `当前值 "${raw}" 不在字典里,返回了 undefined`,
93
+ });
94
+ return hit ? mapping[raw] : undefined;
95
+ }
96
+ /** 绑插槽而不是绑图层名:找到把 mainComponent 绑在这个属性上的后代实例 */
97
+ getInstanceSwap(prop) {
98
+ const key = this.entryKey(prop);
99
+ if (key === undefined) {
100
+ this.log({ prop, method: 'getInstanceSwap', ok: false, note: '属性不存在' });
101
+ return null;
102
+ }
103
+ const hit = this.node.findOne((n) => {
104
+ if (n.type !== 'INSTANCE')
105
+ return false;
106
+ const refs = n.componentPropertyReferences;
107
+ return !!refs && refs.mainComponent === key;
108
+ });
109
+ if (!hit) {
110
+ this.log({ prop, method: 'getInstanceSwap', ok: false, note: '找不到绑定该属性的子实例' });
111
+ return new ErrorHandle(`找不到 instance swap 插槽 "${prop}"`);
112
+ }
113
+ this.log({ prop, method: 'getInstanceSwap', ok: true });
114
+ return new InstanceHandle(hit, this.ctx);
115
+ }
116
+ findInstance(layerName) {
117
+ const hit = this.node.findOne((n) => n.type === 'INSTANCE' && n.name === layerName);
118
+ return hit ? new InstanceHandle(hit, this.ctx) : new ErrorHandle(`找不到图层 "${layerName}"`);
119
+ }
120
+ findText(layerName) {
121
+ const hit = this.node.findOne((n) => n.type === 'TEXT' && n.name === layerName);
122
+ return hit ? new TextHandle(hit) : new ErrorHandle(`找不到文本图层 "${layerName}"`);
123
+ }
124
+ template() {
125
+ const ref = this.ctx.mainByNode.get(this.node.id);
126
+ return ref ? lookup(ref.key, ref.name) : null;
127
+ }
128
+ hasCodeConnect() {
129
+ return this.template() !== null;
130
+ }
131
+ codeConnectId() {
132
+ const t = this.template();
133
+ return t ? t.id : null;
134
+ }
135
+ /** 递归求值:子模板的输出直接插进父模板的字面量 */
136
+ executeTemplate() {
137
+ const ref = this.ctx.mainByNode.get(this.node.id);
138
+ if (this.ctx.depth > 8) {
139
+ return { example: [{ type: 'ERROR', message: '嵌套层级过深,已截断' }], id: 'depth-limit' };
140
+ }
141
+ const t = this.template();
142
+ if (!t) {
143
+ // 没有映射 → 返回 INSTANCE 段,保留实例身份(真实 CC 在这里渲染成 pill)
144
+ return {
145
+ example: [
146
+ {
147
+ type: 'INSTANCE',
148
+ guid: this.node.id,
149
+ symbolId: ref ? ref.id : '',
150
+ name: ref ? ref.name : this.node.name,
151
+ },
152
+ ],
153
+ id: 'unmapped',
154
+ };
155
+ }
156
+ const child = new InstanceHandle(this.node, { ...this.ctx, depth: this.ctx.depth + 1 });
157
+ try {
158
+ return t.render(child);
159
+ }
160
+ catch (err) {
161
+ return {
162
+ example: [{ type: 'ERROR', message: `模板 ${t.id} 执行失败: ${String(err)}` }],
163
+ id: t.id,
164
+ };
165
+ }
166
+ }
167
+ }
@@ -0,0 +1 @@
1
+ []
@@ -0,0 +1,71 @@
1
+ import type { AccessorCall, ComponentSchema, ResultSection, TemplateMeta } from '../runtime/types';
2
+ import type { Finding } from './validate';
3
+ type Candidate = {
4
+ id: string;
5
+ name: string;
6
+ mapped: boolean;
7
+ };
8
+ export type UnmappedGroup = {
9
+ componentKey: string;
10
+ componentName: string;
11
+ count: number;
12
+ sampleNodeId: string;
13
+ mapped: boolean;
14
+ codeComponent: string | null;
15
+ };
16
+ export type ToUi = {
17
+ type: 'state';
18
+ state: 'empty' | 'unmapped' | 'error';
19
+ message: string;
20
+ schema?: ComponentSchema;
21
+ /** 该实例在 Figma 里的完整节点地址,拿不到 figma.fileKey 时为 null;仅 unmapped 用得上 */
22
+ figmaUrl?: string | null;
23
+ /** 上一次「一个容器里多个实例」的候选名单,供面板画「返回列表」 */
24
+ candidates?: Candidate[];
25
+ } | {
26
+ type: 'state';
27
+ state: 'candidates';
28
+ message: string;
29
+ candidates: Candidate[];
30
+ } | {
31
+ type: 'state';
32
+ state: 'scan';
33
+ message: string;
34
+ groups: UnmappedGroup[];
35
+ } | {
36
+ type: 'state';
37
+ state: 'ok';
38
+ message: '';
39
+ schema: ComponentSchema;
40
+ template: {
41
+ id: string;
42
+ meta: TemplateMeta;
43
+ };
44
+ snippet: string;
45
+ imports: string[];
46
+ sections: ResultSection[];
47
+ findings: Finding[];
48
+ calls: AccessorCall[];
49
+ candidates?: Candidate[];
50
+ } | {
51
+ type: 'download';
52
+ filename: string;
53
+ json: string;
54
+ };
55
+ export type FromUi = {
56
+ type: 'refresh';
57
+ } | {
58
+ type: 'pick';
59
+ id: string;
60
+ } | {
61
+ type: 'backToList';
62
+ } | {
63
+ type: 'exportSchema';
64
+ } | {
65
+ type: 'scan';
66
+ } | {
67
+ type: 'exportAllSchemas';
68
+ } | {
69
+ type: 'exportMappingTable';
70
+ };
71
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import type { ComponentSchema } from '../runtime/types';
2
+ /**
3
+ * VARIANT 属性的定义住在 ComponentSet 上,其余属性也一并在那儿
4
+ * (ComponentSet 的 componentPropertyDefinitions 同时包含 VARIANT 和 BOOLEAN/TEXT/INSTANCE_SWAP)。
5
+ * 所以只取这一个 owner 不会漏属性,后面所有身份(key / name / id)都以它为准。
6
+ */
7
+ export declare function definitionOwner(main: ComponentNode): ComponentNode | ComponentSetNode;
8
+ /** Step 3:组件 → 属性 schema */
9
+ export declare function extractSchema(main: ComponentNode): Promise<ComponentSchema>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * VARIANT 属性的定义住在 ComponentSet 上,其余属性也一并在那儿
3
+ * (ComponentSet 的 componentPropertyDefinitions 同时包含 VARIANT 和 BOOLEAN/TEXT/INSTANCE_SWAP)。
4
+ * 所以只取这一个 owner 不会漏属性,后面所有身份(key / name / id)都以它为准。
5
+ */
6
+ export function definitionOwner(main) {
7
+ return main.parent && main.parent.type === 'COMPONENT_SET' ? main.parent : main;
8
+ }
9
+ /** Step 3:组件 → 属性 schema */
10
+ export async function extractSchema(main) {
11
+ const owner = definitionOwner(main);
12
+ // 注意:这个 getter 在「是 variant 但拿不到所属 ComponentSet」时会 **抛错**,
13
+ // 不是返回 undefined —— 所以必须 try/catch,`?? {}` 是无效防护。
14
+ let defs = {};
15
+ try {
16
+ defs = owner.componentPropertyDefinitions;
17
+ }
18
+ catch {
19
+ defs = {};
20
+ }
21
+ const properties = Object.keys(defs).map((key) => {
22
+ const def = defs[key];
23
+ return {
24
+ // key 带 "#123:4" 后缀(VARIANT 除外)。只在展示层剥掉,查表时仍用完整 key
25
+ name: key.split('#')[0],
26
+ type: def.type,
27
+ variantOptions: def.variantOptions ? def.variantOptions.slice() : undefined,
28
+ defaultValue: typeof def.defaultValue === 'string' || typeof def.defaultValue === 'boolean'
29
+ ? def.defaultValue
30
+ : undefined,
31
+ };
32
+ });
33
+ return {
34
+ componentId: owner.id,
35
+ componentKey: owner.key,
36
+ componentName: owner.name,
37
+ properties,
38
+ };
39
+ }
@@ -0,0 +1,16 @@
1
+ import type { AccessorCall, ComponentSchema } from '../runtime/types';
2
+ export type Finding = {
3
+ level: 'error' | 'warn';
4
+ text: string;
5
+ };
6
+ /**
7
+ * Step 6 的自动化版本。
8
+ *
9
+ * 思路:不让模板重复声明它消费了什么,而是在 render 期间记录每一次 accessor 调用,
10
+ * 事后拿这份调用记录和 schema 对账。「穷举缺失」和「属性未映射」就都能被机器发现。
11
+ *
12
+ * @param calls 真实一遍 render 的调用记录 —— 用来报错
13
+ * @param coverage 探测一遍(所有 boolean 视为 true)的调用记录 —— 用来算覆盖率,
14
+ * 避免条件分支里的属性被误报成未映射
15
+ */
16
+ export declare function validate(schema: ComponentSchema, calls: AccessorCall[], coverage?: AccessorCall[]): Finding[];
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Step 6 的自动化版本。
3
+ *
4
+ * 思路:不让模板重复声明它消费了什么,而是在 render 期间记录每一次 accessor 调用,
5
+ * 事后拿这份调用记录和 schema 对账。「穷举缺失」和「属性未映射」就都能被机器发现。
6
+ *
7
+ * @param calls 真实一遍 render 的调用记录 —— 用来报错
8
+ * @param coverage 探测一遍(所有 boolean 视为 true)的调用记录 —— 用来算覆盖率,
9
+ * 避免条件分支里的属性被误报成未映射
10
+ */
11
+ export function validate(schema, calls, coverage = calls) {
12
+ const real = calls.filter((c) => c.depth === 0);
13
+ const touched = new Set(coverage.filter((c) => c.depth === 0).map((c) => c.prop));
14
+ const findings = [];
15
+ for (const p of schema.properties) {
16
+ if (!touched.has(p.name)) {
17
+ findings.push({ level: 'warn', text: `属性 "${p.name}" (${p.type}) 未被模板消费` });
18
+ continue;
19
+ }
20
+ if (p.type === 'VARIANT' && p.variantOptions) {
21
+ for (const c of real) {
22
+ if (c.prop !== p.name || c.method !== 'getEnum' || !c.mappingKeys)
23
+ continue;
24
+ const keys = c.mappingKeys;
25
+ const missing = p.variantOptions.filter((v) => keys.indexOf(v) === -1);
26
+ if (missing.length > 0) {
27
+ findings.push({
28
+ level: 'error',
29
+ text: `"${p.name}" 的 getEnum 字典漏了 ${missing.join('、')} —— 选到这些值时会静默产出 undefined`,
30
+ });
31
+ }
32
+ }
33
+ }
34
+ }
35
+ for (const c of real) {
36
+ if (!c.ok)
37
+ findings.push({ level: 'error', text: `${c.method}("${c.prop}"):${c.note ?? '失败'}` });
38
+ }
39
+ return findings;
40
+ }
@@ -0,0 +1,3 @@
1
+ import type { Template } from './types';
2
+ /** 只做类型收窄,让 .figma.ts 里能拿到 accessor 的补全 */
3
+ export declare function defineTemplate(t: Template): Template;
@@ -0,0 +1,4 @@
1
+ /** 只做类型收窄,让 .figma.ts 里能拿到 accessor 的补全 */
2
+ export function defineTemplate(t) {
3
+ return t;
4
+ }
@@ -5,17 +5,6 @@
5
5
  *
6
6
  * import { defineTemplate, code } from 'mini-figma-code-connect'
7
7
  */
8
- export { defineTemplate } from './define'
9
- export { code } from './tagged'
10
- export type {
11
- AccessorCall,
12
- ComponentSchema,
13
- ErrorLike,
14
- FigmaPropertyType,
15
- InstanceLike,
16
- PropertyDef,
17
- ResultSection,
18
- Template,
19
- TemplateResult,
20
- TextLike,
21
- } from './types'
8
+ export { defineTemplate } from './define';
9
+ export { code } from './tagged';
10
+ export type { AccessorCall, ComponentSchema, ErrorLike, FigmaPropertyType, InstanceLike, PropertyDef, ResultSection, Template, TemplateResult, TextLike, } from './types';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 包的公开入口。消费方的 .figma.ts 文件从这里导入,不用相对路径 `'../runtime/define'`——
3
+ * .figma.ts 现在就放在消费方自己的仓库里(比如 trex-website/apps/rexy/figma-mappings/),
4
+ * 跟这个引擎仓库没有相对路径关系,只能靠包名导入:
5
+ *
6
+ * import { defineTemplate, code } from 'mini-figma-code-connect'
7
+ */
8
+ export { defineTemplate } from './define';
9
+ export { code } from './tagged';
@@ -0,0 +1,5 @@
1
+ import type { Template } from './types';
2
+ import { templates } from './registry.generated';
3
+ export { templates };
4
+ /** 先按 componentKey 精确匹配(绑定过的),再退回组件名 */
5
+ export declare function lookup(componentKey: string, componentName: string): Template | null;
@@ -0,0 +1,3 @@
1
+ /** build-package stub — 消费方 buildPlugin 会重定向到自己的 .generated */
2
+ import type { Template } from './types';
3
+ export declare const templates: Template[];
@@ -0,0 +1 @@
1
+ export const templates = [];
@@ -1,14 +1,13 @@
1
- import type { Template } from './types'
2
1
  // templates 数组不再手写维护 —— 由 scripts/generate-registry.mjs 扫 .figma.ts 自动生成,
3
2
  // 每次 build 前重跑,文件挪了/加了/删了自动跟上,不需要手动加 import。
4
3
  // 真实 Code Connect 是把模板发布到 Figma 服务端(CLI publish 或 MCP add_code_connect_map),
5
4
  // 由 Figma 在读取时执行;这里为了「装进插件就能跑」,改成插件内注册表。
6
- import { templates } from './registry.generated'
7
- export { templates }
8
-
5
+ import { templates } from './registry.generated';
6
+ export { templates };
9
7
  /** 先按 componentKey 精确匹配(绑定过的),再退回组件名 */
10
- export function lookup(componentKey: string, componentName: string): Template | null {
11
- const byKey = templates.find((t) => t.match.componentKey && t.match.componentKey === componentKey)
12
- if (byKey) return byKey
13
- return templates.find((t) => t.match.componentName === componentName) ?? null
8
+ export function lookup(componentKey, componentName) {
9
+ const byKey = templates.find((t) => t.match.componentKey && t.match.componentKey === componentKey);
10
+ if (byKey)
11
+ return byKey;
12
+ return templates.find((t) => t.match.componentName === componentName) ?? null;
14
13
  }
@@ -0,0 +1,6 @@
1
+ import type { ResultSection } from './types';
2
+ /** 去掉模板字面量带来的公共缩进 */
3
+ export declare function dedent(text: string): string;
4
+ /** ResultSection[] → 给人看的字符串。真正的消费端(Dev Mode / MCP)拿的是数组本身 */
5
+ export declare function renderToString(sections: ResultSection[]): string;
6
+ export declare function collectErrors(sections: ResultSection[]): string[];
@@ -0,0 +1,27 @@
1
+ /** 去掉模板字面量带来的公共缩进 */
2
+ export function dedent(text) {
3
+ const lines = text.replace(/^\n+/, '').replace(/\s+$/, '').split('\n');
4
+ const indents = lines.filter((l) => l.trim() !== '').map((l) => /^[ \t]*/.exec(l)[0].length);
5
+ const min = indents.length ? Math.min(...indents) : 0;
6
+ return lines.map((l) => l.slice(min)).join('\n');
7
+ }
8
+ /** ResultSection[] → 给人看的字符串。真正的消费端(Dev Mode / MCP)拿的是数组本身 */
9
+ export function renderToString(sections) {
10
+ let out = '';
11
+ for (const s of sections) {
12
+ if (s.type === 'CODE')
13
+ out += s.code;
14
+ else if (s.type === 'INSTANCE')
15
+ out += `{/* <${s.name}> 未连接 Code Connect */}`;
16
+ else
17
+ out += `/* ⚠ ${s.message} */`;
18
+ }
19
+ return dedent(out);
20
+ }
21
+ export function collectErrors(sections) {
22
+ const out = [];
23
+ for (const s of sections)
24
+ if (s.type === 'ERROR')
25
+ out.push(s.message);
26
+ return out;
27
+ }
@@ -0,0 +1,6 @@
1
+ import type { ResultSection } from './types';
2
+ export type Interpolable = ResultSection[] | string | number | boolean | null | undefined;
3
+ export declare function code(strings: TemplateStringsArray, ...values: Interpolable[]): ResultSection[];
4
+ /** 真实 CC 里这些只影响语法高亮,运行时行为完全一致 */
5
+ export declare const tsx: typeof code;
6
+ export declare const html: typeof code;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * figma.code`...` 的简版。
3
+ *
4
+ * 职责只有两件:把插值切成 ResultSection[],把嵌套的 section 数组摊平。
5
+ * 它 **不解析代码语义** —— 这正是真实 Code Connect 的实现边界,
6
+ * 也是为什么用 `+` 或 `.join()` 拼 section 会得到 [object Object]。
7
+ */
8
+ function tag(strings, values) {
9
+ const out = [];
10
+ const pushCode = (s) => {
11
+ if (s === '')
12
+ return;
13
+ const last = out[out.length - 1];
14
+ if (last && last.type === 'CODE')
15
+ last.code += s;
16
+ else
17
+ out.push({ type: 'CODE', code: s });
18
+ };
19
+ strings.forEach((chunk, i) => {
20
+ pushCode(chunk);
21
+ if (i >= values.length)
22
+ return;
23
+ const v = values[i];
24
+ // null / undefined / false 一律不输出,方便写条件插值
25
+ if (v === null || v === undefined || v === false)
26
+ return;
27
+ if (Array.isArray(v)) {
28
+ for (const sec of v) {
29
+ if (sec.type === 'CODE')
30
+ pushCode(sec.code);
31
+ else
32
+ out.push(sec);
33
+ }
34
+ return;
35
+ }
36
+ pushCode(String(v));
37
+ });
38
+ return out;
39
+ }
40
+ export function code(strings, ...values) {
41
+ return tag(strings, values);
42
+ }
43
+ /** 真实 CC 里这些只影响语法高亮,运行时行为完全一致 */
44
+ export const tsx = code;
45
+ export const html = code;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * 迷你 Code Connect 运行时的类型定义。
3
+ *
4
+ * 关键一点,也是整套实现的地基:模板的输出不是字符串,而是 ResultSection[]。
5
+ * 这样才能在拼装时保留实例身份、并让错误就地降级而不是拖垮整段 snippet。
6
+ */
7
+ /** 一段普通代码文本 */
8
+ export type CodeSection = {
9
+ type: 'CODE';
10
+ code: string;
11
+ };
12
+ /** 「有实例但没有映射」的占位段:保留实例身份,真实 CC 在这里渲染成可展开的 pill */
13
+ export type InstanceSection = {
14
+ type: 'INSTANCE';
15
+ guid: string;
16
+ symbolId: string;
17
+ name: string;
18
+ };
19
+ /** 就地降级的错误段 */
20
+ export type ErrorSection = {
21
+ type: 'ERROR';
22
+ message: string;
23
+ };
24
+ export type ResultSection = CodeSection | InstanceSection | ErrorSection;
25
+ /** Figma 组件属性的封闭类型集(真实 CC 还有 SLOT,简版不做) */
26
+ export type FigmaPropertyType = 'TEXT' | 'BOOLEAN' | 'VARIANT' | 'INSTANCE_SWAP';
27
+ export type PropertyDef = {
28
+ name: string;
29
+ type: FigmaPropertyType;
30
+ variantOptions?: string[];
31
+ defaultValue?: string | boolean;
32
+ };
33
+ /** Step 3 的产出:组件的属性 schema —— 映射的「左手边」 */
34
+ export type ComponentSchema = {
35
+ componentId: string;
36
+ componentKey: string;
37
+ componentName: string;
38
+ properties: PropertyDef[];
39
+ };
40
+ export type TemplateResult = {
41
+ example: ResultSection[];
42
+ id: string;
43
+ imports?: string[];
44
+ metadata?: {
45
+ nestable?: boolean;
46
+ props?: Record<string, unknown>;
47
+ };
48
+ };
49
+ /** 头部三行绑定注释的结构化版本(真实 CC 是在构建时解析注释) */
50
+ export type TemplateMeta = {
51
+ url: string;
52
+ source: string;
53
+ component: string;
54
+ };
55
+ export type Template = {
56
+ meta: TemplateMeta;
57
+ id: string;
58
+ /** 怎么认出这个模板对应哪个 Figma 组件:优先 componentKey,退回组件名 */
59
+ match: {
60
+ componentName: string;
61
+ componentKey?: string;
62
+ };
63
+ /**
64
+ * 映射的实体是这个函数,不是任何一段固定代码。
65
+ * 写一份,覆盖该组件的全部 variant 组合。
66
+ */
67
+ render: (instance: InstanceLike) => TemplateResult;
68
+ };
69
+ /** 模板能看到的实例接口(accessor 层) */
70
+ export interface InstanceLike {
71
+ readonly type: 'INSTANCE';
72
+ readonly name: string;
73
+ getString(prop: string): string;
74
+ getBoolean(prop: string): boolean;
75
+ getBoolean<T>(prop: string, mapping: {
76
+ true: T;
77
+ false: T;
78
+ }): T;
79
+ getEnum<T>(prop: string, mapping: Record<string, T>): T | undefined;
80
+ getInstanceSwap(prop: string): InstanceLike | ErrorLike | null;
81
+ findInstance(layerName: string): InstanceLike | ErrorLike;
82
+ findText(layerName: string): TextLike | ErrorLike;
83
+ hasCodeConnect(): boolean;
84
+ codeConnectId(): string | null;
85
+ executeTemplate(): TemplateResult | null;
86
+ }
87
+ /** 注意 type 是 'ERROR' 而不是 null —— 查找失败返回的是 truthy 的句柄。
88
+ * 这是真实 CC 的实现权衡:为了把 message 带下去渲染成 ERROR 段而放弃 null 惯例。
89
+ * 代价就是模板里每次都得写 `x.type === 'INSTANCE'` 检查。 */
90
+ export interface ErrorLike {
91
+ readonly type: 'ERROR';
92
+ readonly message: string;
93
+ }
94
+ export interface TextLike {
95
+ readonly type: 'TEXT';
96
+ readonly name: string;
97
+ readonly textContent: string;
98
+ }
99
+ /** 一次 accessor 调用的记录,供 Step 6 的自动校验使用 */
100
+ export type AccessorCall = {
101
+ depth: number;
102
+ prop: string;
103
+ method: 'getString' | 'getBoolean' | 'getEnum' | 'getInstanceSwap';
104
+ mappingKeys?: string[];
105
+ ok: boolean;
106
+ note?: string;
107
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 迷你 Code Connect 运行时的类型定义。
3
+ *
4
+ * 关键一点,也是整套实现的地基:模板的输出不是字符串,而是 ResultSection[]。
5
+ * 这样才能在拼装时保留实例身份、并让错误就地降级而不是拖垮整段 snippet。
6
+ */
7
+ export {};