@elementor/editor-audits 4.4.0-1105 → 4.4.0-1106

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elementor/editor-audits",
3
- "version": "4.4.0-1105",
3
+ "version": "4.4.0-1106",
4
4
  "private": false,
5
5
  "author": "Elementor Team",
6
6
  "homepage": "https://elementor.com/",
@@ -39,19 +39,21 @@
39
39
  "dev": "tsup --config=../../tsup.dev.ts"
40
40
  },
41
41
  "dependencies": {
42
- "@elementor/editor": "4.4.0-1105",
43
- "@elementor/editor-mcp": "4.4.0-1105",
44
- "@elementor/editor-app-bar": "4.4.0-1105",
45
- "@elementor/editor-elements": "4.4.0-1105",
46
- "@elementor/editor-floating-panels": "4.4.0-1105",
47
- "@elementor/editor-notifications": "4.4.0-1105",
48
- "@elementor/editor-v1-adapters": "4.4.0-1105",
49
- "@elementor/events": "4.4.0-1105",
50
- "@elementor/http-client": "4.4.0-1105",
42
+ "@elementor/editor": "4.4.0-1106",
43
+ "@elementor/editor-mcp": "4.4.0-1106",
44
+ "@elementor/editor-app-bar": "4.4.0-1106",
45
+ "@elementor/editor-elements": "4.4.0-1106",
46
+ "@elementor/editor-floating-panels": "4.4.0-1106",
47
+ "@elementor/editor-notifications": "4.4.0-1106",
48
+ "@elementor/editor-props": "4.4.0-1106",
49
+ "@elementor/editor-styles": "4.4.0-1106",
50
+ "@elementor/editor-v1-adapters": "4.4.0-1106",
51
+ "@elementor/events": "4.4.0-1106",
52
+ "@elementor/http-client": "4.4.0-1106",
51
53
  "@elementor/icons": "~1.77.0",
52
- "@elementor/locations": "4.4.0-1105",
53
- "@elementor/session": "4.4.0-1105",
54
- "@elementor/store": "4.4.0-1105",
54
+ "@elementor/locations": "4.4.0-1106",
55
+ "@elementor/session": "4.4.0-1106",
56
+ "@elementor/store": "4.4.0-1106",
55
57
  "@elementor/ui": "1.37.5"
56
58
  },
