From 48741313cc0e8a825338fff712735b4970e0b391 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 30 Jun 2026 15:28:37 -0400 Subject: [PATCH 1/6] Automated Testing: Enforce no-unresolved checks for test files --- package-lock.json | 25 +++--- packages/block-library/package.json | 1 + packages/block-library/src/cover/test/edit.js | 2 +- .../stories/ComponentsProvider.stories.jsx | 6 +- packages/editor/package.json | 1 + .../post-revisions-preview/test/block-diff.js | 4 +- packages/is-shallow-equal/package.json | 1 + test/e2e/package.json | 1 + test/integration/package.json | 3 + test/performance/package.json | 3 +- tools/eslint/config.mjs | 4 +- tools/eslint/import-resolver.cjs | 84 +++++++++++++++++-- 12 files changed, 110 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 49e93fa5323c69..5e6db3307d83dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43127,6 +43127,15 @@ "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==", "license": "Apache-2.0" }, + "node_modules/wasm-vips": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/wasm-vips/-/wasm-vips-0.0.18.tgz", + "integrity": "sha512-AJyCvxZj/3qceKNnh+YyEobu/IaJFoPN7x7SxyyHmYBS3kASMqJqxQEuN0ZHKQDWsCJ8armfx4Tq3uKrNc+nMA==", + "license": "MIT", + "engines": { + "node": ">=17.0.0" + } + }, "node_modules/watchpack": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", @@ -45246,6 +45255,7 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@wordpress/integration-tests": "file:../../test/integration", "@wordpress/stylelint-tools": "file:../../tools/stylelint", "deep-freeze": "^0.0.1" }, @@ -46982,6 +46992,7 @@ "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@wordpress/block-library": "file:../block-library", "deep-freeze": "^0.0.1" }, "engines": { @@ -47589,6 +47600,7 @@ "version": "5.49.0", "license": "GPL-2.0-or-later", "devDependencies": { + "@wordpress/lazy-import": "file:../lazy-import", "benchmark": "^2.1.4" }, "engines": { @@ -49494,15 +49506,6 @@ "npm": ">=8.19.2" } }, - "packages/vips/node_modules/wasm-vips": { - "version": "0.0.18", - "resolved": "https://registry.npmjs.org/wasm-vips/-/wasm-vips-0.0.18.tgz", - "integrity": "sha512-AJyCvxZj/3qceKNnh+YyEobu/IaJFoPN7x7SxyyHmYBS3kASMqJqxQEuN0ZHKQDWsCJ8armfx4Tq3uKrNc+nMA==", - "license": "MIT", - "engines": { - "node": ">=17.0.0" - } - }, "packages/warning": { "name": "@wordpress/warning", "version": "3.49.0", @@ -50199,6 +50202,7 @@ "@y/websocket-server": "^0.1.1", "esbuild": "^0.27.2", "filenamify": "^4.2.0", + "wasm-vips": "^0.0.18", "ws": "^8.18.3", "y-websocket": "^3.0.0" } @@ -50254,7 +50258,8 @@ "@playwright/test": "^1.58.2", "@types/node": "^20.19.39", "@wordpress/e2e-test-utils-playwright": "file:../../packages/e2e-test-utils-playwright", - "@wordpress/scripts": "file:../../packages/scripts" + "@wordpress/scripts": "file:../../packages/scripts", + "wasm-vips": "^0.0.18" } }, "test/storybook-playwright": { diff --git a/packages/block-library/package.json b/packages/block-library/package.json index 9cbcbc1709ddaa..5100ca90fbfbee 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -145,6 +145,7 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@wordpress/integration-tests": "file:../../test/integration", "@wordpress/stylelint-tools": "file:../../tools/stylelint", "deep-freeze": "^0.0.1" }, diff --git a/packages/block-library/src/cover/test/edit.js b/packages/block-library/src/cover/test/edit.js index 3b1dde597adf6f..fbd7769b726835 100644 --- a/packages/block-library/src/cover/test/edit.js +++ b/packages/block-library/src/cover/test/edit.js @@ -10,7 +10,7 @@ import userEvent from '@testing-library/user-event'; import { initializeEditor, selectBlock, -} from 'test/integration/helpers/integration-test-editor'; +} from '@wordpress/integration-tests/helpers/integration-test-editor'; const defaultSettings = { __experimentalFeatures: { diff --git a/packages/components/src/context/stories/ComponentsProvider.stories.jsx b/packages/components/src/context/stories/ComponentsProvider.stories.jsx index 84c2aa30868603..91ac61cdd8e0dd 100644 --- a/packages/components/src/context/stories/ComponentsProvider.stories.jsx +++ b/packages/components/src/context/stories/ComponentsProvider.stories.jsx @@ -6,9 +6,9 @@ import { createContext, useContext, memo, useState } from '@wordpress/element'; /** * Internal dependencies */ -import { Card, CardBody } from '../../../card'; -import { View } from '../../../view'; -import { Text } from '../../../text'; +import { Card, CardBody } from '../../card'; +import { View } from '../../view'; +import { Text } from '../../text'; import { ContextSystemProvider } from '../index'; const SomeContext = createContext(); diff --git a/packages/editor/package.json b/packages/editor/package.json index 2882230a163366..aff3f31836152d 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -121,6 +121,7 @@ "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@wordpress/block-library": "file:../block-library", "deep-freeze": "^0.0.1" }, "peerDependencies": { diff --git a/packages/editor/src/components/post-revisions-preview/test/block-diff.js b/packages/editor/src/components/post-revisions-preview/test/block-diff.js index 71f9a4683b02b0..f3c0677b955d5b 100644 --- a/packages/editor/src/components/post-revisions-preview/test/block-diff.js +++ b/packages/editor/src/components/post-revisions-preview/test/block-diff.js @@ -9,8 +9,8 @@ import { serialize, } from '@wordpress/blocks'; import { RichTextData } from '@wordpress/rich-text'; -import * as paragraphBlock from '@wordpress/block-library/src/paragraph'; -import * as groupBlock from '@wordpress/block-library/src/group'; +import * as paragraphBlock from '@wordpress/block-library/build-module/paragraph/index.mjs'; +import * as groupBlock from '@wordpress/block-library/build-module/group/index.mjs'; /** * Internal dependencies diff --git a/packages/is-shallow-equal/package.json b/packages/is-shallow-equal/package.json index 76912c37b9f598..6ca7bb0d4043b2 100644 --- a/packages/is-shallow-equal/package.json +++ b/packages/is-shallow-equal/package.json @@ -45,6 +45,7 @@ "types": "build-types", "sideEffects": false, "devDependencies": { + "@wordpress/lazy-import": "file:../lazy-import", "benchmark": "^2.1.4" }, "publishConfig": { diff --git a/test/e2e/package.json b/test/e2e/package.json index c5bd37bba3ba25..142d258bab7a62 100644 --- a/test/e2e/package.json +++ b/test/e2e/package.json @@ -29,6 +29,7 @@ "@y/websocket-server": "^0.1.1", "esbuild": "^0.27.2", "filenamify": "^4.2.0", + "wasm-vips": "^0.0.18", "ws": "^8.18.3", "y-websocket": "^3.0.0" }, diff --git a/test/integration/package.json b/test/integration/package.json index 649feb38b6c096..6cb61779b06d71 100644 --- a/test/integration/package.json +++ b/test/integration/package.json @@ -16,6 +16,9 @@ "url": "git+https://github.com/WordPress/gutenberg.git", "directory": "test/integration" }, + "exports": { + "./helpers/integration-test-editor": "./helpers/integration-test-editor.js" + }, "bugs": { "url": "https://github.com/WordPress/gutenberg/issues" }, diff --git a/test/performance/package.json b/test/performance/package.json index f3ff9d67bd4636..d60c76e6d25df5 100644 --- a/test/performance/package.json +++ b/test/performance/package.json @@ -24,7 +24,8 @@ "@playwright/test": "^1.58.2", "@types/node": "^20.19.39", "@wordpress/e2e-test-utils-playwright": "file:../../packages/e2e-test-utils-playwright", - "@wordpress/scripts": "file:../../packages/scripts" + "@wordpress/scripts": "file:../../packages/scripts", + "wasm-vips": "^0.0.18" }, "publishConfig": { "access": "public" diff --git a/tools/eslint/config.mjs b/tools/eslint/config.mjs index 3a745970910615..9bc321eeac9caa 100644 --- a/tools/eslint/config.mjs +++ b/tools/eslint/config.mjs @@ -374,9 +374,6 @@ export default dedupePlugins( [ { files: developmentFiles, rules: { - 'import/default': 'off', - 'import/no-unresolved': 'off', - 'import/named': 'off', '@wordpress/data-no-store-string-literals': 'off', }, }, @@ -387,6 +384,7 @@ export default dedupePlugins( [ files: [ '**/fixtures/**' ], rules: { 'import/no-extraneous-dependencies': 'off', + 'import/no-unresolved': 'off', }, }, diff --git a/tools/eslint/import-resolver.cjs b/tools/eslint/import-resolver.cjs index 3d08282515733d..41bdf1127bd613 100644 --- a/tools/eslint/import-resolver.cjs +++ b/tools/eslint/import-resolver.cjs @@ -8,11 +8,81 @@ const PACKAGES_DIR = path.resolve( __dirname, '../../packages' ); exports.interfaceVersion = 2; +/** + * @typedef ExportEntryObject + * + * @property {string} import ESM import entrypoint. + * @property {string} default Default export entrypoint. + */ + +/** + * @typedef {string|ExportEntryObject} ExportEntry + */ + +/** + * Given an export entry and a subpath, returns the resolved export path for + * the matching entry. Defaults to the ESM entrypoint if available, falling back + * to the default export or the root entry if it is a string. + * + * @param {ExportEntry} exportEntry Export entry from package manifest. + * + * @return {string|undefined} The resolved export path, or undefined if no + * matching entry is found. + */ +function getResolvedExportPath( exportEntry ) { + return typeof exportEntry === 'string' + ? exportEntry + : exportEntry?.import ?? exportEntry?.default; +} + +/** + * Given a package entrypoint identifier, returns the resolved export path for + * the matching entry. Supports matching wildcard entries, and defaults to the + * ESM entrypoint if available. + * + * @param {string} subpath Package entrypoint identifier. + * @param {Record} exportMap Export map from package manifest. + * + * @return {string|void|undefined} The resolved export path, or undefined if no + * matching entry is found. + */ +function getResolvedExport( subpath, exportMap ) { + if ( Object.hasOwn( exportMap, subpath ) ) { + return getResolvedExportPath( exportMap[ subpath ] ); + } + + for ( const key in exportMap ) { + if ( ! Object.hasOwn( exportMap, key ) ) { + continue; + } + + const wildcardIndex = key.indexOf( '*' ); + if ( wildcardIndex === -1 ) { + continue; + } + + if ( subpath.startsWith( key.substring( 0, wildcardIndex ) ) ) { + const wildcardPath = subpath.substring( wildcardIndex ); + const resolvedPath = getResolvedExportPath( exportMap[ key ] ); + return resolvedPath?.replace( '*', wildcardPath ); + } + } +} + exports.resolve = function ( source, file, config ) { const resolve = ( sourcePath ) => resolverNode.resolve( sourcePath, file, { ...config, - extensions: [ '.tsx', '.ts', '.mjs', '.js', '.json', '.node' ], + extensions: [ + '.tsx', + '.ts', + '.mjs', + '.js', + '.jsx', + '.cjs', + '.json', + '.node', + ], } ); if ( source.startsWith( '@wordpress/' ) ) { @@ -30,11 +100,15 @@ exports.resolve = function ( source, file, config ) { try { const manifestPath = path.join( packagePath, 'package.json' ); const manifest = JSON.parse( readFileSync( manifestPath, 'utf8' ) ); - const subpath = path.join( '.', pathParts.join( '/' ) ); - const exportPath = manifest.exports?.[ subpath ]?.import; + let subpath = '.'; + if ( pathParts.length > 0 ) { + subpath += '/' + pathParts.join( '/' ); + } + const exportPath = getResolvedExport( subpath, manifest.exports ); + const sourcePath = exportPath - .replace( 'build-module', 'src' ) - .replace( /\.mjs$/, '.js' ); + .replace( /build(-module?)/, 'src' ) + .replace( /\.[cm]?js$/, '.js' ); return resolve( path.join( packagePath, sourcePath ) ); } catch { From a572626e8c2ebaa5a779f449168dfdd9f121bdce Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 30 Jun 2026 15:46:17 -0400 Subject: [PATCH 2/6] Use correct regex syntax for optional group --- tools/eslint/import-resolver.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/eslint/import-resolver.cjs b/tools/eslint/import-resolver.cjs index 41bdf1127bd613..a34b0198d02faa 100644 --- a/tools/eslint/import-resolver.cjs +++ b/tools/eslint/import-resolver.cjs @@ -107,7 +107,7 @@ exports.resolve = function ( source, file, config ) { const exportPath = getResolvedExport( subpath, manifest.exports ); const sourcePath = exportPath - .replace( /build(-module?)/, 'src' ) + .replace( /build(-module)?/, 'src' ) .replace( /\.[cm]?js$/, '.js' ); return resolve( path.join( packagePath, sourcePath ) ); From a7451a6a6cb34eff900525f43a4e38aa67259383 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 30 Jun 2026 16:11:37 -0400 Subject: [PATCH 3/6] Put exports in expected manifest order --- test/integration/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration/package.json b/test/integration/package.json index 6cb61779b06d71..059a3ccfa0a60f 100644 --- a/test/integration/package.json +++ b/test/integration/package.json @@ -16,12 +16,12 @@ "url": "git+https://github.com/WordPress/gutenberg.git", "directory": "test/integration" }, - "exports": { - "./helpers/integration-test-editor": "./helpers/integration-test-editor.js" - }, "bugs": { "url": "https://github.com/WordPress/gutenberg/issues" }, + "exports": { + "./helpers/integration-test-editor": "./helpers/integration-test-editor.js" + }, "devDependencies": { "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", From 420b2cc9f870bf4946bbf0eb81151ee048175b93 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 30 Jun 2026 16:14:07 -0400 Subject: [PATCH 4/6] Add custom import resolution for CSS imports --- packages/block-editor/package.json | 2 +- .../block-patterns-list/stories/index.story.jsx | 2 +- storybook/stories/playground/zoom-out/index.jsx | 2 +- tools/eslint/import-resolver.cjs | 13 +++++++++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index 76bdd71295ebb3..561ce97aca6f2c 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -39,7 +39,7 @@ "require": "./build/index.cjs" }, "./package.json": "./package.json", - "./build-style/": "./build-style/" + "./build-style/*": "./build-style/*" }, "wpScript": true, "wpScriptModuleExports": { diff --git a/packages/block-editor/src/components/block-patterns-list/stories/index.story.jsx b/packages/block-editor/src/components/block-patterns-list/stories/index.story.jsx index d30b99241966eb..39860ce17f2929 100644 --- a/packages/block-editor/src/components/block-patterns-list/stories/index.story.jsx +++ b/packages/block-editor/src/components/block-patterns-list/stories/index.story.jsx @@ -11,7 +11,7 @@ import { ExperimentalBlockEditorProvider } from '../../provider'; import patterns from './fixtures'; // Reason: Styles are contained in ExperimentalBlockEditorProvider iframe. // eslint-disable-next-line @wordpress/no-non-module-stylesheet-imports -import blockLibraryStyles from '../../../../../block-library/build-style/style.css?raw'; +import blockLibraryStyles from '@wordpress/block-library/build-style/style.css?raw'; const blockEditorSettings = { styles: [ { css: blockLibraryStyles } ], diff --git a/storybook/stories/playground/zoom-out/index.jsx b/storybook/stories/playground/zoom-out/index.jsx index 389f7965150243..5a48617246bfcb 100644 --- a/storybook/stories/playground/zoom-out/index.jsx +++ b/storybook/stories/playground/zoom-out/index.jsx @@ -13,7 +13,7 @@ import { pattern } from './pattern'; import { editorStyles } from '../editor-styles'; // Reason: Styles are contained in BlockCanvas iframe. // eslint-disable-next-line @wordpress/no-non-module-stylesheet-imports -import contentCss from '../../../../packages/block-editor/build-style/content.css?raw'; +import contentCss from '@wordpress/block-editor/build-style/content.css?raw'; // Temporary hack to access private APIs before stabilizing zoom level. const { unlock } = __dangerousOptInToUnstableAPIsOnlyForCoreModules( diff --git a/tools/eslint/import-resolver.cjs b/tools/eslint/import-resolver.cjs index a34b0198d02faa..7d7d3f6eb12be0 100644 --- a/tools/eslint/import-resolver.cjs +++ b/tools/eslint/import-resolver.cjs @@ -104,9 +104,22 @@ exports.resolve = function ( source, file, config ) { if ( pathParts.length > 0 ) { subpath += '/' + pathParts.join( '/' ); } + + // Storybook uses the React Vite builder, and Vite supports + // importing assets as strings by appending `?raw`. Ignore this + // and other querystrings in the path when resolving the export. + // + // See: https://vite.dev/guide/assets#importing-asset-as-string + subpath = subpath.split( '?' )[ 0 ]; + const exportPath = getResolvedExport( subpath, manifest.exports ); const sourcePath = exportPath + // Remap build-style CSS files to src SCSS files. By default, + // wp-build emits a CSS file for each SCSS file in src. This is + // controlled by wpStyleEntryPoints which we don't fully + // recreate here (yet), but generally we don't override this. + .replace( /build-style\/(.+?)\.css/, 'src/$1.scss' ) .replace( /build(-module)?/, 'src' ) .replace( /\.[cm]?js$/, '.js' ); From 7b219052be0c5d7323164baf878b8ea645e309b0 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 30 Jun 2026 16:29:06 -0400 Subject: [PATCH 5/6] Testing: Add mapping to emulate block-library subpath exports --- test/unit/jest.config.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/unit/jest.config.js b/test/unit/jest.config.js index 19bdca4c8bd0b3..6bb44b3b09f6e1 100644 --- a/test/unit/jest.config.js +++ b/test/unit/jest.config.js @@ -48,6 +48,8 @@ module.exports = { 'packages/$1/src', '@wordpress/theme/design-tokens.js': '/packages/theme/src/prebuilt/js/design-tokens.mjs', + '@wordpress/block-library/build-module/(.*).mjs': + '/packages/block-library/src/$1.js', '.+\\.wasm$': '/test/unit/config/wasm-stub.js', }, preset: jestPresetDefaultDir, From 9c64d862965a910418fd9dea7b923356d913ce94 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 1 Jul 2026 11:46:48 -0400 Subject: [PATCH 6/6] Block Serialization Spec Parser: Prebuild JS for lint When analyzing the test files, it references built artifacts which must exist as otherwise they are unresolvable imports. --- packages/block-serialization-spec-parser/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/block-serialization-spec-parser/package.json b/packages/block-serialization-spec-parser/package.json index a98564691fe5af..140baf94859a57 100644 --- a/packages/block-serialization-spec-parser/package.json +++ b/packages/block-serialization-spec-parser/package.json @@ -40,6 +40,7 @@ "access": "public" }, "scripts": { + "prelint:js": "npm run build:js", "build": "concurrently \"npm run build:js\" \"npm run build:php\"", "build:js": "pegjs --format commonjs -o ./parser.js ./grammar.pegjs", "build:php": "node bin/create-php-parser.js"