@nordcraft/search 1.0.86 → 1.0.88

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 (23) hide show
  1. package/dist/rules/issues/miscellaneous/createStaticSizeConstraintRule.d.ts +5 -0
  2. package/dist/rules/issues/miscellaneous/createStaticSizeConstraintRule.js +74 -0
  3. package/dist/rules/issues/miscellaneous/createStaticSizeConstraintRule.js.map +1 -0
  4. package/dist/rules/issues/miscellaneous/miscRules.index.d.ts +3 -0
  5. package/dist/rules/issues/miscellaneous/miscRules.index.js +10 -1
  6. package/dist/rules/issues/miscellaneous/miscRules.index.js.map +1 -1
  7. package/dist/rules/issues/slots/unknownComponentSlotRule.js +1 -1
  8. package/dist/rules/issues/slots/unknownComponentSlotRule.js.map +1 -1
  9. package/dist/rules/issues/style/invalidStyleSyntaxRule.js +16 -0
  10. package/dist/rules/issues/style/invalidStyleSyntaxRule.js.map +1 -1
  11. package/dist/types.d.ts +1 -1
  12. package/package.json +3 -3
  13. package/src/rules/issues/actions/unknownActionEventRule.test.ts +0 -1
  14. package/src/rules/issues/events/noReferenceEventRule.test.ts +0 -4
  15. package/src/rules/issues/events/unknownEventRule.test.ts +0 -4
  16. package/src/rules/issues/events/unknownTriggerEventRule.test.ts +0 -2
  17. package/src/rules/issues/miscellaneous/createStaticSizeConstraintRule.test.ts +153 -0
  18. package/src/rules/issues/miscellaneous/createStaticSizeConstraintRule.ts +90 -0
  19. package/src/rules/issues/miscellaneous/miscRules.index.ts +10 -1
  20. package/src/rules/issues/slots/unknownComponentSlotRule.ts +1 -1
  21. package/src/rules/issues/style/invalidStyleSyntaxRule.test.ts +386 -0
  22. package/src/rules/issues/style/invalidStyleSyntaxRule.ts +21 -0
  23. package/src/types.ts +10 -9
@@ -0,0 +1,5 @@
1
+ import type { Level, Rule } from '../../../types';
2
+ export declare function createStaticSizeConstraintRule(tag: string, maxSize: number, level?: Level): Rule<{
3
+ tag: string;
4
+ size: number;
5
+ }>;
@@ -0,0 +1,74 @@
1
+ import { isDefined } from '@nordcraft/core/dist/utils/util';
2
+ import { VOID_HTML_ELEMENTS } from '@nordcraft/ssr/dist/const';
3
+ export function createStaticSizeConstraintRule(tag, maxSize, level = 'info') {
4
+ return {
5
+ code: 'size constraint',
6
+ category: 'Performance',
7
+ level: level,
8
+ visit: (report, args) => {
9
+ if (args.nodeType === 'component-node' &&
10
+ args.value.type === 'element' &&
11
+ args.value.tag === tag) {
12
+ let size = 0;
13
+ const component = args.component;
14
+ const evaluateElement = (element) => {
15
+ if (!element ||
16
+ ['element', 'text', 'slot', 'component'].includes(element.type) ===
17
+ false) {
18
+ return '';
19
+ }
20
+ const children = (element.children ?? []).map((child) => evaluateElement(component.nodes?.[child]));
21
+ const tag = element.type === 'element' ? element.tag : 'span';
22
+ const attributes = {};
23
+ if (element.type === 'element' || element.type === 'component') {
24
+ Object.entries(element.attrs ?? {}).forEach(([key, value]) => {
25
+ if (value?.type === 'value' && isDefined(value.value)) {
26
+ attributes[key] = String(value.value);
27
+ }
28
+ });
29
+ }
30
+ const attributeString = Object.entries(attributes)
31
+ .map(([key, value]) => `${key}="${value}"`)
32
+ .join(' ');
33
+ if (VOID_HTML_ELEMENTS.includes(tag)) {
34
+ return attributeString
35
+ ? `<${tag} ${attributeString} />`
36
+ : `<${tag} />`;
37
+ }
38
+ else {
39
+ const openingTag = attributeString
40
+ ? `<${tag} ${attributeString}>`
41
+ : `<${tag}>`;
42
+ return `${openingTag}${children.join('')}</${tag}>`;
43
+ }
44
+ };
45
+ const staticElement = evaluateElement(args.value);
46
+ size = new Blob([staticElement]).size;
47
+ if (size > maxSize) {
48
+ report({
49
+ path: args.path,
50
+ info: {
51
+ title: `Element size exceeds the suggested maximum allowed size of ${sizeFormatter(maxSize)}`,
52
+ description: `The <${tag}> element has a size of ${sizeFormatter(size)}, which exceeds the suggested limit of ${sizeFormatter(maxSize)}. Consider simplifying the content or structure of this element to reduce its size and improve performance.`,
53
+ },
54
+ details: { tag, size },
55
+ });
56
+ }
57
+ }
58
+ },
59
+ };
60
+ }
61
+ const sizeFormatter = (sizeInBytes) => {
62
+ const units = ['byte', 'KB', 'MB', 'GB', 'TB'];
63
+ let unitIndex = 0;
64
+ let size = sizeInBytes;
65
+ while (size >= 1024 && unitIndex < units.length - 1) {
66
+ size /= 1024;
67
+ unitIndex++;
68
+ }
69
+ const unit = unitIndex === 0 && size !== 1 ? 'bytes' : units[unitIndex];
70
+ return `${new Intl.NumberFormat(undefined, {
71
+ maximumFractionDigits: unitIndex === 0 ? 0 : 2,
72
+ }).format(size)} ${unit}`;
73
+ };
74
+ //# sourceMappingURL=createStaticSizeConstraintRule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createStaticSizeConstraintRule.js","sourceRoot":"","sources":["../../../../src/rules/issues/miscellaneous/createStaticSizeConstraintRule.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,8 @@
1
1
  declare const _default: (import("../../../types").Rule<{
2
2
  node: string;
3
+ }, import("../../../types").NodeType, import("../../../types").NodeType> | import("../../../types").Rule<{
4
+ tag: string;
5
+ size: number;
3
6
  }, import("../../../types").NodeType, import("../../../types").NodeType> | import("../../../types").Rule<{
4
7
  name: string;
5
8
  }, import("../../../types").NodeType, import("../../../types").NodeType>)[];
@@ -1,5 +1,14 @@
1
+ import { createStaticSizeConstraintRule } from './createStaticSizeConstraintRule';
1
2
  import { noReferenceNodeRule } from './noReferenceNodeRule';
2
3
  import { requireExtensionRule } from './requireExtensionRule';
3
4
  import { unknownCookieRule } from './unknownCookieRule';