57
59
  "peerDependencies": {
@@ -4,13 +4,16 @@ import { type PageContextResponse } from '../types';
4
4
  import { isNonceInvalidError, refreshAuditsNonce } from '../utils/session-expiration';
5
5
  import { getWindowConfig } from '../utils/window-config';
6
6
 
7
- export async function fetchPageContext( documentId: number, attachmentIds: number[] ): Promise< PageContextResponse > {
8
- return requestPageContext( documentId, attachmentIds, true );
7
+ export async function fetchPageContext(
8
+ documentId: number,
9
+ imageSizeRequests: string[]
10
+ ): Promise< PageContextResponse > {
11
+ return requestPageContext( documentId, imageSizeRequests, true );
9
12
  }
10
13
 
11
14
  async function requestPageContext(
12
15
  documentId: number,
13
- attachmentIds: number[],
16
+ imageSizeRequests: string[],
14
17
  allowNonceRetry: boolean
15
18
  ): Promise< PageContextResponse > {
16
19
  const { restNamespace, nonce } = getWindowConfig();
@@ -20,7 +23,7 @@ async function requestPageContext(
20
23
  const response = await httpService().get< PageContextResponse >( url, {
21
24
  params: {
22
25
  document_id: documentId,
23
- attachment_ids: attachmentIds,
26
+ image_size_requests: imageSizeRequests,
24
27
  },
25
28
  headers: { 'X-WP-Nonce': nonce },
26
29
  } );
@@ -33,6 +36,6 @@ async function requestPageContext(
33
36
 
34
37
  await refreshAuditsNonce();
35
38
 
36
- return requestPageContext( documentId, attachmentIds, false );
39
+ return requestPageContext( documentId, imageSizeRequests, false );
37
40
  }
38
41
  }
@@ -1,7 +1,11 @@
1
1
  import { __, sprintf } from '@wordpress/i18n';
2
2
 
3
- import { type Audit, type AuditViolation } from '../types';
4
- import { hasPageImages, walkImageLikeSources } from '../utils/image-like-sources';
3
+ import { type Audit, type AuditViolation, type ElementSnapshotNode } from '../types';
4
+ import { walkAtomicBackgroundImageSources } from '../utils/atomic-background-image-sources';
5
+ import { walkAtomicImageSources } from '../utils/atomic-image-sources';
6
+ import { walkBackgroundImageSources } from '../utils/background-image-sources';
7
+ import { type ImageLikeMedia, walkImageLikeSources } from '../utils/image-like-sources';
8
+ import { buildImageSizeKey } from '../utils/image-size-key';
5
9
 
6
10
  const SIZE_THRESHOLD_BYTES = 500 * 1024;
7
11
  const BYTES_PER_KB = 1024;
@@ -15,21 +19,22 @@ export const audit: Audit = {
15
19
  severity: 'warning',
16
20
  weight: 7,
17
21
  evaluate: ( ctx ) => {
18
- if ( ! hasPageImages( ctx.elements.tree ) ) {
19
- return { status: 'skipped', reason: __( 'No images', 'elementor' ) };
20
- }
21
-
22
22
  const widgetMaxKb = new Map< string, number >();
23
23
  let oversizedImageCount = 0;
24
+ let hasAnyImage = false;
25
+
26
+ const evaluateSource = ( node: ElementSnapshotNode, media: ImageLikeMedia ) => {
27
+ if ( media.id || media.url ) {
28
+ hasAnyImage = true;
29
+ }
24
30
 
25
- walkImageLikeSources( ctx.elements.tree, ( { node, media } ) => {
26
31
  const id = media.id;
27
32
 
28
33
  if ( ! id ) {
29
34
  return;
30
35
  }
31
36
 
32
- const size = ctx.pageContext.image_sizes[ id ];
37
+ const size = ctx.pageContext.image_sizes[ buildImageSizeKey( { id, size: media.size } ) ];
33
38
 
34
39
  if ( ! size || size.filesize_bytes <= SIZE_THRESHOLD_BYTES ) {
35
40
  return;
@@ -40,7 +45,16 @@ export const audit: Audit = {
40
45
  const kb = Math.round( size.filesize_bytes / BYTES_PER_KB );
41
46
  const currentMax = widgetMaxKb.get( node.id ) ?? 0;
42
47
  widgetMaxKb.set( node.id, Math.max( currentMax, kb ) );
43
- } );
48
+ };
49
+
50
+ walkImageLikeSources( ctx.elements.tree, ( { node, media } ) => evaluateSource( node, media ) );
51
+ walkBackgroundImageSources( ctx.elements.tree, ( { node, media } ) => evaluateSource( node, media ) );
52
+ walkAtomicImageSources( ctx.elements.tree, ( { node, media } ) => evaluateSource( node, media ) );
53
+ walkAtomicBackgroundImageSources( ctx.elements.tree, ( { node, media } ) => evaluateSource( node, media ) );
54
+
55
+ if ( ! hasAnyImage ) {
56
+ return { status: 'skipped', reason: __( 'No images', 'elementor' ) };
57
+ }
44
58
 
45
59
  const violations: AuditViolation[] = Array.from( widgetMaxKb.entries() ).map( ( [ elementId, kb ] ) => ( {
46
60
  auditId: audit.id,
package/src/runner.ts CHANGED
@@ -12,14 +12,14 @@ import {
12
12
  type PageAuditReport,
13
13
  } from './types';
14
14
  import { computeReport } from './utils/compute-report';
15
- import { extractAttachmentIds } from './utils/page-attachments';
15
+ import { extractImageSizeRequests } from './utils/page-attachments';
16
16
  import { readKitSnapshot } from './utils/read-kit-snapshot';
17
17
  import { buildSnapshotTree } from './utils/v1-snapshot';
18
18
 
19
19
  export async function runPageAudit( documentId: number ): Promise< PageAuditReport > {
20
20
  const tree = buildSnapshotTree( getElements() );
21
- const attachmentIds = extractAttachmentIds( tree );
22
- const pageContext = await fetchPageContext( documentId, attachmentIds );
21
+ const imageSizeRequests = extractImageSizeRequests( tree );
22
+ const pageContext = await fetchPageContext( documentId, imageSizeRequests );
23
23
 
24
24
  const elements: ElementsModelSnapshot = { documentId, tree };
25
25
  const kit: KitSnapshot = await readKitSnapshot( pageContext.kit_id );
package/src/types.ts CHANGED
@@ -46,8 +46,9 @@ export type PageContextResponse = {
46
46
  post_title: string | null;
47
47
  post_excerpt: string | null;
48
48
  featured_image_id: number | null;
49
+ /** Keyed by the composite `buildImageSizeKey` result (`"{attachmentId}:{size}"`), since one attachment can resolve to different files at different sizes. */
49
50
  image_sizes: Record<
50
- number,
51
+ string,
51
52
  {
52
53
  width: number;
53
54
  height: number;
@@ -0,0 +1,56 @@
1
+ import { getElementStyles } from '@elementor/editor-elements';
2
+ import {
3
+ backgroundImageOverlayPropTypeUtil,
4
+ backgroundOverlayPropTypeUtil,
5
+ backgroundPropTypeUtil,
6
+ } from '@elementor/editor-props';
7
+ import { type StyleDefinition } from '@elementor/editor-styles';
8
+
9
+ import { type ElementSnapshotNode } from '../types';
10
+ import { extractAtomicImageMedia } from './atomic-image-sources';
11
+ import { type ImageLikeMedia } from './image-like-sources';
12
+ import { walkElements } from './walk';
13
+
14
+ export type AtomicBackgroundImageSourceVisit = {
15
+ node: ElementSnapshotNode;
16
+ media: ImageLikeMedia;
17
+ };
18
+
19
+ export function walkAtomicBackgroundImageSources(
20
+ tree: ElementSnapshotNode[],
21
+ visit: ( source: AtomicBackgroundImageSourceVisit ) => void
22
+ ): void {
23
+ walkElements( tree, ( node ) => {
24
+ const styles = getElementStyles( node.id );
25
+
26
+ if ( ! styles ) {
27
+ return;
28
+ }
29
+
30
+ for ( const media of extractBackgroundImageMedia( styles ) ) {
31
+ visit( { node, media } );
32
+ }
33
+ } );
34
+ }
35
+
36
+ function extractBackgroundImageMedia( styles: Record< string, StyleDefinition > ): ImageLikeMedia[] {
37
+ const sources: ImageLikeMedia[] = [];
38
+
39
+ for ( const style of Object.values( styles ) ) {
40
+ for ( const variant of style.variants ) {
41
+ const background = backgroundPropTypeUtil.extract( variant.props.background );
42
+ const overlays = backgroundOverlayPropTypeUtil.extract( background?.[ 'background-overlay' ] ) ?? [];
43
+
44
+ for ( const overlay of overlays ) {
45
+ const imageOverlay = backgroundImageOverlayPropTypeUtil.extract( overlay );
46
+ const media = extractAtomicImageMedia( imageOverlay?.image );
47
+
48
+ if ( media ) {
49
+ sources.push( media );
50
+ }
51
+ }
52
+ }
53
+ }
54
+
55
+ return sources;
56
+ }
@@ -0,0 +1,62 @@
1
+ import {
2
+ imageAttachmentIdPropType,
3
+ imagePropTypeUtil,
4
+ imageSrcPropTypeUtil,
5
+ stringPropTypeUtil,
6
+ urlPropTypeUtil,
7
+ } from '@elementor/editor-props';
8
+
9
+ import { type ElementSnapshotNode } from '../types';
10
+ import { type ImageLikeMedia } from './image-like-sources';
11
+ import { walkElements } from './walk';
12
+
13
+ const ATOMIC_IMAGE_PROP_KEYS = [ 'image', 'poster' ] as const;
14
+
15
+ export type AtomicImageSourceVisit = {
16
+ node: ElementSnapshotNode;
17
+ media: ImageLikeMedia;
18
+ };
19
+
20
+ export function walkAtomicImageSources(
21
+ tree: ElementSnapshotNode[],
22
+ visit: ( source: AtomicImageSourceVisit ) => void
23
+ ): void {
24
+ walkElements( tree, ( node ) => {
25
+ if ( node.elType !== 'widget' ) {
26
+ return;
27
+ }
28
+
29
+ for ( const key of ATOMIC_IMAGE_PROP_KEYS ) {
30
+ const media = extractAtomicImageMedia( node.settings[ key ] );
31
+
32
+ if ( media ) {
33
+ visit( { node, media } );
34
+ }
35
+ }
36
+ } );
37
+ }
38
+
39
+ export function extractAtomicImageMedia( imageProp: unknown ): ImageLikeMedia | null {
40
+ const image = imagePropTypeUtil.extract( imageProp );
41
+
42
+ if ( ! image ) {
43
+ return null;
44
+ }
45
+
46
+ const src = imageSrcPropTypeUtil.extract( image.src );
47
+
48
+ if ( ! src ) {
49
+ return null;
50
+ }
51
+
52
+ const id = imageAttachmentIdPropType.extract( src.id ) ?? undefined;
53
+ const url = urlPropTypeUtil.extract( src.url ) ?? undefined;
54
+
55
+ if ( ! id && ! url ) {
56
+ return null;
57
+ }
58
+
59
+ const size = stringPropTypeUtil.extract( image.size ) ?? undefined;
60
+
61
+ return { id, url, size };
62
+ }
@@ -0,0 +1,32 @@
1
+ import { type ElementSnapshotNode } from '../types';
2
+ import { type ImageLikeMedia } from './image-like-sources';
3
+ import { walkElements } from './walk';
4
+
5
+ const BACKGROUND_IMAGE_SETTING_KEYS = [
6
+ 'background_image',
7
+ 'background_hover_image',
8
+ 'background_overlay_image',
9
+ 'background_overlay_hover_image',
10
+ '_background_image',
11
+ '_background_hover_image',
12
+ ] as const;
13
+
14
+ export type BackgroundImageSourceVisit = {
15
+ node: ElementSnapshotNode;
16
+ media: ImageLikeMedia;
17
+ };
18
+
19
+ export function walkBackgroundImageSources(
20
+ tree: ElementSnapshotNode[],
21
+ visit: ( source: BackgroundImageSourceVisit ) => void
22
+ ): void {
23
+ walkElements( tree, ( node ) => {
24
+ for ( const key of BACKGROUND_IMAGE_SETTING_KEYS ) {
25
+ const media = node.settings[ key ] as ImageLikeMedia | undefined;
26
+
27
+ if ( media?.id || media?.url ) {
28
+ visit( { node, media } );
29
+ }
30
+ }
31
+ } );
32
+ }
@@ -1,5 +1,6 @@
1
1
  import { type PageContextResponse } from '../types';
2
2
  import { type ImageLikeMedia } from './image-like-sources';
3
+ import { buildImageSizeKey } from './image-size-key';
3
4
 
4
5
  export function hasMeaningfulAlt( media: ImageLikeMedia, pageContext: PageContextResponse ): boolean {
5
6
  if ( ! media.id && ! media.url ) {
@@ -7,7 +8,8 @@ export function hasMeaningfulAlt( media: ImageLikeMedia, pageContext: PageContex
7
8
  }
8
9
 
9
10
  if ( media.id ) {
10
- const alt = pageContext.image_sizes[ media.id ]?.alt ?? '';
11
+ const key = buildImageSizeKey( { id: media.id, size: media.size } );
12
+ const alt = pageContext.image_sizes[ key ]?.alt ?? '';
11
13
  return alt.trim().length > 0;
12
14
  }
13
15
 
@@ -7,6 +7,7 @@ export type ImageLikeMedia = {
7
7
  id?: number;
8
8
  url?: string;
9
9
  alt?: string;
10
+ size?: string;
10
11
  };
11
12
 
12
13
  export type ImageLikeSourceVisit = {
@@ -51,7 +52,7 @@ function collectMediaFromNode( node: ElementSnapshotNode ): ImageLikeMedia[] {
51
52
  const image = node.settings.image as ImageLikeMedia | undefined;
52
53
 
53
54
  if ( image?.id || image?.url ) {
54
- sources.push( image );
55
+ sources.push( { ...image, size: node.settings.image_size as string | undefined } );
55
56
  }
56
57
 
57
58
  const gallery = ( node.settings.carousel ?? node.settings.gallery ?? node.settings.wp_gallery ) as
@@ -59,9 +60,11 @@ function collectMediaFromNode( node: ElementSnapshotNode ): ImageLikeMedia[] {
59
60
  | undefined;
60
61
 
61
62
  if ( Array.isArray( gallery ) ) {
63
+ const gallerySize = node.settings.thumbnail_size as string | undefined;
64
+
62
65
  for ( const item of gallery ) {
63
66
  if ( item?.id || item?.url ) {
64
- sources.push( item );
67
+ sources.push( { ...item, size: gallerySize } );
65
68
  }
66
69
  }
67
70
  }
@@ -0,0 +1,8 @@
1
+ export type ImageSizeRequest = {
2
+ id: number;
3
+ size?: string;
4
+ };
5
+
6
+ export function buildImageSizeKey( { id, size }: ImageSizeRequest ): string {
7
+ return `${ id }:${ size || 'full' }`;
8
+ }
@@ -1,14 +1,23 @@
1
1
  import { type ElementSnapshotNode } from '../types';
2
- import { walkImageLikeSources } from './image-like-sources';
2
+ import { walkAtomicBackgroundImageSources } from './atomic-background-image-sources';
3
+ import { walkAtomicImageSources } from './atomic-image-sources';
4
+ import { walkBackgroundImageSources } from './background-image-sources';
5
+ import { type ImageLikeMedia, walkImageLikeSources } from './image-like-sources';
6
+ import { buildImageSizeKey } from './image-size-key';
3
7
 
4
- export function extractAttachmentIds( tree: ElementSnapshotNode[] ): number[] {
5
- const ids = new Set< number >();
8
+ export function extractImageSizeRequests( tree: ElementSnapshotNode[] ): string[] {
9
+ const keys = new Set< string >();
6
10
 
7
- walkImageLikeSources( tree, ( { media } ) => {
11
+ const collect = ( media: ImageLikeMedia ) => {
8
12
  if ( media.id ) {
9
- ids.add( media.id );
13
+ keys.add( buildImageSizeKey( { id: media.id, size: media.size } ) );
10
14
  }
11
- } );
15
+ };
12
16
 
13
- return Array.from( ids ).sort( ( a, b ) => a - b );
17
+ walkImageLikeSources( tree, ( { media } ) => collect( media ) );
18
+ walkBackgroundImageSources( tree, ( { media } ) => collect( media ) );
19
+ walkAtomicImageSources( tree, ( { media } ) => collect( media ) );
20
+ walkAtomicBackgroundImageSources( tree, ( { media } ) => collect( media ) );
21
+
22
+ return Array.from( keys ).sort();
14
23
  }