canicode 0.12.2 → 0.12.4
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/CHANGELOG.md +172 -0
- package/README.md +46 -65
- package/dist/cli/index.js +154 -46
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +40 -1
- package/dist/index.js +115 -44
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +135 -45
- package/dist/mcp/server.js.map +1 -1
- package/package.json +3 -1
- package/skills/canicode-roundtrip/SKILL.md +86 -0
- package/skills/canicode-roundtrip/helpers-bootstrap.js +1 -1
- package/skills/canicode-roundtrip/helpers-installer.js +2 -2
- package/skills/canicode-roundtrip/helpers.js +429 -0
- package/skills/cursor/canicode-roundtrip/SKILL.md +86 -0
- package/skills/cursor/canicode-roundtrip/helpers-bootstrap.js +1 -1
- package/skills/cursor/canicode-roundtrip/helpers-installer.js +2 -2
- package/skills/cursor/canicode-roundtrip/helpers.js +429 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { GetFileResponse, Node } from '@figma/rest-api-spec';
|
|
3
3
|
|
|
4
|
-
var version = "0.12.
|
|
4
|
+
var version = "0.12.4";
|
|
5
5
|
|
|
6
6
|
declare const SeveritySchema: z.ZodEnum<{
|
|
7
7
|
blocking: "blocking";
|
|
@@ -480,6 +480,26 @@ interface RuleContext {
|
|
|
480
480
|
* sync with `AnalysisNode.type` without a translation layer.
|
|
481
481
|
*/
|
|
482
482
|
rootNodeType: string;
|
|
483
|
+
/**
|
|
484
|
+
* #557: Root node of the current analysis subtree. Set by
|
|
485
|
+
* `RuleEngine.analyze` to the same node `traverseAndCheck` walks — i.e.
|
|
486
|
+
* `file.document` for full-file analysis or the resolved subtree when
|
|
487
|
+
* `targetNodeId` scopes the run.
|
|
488
|
+
*
|
|
489
|
+
* Distinct from `file.document`, which is **always** the full file root.
|
|
490
|
+
* Rules that build scope-wide indices (e.g. Stage 3 fingerprint pass in
|
|
491
|
+
* `missing-component`) MUST walk `analysisRoot`, not `file.document` —
|
|
492
|
+
* walking the full file under a scoped run produces silent false
|
|
493
|
+
* negatives because the index's first-occurrence id may resolve to a
|
|
494
|
+
* node outside the current scope, and the in-scope duplicate then never
|
|
495
|
+
* matches as "first" so the issue never surfaces.
|
|
496
|
+
*
|
|
497
|
+
* Optional purely so existing rule unit tests that construct
|
|
498
|
+
* `RuleContext` literals do not have to backfill the field. The engine
|
|
499
|
+
* always sets it; rule code reading this should default to
|
|
500
|
+
* `context.file.document` for safety on the unit-test path.
|
|
501
|
+
*/
|
|
502
|
+
analysisRoot?: AnalysisNode;
|
|
483
503
|
/**
|
|
484
504
|
* ADR-022: lookup canicode-authored acknowledgments by `(nodeId, ruleId)`.
|
|
485
505
|
* The rule engine builds this from `RuleEngineOptions.acknowledgments` and
|
|
@@ -519,6 +539,19 @@ interface RuleViolation {
|
|
|
519
539
|
* string keeps lowercase prose.
|
|
520
540
|
*/
|
|
521
541
|
suggestedName?: string;
|
|
542
|
+
/**
|
|
543
|
+
* Phase 3 (#508 / #560): when a rule emits a single issue that represents
|
|
544
|
+
* a group of N nodes (e.g. `missing-component:structure-repetition` —
|
|
545
|
+
* one issue per fingerprint group covering N qualifying FRAMEs), this
|
|
546
|
+
* lists every node id in the group **including** `nodeId`. Document
|
|
547
|
+
* order, scope-aware. Apply primitives (componentize + replace-with-
|
|
548
|
+
* instance) iterate this list to drive the full componentize+swap loop
|
|
549
|
+
* from a single user answer.
|
|
550
|
+
*
|
|
551
|
+
* Optional: rules that emit one issue per node (the typical path) leave
|
|
552
|
+
* this undefined.
|
|
553
|
+
*/
|
|
554
|
+
groupMembers?: string[];
|
|
522
555
|
}
|
|
523
556
|
/**
|
|
524
557
|
* Rule check function signature
|
|
@@ -836,6 +869,7 @@ declare const GotchaSurveyQuestionSchema: z.ZodObject<{
|
|
|
836
869
|
sourceChildId: z.ZodOptional<z.ZodString>;
|
|
837
870
|
replicas: z.ZodOptional<z.ZodNumber>;
|
|
838
871
|
replicaNodeIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
872
|
+
groupMembers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
839
873
|
}, z.core.$strip>;
|
|
840
874
|
type GotchaSurveyQuestion = z.infer<typeof GotchaSurveyQuestionSchema>;
|
|
841
875
|
/**
|
|
@@ -903,6 +937,7 @@ declare const SurveyQuestionBatchSchema: z.ZodObject<{
|
|
|
903
937
|
sourceChildId: z.ZodOptional<z.ZodString>;
|
|
904
938
|
replicas: z.ZodOptional<z.ZodNumber>;
|
|
905
939
|
replicaNodeIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
940
|
+
groupMembers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
906
941
|
}, z.core.$strip>>;
|
|
907
942
|
totalScenes: z.ZodNumber;
|
|
908
943
|
}, z.core.$strip>;
|
|
@@ -967,6 +1002,7 @@ declare const SurveyQuestionGroupSchema: z.ZodObject<{
|
|
|
967
1002
|
sourceChildId: z.ZodOptional<z.ZodString>;
|
|
968
1003
|
replicas: z.ZodOptional<z.ZodNumber>;
|
|
969
1004
|
replicaNodeIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1005
|
+
groupMembers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
970
1006
|
}, z.core.$strip>>;
|
|
971
1007
|
totalScenes: z.ZodNumber;
|
|
972
1008
|
}, z.core.$strip>>;
|
|
@@ -1033,6 +1069,7 @@ declare const GroupedSurveySchema: z.ZodObject<{
|
|
|
1033
1069
|
sourceChildId: z.ZodOptional<z.ZodString>;
|
|
1034
1070
|
replicas: z.ZodOptional<z.ZodNumber>;
|
|
1035
1071
|
replicaNodeIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1072
|
+
groupMembers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1036
1073
|
}, z.core.$strip>>;
|
|
1037
1074
|
totalScenes: z.ZodNumber;
|
|
1038
1075
|
}, z.core.$strip>>;
|
|
@@ -1098,6 +1135,7 @@ declare const GotchaSurveySchema: z.ZodObject<{
|
|
|
1098
1135
|
sourceChildId: z.ZodOptional<z.ZodString>;
|
|
1099
1136
|
replicas: z.ZodOptional<z.ZodNumber>;
|
|
1100
1137
|
replicaNodeIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1138
|
+
groupMembers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1101
1139
|
}, z.core.$strip>>;
|
|
1102
1140
|
groupedQuestions: z.ZodObject<{
|
|
1103
1141
|
groups: z.ZodArray<z.ZodObject<{
|
|
@@ -1160,6 +1198,7 @@ declare const GotchaSurveySchema: z.ZodObject<{
|
|
|
1160
1198
|
sourceChildId: z.ZodOptional<z.ZodString>;
|
|
1161
1199
|
replicas: z.ZodOptional<z.ZodNumber>;
|
|
1162
1200
|
replicaNodeIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1201
|
+
groupMembers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1163
1202
|
}, z.core.$strip>>;
|
|
1164
1203
|
totalScenes: z.ZodNumber;
|
|
1165
1204
|
}, z.core.$strip>>;
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import 'crypto';
|
|
|
6
6
|
import { homedir } from 'os';
|
|
7
7
|
|
|
8
8
|
// package.json
|
|
9
|
-
var version = "0.12.
|
|
9
|
+
var version = "0.12.4";
|
|
10
10
|
var SeveritySchema = z.enum([
|
|
11
11
|
"blocking",
|
|
12
12
|
"risk",
|
|
@@ -423,7 +423,25 @@ var GotchaSurveyQuestionSchema = z.object({
|
|
|
423
423
|
// `[nodeId, ...replicaNodeIds]` so the same answer lands on every replica.
|
|
424
424
|
// Single-instance questions omit both fields.
|
|
425
425
|
replicas: z.number().int().min(2).optional(),
|
|
426
|
-
replicaNodeIds: z.array(z.string()).optional()
|
|
426
|
+
replicaNodeIds: z.array(z.string()).optional(),
|
|
427
|
+
/**
|
|
428
|
+
* Phase 3 (#508 / #560 / delta 4a): every node id in the rule's emitted
|
|
429
|
+
* group, including `nodeId`. Currently only populated for
|
|
430
|
+
* `missing-component:structure-repetition` (Stage 3) — one question per
|
|
431
|
+
* fingerprint group, the apply step (delta 4b) iterates this list to
|
|
432
|
+
* componentize the first member and swap the rest.
|
|
433
|
+
*
|
|
434
|
+
* Distinct from `replicas` / `replicaNodeIds`: those collapse N
|
|
435
|
+
* instance-child questions that pre-existed as separate violations into
|
|
436
|
+
* one. `groupMembers` carries N original group members from a single
|
|
437
|
+
* group-shaped violation that never became N separate issues. The apply
|
|
438
|
+
* step distinguishes by which field is set.
|
|
439
|
+
*/
|
|
440
|
+
// `.min(2)` locks the contract: a group-shaped emit always carries at
|
|
441
|
+
// least 2 members (Stage 3 short-circuits below `structureMinRepetitions`
|
|
442
|
+
// = 2). A future rule emitting `[]` or `[oneId]` is a programming error,
|
|
443
|
+
// not a runtime case to handle gracefully.
|
|
444
|
+
groupMembers: z.array(z.string()).min(2).optional()
|
|
427
445
|
});
|
|
428
446
|
var SurveyQuestionBatchSchema = z.object({
|
|
429
447
|
ruleId: z.string(),
|
|
@@ -995,6 +1013,7 @@ var RuleEngine = class {
|
|
|
995
1013
|
analysisState,
|
|
996
1014
|
scope,
|
|
997
1015
|
rootNodeType,
|
|
1016
|
+
rootNode,
|
|
998
1017
|
void 0,
|
|
999
1018
|
void 0
|
|
1000
1019
|
);
|
|
@@ -1031,7 +1050,7 @@ var RuleEngine = class {
|
|
|
1031
1050
|
/**
|
|
1032
1051
|
* Recursively traverse the tree and run rules
|
|
1033
1052
|
*/
|
|
1034
|
-
traverseAndCheck(node, file, rules, maxDepth, issues, failedRules, depth, path, ancestorTypes, componentDepth, analysisState, scope, rootNodeType, parent, siblings) {
|
|
1053
|
+
traverseAndCheck(node, file, rules, maxDepth, issues, failedRules, depth, path, ancestorTypes, componentDepth, analysisState, scope, rootNodeType, analysisRoot, parent, siblings) {
|
|
1035
1054
|
const nodePath = [...path, node.name];
|
|
1036
1055
|
const isComponentBoundary = node.type === "COMPONENT" || node.type === "COMPONENT_SET" || node.type === "INSTANCE";
|
|
1037
1056
|
const currentComponentDepth = isComponentBoundary ? 0 : componentDepth;
|
|
@@ -1054,6 +1073,7 @@ var RuleEngine = class {
|
|
|
1054
1073
|
analysisState,
|
|
1055
1074
|
scope,
|
|
1056
1075
|
rootNodeType,
|
|
1076
|
+
analysisRoot,
|
|
1057
1077
|
findAcknowledgment: (nodeId, ruleId) => acknowledgmentsByKey.get(`${normalizeNodeId(nodeId)}::${ruleId}`)
|
|
1058
1078
|
};
|
|
1059
1079
|
for (const rule of rules) {
|
|
@@ -1103,6 +1123,7 @@ var RuleEngine = class {
|
|
|
1103
1123
|
analysisState,
|
|
1104
1124
|
scope,
|
|
1105
1125
|
rootNodeType,
|
|
1126
|
+
analysisRoot,
|
|
1106
1127
|
node,
|
|
1107
1128
|
node.children
|
|
1108
1129
|
);
|
|
@@ -2545,8 +2566,8 @@ var missingComponentMsg = {
|
|
|
2545
2566
|
message: `"${name}" appears ${count} times`,
|
|
2546
2567
|
suggestion: `Extract as a reusable component`
|
|
2547
2568
|
}),
|
|
2548
|
-
structureRepetition: (name,
|
|
2549
|
-
message: `"${name}" and ${
|
|
2569
|
+
structureRepetition: (name, matchCount) => ({
|
|
2570
|
+
message: `"${name}" and ${matchCount} other frame(s) share the same internal structure`,
|
|
2550
2571
|
suggestion: `Extract a shared component from the repeated structure`
|
|
2551
2572
|
}),
|
|
2552
2573
|
styleOverride: (componentName, overrides) => ({
|
|
@@ -2972,6 +2993,10 @@ var deepNesting = defineRule({
|
|
|
2972
2993
|
function isOnGrid(value, gridBase) {
|
|
2973
2994
|
return value % gridBase === 0;
|
|
2974
2995
|
}
|
|
2996
|
+
function hasOwnBoundVariables(obj) {
|
|
2997
|
+
const bv = obj["boundVariables"];
|
|
2998
|
+
return bv !== void 0 && bv !== null && Object.keys(bv).length > 0;
|
|
2999
|
+
}
|
|
2975
3000
|
var rawValueDef = {
|
|
2976
3001
|
id: "raw-value",
|
|
2977
3002
|
name: "Raw Value",
|
|
@@ -2986,6 +3011,7 @@ var rawValueCheck = (node, context) => {
|
|
|
2986
3011
|
if (!hasStyleReference(node, "fill") && !hasBoundVariable(node, "fills")) {
|
|
2987
3012
|
for (const fill of node.fills) {
|
|
2988
3013
|
const fillObj = fill;
|
|
3014
|
+
if (hasOwnBoundVariables(fillObj)) continue;
|
|
2989
3015
|
if (fillObj["type"] === "SOLID" && fillObj["color"]) {
|
|
2990
3016
|
const c = fillObj["color"];
|
|
2991
3017
|
const hex = `#${Math.round((c["r"] ?? 0) * 255).toString(16).padStart(2, "0")}${Math.round((c["g"] ?? 0) * 255).toString(16).padStart(2, "0")}${Math.round((c["b"] ?? 0) * 255).toString(16).padStart(2, "0")}`.toUpperCase();
|
|
@@ -3020,9 +3046,10 @@ var rawValueCheck = (node, context) => {
|
|
|
3020
3046
|
}
|
|
3021
3047
|
}
|
|
3022
3048
|
if (node.effects && Array.isArray(node.effects) && node.effects.length > 0) {
|
|
3023
|
-
if (!hasStyleReference(node, "effect")) {
|
|
3049
|
+
if (!hasStyleReference(node, "effect") && !hasBoundVariable(node, "effects")) {
|
|
3024
3050
|
for (const effect of node.effects) {
|
|
3025
3051
|
const effectObj = effect;
|
|
3052
|
+
if (hasOwnBoundVariables(effectObj)) continue;
|
|
3026
3053
|
if (effectObj["type"] === "DROP_SHADOW" || effectObj["type"] === "INNER_SHADOW") {
|
|
3027
3054
|
const shadowType = effectObj["type"] === "DROP_SHADOW" ? "drop shadow" : "inner shadow";
|
|
3028
3055
|
const offset = effectObj["offset"];
|
|
@@ -3053,6 +3080,7 @@ var rawValueCheck = (node, context) => {
|
|
|
3053
3080
|
}
|
|
3054
3081
|
const spacingKeys = ["paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "itemSpacing"];
|
|
3055
3082
|
for (const key of spacingKeys) {
|
|
3083
|
+
if (key === "itemSpacing" && node.primaryAxisAlignItems === "SPACE_BETWEEN") continue;
|
|
3056
3084
|
const value = node[key];
|
|
3057
3085
|
if (value !== void 0 && value > 0 && !hasBoundVariable(node, key)) {
|
|
3058
3086
|
const label = key === "itemSpacing" ? "gap" : key.replace("padding", "padding-").toLowerCase();
|
|
@@ -3085,14 +3113,15 @@ var irregularSpacingCheck = (node, context, options) => {
|
|
|
3085
3113
|
const spacingEntries = [];
|
|
3086
3114
|
for (const key of ["paddingLeft", "paddingRight", "paddingTop", "paddingBottom"]) {
|
|
3087
3115
|
const v = node[key];
|
|
3088
|
-
if (v !== void 0 && v > 0) spacingEntries.push({ value: v, subType: "padding" });
|
|
3116
|
+
if (v !== void 0 && v > 0) spacingEntries.push({ key, value: v, subType: "padding" });
|
|
3089
3117
|
}
|
|
3090
|
-
if (node.itemSpacing !== void 0 && node.itemSpacing > 0) {
|
|
3091
|
-
spacingEntries.push({ value: node.itemSpacing, subType: "gap" });
|
|
3118
|
+
if (node.itemSpacing !== void 0 && node.itemSpacing > 0 && node.primaryAxisAlignItems !== "SPACE_BETWEEN") {
|
|
3119
|
+
spacingEntries.push({ key: "itemSpacing", value: node.itemSpacing, subType: "gap" });
|
|
3092
3120
|
}
|
|
3093
3121
|
const commonValues = [1, 2];
|
|
3094
3122
|
for (const entry of spacingEntries) {
|
|
3095
3123
|
if (commonValues.includes(entry.value)) continue;
|
|
3124
|
+
if (hasBoundVariable(node, entry.key)) continue;
|
|
3096
3125
|
if (!isOnGrid(entry.value, gridBase)) {
|
|
3097
3126
|
return {
|
|
3098
3127
|
ruleId: irregularSpacingDef.id,
|
|
@@ -3281,6 +3310,41 @@ function getSeenStage1(context) {
|
|
|
3281
3310
|
function getSeenStage4(context) {
|
|
3282
3311
|
return getAnalysisState(context, SEEN_STAGE4_KEY, () => /* @__PURE__ */ new Set());
|
|
3283
3312
|
}
|
|
3313
|
+
function stage3GroupsKey(maxDepth) {
|
|
3314
|
+
return `missing-component:stage3Groups:depth=${maxDepth}`;
|
|
3315
|
+
}
|
|
3316
|
+
function nodeQualifiesForStage3(node, parent, insideInstance) {
|
|
3317
|
+
if (insideInstance) return false;
|
|
3318
|
+
if (node.type !== "FRAME") return false;
|
|
3319
|
+
if (parent?.type === "COMPONENT_SET") return false;
|
|
3320
|
+
if (!node.children || node.children.length === 0) return false;
|
|
3321
|
+
return true;
|
|
3322
|
+
}
|
|
3323
|
+
function buildStage3Groups(root, maxFingerprintDepth) {
|
|
3324
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3325
|
+
const walk = (node, parent, ancestorIsInstance) => {
|
|
3326
|
+
const insideInstance = ancestorIsInstance || node.type === "INSTANCE";
|
|
3327
|
+
if (nodeQualifiesForStage3(node, parent, ancestorIsInstance)) {
|
|
3328
|
+
const fp = buildFingerprint(node, maxFingerprintDepth);
|
|
3329
|
+
const existing = groups.get(fp);
|
|
3330
|
+
if (existing) existing.memberIds.push(node.id);
|
|
3331
|
+
else groups.set(fp, { memberIds: [node.id] });
|
|
3332
|
+
}
|
|
3333
|
+
if (node.children) {
|
|
3334
|
+
for (const child of node.children) walk(child, node, insideInstance);
|
|
3335
|
+
}
|
|
3336
|
+
};
|
|
3337
|
+
walk(root, null, false);
|
|
3338
|
+
return groups;
|
|
3339
|
+
}
|
|
3340
|
+
function getStage3Groups(context, maxFingerprintDepth) {
|
|
3341
|
+
const root = context.analysisRoot ?? context.file.document;
|
|
3342
|
+
return getAnalysisState(
|
|
3343
|
+
context,
|
|
3344
|
+
stage3GroupsKey(maxFingerprintDepth),
|
|
3345
|
+
() => buildStage3Groups(root, maxFingerprintDepth)
|
|
3346
|
+
);
|
|
3347
|
+
}
|
|
3284
3348
|
var missingComponentDef = {
|
|
3285
3349
|
id: "missing-component",
|
|
3286
3350
|
name: "Missing Component",
|
|
@@ -3295,7 +3359,8 @@ var missingComponentCheck = (node, context, options) => {
|
|
|
3295
3359
|
const matchingComponent = Object.values(components).find(
|
|
3296
3360
|
(c) => c.name.toLowerCase() === node.name.toLowerCase()
|
|
3297
3361
|
);
|
|
3298
|
-
const
|
|
3362
|
+
const scopeRoot = context.analysisRoot ?? context.file.document;
|
|
3363
|
+
const frameNames = collectFrameNames(scopeRoot);
|
|
3299
3364
|
const sameNameFrames = frameNames.get(node.name);
|
|
3300
3365
|
const firstFrame = sameNameFrames?.[0];
|
|
3301
3366
|
if (matchingComponent) {
|
|
@@ -3323,37 +3388,32 @@ var missingComponentCheck = (node, context, options) => {
|
|
|
3323
3388
|
};
|
|
3324
3389
|
}
|
|
3325
3390
|
}
|
|
3326
|
-
if (isInsideInstance(context))
|
|
3327
|
-
|
|
3328
|
-
|
|
3391
|
+
if (!nodeQualifiesForStage3(node, context.parent ?? null, isInsideInstance(context))) {
|
|
3392
|
+
return null;
|
|
3393
|
+
}
|
|
3329
3394
|
const structureMinRepetitions = options?.["structureMinRepetitions"] ?? getRuleOption("missing-component", "structureMinRepetitions", 2);
|
|
3330
3395
|
const maxFingerprintDepth = options?.["maxFingerprintDepth"] ?? getRuleOption("missing-component", "maxFingerprintDepth", 3);
|
|
3396
|
+
const groups = getStage3Groups(context, maxFingerprintDepth);
|
|
3331
3397
|
const fingerprint = buildFingerprint(node, maxFingerprintDepth);
|
|
3332
|
-
const
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
);
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
)
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
nodePath: context.path.join(" > "),
|
|
3352
|
-
...missingComponentMsg.structureRepetition(node.name, count - 1)
|
|
3353
|
-
};
|
|
3354
|
-
}
|
|
3355
|
-
}
|
|
3356
|
-
return null;
|
|
3398
|
+
const group = groups.get(fingerprint);
|
|
3399
|
+
if (!group) return null;
|
|
3400
|
+
if (group.memberIds.length < structureMinRepetitions) return null;
|
|
3401
|
+
if (group.memberIds[0] !== node.id) return null;
|
|
3402
|
+
return {
|
|
3403
|
+
ruleId: missingComponentDef.id,
|
|
3404
|
+
subType: "structure-repetition",
|
|
3405
|
+
nodeId: node.id,
|
|
3406
|
+
nodePath: context.path.join(" > "),
|
|
3407
|
+
...missingComponentMsg.structureRepetition(
|
|
3408
|
+
node.name,
|
|
3409
|
+
group.memberIds.length - 1
|
|
3410
|
+
),
|
|
3411
|
+
// #560 / delta 4a: surface the full group so the apply step can drive
|
|
3412
|
+
// the componentize+swap loop from a single user answer. `nodeId` is
|
|
3413
|
+
// the document-order first member; the rest are siblings or cross-
|
|
3414
|
+
// parent matches found by the scope-wide pass (#557).
|
|
3415
|
+
groupMembers: [...group.memberIds]
|
|
3416
|
+
};
|
|
3357
3417
|
}
|
|
3358
3418
|
if (node.type === "INSTANCE" && node.componentId) {
|
|
3359
3419
|
const seenStage4 = getSeenStage4(context);
|
|
@@ -3443,17 +3503,28 @@ var variantStructureMismatch = defineRule({
|
|
|
3443
3503
|
var CODE_CONNECT_SETUP_KEY = "unmapped-component:setup-detected";
|
|
3444
3504
|
var CODE_CONNECT_MAPPINGS_KEY = "unmapped-component:mappings";
|
|
3445
3505
|
var SEEN_MAIN_IDS_KEY = "unmapped-component:seen-main-ids";
|
|
3506
|
+
function browserCwd() {
|
|
3507
|
+
return typeof process !== "undefined" && typeof process.cwd === "function" ? process.cwd() : null;
|
|
3508
|
+
}
|
|
3446
3509
|
function codeConnectIsSetUp(context) {
|
|
3447
3510
|
return getAnalysisState(context, CODE_CONNECT_SETUP_KEY, () => {
|
|
3448
|
-
|
|
3511
|
+
const cwd = browserCwd();
|
|
3512
|
+
if (cwd === null) return false;
|
|
3513
|
+
return existsSync(join(cwd, "figma.config.json"));
|
|
3449
3514
|
});
|
|
3450
3515
|
}
|
|
3451
3516
|
function codeConnectMappings(context) {
|
|
3452
|
-
return getAnalysisState(
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3517
|
+
return getAnalysisState(context, CODE_CONNECT_MAPPINGS_KEY, () => {
|
|
3518
|
+
const cwd = browserCwd();
|
|
3519
|
+
if (cwd === null) {
|
|
3520
|
+
return {
|
|
3521
|
+
mappedNodeIds: /* @__PURE__ */ new Set(),
|
|
3522
|
+
scannedFiles: [],
|
|
3523
|
+
skipReason: "no-config"
|
|
3524
|
+
};
|
|
3525
|
+
}
|
|
3526
|
+
return parseCodeConnectMappings(cwd);
|
|
3527
|
+
});
|
|
3457
3528
|
}
|
|
3458
3529
|
function seenMainIds(context) {
|
|
3459
3530
|
return getAnalysisState(context, SEEN_MAIN_IDS_KEY, () => /* @__PURE__ */ new Set());
|