4
- export default [noReferenceNodeRule, requireExtensionRule, unknownCookieRule];
5
+ export default [
6
+ noReferenceNodeRule,
7
+ requireExtensionRule,
8
+ unknownCookieRule,
9
+ // 100 KB is a large SVG
10
+ createStaticSizeConstraintRule('svg', 100 * 1024),
11
+ // 50 KB is a large img element (with potential base64 encoded image)
12
+ createStaticSizeConstraintRule('img', 50 * 1024),
13
+ ];
5
14
  //# sourceMappingURL=miscRules.index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"miscRules.index.js","sourceRoot":"","sources":["../../../../src/rules/issues/miscellaneous/miscRules.index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAC3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAEvD,eAAe,CAAC,mBAAmB,EAAE,oBAAoB,EAAE,iBAAiB,CAAC,CAAA"}
1
+ {"version":3,"file":"miscRules.index.js","sourceRoot":"","sources":["../../../../src/rules/issues/miscellaneous/miscRules.index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,8BAA8B,EAAE,MAAM,kCAAkC,CAAA;AACjF,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAC3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAEvD,eAAe;IACb,mBAAmB;IACnB,oBAAoB;IACpB,iBAAiB;IACjB,wBAAwB;IACxB,8BAA8B,CAAC,KAAK,EAAE,GAAG,GAAG,IAAI,CAAC;IACjD,qEAAqE;IACrE,8BAA8B,CAAC,KAAK,EAAE,EAAE,GAAG,IAAI,CAAC;CACjD,CAAA"}
@@ -8,7 +8,7 @@ export const unknownComponentSlotRule = {
8
8
  return;
9
9
  }
10
10
  // We only want to check the immediate children of a "sub component"
11
- if (value.type !== 'component' || value.children.length === 0) {
11
+ if (value.type !== 'component' || (value?.children ?? []).length === 0) {
12
12
  return;
13
13
  }
14
14
  const [, currentComponentName, ,] = path;
@@ -1 +1 @@
1
- {"version":3,"file":"unknownComponentSlotRule.js","sourceRoot":"","sources":["../../../../src/rules/issues/slots/unknownComponentSlotRule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,gDAAgD,CAAA;AAIhF,MAAM,CAAC,MAAM,wBAAwB,GAA+B;IAClE,IAAI,EAAE,wBAAwB;IAC9B,KAAK,EAAE,OAAO;IACd,QAAQ,EAAE,mBAAmB;IAC7B,KAAK,EAAE,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;QACnD,IAAI,QAAQ,KAAK,gBAAgB,EAAE,CAAC;YAClC,OAAM;QACR,CAAC;QAED,oEAAoE;QACpE,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC9D,OAAM;QACR,CAAC;QAED,MAAM,CAAC,EAAE,oBAAoB,EAAE,AAAD,EAAG,GAAG,IAAI,CAAA;QAExC,MAAM,SAAS,GAAG,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAC9C,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAM;QACR,CAAC;QACD,sCAAsC;QACtC,MAAM,YAAY,GAAG,IAAI,eAAe,CAAC;YACvC,SAAS;YACT,YAAY,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC;YAC9C,WAAW,EAAE,SAAS;YACtB,cAAc,EAAE;gBACd,QAAQ,EAAE,KAAK,CAAC,QAAQ;gBACxB,QAAQ,EAAE,KAAK,CAAC,QAAQ;aACzB;SACF,CAAC,CAAA;QAEF,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,KAAK,IAAI,EAAE,CAAC;aACxD,MAAM,CAAC,CAAC,IAAI,EAAyB,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC;aAC7D,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,IAAI,SAAS,CAAC,CAAA;QAExC,0EAA0E;QAC1E,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC;YAChC,MAAM,SAAS,GAAG,KAAK,CAAC,UAAU,CAAC,oBAAoB,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,CAAA;YACxE,MAAM,QAAQ,GAAG,SAAS,EAAE,IAAI,IAAI,SAAS,CAAA;YAE7C,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACpC,MAAM,CAAC;oBACL,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC;oBACxC,IAAI,EAAE;wBACJ,KAAK,EAAE,wBAAwB;wBAC/B,WAAW,EAAE,KAAK,QAAQ,gEAAgE;qBAC3F;oBACD,OAAO,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE;iBAChC,CAAC,CAAA;YACJ,CAAC;QAAA,CACF,CAAC,CAAA;IAAA,CACH;CACF,CAAA"}
1
+ {"version":3,"file":"unknownComponentSlotRule.js","sourceRoot":"","sources":["../../../../src/rules/issues/slots/unknownComponentSlotRule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,gDAAgD,CAAA;AAIhF,MAAM,CAAC,MAAM,wBAAwB,GAA+B;IAClE,IAAI,EAAE,wBAAwB;IAC9B,KAAK,EAAE,OAAO;IACd,QAAQ,EAAE,mBAAmB;IAC7B,KAAK,EAAE,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;QACnD,IAAI,QAAQ,KAAK,gBAAgB,EAAE,CAAC;YAClC,OAAM;QACR,CAAC;QAED,oEAAoE;QACpE,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,IAAI,CAAC,KAAK,EAAE,QAAQ,IAAI,EAAE,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvE,OAAM;QACR,CAAC;QAED,MAAM,CAAC,EAAE,oBAAoB,EAAE,AAAD,EAAG,GAAG,IAAI,CAAA;QAExC,MAAM,SAAS,GAAG,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAC9C,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAM;QACR,CAAC;QACD,sCAAsC;QACtC,MAAM,YAAY,GAAG,IAAI,eAAe,CAAC;YACvC,SAAS;YACT,YAAY,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC;YAC9C,WAAW,EAAE,SAAS;YACtB,cAAc,EAAE;gBACd,QAAQ,EAAE,KAAK,CAAC,QAAQ;gBACxB,QAAQ,EAAE,KAAK,CAAC,QAAQ;aACzB;SACF,CAAC,CAAA;QAEF,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,KAAK,IAAI,EAAE,CAAC;aACxD,MAAM,CAAC,CAAC,IAAI,EAAyB,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC;aAC7D,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,IAAI,SAAS,CAAC,CAAA;QAExC,0EAA0E;QAC1E,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC;YAChC,MAAM,SAAS,GAAG,KAAK,CAAC,UAAU,CAAC,oBAAoB,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,CAAA;YACxE,MAAM,QAAQ,GAAG,SAAS,EAAE,IAAI,IAAI,SAAS,CAAA;YAE7C,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACpC,MAAM,CAAC;oBACL,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC;oBACxC,IAAI,EAAE;wBACJ,KAAK,EAAE,wBAAwB;wBAC/B,WAAW,EAAE,KAAK,QAAQ,gEAAgE;qBAC3F;oBACD,OAAO,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE;iBAChC,CAAC,CAAA;YACJ,CAAC;QAAA,CACF,CAAC,CAAA;IAAA,CACH;CACF,CAAA"}
@@ -8,6 +8,22 @@ export const invalidStyleSyntaxRule = {
8
8
  if (nodeType !== 'style-declaration') {
9
9
  return;
10
10
  }
11
+ // Check for variable/formula references: Variables., Formulas., Event., Attributes., Apis., Parameters., ListItem., URLParameters.
12
+ if (typeof value.styleValue === 'string') {
13
+ const hasVariableReference = /\b(Variables|Formulas|Event|Attributes|Apis|Parameters|ListItem|URLParameters)\.\w+/i.test(value.styleValue);
14
+ if (hasVariableReference) {
15
+ report({
16
+ path,
17
+ info: {
18
+ title: `Formulas detected in style declaration`,
19
+ description: `The style declaration for the property "${value.styleProperty}" contains Nordcraft formula syntax (e.g., references like "Variables.xxx", "Event.xxx", "Attributes.xxx", etc.). Formulas should not be used directly in CSS style values. Use style-variables or computed styles instead.`,
20
+ },
21
+ details: { property: value.styleProperty },
22
+ fixes: ['delete-style-property'],
23
+ });
24
+ return;
25
+ }
26
+ }
11
27
  const valid = memo(`valid-style-${value.styleProperty}:${value.styleValue}`, () => {
12
28
  try {
13
29
  parse(`${value.styleProperty}: ${value.styleValue}`);
@@ -1 +1 @@
1
- {"version":3,"file":"invalidStyleSyntaxRule.js","sourceRoot":"","sources":["../../../../src/rules/issues/style/invalidStyleSyntaxRule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAE/B,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAElE,MAAM,CAAC,MAAM,sBAAsB,GAE9B;IACH,IAAI,EAAE,sBAAsB;IAC5B,KAAK,EAAE,OAAO;IACd,QAAQ,EAAE,SAAS;IACnB,KAAK,EAAE,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;QAClD,IAAI,QAAQ,KAAK,mBAAmB,EAAE,CAAC;YACrC,OAAM;QACR,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,CAChB,eAAe,KAAK,CAAC,aAAa,IAAI,KAAK,CAAC,UAAU,EAAE,EACxD,GAAG,EAAE,CAAC;YACJ,IAAI,CAAC;gBACH,KAAK,CAAC,GAAG,KAAK,CAAC,aAAa,KAAK,KAAK,CAAC,UAAU,EAAE,CAAC,CAAA;gBACpD,OAAO,IAAI,CAAA;YACb,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,KAAK,CAAA;YACd,CAAC;QAAA,CACF,CACF,CAAA;QACD,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,CAAC;gBACL,IAAI;gBACJ,IAAI,EAAE;oBACJ,KAAK,EAAE,2BAA2B;oBAClC,WAAW,EAAE,2CAA2C,KAAK,CAAC,aAAa,kIAAkI;iBAC9M;gBACD,OAAO,EAAE,EAAE,QAAQ,EAAE,KAAK,CAAC,aAAa,EAAE;gBAC1C,KAAK,EAAE,CAAC,uBAAuB,CAAC;aACjC,CAAC,CAAA;QACJ,CAAC;IAAA,CACF;IACD,KAAK,EAAE;QACL,uBAAuB,EAAE,iBAAiB;KAC3C;CACF,CAAA"}
1
+ {"version":3,"file":"invalidStyleSyntaxRule.js","sourceRoot":"","sources":["../../../../src/rules/issues/style/invalidStyleSyntaxRule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAE/B,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAElE,MAAM,CAAC,MAAM,sBAAsB,GAE9B;IACH,IAAI,EAAE,sBAAsB;IAC5B,KAAK,EAAE,OAAO;IACd,QAAQ,EAAE,SAAS;IACnB,KAAK,EAAE,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;QAClD,IAAI,QAAQ,KAAK,mBAAmB,EAAE,CAAC;YACrC,OAAM;QACR,CAAC;QAED,mIAAmI;QACnI,IAAI,OAAO,KAAK,CAAC,UAAU,KAAK,QAAQ,EAAE,CAAC;YACzC,MAAM,oBAAoB,GACxB,sFAAsF,CAAC,IAAI,CACzF,KAAK,CAAC,UAAU,CACjB,CAAA;YACH,IAAI,oBAAoB,EAAE,CAAC;gBACzB,MAAM,CAAC;oBACL,IAAI;oBACJ,IAAI,EAAE;wBACJ,KAAK,EAAE,wCAAwC;wBAC/C,WAAW,EAAE,2CAA2C,KAAK,CAAC,aAAa,6NAA6N;qBACzS;oBACD,OAAO,EAAE,EAAE,QAAQ,EAAE,KAAK,CAAC,aAAa,EAAE;oBAC1C,KAAK,EAAE,CAAC,uBAAuB,CAAC;iBACjC,CAAC,CAAA;gBACF,OAAM;YACR,CAAC;QACH,CAAC;QAED,MAAM,KAAK,GAAG,IAAI,CAChB,eAAe,KAAK,CAAC,aAAa,IAAI,KAAK,CAAC,UAAU,EAAE,EACxD,GAAG,EAAE,CAAC;YACJ,IAAI,CAAC;gBACH,KAAK,CAAC,GAAG,KAAK,CAAC,aAAa,KAAK,KAAK,CAAC,UAAU,EAAE,CAAC,CAAA;gBACpD,OAAO,IAAI,CAAA;YACb,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,KAAK,CAAA;YACd,CAAC;QAAA,CACF,CACF,CAAA;QACD,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,CAAC;gBACL,IAAI;gBACJ,IAAI,EAAE;oBACJ,KAAK,EAAE,2BAA2B;oBAClC,WAAW,EAAE,2CAA2C,KAAK,CAAC,aAAa,kIAAkI;iBAC9M;gBACD,OAAO,EAAE,EAAE,QAAQ,EAAE,KAAK,CAAC,aAAa,EAAE;gBAC1C,KAAK,EAAE,CAAC,uBAAuB,CAAC;aACjC,CAAC,CAAA;QACJ,CAAC;IAAA,CACF;IACD,KAAK,EAAE;QACL,uBAAuB,EAAE,iBAAiB;KAC3C;CACF,CAAA"}
package/dist/types.d.ts CHANGED
@@ -31,7 +31,7 @@ import type { InvalidStyleSyntaxRuleFix } from './rules/issues/style/invalidStyl
31
31
  import type { LegacyStyleVariableRuleFix } from './rules/issues/style/legacyStyleVariableRule';
32
32
  import type { NoReferenceVariableRuleFix } from './rules/issues/variables/noReferenceVariableRule';
33
33
  import type { NoPostNavigateActionRuleFix } from './rules/issues/workflows/noPostNavigateAction';
34
- export type Code = 'duplicate action argument name' | 'duplicate event trigger' | 'duplicate formula argument name' | 'duplicate url parameter' | 'duplicate workflow parameter' | 'duplicate route' | 'invalid api parser mode' | 'invalid api proxy body setting' | 'invalid api proxy cookie setting' | 'invalid element child' | 'invalid style syntax' | 'invalid component structure' | 'legacy action' | 'legacy api' | 'legacy formula' | 'legacy style variable' | 'legacy theme' | 'no context consumers' | 'no post navigate action' | 'no-console' | 'no-reference api input' | 'no-reference api' | 'no-reference api service' | 'no-reference attribute' | 'no-reference attribute in instance' | 'no-reference component formula' | 'no-reference component workflow' | 'no-reference component' | 'no-reference event' | 'no-reference global css variable' | 'no-reference node' | 'no-reference project action' | 'no-reference project formula' | 'no-reference variable' | 'no-static-node-condition' | 'no-unnecessary-condition-falsy' | 'no-unnecessary-condition-truthy' | 'non-empty void element' | 'required api autofetch' | 'required direct child' | 'required direct parent' | 'required element attribute' | 'required extension' | 'required meta tag' | 'image without dimension' | 'unknown action argument' | 'unknown action event' | 'unknown api input' | 'unknown api' | 'unknown api service' | 'unknown attribute' | 'unknown classname' | 'unknown component attribute' | 'unknown component formula input' | 'unknown component slot' | 'unknown context formula' | 'unknown context provider formula' | 'unknown context provider workflow' | 'unknown context provider' | 'unknown context workflow' | 'unknown cookie' | 'unknown component' | 'unknown event' | 'unknown fetch input' | 'unknown formula' | 'unknown project action' | 'unknown project formula input' | 'unknown project formula' | 'unknown repeat index formula' | 'unknown repeat item formula' | 'unknown set url parameter' | 'unknown set url parameters' | 'unknown trigger event' | 'unknown trigger workflow' | 'unknown url parameter' | 'unknown variable setter' | 'unknown variable' | 'unknown css variable' | 'unknown trigger workflow parameter' | 'unknown workflow parameter';
34
+ export type Code = 'duplicate action argument name' | 'duplicate event trigger' | 'duplicate formula argument name' | 'duplicate route' | 'duplicate url parameter' | 'duplicate workflow parameter' | 'image without dimension' | 'invalid api parser mode' | 'invalid api proxy body setting' | 'invalid api proxy cookie setting' | 'invalid component structure' | 'invalid element child' | 'invalid style syntax' | 'legacy action' | 'legacy api' | 'legacy formula' | 'legacy style variable' | 'legacy theme' | 'no context consumers' | 'no post navigate action' | 'no-console' | 'no-reference api input' | 'no-reference api service' | 'no-reference api' | 'no-reference attribute in instance' | 'no-reference attribute' | 'no-reference component formula' | 'no-reference component workflow' | 'no-reference component' | 'no-reference event' | 'no-reference global css variable' | 'no-reference node' | 'no-reference project action' | 'no-reference project formula' | 'no-reference variable' | 'no-static-node-condition' | 'no-unnecessary-condition-falsy' | 'no-unnecessary-condition-truthy' | 'non-empty void element' | 'required api autofetch' | 'required direct child' | 'required direct parent' | 'required element attribute' | 'required extension' | 'required meta tag' | 'size constraint' | 'unknown action argument' | 'unknown action event' | 'unknown api input' | 'unknown api service' | 'unknown api' | 'unknown attribute' | 'unknown classname' | 'unknown component attribute' | 'unknown component formula input' | 'unknown component slot' | 'unknown component' | 'unknown context formula' | 'unknown context provider formula' | 'unknown context provider workflow' | 'unknown context provider' | 'unknown context workflow' | 'unknown cookie' | 'unknown css variable' | 'unknown event' | 'unknown fetch input' | 'unknown formula' | 'unknown project action' | 'unknown project formula input' | 'unknown project formula' | 'unknown repeat index formula' | 'unknown repeat item formula' | 'unknown set url parameter' | 'unknown set url parameters' | 'unknown trigger event' | 'unknown trigger workflow parameter' | 'unknown trigger workflow' | 'unknown url parameter' | 'unknown variable setter' | 'unknown variable' | 'unknown workflow parameter';
35
35
  export type Category = 'Unknown Reference' | 'No References' | 'SEO' | 'Accessibility' | 'Deprecation' | 'Performance' | 'Security' | 'Quality' | 'Other';
36
36
  export type Level = 'error' | 'warning' | 'info';
37
37
  export type Result = {
package/package.json CHANGED
@@ -10,8 +10,8 @@
10
10
  "directory": "packages/search"
11
11
  },
12
12
  "dependencies": {
13
- "@nordcraft/ssr": "1.0.86",
14
- "@nordcraft/core": "1.0.86",
13
+ "@nordcraft/ssr": "1.0.88",
14
+ "@nordcraft/core": "1.0.88",
15
15
  "jsondiffpatch": "0.7.3",
16
16
  "postcss": "8.5.6",
17
17
  "zod": "4.2.1"
@@ -26,5 +26,5 @@
26
26
  "test:watch:only": "bun test --watch --only"
27
27
  },
28
28
  "files": ["dist", "src"],
29
- "version": "1.0.86"
29
+ "version": "1.0.88"
30
30
  }
@@ -1,4 +1,3 @@
1
- /* eslint-disable inclusive-language/use-inclusive-words */
2
1
  import type { CustomActionModel } from '@nordcraft/core/dist/component/component.types'
3
2
  import { describe, expect, test } from 'bun:test'
4
3
  import { fixProject } from '../../../fixProject'
@@ -42,7 +42,6 @@ describe('find noReferenceEventRule', () => {
42
42
  events: [
43
43
  {
44
44
  name: 'unused-event',
45
- // eslint-disable-next-line inclusive-language/use-inclusive-words
46
45
  dummyEvent: {
47
46
  name: 'Name',
48
47
  },
@@ -120,7 +119,6 @@ describe('find noReferenceEventRule', () => {
120
119
  events: [
121
120
  {
122
121
  name: 'known-event',
123
- // eslint-disable-next-line inclusive-language/use-inclusive-words
124
122
  dummyEvent: {
125
123
  name: 'Name',
126
124
  },
@@ -130,7 +128,6 @@ describe('find noReferenceEventRule', () => {
130
128
  },
131
129
  {
132
130
  name: 'used-event',
133
- // eslint-disable-next-line inclusive-language/use-inclusive-words
134
131
  dummyEvent: {
135
132
  name: 'Name',
136
133
  },
@@ -184,7 +181,6 @@ describe('fix noReferenceEventRule', () => {
184
181
  events: [
185
182
  {
186
183
  name: 'unused-event',
187
- // eslint-disable-next-line inclusive-language/use-inclusive-words
188
184
  dummyEvent: {
189
185
  name: 'Name',
190
186
  },
@@ -66,7 +66,6 @@ describe('unknownEvent', () => {
66
66
  events: [
67
67
  {
68
68
  name: 'known-event',
69
- // eslint-disable-next-line inclusive-language/use-inclusive-words
70
69
  dummyEvent: {
71
70
  name: 'Name',
72
71
  },
@@ -94,7 +93,6 @@ describe('unknownEvent', () => {
94
93
  events: [
95
94
  {
96
95
  name: 'known-event',
97
- // eslint-disable-next-line inclusive-language/use-inclusive-words
98
96
  dummyEvent: {
99
97
  name: 'Name',
100
98
  },
@@ -187,7 +185,6 @@ describe('unknownEvent', () => {
187
185
  events: [
188
186
  {
189
187
  name: 'known-event',
190
- // eslint-disable-next-line inclusive-language/use-inclusive-words
191
188
  dummyEvent: {
192
189
  name: 'Name',
193
190
  },
@@ -215,7 +212,6 @@ describe('unknownEvent', () => {
215
212
  events: [
216
213
  {
217
214
  name: 'known-event',
218
- // eslint-disable-next-line inclusive-language/use-inclusive-words
219
215
  dummyEvent: {
220
216
  name: 'Name',
221
217
  },
@@ -40,7 +40,6 @@ describe('unknownTriggerEventRule', () => {
40
40
  events: [
41
41
  {
42
42
  name: 'known-event',
43
- // eslint-disable-next-line inclusive-language/use-inclusive-words
44
43
  dummyEvent: {
45
44
  name: 'Name',
46
45
  },
@@ -97,7 +96,6 @@ describe('unknownTriggerEventRule', () => {
97
96
  events: [
98
97
  {
99
98
  name: 'known-event',
100
- // eslint-disable-next-line inclusive-language/use-inclusive-words
101
99
  dummyEvent: {
102
100
  name: 'Name',
103
101
  },
@@ -0,0 +1,153 @@
1
+ import { valueFormula } from '@nordcraft/core/dist/formula/formulaUtils'
2
+ import { describe, expect, test } from 'bun:test'
3
+ import { searchProject } from '../../../searchProject'
4
+ import { createStaticSizeConstraintRule } from './createStaticSizeConstraintRule'
5
+
6
+ describe('createStaticSizeConstraintRule', () => {
7
+ test('should calculate element size correctly for SVGs', () => {
8
+ const problems = Array.from(
9
+ searchProject({
10
+ files: {
11
+ formulas: {},
12
+ components: {
13
+ test: {
14
+ name: 'test',
15
+ nodes: {
16
+ root: {
17
+ type: 'element',
18
+ attrs: {},
19
+ classes: {},
20
+ events: {},
21
+ tag: 'img',
22
+ children: ['svg'],
23
+ style: {},
24
+ },
25
+ svg: {
26
+ type: 'element',
27
+ attrs: {},
28
+ classes: {},
29
+ events: {},
30
+ tag: 'svg',
31
+ children: ['path'],
32
+ style: {},
33
+ },
34
+ path: {
35
+ type: 'element',
36
+ attrs: {},
37
+ classes: {},
38
+ events: {},
39
+ tag: 'path',
40
+ children: [],
41
+ style: {},
42
+ },
43
+ },
44
+ formulas: {},
45
+ apis: {},
46
+ attributes: {},
47
+ variables: {},
48
+ },
49
+ },
50
+ },
51
+ rules: [createStaticSizeConstraintRule('svg', 1)],
52
+ }),
53
+ )
54
+
55
+ expect(problems).toHaveLength(1)
56
+ expect(problems[0].code).toBe('size constraint')
57
+ expect(problems[0].path).toEqual(['components', 'test', 'nodes', 'svg'])
58
+ const expectedSize = new Blob(['<svg><path></path></svg>']).size
59
+ expect(problems[0].details.size).toEqual(expectedSize)
60
+ })
61
+ test('should calculate element size correctly for imgs', () => {
62
+ const problems = Array.from(
63
+ searchProject({
64
+ files: {
65
+ formulas: {},
66
+ components: {
67
+ test: {
68
+ name: 'test',
69
+ nodes: {
70
+ root: {
71
+ type: 'element',
72
+ attrs: {
73
+ src: valueFormula(
74
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8xAAADMElEQVR4nOzVwQnAIBQFQYXff81RUkQCOyDj1YOPnbXWPmeTRef+/3O/OyBjzh3CD95BfqICMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMO0TAAD//2Anhf4QtqobAAAAAElFTkSuQmCC',
75
+ ),
76
+ },
77
+ classes: {},
78
+ events: {},
79
+ tag: 'img',
80
+ children: [],
81
+ style: {},
82
+ },
83
+ },
84
+ formulas: {},
85
+ apis: {},
86
+ attributes: {},
87
+ variables: {},
88
+ },
89
+ },
90
+ },
91
+ rules: [createStaticSizeConstraintRule('img', 1)],
92
+ }),
93
+ )
94
+
95
+ expect(problems).toHaveLength(1)
96
+ expect(problems[0].code).toBe('size constraint')
97
+ expect(problems[0].path).toEqual(['components', 'test', 'nodes', 'root'])
98
+ const expectedSize = new Blob([
99
+ '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8xAAADMElEQVR4nOzVwQnAIBQFQYXff81RUkQCOyDj1YOPnbXWPmeTRef+/3O/OyBjzh3CD95BfqICMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMO0TAAD//2Anhf4QtqobAAAAAElFTkSuQmCC" />',
100
+ ]).size
101
+ expect(problems[0].details.size).toEqual(expectedSize)
102
+ })
103
+ test('should ignore elements that are not too large', () => {
104
+ const problems = Array.from(
105
+ searchProject({
106
+ files: {
107
+ formulas: {},
108
+ components: {
109
+ test: {
110
+ name: 'test',
111
+ nodes: {
112
+ root: {
113
+ type: 'element',
114
+ attrs: {},
115
+ classes: {},
116
+ events: {},
117
+ tag: 'img',
118
+ children: ['svg'],
119
+ style: {},
120
+ },
121
+ svg: {
122
+ type: 'element',
123
+ attrs: {},
124
+ classes: {},
125
+ events: {},
126
+ tag: 'svg',
127
+ children: ['path'],
128
+ style: {},
129
+ },
130
+ path: {
131
+ type: 'element',
132
+ attrs: {},
133
+ classes: {},
134
+ events: {},
135
+ tag: 'path',
136
+ children: [],
137
+ style: {},
138
+ },
139
+ },
140
+ formulas: {},
141
+ apis: {},
142
+ attributes: {},
143
+ variables: {},
144
+ },
145
+ },
146
+ },
147
+ rules: [createStaticSizeConstraintRule('svg', 100)],
148
+ }),
149
+ )
150
+
151
+ expect(problems).toHaveLength(0)
152
+ })
153
+ })
@@ -0,0 +1,90 @@
1
+ import type { NodeModel } from '@nordcraft/core/dist/component/component.types'
2
+ import { isDefined } from '@nordcraft/core/dist/utils/util'
3
+ import { VOID_HTML_ELEMENTS } from '@nordcraft/ssr/dist/const'
4
+ import type { Level, Rule } from '../../../types'
5
+
6
+ export function createStaticSizeConstraintRule(
7
+ tag: string,
8
+ maxSize: number,
9
+ level: Level = 'info',
10
+ ): Rule<{
11
+ tag: string
12
+ size: number
13
+ }> {
14
+ return {
15
+ code: 'size constraint',
16
+ category: 'Performance',
17
+ level: level,
18
+ visit: (report, args) => {
19
+ if (
20
+ args.nodeType === 'component-node' &&
21
+ args.value.type === 'element' &&
22
+ args.value.tag === tag
23
+ ) {
24
+ let size = 0
25
+ const component = args.component
26
+ const evaluateElement = (element?: NodeModel): string => {
27
+ if (
28
+ !element ||
29
+ ['element', 'text', 'slot', 'component'].includes(element.type) ===
30
+ false
31
+ ) {
32
+ return ''
33
+ }
34
+ const children = (element.children ?? []).map((child) =>
35
+ evaluateElement(component.nodes?.[child]),
36
+ )
37
+ const tag = element.type === 'element' ? element.tag : 'span'
38
+ const attributes: Record<string, string> = {}
39
+ if (element.type === 'element' || element.type === 'component') {
40
+ Object.entries(element.attrs ?? {}).forEach(([key, value]) => {
41
+ if (value?.type === 'value' && isDefined(value.value)) {
42
+ attributes[key] = String(value.value)
43
+ }
44
+ })
45
+ }
46
+ const attributeString = Object.entries(attributes)
47
+ .map(([key, value]) => `${key}="${value}"`)
48
+ .join(' ')
49
+ if (VOID_HTML_ELEMENTS.includes(tag)) {
50
+ return attributeString
51
+ ? `<${tag} ${attributeString} />`
52
+ : `<${tag} />`
53
+ } else {
54
+ const openingTag = attributeString
55
+ ? `<${tag} ${attributeString}>`
56
+ : `<${tag}>`
57
+ return `${openingTag}${children.join('')}</${tag}>`
58
+ }
59
+ }
60
+ const staticElement = evaluateElement(args.value)
61
+ size = new Blob([staticElement]).size
62
+ if (size > maxSize) {
63
+ report({
64
+ path: args.path,
65
+ info: {
66
+ title: `Element size exceeds the suggested maximum allowed size of ${sizeFormatter(maxSize)}`,
67
+ description: `The <${tag}> element has a size of ${sizeFormatter(size)}, which exceeds the suggested limit of ${sizeFormatter(maxSize)}. Consider simplifying the content or structure of this element to reduce its size and improve performance.`,
68
+ },
69
+ details: { tag, size },
70
+ })
71
+ }
72
+ }
73
+ },
74
+ }
75
+ }
76
+
77
+ const sizeFormatter = (sizeInBytes: number) => {
78
+ const units = ['byte', 'KB', 'MB', 'GB', 'TB']
79
+ let unitIndex = 0
80
+ let size = sizeInBytes
81
+
82
+ while (size >= 1024 && unitIndex < units.length - 1) {
83
+ size /= 1024
84
+ unitIndex++
85
+ }
86
+ const unit = unitIndex === 0 && size !== 1 ? 'bytes' : units[unitIndex]
87
+ return `${new Intl.NumberFormat(undefined, {
88
+ maximumFractionDigits: unitIndex === 0 ? 0 : 2,
89
+ }).format(size)} ${unit}`
90
+ }
@@ -1,5 +1,14 @@
1
+ import { createStaticSizeConstraintRule } from './createStaticSizeConstraintRule'
1
2
  import { noReferenceNodeRule } from './noReferenceNodeRule'
2
3
  import { requireExtensionRule } from './requireExtensionRule'
3
4
  import { unknownCookieRule } from './unknownCookieRule'
4
5
 
5
- export default [noReferenceNodeRule, requireExtensionRule, unknownCookieRule]
6
+ export default [
7
+ noReferenceNodeRule,
8
+ requireExtensionRule,
9
+ unknownCookieRule,
10
+ // 100 KB is a large SVG
11
+ createStaticSizeConstraintRule('svg', 100 * 1024),
12
+ // 50 KB is a large img element (with potential base64 encoded image)
13
+ createStaticSizeConstraintRule('img', 50 * 1024),
14
+ ]
@@ -12,7 +12,7 @@ export const unknownComponentSlotRule: Rule<{ slotName: string }> = {
12
12
  }
13
13
 
14
14
  // We only want to check the immediate children of a "sub component"
15
- if (value.type !== 'component' || value.children.length === 0) {
15
+ if (value.type !== 'component' || (value?.children ?? []).length === 0) {
16
16
  return
17
17
  }
18
18
 
@@ -203,6 +203,298 @@ describe('find invalidStyleSyntaxRule', () => {
203
203
  })
204
204
  })
205
205
 
206
+ describe('find formulas in style syntax', () => {
207
+ test('should find Variables. references in style syntax', () => {
208
+ const problems = Array.from(
209
+ searchProject({
210
+ files: {
211
+ formulas: {},
212
+ components: {
213
+ test: {
214
+ name: 'test',
215
+ nodes: {
216
+ root: {
217
+ tag: 'div',
218
+ type: 'element',
219
+ attrs: {},
220
+ style: {
221
+ transform: 'translateX(Variables.offsetX)',
222
+ width: '100px',
223
+ height: 'Variables.height',
224
+ },
225
+ events: {},
226
+ classes: {},
227
+ children: [],
228
+ },
229
+ },
230
+ formulas: {},
231
+ apis: {},
232
+ attributes: {},
233
+ variables: {},
234
+ },
235
+ },
236
+ },
237
+ rules: [invalidStyleSyntaxRule],
238
+ }),
239
+ )
240
+
241
+ expect(problems).toMatchObject([
242
+ {
243
+ code: 'invalid style syntax',
244
+ path: ['components', 'test', 'nodes', 'root', 'style', 'transform'],
245
+ details: { property: 'transform' },
246
+ },
247
+ {
248
+ code: 'invalid style syntax',
249
+ path: ['components', 'test', 'nodes', 'root', 'style', 'height'],
250
+ details: { property: 'height' },
251
+ },
252
+ ])
253
+ })
254
+
255
+ test('should find Formulas., Event., Attributes., Apis., Parameters., ListItem., URLParameters. references', () => {
256
+ const problems = Array.from(
257
+ searchProject({
258
+ files: {
259
+ formulas: {},
260
+ components: {
261
+ test: {
262
+ name: 'test',
263
+ nodes: {
264
+ root: {
265
+ tag: 'div',
266
+ type: 'element',
267
+ attrs: {},
268
+ style: {
269
+ color: 'Formulas.getColor()',
270
+ top: 'Event.clientY',
271
+ margin: 'Attributes.margin',
272
+ background: 'Apis.getBackground()',
273
+ fontSize: 'Parameters.size',
274
+ backgroundColor: 'ListItem.color',
275
+ width: 'URLParameters.width',
276
+ },
277
+ events: {},
278
+ classes: {},
279
+ children: [],
280
+ },
281
+ },
282
+ formulas: {},
283
+ apis: {},
284
+ attributes: {},
285
+ variables: {},
286
+ },
287
+ },
288
+ },
289
+ rules: [invalidStyleSyntaxRule],
290
+ }),
291
+ )
292
+
293
+ expect(problems).toHaveLength(7)
294
+ expect(problems.map((p) => p.details.property)).toEqual([
295
+ 'color',
296
+ 'top',
297
+ 'margin',
298
+ 'background',
299
+ 'fontSize',
300
+ 'backgroundColor',
301
+ 'width',
302
+ ])
303
+ })
304
+
305
+ test('should not find formulas in valid CSS values', () => {
306
+ const problems = Array.from(
307
+ searchProject({
308
+ files: {
309
+ formulas: {},
310
+ components: {
311
+ test: {
312
+ name: 'test',
313
+ nodes: {
314
+ root: {
315
+ tag: 'div',
316
+ type: 'element',
317
+ attrs: {},
318
+ style: {
319
+ width: '100px',
320
+ height: '50%',
321
+ color: '#ffffff',
322
+ backgroundColor: 'var(--my-var)',
323
+ transform: 'translateX(10px)',
324
+ margin: '10px 20px',
325
+ },
326
+ events: {},
327
+ classes: {},
328
+ children: [],
329
+ },
330
+ },
331
+ formulas: {},
332
+ apis: {},
333
+ attributes: {},
334
+ variables: {},
335
+ },
336
+ },
337
+ },
338
+ rules: [invalidStyleSyntaxRule],
339
+ }),
340
+ )
341
+
342
+ expect(problems).toHaveLength(0)
343
+ })
344
+
345
+ test('should not find formulas in numeric style values', () => {
346
+ const problems = Array.from(
347
+ searchProject({
348
+ files: {
349
+ formulas: {},
350
+ components: {
351
+ test: {
352
+ name: 'test',
353
+ nodes: {
354
+ root: {
355
+ tag: 'div',
356
+ type: 'element',
357
+ attrs: {},
358
+ style: {
359
+ opacity: 0.5,
360
+ zIndex: 10,
361
+ flex: 1,
362
+ },
363
+ events: {},
364
+ classes: {},
365
+ children: [],
366
+ },
367
+ },
368
+ formulas: {},
369
+ apis: {},
370
+ attributes: {},
371
+ variables: {},
372
+ },
373
+ },
374
+ },
375
+ rules: [invalidStyleSyntaxRule],
376
+ }),
377
+ )
378
+
379
+ expect(problems).toHaveLength(0)
380
+ })
381
+
382
+ test('should find formulas in variant styles', () => {
383
+ const problems = Array.from(
384
+ searchProject({
385
+ files: {
386
+ formulas: {},
387
+ components: {
388
+ test: {
389
+ name: 'test',
390
+ nodes: {
391
+ root: {
392
+ tag: 'div',
393
+ type: 'element',
394
+ attrs: {},
395
+ style: {
396
+ width: '100px',
397
+ },
398
+ events: {},
399
+ classes: {},
400
+ children: [],
401
+ variants: [
402
+ {
403
+ style: {
404
+ transform: 'translateX(Variables.offsetX)',
405
+ color: 'Event.color',
406
+ },
407
+ hover: true,
408
+ },
409
+ ],
410
+ },
411
+ },
412
+ formulas: {},
413
+ apis: {},
414
+ attributes: {},
415
+ variables: {},
416
+ },
417
+ },
418
+ },
419
+ rules: [invalidStyleSyntaxRule],
420
+ }),
421
+ )
422
+
423
+ expect(problems).toMatchObject([
424
+ {
425
+ code: 'invalid style syntax',
426
+ path: [
427
+ 'components',
428
+ 'test',
429
+ 'nodes',
430
+ 'root',
431
+ 'variants',
432
+ 0,
433
+ 'style',
434
+ 'transform',
435
+ ],
436
+ details: { property: 'transform' },
437
+ },
438
+ {
439
+ code: 'invalid style syntax',
440
+ path: [
441
+ 'components',
442
+ 'test',
443
+ 'nodes',
444
+ 'root',
445
+ 'variants',
446
+ 0,
447
+ 'style',
448
+ 'color',
449
+ ],
450
+ details: { property: 'color' },
451
+ },
452
+ ])
453
+ })
454
+
455
+ test('should handle case-insensitive matching for formulas', () => {
456
+ const problems = Array.from(
457
+ searchProject({
458
+ files: {
459
+ formulas: {},
460
+ components: {
461
+ test: {
462
+ name: 'test',
463
+ nodes: {
464
+ root: {
465
+ tag: 'div',
466
+ type: 'element',
467
+ attrs: {},
468
+ style: {
469
+ width: 'VARIABLES.offsetX',
470
+ height: 'variables.height',
471
+ color: 'FORMULAS.getColor()',
472
+ },
473
+ events: {},
474
+ classes: {},
475
+ children: [],
476
+ },
477
+ },
478
+ formulas: {},
479
+ apis: {},
480
+ attributes: {},
481
+ variables: {},
482
+ },
483
+ },
484
+ },
485
+ rules: [invalidStyleSyntaxRule],
486
+ }),
487
+ )
488
+
489
+ expect(problems).toHaveLength(3)
490
+ expect(problems.map((p) => p.details.property)).toEqual([
491
+ 'width',
492
+ 'height',
493
+ 'color',
494
+ ])
495
+ })
496
+ })
497
+
206
498
  describe('fix invalidStyleSyntaxRule', () => {
207
499
  test('should remove an invalid style property', () => {
208
500
  const files: ProjectFiles = {
@@ -248,6 +540,49 @@ describe('fix invalidStyleSyntaxRule', () => {
248
540
  }
249
541
  `)
250
542
  })
543
+ test('should remove a style property with formula syntax', () => {
544
+ const files: ProjectFiles = {
545
+ formulas: {},
546
+ components: {
547
+ test: {
548
+ name: 'test',
549
+ nodes: {
550
+ root: {
551
+ tag: 'div',
552
+ type: 'element',
553
+ attrs: {},
554
+ style: {
555
+ width: '100px',
556
+ transform: 'translateX(Variables.offsetX)',
557
+ height: '50px',
558
+ color: 'Variables.color',
559
+ },
560
+ events: {},
561
+ classes: {},
562
+ children: [],
563
+ },
564
+ },
565
+ formulas: {},
566
+ apis: {},
567
+ attributes: {},
568
+ variables: {},
569
+ },
570
+ },
571
+ }
572
+ const fixedFiles = fixProject({
573
+ files,
574
+ rule: invalidStyleSyntaxRule,
575
+ fixType: 'delete-style-property',
576
+ })
577
+ expect((fixedFiles.components.test!.nodes?.root as ElementNodeModel).style)
578
+ .toMatchInlineSnapshot(`
579
+ {
580
+ "height": "50px",
581
+ "width": "100px",
582
+ }
583
+ `)
584
+ })
585
+
251
586
  test('should remove an invalid style variant style property', () => {
252
587
  const files: ProjectFiles = {
253
588
  formulas: {},
@@ -300,4 +635,55 @@ describe('fix invalidStyleSyntaxRule', () => {
300
635
  }
301
636
  `)
302
637
  })
638
+
639
+ test('should remove a variant style property with formula syntax', () => {
640
+ const files: ProjectFiles = {
641
+ formulas: {},
642
+ components: {
643
+ test: {
644
+ name: 'test',
645
+ nodes: {
646
+ root: {
647
+ tag: 'div',
648
+ type: 'element',
649
+ attrs: {},
650
+ style: {},
651
+ events: {},
652
+ classes: {},
653
+ children: [],
654
+ variants: [
655
+ {
656
+ style: {
657
+ width: '100px',
658
+ transform: 'translateX(Variables.offsetX)',
659
+ height: '50px',
660
+ color: 'Event.color',
661
+ },
662
+ hover: true,
663
+ },
664
+ ],
665
+ },
666
+ },
667
+ formulas: {},
668
+ apis: {},
669
+ attributes: {},
670
+ variables: {},
671
+ },
672
+ },
673
+ }
674
+ const fixedFiles = fixProject({
675
+ files,
676
+ rule: invalidStyleSyntaxRule,
677
+ fixType: 'delete-style-property',
678
+ })
679
+ expect(
680
+ (fixedFiles.components.test!.nodes?.root as ElementNodeModel)
681
+ .variants?.[0].style,
682
+ ).toMatchInlineSnapshot(`
683
+ {
684
+ "height": "50px",
685
+ "width": "100px",
686
+ }
687
+ `)
688
+ })
303
689
  })
@@ -12,6 +12,27 @@ export const invalidStyleSyntaxRule: Rule<{
12
12
  if (nodeType !== 'style-declaration') {
13
13
  return
14
14
  }
15
+
16
+ // Check for variable/formula references: Variables., Formulas., Event., Attributes., Apis., Parameters., ListItem., URLParameters.
17
+ if (typeof value.styleValue === 'string') {
18
+ const hasVariableReference =
19
+ /\b(Variables|Formulas|Event|Attributes|Apis|Parameters|ListItem|URLParameters)\.\w+/i.test(
20
+ value.styleValue,
21
+ )
22
+ if (hasVariableReference) {
23
+ report({
24
+ path,
25
+ info: {
26
+ title: `Formulas detected in style declaration`,
27
+ description: `The style declaration for the property "${value.styleProperty}" contains Nordcraft formula syntax (e.g., references like "Variables.xxx", "Event.xxx", "Attributes.xxx", etc.). Formulas should not be used directly in CSS style values. Use style-variables or computed styles instead.`,
28
+ },
29
+ details: { property: value.styleProperty },
30
+ fixes: ['delete-style-property'],
31
+ })
32
+ return
33
+ }
34
+ }
35
+
15
36
  const valid = memo(
16
37
  `valid-style-${value.styleProperty}:${value.styleValue}`,
17
38
  () => {
package/src/types.ts CHANGED
@@ -59,15 +59,16 @@ export type Code =
59
59
  | 'duplicate action argument name'
60
60
  | 'duplicate event trigger'
61
61
  | 'duplicate formula argument name'
62
+ | 'duplicate route'
62
63
  | 'duplicate url parameter'
63
64
  | 'duplicate workflow parameter'
64
- | 'duplicate route'
65
+ | 'image without dimension'
65
66
  | 'invalid api parser mode'
66
67
  | 'invalid api proxy body setting'
67
68
  | 'invalid api proxy cookie setting'
69
+ | 'invalid component structure'
68
70
  | 'invalid element child'
69
71
  | 'invalid style syntax'
70
- | 'invalid component structure'
71
72
  | 'legacy action'
72
73
  | 'legacy api'
73
74
  | 'legacy formula'
@@ -77,10 +78,10 @@ export type Code =
77
78
  | 'no post navigate action'
78
79
  | 'no-console'
79
80
  | 'no-reference api input'
80
- | 'no-reference api'
81
81
  | 'no-reference api service'
82
- | 'no-reference attribute'
82
+ | 'no-reference api'
83
83
  | 'no-reference attribute in instance'
84
+ | 'no-reference attribute'
84
85
  | 'no-reference component formula'
85
86
  | 'no-reference component workflow'
86
87
  | 'no-reference component'
@@ -100,24 +101,25 @@ export type Code =
100
101
  | 'required element attribute'
101
102
  | 'required extension'
102
103
  | 'required meta tag'
103
- | 'image without dimension'
104
+ | 'size constraint'
104
105
  | 'unknown action argument'
105
106
  | 'unknown action event'
106
107
  | 'unknown api input'
107
- | 'unknown api'
108
108
  | 'unknown api service'
109
+ | 'unknown api'
109
110
  | 'unknown attribute'
110
111
  | 'unknown classname'
111
112
  | 'unknown component attribute'
112
113
  | 'unknown component formula input'
113
114
  | 'unknown component slot'
115
+ | 'unknown component'
114
116
  | 'unknown context formula'
115
117
  | 'unknown context provider formula'
116
118
  | 'unknown context provider workflow'
117
119
  | 'unknown context provider'
118
120
  | 'unknown context workflow'
119
121
  | 'unknown cookie'
120
- | 'unknown component'
122
+ | 'unknown css variable'
121
123
  | 'unknown event'
122
124
  | 'unknown fetch input'
123
125
  | 'unknown formula'
@@ -129,12 +131,11 @@ export type Code =
129
131
  | 'unknown set url parameter'
130
132
  | 'unknown set url parameters'
131
133
  | 'unknown trigger event'
134
+ | 'unknown trigger workflow parameter'
132
135
  | 'unknown trigger workflow'
133
136
  | 'unknown url parameter'
134
137
  | 'unknown variable setter'
135
138
  | 'unknown variable'
136
- | 'unknown css variable'
137
- | 'unknown trigger workflow parameter'
138
139
  | 'unknown workflow parameter'
139
140
 
140
141
  export type Category =