flutterflow-mcp 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.
- package/README.md +124 -0
- package/build/api/flutterflow.d.ts +11 -0
- package/build/api/flutterflow.js +61 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +54 -0
- package/build/prompts/dev-workflow.d.ts +2 -0
- package/build/prompts/dev-workflow.js +68 -0
- package/build/prompts/generate-page.d.ts +2 -0
- package/build/prompts/generate-page.js +36 -0
- package/build/prompts/inspect-project.d.ts +2 -0
- package/build/prompts/inspect-project.js +30 -0
- package/build/prompts/modify-component.d.ts +2 -0
- package/build/prompts/modify-component.js +39 -0
- package/build/resources/docs.d.ts +2 -0
- package/build/resources/docs.js +76 -0
- package/build/resources/projects.d.ts +3 -0
- package/build/resources/projects.js +60 -0
- package/build/tools/find-component-usages.d.ts +7 -0
- package/build/tools/find-component-usages.js +225 -0
- package/build/tools/find-page-navigations.d.ts +7 -0
- package/build/tools/find-page-navigations.js +228 -0
- package/build/tools/get-component-summary.d.ts +22 -0
- package/build/tools/get-component-summary.js +193 -0
- package/build/tools/get-page-by-name.d.ts +3 -0
- package/build/tools/get-page-by-name.js +56 -0
- package/build/tools/get-page-summary.d.ts +22 -0
- package/build/tools/get-page-summary.js +220 -0
- package/build/tools/get-yaml-docs.d.ts +6 -0
- package/build/tools/get-yaml-docs.js +217 -0
- package/build/tools/get-yaml.d.ts +3 -0
- package/build/tools/get-yaml.js +47 -0
- package/build/tools/list-files.d.ts +3 -0
- package/build/tools/list-files.js +30 -0
- package/build/tools/list-pages.d.ts +25 -0
- package/build/tools/list-pages.js +101 -0
- package/build/tools/list-projects.d.ts +3 -0
- package/build/tools/list-projects.js +19 -0
- package/build/tools/sync-project.d.ts +3 -0
- package/build/tools/sync-project.js +144 -0
- package/build/tools/update-yaml.d.ts +3 -0
- package/build/tools/update-yaml.js +24 -0
- package/build/tools/validate-yaml.d.ts +3 -0
- package/build/tools/validate-yaml.js +22 -0
- package/build/utils/cache.d.ts +48 -0
- package/build/utils/cache.js +162 -0
- package/build/utils/decode-yaml.d.ts +7 -0
- package/build/utils/decode-yaml.js +31 -0
- package/build/utils/page-summary/action-summarizer.d.ts +9 -0
- package/build/utils/page-summary/action-summarizer.js +291 -0
- package/build/utils/page-summary/formatter.d.ts +13 -0
- package/build/utils/page-summary/formatter.js +121 -0
- package/build/utils/page-summary/node-extractor.d.ts +17 -0
- package/build/utils/page-summary/node-extractor.js +207 -0
- package/build/utils/page-summary/tree-walker.d.ts +6 -0
- package/build/utils/page-summary/tree-walker.js +55 -0
- package/build/utils/page-summary/types.d.ts +56 -0
- package/build/utils/page-summary/types.js +4 -0
- package/build/utils/parse-folders.d.ts +9 -0
- package/build/utils/parse-folders.js +29 -0
- package/docs/ff-yaml/00-overview.md +137 -0
- package/docs/ff-yaml/01-project-files.md +513 -0
- package/docs/ff-yaml/02-pages.md +572 -0
- package/docs/ff-yaml/03-components.md +413 -0
- package/docs/ff-yaml/04-widgets/README.md +122 -0
- package/docs/ff-yaml/04-widgets/button.md +444 -0
- package/docs/ff-yaml/04-widgets/container.md +358 -0
- package/docs/ff-yaml/04-widgets/dropdown.md +579 -0
- package/docs/ff-yaml/04-widgets/form.md +256 -0
- package/docs/ff-yaml/04-widgets/image.md +276 -0
- package/docs/ff-yaml/04-widgets/layout.md +355 -0
- package/docs/ff-yaml/04-widgets/misc.md +553 -0
- package/docs/ff-yaml/04-widgets/text-field.md +326 -0
- package/docs/ff-yaml/04-widgets/text.md +302 -0
- package/docs/ff-yaml/05-actions.md +843 -0
- package/docs/ff-yaml/06-variables.md +834 -0
- package/docs/ff-yaml/07-data.md +591 -0
- package/docs/ff-yaml/08-custom-code.md +715 -0
- package/docs/ff-yaml/09-theming.md +592 -0
- package/docs/ff-yaml/10-editing-guide.md +454 -0
- package/docs/ff-yaml/README.md +105 -0
- package/package.json +55 -0
- package/skills/ff-widget-patterns.md +141 -0
- package/skills/ff-yaml-dev.md +58 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Summarize trigger_actions chains for a widget node.
|
|
3
|
+
*
|
|
4
|
+
* Walks the rootAction→followUpAction→conditionActions chain in the trigger
|
|
5
|
+
* file, reads individual action files, and classifies each action by type.
|
|
6
|
+
*/
|
|
7
|
+
import YAML from "yaml";
|
|
8
|
+
import { cacheRead, listCachedKeys } from "../cache.js";
|
|
9
|
+
/**
|
|
10
|
+
* Collect all action keys referenced in a trigger chain.
|
|
11
|
+
* Flattens followUpAction chains, conditionActions, and parallelActions.
|
|
12
|
+
*/
|
|
13
|
+
function collectActionKeys(node) {
|
|
14
|
+
const keys = [];
|
|
15
|
+
// Direct action reference
|
|
16
|
+
const action = node.action;
|
|
17
|
+
if (action?.key) {
|
|
18
|
+
keys.push(action.key);
|
|
19
|
+
}
|
|
20
|
+
// Conditional branches
|
|
21
|
+
const cond = node.conditionActions;
|
|
22
|
+
if (cond) {
|
|
23
|
+
// True branches
|
|
24
|
+
const trueActions = cond.trueActions;
|
|
25
|
+
if (Array.isArray(trueActions)) {
|
|
26
|
+
for (const branch of trueActions) {
|
|
27
|
+
const trueAction = branch.trueAction;
|
|
28
|
+
if (trueAction) {
|
|
29
|
+
keys.push(...collectActionKeys(trueAction));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// False branch
|
|
34
|
+
const falseAction = cond.falseAction;
|
|
35
|
+
if (falseAction && !("terminate" in falseAction)) {
|
|
36
|
+
keys.push(...collectActionKeys(falseAction));
|
|
37
|
+
}
|
|
38
|
+
// followUpAction after conditionActions
|
|
39
|
+
const condFollowUp = cond.followUpAction;
|
|
40
|
+
if (condFollowUp) {
|
|
41
|
+
keys.push(...collectActionKeys(condFollowUp));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Parallel actions
|
|
45
|
+
const parallel = node.parallelActions;
|
|
46
|
+
if (parallel) {
|
|
47
|
+
const actions = parallel.actions;
|
|
48
|
+
if (Array.isArray(actions)) {
|
|
49
|
+
for (const branch of actions) {
|
|
50
|
+
keys.push(...collectActionKeys(branch));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Follow-up chain
|
|
55
|
+
const followUp = node.followUpAction;
|
|
56
|
+
if (followUp) {
|
|
57
|
+
keys.push(...collectActionKeys(followUp));
|
|
58
|
+
}
|
|
59
|
+
return keys;
|
|
60
|
+
}
|
|
61
|
+
/** Known top-level keys that identify an action type. */
|
|
62
|
+
const ACTION_TYPE_KEYS = [
|
|
63
|
+
"navigate", "customAction", "database", "localStateUpdate", "waitAction",
|
|
64
|
+
"alertDialog", "bottomSheet", "revenueCat", "auth", "rebuild", "scrollTo",
|
|
65
|
+
"copyToClipboard", "share", "hapticFeedback",
|
|
66
|
+
];
|
|
67
|
+
/**
|
|
68
|
+
* Recursively search an object tree for the first recognizable action.
|
|
69
|
+
* Used to unwrap disableAction nodes where the real action is buried
|
|
70
|
+
* inside conditionalActions or other nesting.
|
|
71
|
+
*/
|
|
72
|
+
function findDeepAction(obj, depth = 0) {
|
|
73
|
+
if (!obj || typeof obj !== "object" || depth > 8)
|
|
74
|
+
return null;
|
|
75
|
+
const o = obj;
|
|
76
|
+
// Check if this object itself is a classifiable action
|
|
77
|
+
for (const key of ACTION_TYPE_KEYS) {
|
|
78
|
+
if (key in o)
|
|
79
|
+
return classifyAction(o);
|
|
80
|
+
}
|
|
81
|
+
// Recurse into object values
|
|
82
|
+
for (const val of Object.values(o)) {
|
|
83
|
+
if (val && typeof val === "object") {
|
|
84
|
+
const found = findDeepAction(val, depth + 1);
|
|
85
|
+
if (found)
|
|
86
|
+
return found;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Classify an action YAML into a human-readable summary.
|
|
93
|
+
*/
|
|
94
|
+
function classifyAction(doc) {
|
|
95
|
+
// Disabled action — unwrap and recursively find the inner action
|
|
96
|
+
if ("disableAction" in doc) {
|
|
97
|
+
const da = doc.disableAction;
|
|
98
|
+
const actionNode = da.actionNode;
|
|
99
|
+
if (actionNode) {
|
|
100
|
+
const inner = findDeepAction(actionNode);
|
|
101
|
+
if (inner) {
|
|
102
|
+
return {
|
|
103
|
+
type: `[DISABLED] ${inner.type}`,
|
|
104
|
+
detail: inner.detail,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { type: "[DISABLED]", detail: "" };
|
|
109
|
+
}
|
|
110
|
+
// Navigate
|
|
111
|
+
if ("navigate" in doc) {
|
|
112
|
+
const nav = doc.navigate;
|
|
113
|
+
if (nav.isNavigateBack) {
|
|
114
|
+
return { type: "navigate", detail: "back" };
|
|
115
|
+
}
|
|
116
|
+
return { type: "navigate", detail: "to page" };
|
|
117
|
+
}
|
|
118
|
+
// Custom action
|
|
119
|
+
if ("customAction" in doc) {
|
|
120
|
+
const ca = doc.customAction;
|
|
121
|
+
const id = ca.customActionIdentifier;
|
|
122
|
+
const name = doc.outputVariableName;
|
|
123
|
+
return {
|
|
124
|
+
type: "customAction",
|
|
125
|
+
detail: name || id?.key || "unknown",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// Database
|
|
129
|
+
if ("database" in doc) {
|
|
130
|
+
const db = doc.database;
|
|
131
|
+
const pg = db.postgresAction;
|
|
132
|
+
if (pg) {
|
|
133
|
+
const table = pg.tableIdentifier;
|
|
134
|
+
const tableName = table?.name || "table";
|
|
135
|
+
const op = "insert" in pg ? "insert" : "update" in pg ? "update" : "query" in pg ? "query" : "delete" in pg ? "delete" : "op";
|
|
136
|
+
return { type: "database", detail: `${op} ${tableName}` };
|
|
137
|
+
}
|
|
138
|
+
const firestore = db.firestoreAction;
|
|
139
|
+
if (firestore) {
|
|
140
|
+
return { type: "database", detail: "firestore" };
|
|
141
|
+
}
|
|
142
|
+
return { type: "database", detail: "" };
|
|
143
|
+
}
|
|
144
|
+
// Local state update
|
|
145
|
+
if ("localStateUpdate" in doc) {
|
|
146
|
+
const lsu = doc.localStateUpdate;
|
|
147
|
+
const stateType = lsu.stateVariableType || "";
|
|
148
|
+
const updates = lsu.updates;
|
|
149
|
+
if (stateType === "APP_STATE") {
|
|
150
|
+
return { type: "updateAppState", detail: "" };
|
|
151
|
+
}
|
|
152
|
+
if (updates && updates.length > 0) {
|
|
153
|
+
const first = updates[0];
|
|
154
|
+
if ("increment" in first)
|
|
155
|
+
return { type: "updateState", detail: "increment" };
|
|
156
|
+
if ("dataStructUpdate" in first)
|
|
157
|
+
return { type: "updateState", detail: "struct" };
|
|
158
|
+
}
|
|
159
|
+
return { type: "updateState", detail: "" };
|
|
160
|
+
}
|
|
161
|
+
// Wait
|
|
162
|
+
if ("waitAction" in doc) {
|
|
163
|
+
const wa = doc.waitAction;
|
|
164
|
+
const dur = wa.durationMillisValue;
|
|
165
|
+
const ms = dur?.inputValue;
|
|
166
|
+
return { type: "wait", detail: ms ? `${ms}ms` : "" };
|
|
167
|
+
}
|
|
168
|
+
// Alert dialog
|
|
169
|
+
if ("alertDialog" in doc) {
|
|
170
|
+
return { type: "alertDialog", detail: "" };
|
|
171
|
+
}
|
|
172
|
+
// Bottom sheet
|
|
173
|
+
if ("bottomSheet" in doc) {
|
|
174
|
+
return { type: "bottomSheet", detail: "" };
|
|
175
|
+
}
|
|
176
|
+
// RevenueCat
|
|
177
|
+
if ("revenueCat" in doc) {
|
|
178
|
+
const rc = doc.revenueCat;
|
|
179
|
+
if ("purchase" in rc)
|
|
180
|
+
return { type: "revenueCat", detail: "purchase" };
|
|
181
|
+
if ("restore" in rc)
|
|
182
|
+
return { type: "revenueCat", detail: "restore" };
|
|
183
|
+
if ("paywall" in rc) {
|
|
184
|
+
const pw = rc.paywall;
|
|
185
|
+
const eid = pw.entitlementId;
|
|
186
|
+
const iv = eid?.inputValue;
|
|
187
|
+
const name = iv?.serializedValue || "";
|
|
188
|
+
return { type: "revenueCat", detail: name ? `paywall (${name})` : "paywall" };
|
|
189
|
+
}
|
|
190
|
+
return { type: "revenueCat", detail: "" };
|
|
191
|
+
}
|
|
192
|
+
// Authentication
|
|
193
|
+
if ("auth" in doc) {
|
|
194
|
+
return { type: "auth", detail: "" };
|
|
195
|
+
}
|
|
196
|
+
// Rebuild (update widget / rebuild)
|
|
197
|
+
if ("rebuild" in doc) {
|
|
198
|
+
return { type: "rebuild", detail: "" };
|
|
199
|
+
}
|
|
200
|
+
// Scroll to
|
|
201
|
+
if ("scrollTo" in doc) {
|
|
202
|
+
return { type: "scrollTo", detail: "" };
|
|
203
|
+
}
|
|
204
|
+
// Copy to clipboard
|
|
205
|
+
if ("copyToClipboard" in doc) {
|
|
206
|
+
return { type: "copyToClipboard", detail: "" };
|
|
207
|
+
}
|
|
208
|
+
// Share
|
|
209
|
+
if ("share" in doc) {
|
|
210
|
+
return { type: "share", detail: "" };
|
|
211
|
+
}
|
|
212
|
+
// Haptic feedback
|
|
213
|
+
if ("hapticFeedback" in doc) {
|
|
214
|
+
return { type: "haptic", detail: "" };
|
|
215
|
+
}
|
|
216
|
+
// Terminate sentinel — skip
|
|
217
|
+
if ("terminate" in doc) {
|
|
218
|
+
return { type: "terminate", detail: "" };
|
|
219
|
+
}
|
|
220
|
+
// Fallback: use first key
|
|
221
|
+
const actionKeys = Object.keys(doc).filter((k) => k !== "key" && k !== "outputVariableName");
|
|
222
|
+
return { type: actionKeys[0] || "unknown", detail: "" };
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Read all trigger_actions for a node and return summaries.
|
|
226
|
+
*
|
|
227
|
+
* @param projectId - FF project ID
|
|
228
|
+
* @param nodeFileKeyPrefix - Cache prefix for the node's trigger actions,
|
|
229
|
+
* e.g. "page/id-Scaffold_xxx/page-widget-tree-outline/node/id-Button_yyy"
|
|
230
|
+
*/
|
|
231
|
+
export async function summarizeTriggers(projectId, nodeFileKeyPrefix) {
|
|
232
|
+
const triggerPrefix = `${nodeFileKeyPrefix}/trigger_actions/`;
|
|
233
|
+
const allKeys = await listCachedKeys(projectId, triggerPrefix);
|
|
234
|
+
// Find trigger definition files (e.g. trigger_actions/id-ON_TAP)
|
|
235
|
+
// These don't have /action/ in the path
|
|
236
|
+
const triggerDefKeys = allKeys.filter((k) => !k.includes("/action/") && k.startsWith(triggerPrefix + "id-"));
|
|
237
|
+
// Deduplicate: a key like "trigger_actions/id-ON_TAP" may also appear as a
|
|
238
|
+
// directory prefix "trigger_actions/id-ON_TAP/action/..." — we only want the
|
|
239
|
+
// file, which is the shortest matching key
|
|
240
|
+
const triggerFiles = triggerDefKeys.filter((k) => {
|
|
241
|
+
const afterPrefix = k.slice(triggerPrefix.length);
|
|
242
|
+
// Should be just "id-ON_TAP" with no further slashes
|
|
243
|
+
return !afterPrefix.includes("/");
|
|
244
|
+
});
|
|
245
|
+
const results = [];
|
|
246
|
+
for (const triggerFileKey of triggerFiles) {
|
|
247
|
+
const content = await cacheRead(projectId, triggerFileKey);
|
|
248
|
+
if (!content)
|
|
249
|
+
continue;
|
|
250
|
+
let doc;
|
|
251
|
+
try {
|
|
252
|
+
doc = YAML.parse(content);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
// Extract trigger type
|
|
258
|
+
const trigger = doc.trigger;
|
|
259
|
+
const triggerType = trigger?.triggerType || "UNKNOWN";
|
|
260
|
+
// Collect action keys from chain
|
|
261
|
+
const rootAction = doc.rootAction;
|
|
262
|
+
if (!rootAction)
|
|
263
|
+
continue;
|
|
264
|
+
const actionKeys = collectActionKeys(rootAction);
|
|
265
|
+
// Deduplicate while preserving order
|
|
266
|
+
const uniqueKeys = [...new Set(actionKeys)];
|
|
267
|
+
// Read each action file
|
|
268
|
+
const actionPrefix = `${triggerFileKey}/action/`;
|
|
269
|
+
const actions = [];
|
|
270
|
+
for (const actionKey of uniqueKeys) {
|
|
271
|
+
const actionFileKey = `${actionPrefix}id-${actionKey}`;
|
|
272
|
+
const actionContent = await cacheRead(projectId, actionFileKey);
|
|
273
|
+
if (!actionContent)
|
|
274
|
+
continue;
|
|
275
|
+
try {
|
|
276
|
+
const actionDoc = YAML.parse(actionContent);
|
|
277
|
+
const summary = classifyAction(actionDoc);
|
|
278
|
+
if (summary.type !== "terminate") {
|
|
279
|
+
actions.push(summary);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
actions.push({ type: "unknown", detail: actionKey });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (actions.length > 0) {
|
|
287
|
+
results.push({ trigger: triggerType, actions });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return results;
|
|
291
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render a PageMeta + SummaryNode tree as a readable text summary
|
|
3
|
+
* with box-drawing characters and inline trigger summaries.
|
|
4
|
+
*/
|
|
5
|
+
import { ComponentMeta, PageMeta, SummaryNode } from "./types.js";
|
|
6
|
+
/**
|
|
7
|
+
* Format a complete page summary as text.
|
|
8
|
+
*/
|
|
9
|
+
export declare function formatPageSummary(meta: PageMeta, tree: SummaryNode): string;
|
|
10
|
+
/**
|
|
11
|
+
* Format a complete component summary as text.
|
|
12
|
+
*/
|
|
13
|
+
export declare function formatComponentSummary(meta: ComponentMeta, tree: SummaryNode): string;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/** Format a single action into a compact string. */
|
|
2
|
+
function fmtAction(a) {
|
|
3
|
+
if (a.detail)
|
|
4
|
+
return `${a.type}: ${a.detail}`;
|
|
5
|
+
return a.type;
|
|
6
|
+
}
|
|
7
|
+
/** Format a trigger into a compact inline string like "ON_TAP: [navigate to page, customAction: foo]". */
|
|
8
|
+
function fmtTrigger(t) {
|
|
9
|
+
const acts = t.actions.map(fmtAction).join(", ");
|
|
10
|
+
return `${t.trigger} → [${acts}]`;
|
|
11
|
+
}
|
|
12
|
+
/** Format the slot prefix for display. */
|
|
13
|
+
function fmtSlot(slot) {
|
|
14
|
+
if (slot === "children" || slot === "root")
|
|
15
|
+
return "";
|
|
16
|
+
return `[${slot}] `;
|
|
17
|
+
}
|
|
18
|
+
/** Build a display label for a node. */
|
|
19
|
+
function nodeLabel(node) {
|
|
20
|
+
const parts = [];
|
|
21
|
+
parts.push(fmtSlot(node.slot));
|
|
22
|
+
parts.push(node.type);
|
|
23
|
+
if (node.name) {
|
|
24
|
+
parts.push(` (${node.name})`);
|
|
25
|
+
}
|
|
26
|
+
if (node.detail) {
|
|
27
|
+
parts.push(` ${node.detail}`);
|
|
28
|
+
}
|
|
29
|
+
return parts.join("");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Render the widget tree recursively with box-drawing characters.
|
|
33
|
+
*/
|
|
34
|
+
function renderTree(node, prefix, isLast, isRoot, lines) {
|
|
35
|
+
// Build connector
|
|
36
|
+
const connector = isRoot ? "" : isLast ? "└── " : "├── ";
|
|
37
|
+
const label = nodeLabel(node);
|
|
38
|
+
// Triggers inline
|
|
39
|
+
const triggerStr = node.triggers.length > 0
|
|
40
|
+
? " → " + node.triggers.map(fmtTrigger).join("; ")
|
|
41
|
+
: "";
|
|
42
|
+
lines.push(`${prefix}${connector}${label}${triggerStr}`);
|
|
43
|
+
// Child prefix
|
|
44
|
+
const childPrefix = isRoot ? prefix : prefix + (isLast ? " " : "│ ");
|
|
45
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
46
|
+
renderTree(node.children[i], childPrefix, i === node.children.length - 1, false, lines);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Format a complete page summary as text.
|
|
51
|
+
*/
|
|
52
|
+
export function formatPageSummary(meta, tree) {
|
|
53
|
+
const lines = [];
|
|
54
|
+
// Header
|
|
55
|
+
lines.push(`${meta.pageName} (${meta.scaffoldId}) — folder: ${meta.folder}`);
|
|
56
|
+
// Params
|
|
57
|
+
if (meta.params.length > 0) {
|
|
58
|
+
const paramStrs = meta.params.map((p) => {
|
|
59
|
+
const def = p.defaultValue ? `, default: ${p.defaultValue}` : "";
|
|
60
|
+
return `${p.name} (${p.dataType}${def})`;
|
|
61
|
+
});
|
|
62
|
+
lines.push(`Params: ${paramStrs.join(", ")}`);
|
|
63
|
+
}
|
|
64
|
+
// State fields
|
|
65
|
+
if (meta.stateFields.length > 0) {
|
|
66
|
+
const stateStrs = meta.stateFields.map((s) => {
|
|
67
|
+
const def = s.defaultValue ? `, default: ${s.defaultValue}` : "";
|
|
68
|
+
return `${s.name} (${s.dataType}${def})`;
|
|
69
|
+
});
|
|
70
|
+
lines.push(`State: ${stateStrs.join(", ")}`);
|
|
71
|
+
}
|
|
72
|
+
// Scaffold-level triggers
|
|
73
|
+
if (tree.triggers.length > 0) {
|
|
74
|
+
lines.push("");
|
|
75
|
+
for (const t of tree.triggers) {
|
|
76
|
+
lines.push(fmtTrigger(t));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Widget tree
|
|
80
|
+
lines.push("");
|
|
81
|
+
lines.push("Widget Tree:");
|
|
82
|
+
// Render children directly (skip the Scaffold root itself)
|
|
83
|
+
for (let i = 0; i < tree.children.length; i++) {
|
|
84
|
+
renderTree(tree.children[i], "", i === tree.children.length - 1, false, lines);
|
|
85
|
+
}
|
|
86
|
+
return lines.join("\n");
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Format a complete component summary as text.
|
|
90
|
+
*/
|
|
91
|
+
export function formatComponentSummary(meta, tree) {
|
|
92
|
+
const lines = [];
|
|
93
|
+
// Header
|
|
94
|
+
lines.push(`${meta.componentName} (${meta.containerId})`);
|
|
95
|
+
if (meta.description) {
|
|
96
|
+
lines.push(`Description: ${meta.description}`);
|
|
97
|
+
}
|
|
98
|
+
// Params
|
|
99
|
+
if (meta.params.length > 0) {
|
|
100
|
+
const paramStrs = meta.params.map((p) => {
|
|
101
|
+
const def = p.defaultValue ? `, default: ${p.defaultValue}` : "";
|
|
102
|
+
return `${p.name} (${p.dataType}${def})`;
|
|
103
|
+
});
|
|
104
|
+
lines.push(`Params: ${paramStrs.join(", ")}`);
|
|
105
|
+
}
|
|
106
|
+
// Root-level triggers
|
|
107
|
+
if (tree.triggers.length > 0) {
|
|
108
|
+
lines.push("");
|
|
109
|
+
for (const t of tree.triggers) {
|
|
110
|
+
lines.push(fmtTrigger(t));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Widget tree
|
|
114
|
+
lines.push("");
|
|
115
|
+
lines.push("Widget Tree:");
|
|
116
|
+
// Render children directly (skip the Container root itself)
|
|
117
|
+
for (let i = 0; i < tree.children.length; i++) {
|
|
118
|
+
renderTree(tree.children[i], "", i === tree.children.length - 1, false, lines);
|
|
119
|
+
}
|
|
120
|
+
return lines.join("\n");
|
|
121
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface NodeInfo {
|
|
2
|
+
type: string;
|
|
3
|
+
name: string;
|
|
4
|
+
detail: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Infer widget type from the key prefix (e.g. "Text_abc123" → "Text").
|
|
8
|
+
*/
|
|
9
|
+
export declare function inferTypeFromKey(key: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Read a node's cached file and extract type, name, and detail.
|
|
12
|
+
*
|
|
13
|
+
* @param projectId - The FF project ID
|
|
14
|
+
* @param pagePrefix - Cache key prefix for the page, e.g. "page/id-Scaffold_xxx/page-widget-tree-outline"
|
|
15
|
+
* @param nodeKey - The node key, e.g. "Button_uaqbabys"
|
|
16
|
+
*/
|
|
17
|
+
export declare function extractNodeInfo(projectId: string, pagePrefix: string, nodeKey: string): Promise<NodeInfo>;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a node's cached YAML and extract human-readable details
|
|
3
|
+
* based on widget type (text value, button label, image path, etc.).
|
|
4
|
+
*/
|
|
5
|
+
import YAML from "yaml";
|
|
6
|
+
import { cacheRead } from "../cache.js";
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the inputValue from a FF value object.
|
|
9
|
+
* Returns the literal string, or "[dynamic]" for variable references.
|
|
10
|
+
*/
|
|
11
|
+
function resolveValue(obj) {
|
|
12
|
+
if (obj == null)
|
|
13
|
+
return "";
|
|
14
|
+
if (typeof obj === "string" || typeof obj === "number")
|
|
15
|
+
return String(obj);
|
|
16
|
+
if (typeof obj !== "object")
|
|
17
|
+
return "";
|
|
18
|
+
const o = obj;
|
|
19
|
+
// Direct inputValue
|
|
20
|
+
if ("inputValue" in o) {
|
|
21
|
+
const iv = o.inputValue;
|
|
22
|
+
if (typeof iv === "string" || typeof iv === "number")
|
|
23
|
+
return String(iv);
|
|
24
|
+
if (iv && typeof iv === "object") {
|
|
25
|
+
const ivo = iv;
|
|
26
|
+
// serializedValue pattern
|
|
27
|
+
if ("serializedValue" in ivo)
|
|
28
|
+
return String(ivo.serializedValue);
|
|
29
|
+
// themeColor pattern
|
|
30
|
+
if ("themeColor" in ivo)
|
|
31
|
+
return `[theme:${ivo.themeColor}]`;
|
|
32
|
+
// color value pattern
|
|
33
|
+
if ("value" in ivo)
|
|
34
|
+
return String(ivo.value);
|
|
35
|
+
}
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
// Variable reference
|
|
39
|
+
if ("variable" in o)
|
|
40
|
+
return "[dynamic]";
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
/** Extract text value from a text widget's props. */
|
|
44
|
+
function extractText(props) {
|
|
45
|
+
const text = props.text;
|
|
46
|
+
if (!text)
|
|
47
|
+
return "";
|
|
48
|
+
const textValue = text.textValue;
|
|
49
|
+
if (!textValue)
|
|
50
|
+
return "";
|
|
51
|
+
const val = resolveValue(textValue);
|
|
52
|
+
return val ? `"${val}"` : "";
|
|
53
|
+
}
|
|
54
|
+
/** Extract button label from a button widget's props. */
|
|
55
|
+
function extractButton(props) {
|
|
56
|
+
const button = props.button;
|
|
57
|
+
if (!button)
|
|
58
|
+
return "";
|
|
59
|
+
const text = button.text;
|
|
60
|
+
if (!text)
|
|
61
|
+
return "";
|
|
62
|
+
const textValue = text.textValue;
|
|
63
|
+
if (!textValue)
|
|
64
|
+
return "";
|
|
65
|
+
const val = resolveValue(textValue);
|
|
66
|
+
return val ? `"${val}"` : "";
|
|
67
|
+
}
|
|
68
|
+
/** Extract image info from an image widget's props. */
|
|
69
|
+
function extractImage(props) {
|
|
70
|
+
const image = props.image;
|
|
71
|
+
if (!image)
|
|
72
|
+
return "";
|
|
73
|
+
const parts = [];
|
|
74
|
+
// Path
|
|
75
|
+
const pathValue = image.pathValue;
|
|
76
|
+
if (pathValue) {
|
|
77
|
+
const path = resolveValue(pathValue);
|
|
78
|
+
if (path && path !== "[dynamic]") {
|
|
79
|
+
// Extract just the filename from the full asset path
|
|
80
|
+
const filename = path.split("/").pop() || path;
|
|
81
|
+
parts.push(filename);
|
|
82
|
+
}
|
|
83
|
+
else if (path === "[dynamic]") {
|
|
84
|
+
parts.push("[dynamic]");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Dimensions
|
|
88
|
+
const dims = image.dimensions;
|
|
89
|
+
if (dims) {
|
|
90
|
+
const w = dims.width;
|
|
91
|
+
const h = dims.height;
|
|
92
|
+
const wVal = w?.pixelsValue ? resolveValue(w.pixelsValue) : "";
|
|
93
|
+
const hVal = h?.pixelsValue ? resolveValue(h.pixelsValue) : "";
|
|
94
|
+
if (wVal && hVal && wVal !== "Infinity" && hVal !== "Infinity") {
|
|
95
|
+
parts.push(`[${wVal}x${hVal}]`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return parts.join(" ");
|
|
99
|
+
}
|
|
100
|
+
/** Extract icon info. */
|
|
101
|
+
function extractIcon(props) {
|
|
102
|
+
const icon = props.icon;
|
|
103
|
+
if (!icon)
|
|
104
|
+
return "";
|
|
105
|
+
const iconData = icon.iconDataValue;
|
|
106
|
+
if (!iconData)
|
|
107
|
+
return "";
|
|
108
|
+
const iv = iconData.inputValue;
|
|
109
|
+
if (!iv)
|
|
110
|
+
return "";
|
|
111
|
+
return iv.name || "";
|
|
112
|
+
}
|
|
113
|
+
/** Extract text field hint. */
|
|
114
|
+
function extractTextField(props) {
|
|
115
|
+
const tf = props.textField;
|
|
116
|
+
if (!tf)
|
|
117
|
+
return "";
|
|
118
|
+
const decoration = tf.inputDecoration;
|
|
119
|
+
if (!decoration)
|
|
120
|
+
return "";
|
|
121
|
+
const hintText = decoration.hintText;
|
|
122
|
+
if (!hintText)
|
|
123
|
+
return "";
|
|
124
|
+
const textValue = hintText.textValue;
|
|
125
|
+
if (!textValue)
|
|
126
|
+
return "";
|
|
127
|
+
const val = resolveValue(textValue);
|
|
128
|
+
return val ? `hint: "${val}"` : "";
|
|
129
|
+
}
|
|
130
|
+
/** Extract checkbox/toggle/switch label. */
|
|
131
|
+
function extractCheckbox(props) {
|
|
132
|
+
const cb = (props.checkbox || props.toggle || props.switchWidget);
|
|
133
|
+
if (!cb)
|
|
134
|
+
return "";
|
|
135
|
+
const label = cb.labelValue;
|
|
136
|
+
if (!label)
|
|
137
|
+
return "";
|
|
138
|
+
return resolveValue(label);
|
|
139
|
+
}
|
|
140
|
+
/** Type-specific detail extraction. */
|
|
141
|
+
function extractDetail(type, props) {
|
|
142
|
+
switch (type) {
|
|
143
|
+
case "Text":
|
|
144
|
+
case "RichText":
|
|
145
|
+
case "AutoSizeText":
|
|
146
|
+
return extractText(props);
|
|
147
|
+
case "Button":
|
|
148
|
+
case "IconButton":
|
|
149
|
+
case "FFButtonWidget":
|
|
150
|
+
return extractButton(props);
|
|
151
|
+
case "Image":
|
|
152
|
+
case "CachedNetworkImage":
|
|
153
|
+
return extractImage(props);
|
|
154
|
+
case "Icon":
|
|
155
|
+
return extractIcon(props);
|
|
156
|
+
case "TextField":
|
|
157
|
+
case "TextFormField":
|
|
158
|
+
return extractTextField(props);
|
|
159
|
+
case "Checkbox":
|
|
160
|
+
case "CheckboxListTile":
|
|
161
|
+
case "Switch":
|
|
162
|
+
case "ToggleIcon":
|
|
163
|
+
return extractCheckbox(props);
|
|
164
|
+
default:
|
|
165
|
+
return "";
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Infer widget type from the key prefix (e.g. "Text_abc123" → "Text").
|
|
170
|
+
*/
|
|
171
|
+
export function inferTypeFromKey(key) {
|
|
172
|
+
const match = key.match(/^([A-Z][a-zA-Z]*)_/);
|
|
173
|
+
return match ? match[1] : "Unknown";
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Read a node's cached file and extract type, name, and detail.
|
|
177
|
+
*
|
|
178
|
+
* @param projectId - The FF project ID
|
|
179
|
+
* @param pagePrefix - Cache key prefix for the page, e.g. "page/id-Scaffold_xxx/page-widget-tree-outline"
|
|
180
|
+
* @param nodeKey - The node key, e.g. "Button_uaqbabys"
|
|
181
|
+
*/
|
|
182
|
+
export async function extractNodeInfo(projectId, pagePrefix, nodeKey) {
|
|
183
|
+
const fileKey = `${pagePrefix}/node/id-${nodeKey}`;
|
|
184
|
+
const content = await cacheRead(projectId, fileKey);
|
|
185
|
+
if (!content) {
|
|
186
|
+
return {
|
|
187
|
+
type: inferTypeFromKey(nodeKey),
|
|
188
|
+
name: "",
|
|
189
|
+
detail: "",
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
const doc = YAML.parse(content);
|
|
194
|
+
const type = doc.type || inferTypeFromKey(nodeKey);
|
|
195
|
+
const name = doc.name || "";
|
|
196
|
+
const props = doc.props || {};
|
|
197
|
+
const detail = extractDetail(type, props);
|
|
198
|
+
return { type, name, detail };
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return {
|
|
202
|
+
type: inferTypeFromKey(nodeKey),
|
|
203
|
+
name: "",
|
|
204
|
+
detail: "",
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|