@rettangoli/check 0.1.1 → 0.1.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/LICENSE +21 -0
- package/README.md +2 -2
- package/package.json +13 -3
- package/src/core/fe-contracts.js +212 -0
- package/src/core/parsers.js +1 -1
- package/src/core/registry.js +57 -6
- package/src/diagnostics/catalog.js +1 -1
- package/src/rules/feParity.js +2 -2
- package/src/rules/listenerConfig.js +1 -1
- package/src/rules/refs.js +1 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yuusoft
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the “Software”), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software
|
|
10
|
+
is furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included
|
|
13
|
+
in all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
21
|
+
IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Implemented now:
|
|
|
15
15
|
- package + CLI entrypoint (`@rettangoli/check`, `rtgl check`)
|
|
16
16
|
- discovery and component grouping
|
|
17
17
|
- model building for YAML/JS contracts
|
|
18
|
-
- FE parity checks (schema required, forbidden view keys,
|
|
18
|
+
- FE parity checks (schema required, forbidden view keys, unsupported property binding syntax)
|
|
19
19
|
- schema/constants checks
|
|
20
20
|
- listener config checks
|
|
21
21
|
- strict handler naming checks (`handle*` only)
|
|
@@ -112,7 +112,7 @@ Current baseline is clean in strict mode:
|
|
|
112
112
|
|
|
113
113
|
- required `.schema.yaml` per component
|
|
114
114
|
- forbidden metadata keys inside `.view.yaml`
|
|
115
|
-
-
|
|
115
|
+
- unsupported property binding syntax detection
|
|
116
116
|
|
|
117
117
|
That baseline is good, but many contract violations are still caught late (during build, runtime, or manually).
|
|
118
118
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rettangoli/check",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Static contract checker for Rettangoli projects",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/yuusoft-org/rettangoli",
|
|
9
|
+
"directory": "packages/rettangoli-check"
|
|
10
|
+
},
|
|
6
11
|
"main": "./src/index.js",
|
|
7
12
|
"exports": {
|
|
8
13
|
".": "./src/index.js",
|
|
@@ -12,12 +17,16 @@
|
|
|
12
17
|
"rtgl-check": "./src/cli/bin.js"
|
|
13
18
|
},
|
|
14
19
|
"scripts": {
|
|
20
|
+
"ci": "npm run test:no-fe-contracts-import && npm run test:ui-registry-resolution-contract && npm run test:scenarios",
|
|
21
|
+
"test:package": "node ./scripts/package-smoke-test.js",
|
|
15
22
|
"test:scenarios": "node ./test/run-scenarios.js",
|
|
23
|
+
"test:no-fe-contracts-import": "node ./scripts/test-no-fe-contracts-import.js",
|
|
16
24
|
"test:diff-js-exports": "node ./scripts/differential-js-exports.js",
|
|
17
25
|
"test:fuzz-template-pipeline": "node ./scripts/fuzz-template-pipeline.js",
|
|
18
26
|
"test:fuzz-jempl-parser": "node ./scripts/fuzz-jempl-parser.js",
|
|
19
27
|
"test:jempl-parser-contract": "node ./scripts/test-jempl-parser-contract.js",
|
|
20
28
|
"test:scope-graph-range-contract": "node ./scripts/test-scope-graph-range-contract.js",
|
|
29
|
+
"test:ui-registry-resolution-contract": "node ./scripts/test-ui-registry-resolution-contract.js",
|
|
21
30
|
"test:fe-frontend-component-identity-contract": "node ./scripts/test-fe-frontend-component-identity-contract.js",
|
|
22
31
|
"test:fe-frontend-handler-contract": "node ./scripts/test-fe-frontend-handler-contract.js",
|
|
23
32
|
"test:fe-frontend-lifecycle-contract": "node ./scripts/test-fe-frontend-lifecycle-contract.js",
|
|
@@ -33,11 +42,12 @@
|
|
|
33
42
|
"files": [
|
|
34
43
|
"src",
|
|
35
44
|
"README.md",
|
|
36
|
-
"ROADMAP.md"
|
|
45
|
+
"ROADMAP.md",
|
|
46
|
+
"LICENSE"
|
|
37
47
|
],
|
|
38
48
|
"license": "MIT",
|
|
39
49
|
"dependencies": {
|
|
40
|
-
"@rettangoli/
|
|
50
|
+
"@rettangoli/ui": "1.11.0",
|
|
41
51
|
"jempl": "0.3.2-rc2",
|
|
42
52
|
"js-yaml": "^4.1.0",
|
|
43
53
|
"oxc-parser": "^0.112.0",
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
const PROP_PREFIX = ":";
|
|
2
|
+
|
|
3
|
+
const REF_ID_KEY_REGEX = /^[a-z][a-zA-Z0-9]*\*?$/;
|
|
4
|
+
const REF_CLASS_KEY_REGEX = /^[a-zA-Z][a-zA-Z0-9_-]*\*?$/;
|
|
5
|
+
const REF_ID_REGEX = /^[a-z][a-zA-Z0-9]*$/;
|
|
6
|
+
const GLOBAL_REF_KEYS = new Set(["window", "document"]);
|
|
7
|
+
|
|
8
|
+
export const collectBindingNames = (attrsString = "") => {
|
|
9
|
+
if (!attrsString) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const attrAssignmentRegex = /(\S+?)=(?:\"([^\"]*)\"|\'([^\']*)\'|([^\s]*))/g;
|
|
14
|
+
const booleanAttrRegex = /\b(\S+?)(?=\s|$)/g;
|
|
15
|
+
const processedAttrs = new Set();
|
|
16
|
+
const bindingNames = [];
|
|
17
|
+
let match;
|
|
18
|
+
|
|
19
|
+
while ((match = attrAssignmentRegex.exec(attrsString)) !== null) {
|
|
20
|
+
const rawBindingName = match[1];
|
|
21
|
+
processedAttrs.add(rawBindingName);
|
|
22
|
+
bindingNames.push(rawBindingName);
|
|
23
|
+
}
|
|
24
|
+
attrAssignmentRegex.lastIndex = 0;
|
|
25
|
+
|
|
26
|
+
let remainingAttrsString = attrsString;
|
|
27
|
+
const processedMatches = [];
|
|
28
|
+
while ((match = attrAssignmentRegex.exec(attrsString)) !== null) {
|
|
29
|
+
processedMatches.push(match[0]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
processedMatches.forEach((processedMatch) => {
|
|
33
|
+
remainingAttrsString = remainingAttrsString.replace(processedMatch, " ");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
let boolMatch;
|
|
37
|
+
while ((boolMatch = booleanAttrRegex.exec(remainingAttrsString)) !== null) {
|
|
38
|
+
const attrName = boolMatch[1];
|
|
39
|
+
if (attrName.startsWith(".")) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (
|
|
43
|
+
!processedAttrs.has(attrName)
|
|
44
|
+
&& !attrName.startsWith(PROP_PREFIX)
|
|
45
|
+
&& !attrName.includes("=")
|
|
46
|
+
) {
|
|
47
|
+
bindingNames.push(attrName);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return [...new Set(bindingNames)];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const createRefMatchers = (refs) => {
|
|
55
|
+
return Object.entries(refs || {}).map(([refKey, refConfig]) => {
|
|
56
|
+
if (GLOBAL_REF_KEYS.has(refKey)) {
|
|
57
|
+
return {
|
|
58
|
+
refKey,
|
|
59
|
+
refConfig,
|
|
60
|
+
targetType: "global",
|
|
61
|
+
isWildcard: false,
|
|
62
|
+
prefix: refKey,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let targetType = "id";
|
|
67
|
+
let rawKey = refKey;
|
|
68
|
+
if (refKey.startsWith(".")) {
|
|
69
|
+
targetType = "class";
|
|
70
|
+
rawKey = refKey.slice(1);
|
|
71
|
+
} else if (refKey.startsWith("#")) {
|
|
72
|
+
targetType = "id";
|
|
73
|
+
rawKey = refKey.slice(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const reservedBaseKey = rawKey.endsWith("*") ? rawKey.slice(0, -1) : rawKey;
|
|
77
|
+
if (GLOBAL_REF_KEYS.has(reservedBaseKey)) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`[Parser] Invalid ref key '${refKey}'. Reserved global keys must be exactly 'window' or 'document'.`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (targetType === "id" && !REF_ID_KEY_REGEX.test(rawKey)) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`[Parser] Invalid ref key '${refKey}'. Use camelCase IDs (optional '#', optional '*') or class refs with '.' prefix.`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (targetType === "class" && !REF_CLASS_KEY_REGEX.test(rawKey)) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`[Parser] Invalid ref key '${refKey}'. Class refs must start with '.' and use class-compatible names (optional '*').`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const isWildcard = rawKey.endsWith("*");
|
|
95
|
+
const prefix = isWildcard ? rawKey.slice(0, -1) : rawKey;
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
refKey,
|
|
99
|
+
refConfig,
|
|
100
|
+
targetType,
|
|
101
|
+
isWildcard,
|
|
102
|
+
prefix,
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const validateElementIdForRefs = (elementIdForRefs) => {
|
|
108
|
+
if (!REF_ID_REGEX.test(elementIdForRefs)) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`[Parser] Invalid element id '${elementIdForRefs}' for refs. Use camelCase ids only. Kebab-case ids are not supported.`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const assertBooleanEventOption = ({ optionName, optionValue, eventType, refKey }) => {
|
|
116
|
+
if (optionValue === undefined) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (typeof optionValue !== "boolean") {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`[Parser] Invalid '${optionName}' for event '${eventType}' on ref '${refKey}'. Expected boolean.`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const assertNumberEventOption = ({ optionName, optionValue, eventType, refKey }) => {
|
|
127
|
+
if (optionValue === undefined) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (
|
|
131
|
+
typeof optionValue !== "number"
|
|
132
|
+
|| Number.isNaN(optionValue)
|
|
133
|
+
|| !Number.isFinite(optionValue)
|
|
134
|
+
|| optionValue < 0
|
|
135
|
+
) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`[Parser] Invalid '${optionName}' for event '${eventType}' on ref '${refKey}'. Expected non-negative number.`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const validateEventConfig = ({ eventType, eventConfig, refKey }) => {
|
|
143
|
+
if (typeof eventConfig !== "object" || eventConfig === null) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`[Parser] Invalid event config for event '${eventType}' on ref '${refKey}'.`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const hasDebounce = Object.prototype.hasOwnProperty.call(eventConfig, "debounce");
|
|
150
|
+
const hasThrottle = Object.prototype.hasOwnProperty.call(eventConfig, "throttle");
|
|
151
|
+
|
|
152
|
+
assertBooleanEventOption({
|
|
153
|
+
optionName: "preventDefault",
|
|
154
|
+
optionValue: eventConfig.preventDefault,
|
|
155
|
+
eventType,
|
|
156
|
+
refKey,
|
|
157
|
+
});
|
|
158
|
+
assertBooleanEventOption({
|
|
159
|
+
optionName: "stopPropagation",
|
|
160
|
+
optionValue: eventConfig.stopPropagation,
|
|
161
|
+
eventType,
|
|
162
|
+
refKey,
|
|
163
|
+
});
|
|
164
|
+
assertBooleanEventOption({
|
|
165
|
+
optionName: "stopImmediatePropagation",
|
|
166
|
+
optionValue: eventConfig.stopImmediatePropagation,
|
|
167
|
+
eventType,
|
|
168
|
+
refKey,
|
|
169
|
+
});
|
|
170
|
+
assertBooleanEventOption({
|
|
171
|
+
optionName: "targetOnly",
|
|
172
|
+
optionValue: eventConfig.targetOnly,
|
|
173
|
+
eventType,
|
|
174
|
+
refKey,
|
|
175
|
+
});
|
|
176
|
+
assertBooleanEventOption({
|
|
177
|
+
optionName: "once",
|
|
178
|
+
optionValue: eventConfig.once,
|
|
179
|
+
eventType,
|
|
180
|
+
refKey,
|
|
181
|
+
});
|
|
182
|
+
assertNumberEventOption({
|
|
183
|
+
optionName: "debounce",
|
|
184
|
+
optionValue: eventConfig.debounce,
|
|
185
|
+
eventType,
|
|
186
|
+
refKey,
|
|
187
|
+
});
|
|
188
|
+
assertNumberEventOption({
|
|
189
|
+
optionName: "throttle",
|
|
190
|
+
optionValue: eventConfig.throttle,
|
|
191
|
+
eventType,
|
|
192
|
+
refKey,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (hasDebounce && hasThrottle) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`[Parser] Event '${eventType}' on ref '${refKey}' cannot define both 'debounce' and 'throttle'.`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (eventConfig.handler && eventConfig.action) {
|
|
202
|
+
throw new Error("Each listener can have handler or action but not both");
|
|
203
|
+
}
|
|
204
|
+
if (!eventConfig.handler && !eventConfig.action) {
|
|
205
|
+
throw new Error("Each listener must define either handler or action");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
hasDebounce,
|
|
210
|
+
hasThrottle,
|
|
211
|
+
};
|
|
212
|
+
};
|
package/src/core/parsers.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { load as loadYaml } from "js-yaml";
|
|
2
2
|
import { parseSync } from "oxc-parser";
|
|
3
|
-
import { collectBindingNames as collectFeBindingNames } from "
|
|
3
|
+
import { collectBindingNames as collectFeBindingNames } from "./fe-contracts.js";
|
|
4
4
|
import { parse as parseJempl } from "jempl";
|
|
5
5
|
import { parseElementKey as parseYahtmlElementKey } from "yahtml";
|
|
6
6
|
|
package/src/core/registry.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
3
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
5
|
import { load as loadYaml } from "js-yaml";
|
|
5
6
|
import { parseSync } from "oxc-parser";
|
|
@@ -713,6 +714,61 @@ const resolveBundledUiDir = () => {
|
|
|
713
714
|
return path.resolve(currentDir, "../../../rettangoli-ui");
|
|
714
715
|
};
|
|
715
716
|
|
|
717
|
+
const isUiSourceDir = (candidate) => {
|
|
718
|
+
if (!candidate) {
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const packageJsonPath = path.join(candidate, "package.json");
|
|
723
|
+
const componentsDir = path.join(candidate, "src", "components");
|
|
724
|
+
const entryPath = path.join(candidate, "src", "entry-iife-ui.js");
|
|
725
|
+
if (!existsSync(packageJsonPath) || !existsSync(componentsDir) || !existsSync(entryPath)) {
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
try {
|
|
730
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
731
|
+
return packageJson?.name === "@rettangoli/ui";
|
|
732
|
+
} catch {
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
const findUiPackageDir = (resolvedEntryPath) => {
|
|
738
|
+
let currentDir = path.dirname(resolvedEntryPath);
|
|
739
|
+
const filesystemRoot = path.parse(currentDir).root;
|
|
740
|
+
|
|
741
|
+
while (currentDir !== filesystemRoot) {
|
|
742
|
+
if (isUiSourceDir(currentDir)) {
|
|
743
|
+
return currentDir;
|
|
744
|
+
}
|
|
745
|
+
currentDir = path.dirname(currentDir);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
return null;
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
const resolveInstalledUiDir = (baseDir) => {
|
|
752
|
+
try {
|
|
753
|
+
const require = createRequire(path.join(path.resolve(baseDir), "package.json"));
|
|
754
|
+
return findUiPackageDir(require.resolve("@rettangoli/ui"));
|
|
755
|
+
} catch {
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
export const resolveUiSourceDir = ({ workspaceRoot = process.cwd() } = {}) => {
|
|
761
|
+
const candidates = [
|
|
762
|
+
path.resolve(workspaceRoot),
|
|
763
|
+
path.resolve(workspaceRoot, "packages", "rettangoli-ui"),
|
|
764
|
+
resolveBundledUiDir(),
|
|
765
|
+
resolveInstalledUiDir(workspaceRoot),
|
|
766
|
+
resolveInstalledUiDir(path.dirname(fileURLToPath(import.meta.url))),
|
|
767
|
+
];
|
|
768
|
+
|
|
769
|
+
return [...new Set(candidates.filter(Boolean))].find(isUiSourceDir) || null;
|
|
770
|
+
};
|
|
771
|
+
|
|
716
772
|
const collectPrimitiveContractsFromEntries = async ({ uiDir, globalStyleAttrs }) => {
|
|
717
773
|
const result = new Map();
|
|
718
774
|
const entryNames = ["entry-iife-ui.js", "entry-iife-layout.js"];
|
|
@@ -729,12 +785,7 @@ const collectPrimitiveContractsFromEntries = async ({ uiDir, globalStyleAttrs })
|
|
|
729
785
|
};
|
|
730
786
|
|
|
731
787
|
export const buildUiRegistry = async ({ workspaceRoot = process.cwd() } = {}) => {
|
|
732
|
-
const
|
|
733
|
-
path.resolve(workspaceRoot, "packages", "rettangoli-ui"),
|
|
734
|
-
resolveBundledUiDir(),
|
|
735
|
-
];
|
|
736
|
-
|
|
737
|
-
const uiDir = uiCandidates.find((candidate) => existsSync(candidate));
|
|
788
|
+
const uiDir = resolveUiSourceDir({ workspaceRoot });
|
|
738
789
|
if (!uiDir) {
|
|
739
790
|
return new Map();
|
|
740
791
|
}
|
|
@@ -30,7 +30,7 @@ const CATALOG_VERSION = 1;
|
|
|
30
30
|
const CODE_OVERRIDES = {
|
|
31
31
|
"RTGL-CONTRACT-003": {
|
|
32
32
|
title: "Legacy dot-prop binding",
|
|
33
|
-
description: "Legacy
|
|
33
|
+
description: "Legacy property binding syntax is unsupported; property bindings must use ':prop=${...}' in source templates.",
|
|
34
34
|
tags: ["autofix-safe", "template"],
|
|
35
35
|
},
|
|
36
36
|
"RTGL-CHECK-YAHTML-002": {
|
package/src/rules/feParity.js
CHANGED
|
@@ -45,7 +45,7 @@ export const runFeParityRules = ({ models = [] }) => {
|
|
|
45
45
|
severity: "error",
|
|
46
46
|
filePath: viewPath,
|
|
47
47
|
line: model.view.legacyDotPropBindingLine,
|
|
48
|
-
message: `${model.componentKey}: legacy
|
|
48
|
+
message: `${model.componentKey}: legacy property binding syntax is not supported. Use ':prop=\${value}' in .view.yaml.`,
|
|
49
49
|
fix: {
|
|
50
50
|
kind: "line-regex-replace",
|
|
51
51
|
filePath: viewPath,
|
|
@@ -53,7 +53,7 @@ export const runFeParityRules = ({ models = [] }) => {
|
|
|
53
53
|
pattern: "\\.([a-zA-Z][\\w-]*)\\s*=",
|
|
54
54
|
replacement: ":$1=",
|
|
55
55
|
flags: "g",
|
|
56
|
-
description: "Convert legacy '.prop='
|
|
56
|
+
description: "Convert legacy '.prop=' prefix to ':prop='; wrap bare values in '${...}' if needed.",
|
|
57
57
|
safe: true,
|
|
58
58
|
confidence: 0.98,
|
|
59
59
|
},
|
|
@@ -9,7 +9,7 @@ import { getModelFilePath, getYamlPathLine, isObjectRecord } from "./shared.js";
|
|
|
9
9
|
import {
|
|
10
10
|
validateElementIdForRefs,
|
|
11
11
|
validateEventConfig as validateFeEventConfig,
|
|
12
|
-
} from "
|
|
12
|
+
} from "../core/fe-contracts.js";
|
|
13
13
|
import { parseSync } from "oxc-parser";
|
|
14
14
|
import { parseNamedExportedFunctions } from "../core/exportedFunctions.js";
|
|
15
15
|
const GLOBAL_REF_KEYS = new Set(["window", "document"]);
|
package/src/rules/refs.js
CHANGED