@nordcraft/search 1.0.86 → 1.0.87

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.
@@ -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"}
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.87",
14
+ "@nordcraft/core": "1.0.87",
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.87"
30
30
  }
@@ -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
 
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 =