Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e325c79
theme: Clean npm publish surface
ciampo Jun 25, 2026
4f7b989
theme: Add publish surface changelog entry
ciampo Jun 25, 2026
cc3f1d9
theme: Use patterns for package files
ciampo Jun 25, 2026
6307dd7
theme: Narrow published declarations
ciampo Jun 25, 2026
7ac543e
theme: Add package contents validation
ciampo Jun 25, 2026
c47bcfe
theme: Simplify package contents validation
ciampo Jun 25, 2026
a6a9ae9
theme: Strengthen package contents validation
ciampo Jun 25, 2026
8097604
theme: Fix package validation on Windows
ciampo Jun 25, 2026
4ac9744
theme: Move package contents validation to linting
ciampo Jun 26, 2026
8d16432
theme: Validate packed package types with attw
ciampo Jun 26, 2026
6ecec6b
theme: Address package validation feedback
ciampo Jun 30, 2026
4ee380f
theme: Build package-only declarations
ciampo Jun 30, 2026
841ed52
tools: Run package contents tests with Jest
ciampo Jun 30, 2026
c99b5c1
theme: Remove stale Sass export
ciampo Jul 7, 2026
605b7c6
tools: Address package validation feedback
ciampo Jul 7, 2026
3330935
theme: Adapt package validation to ESM
ciampo Jul 10, 2026
dd7df8c
theme: Bundle public package types
ciampo Jul 10, 2026
52a93da
validation: Harden package contents cleanup
ciampo Jul 10, 2026
3aba7f6
theme: Resolve package types during clean builds
ciampo Jul 10, 2026
fd3d0cd
theme: Bundle package types before packing
ciampo Jul 10, 2026
a061e7c
theme: Simplify package publish validation
ciampo Jul 13, 2026
d966645
validation: Exclude singular story files
ciampo Jul 13, 2026
59173df
validation: Let packages opt into publish checks
ciampo Jul 13, 2026
8850a7b
theme: Streamline package publish checks
ciampo Jul 13, 2026
7125481
theme: Remove attw package validation
ciampo Jul 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/static-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ jobs:
- name: Type checking
run: npm run build

- name: Validate package contents
run: npm run lint:package-contents

- name: Audit published dependencies
if: ${{ matrix.annotations }}
run: npm run lint:published-deps -- --fail-unused-ignores
Expand Down
4 changes: 4 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
"lint:js:fix": "npm run lint:js -- --fix",
"lint:js:prune-suppressions": "npm run lint:js -- --prune-suppressions",
"lint:lockfile": "npm run --workspace @wordpress/validation-tools validate-package-lock --",
"lint:package-contents": "npm run --if-present --workspaces lint:package-contents",
"lint:tsconfig": "npm run --workspace @wordpress/validation-tools validate-tsconfig --",
"lint:md:docs": "wp-scripts lint-md-docs",
"prelint:php": "npm run other:update-packages:php",
Expand Down
4 changes: 4 additions & 0 deletions packages/theme/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@

- Add regression test coverage for the `ThemeProvider` wrapper's `display: contents` focus behavior ([#80056](https://github.com/WordPress/gutenberg/pull/80056)).

### Code Quality

- Clean the npm package publish surface, correct ESM declaration export paths, and validate packed test/story artifacts and local export targets ([#79552](https://github.com/WordPress/gutenberg/pull/79552)).

## 0.17.0 (2026-06-30)

### Breaking Changes
Expand Down
13 changes: 10 additions & 3 deletions packages/theme/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,16 @@
"npm": ">=10.2.3"
},
"files": [
"src",
"build-module",
"build-types",
"*.md"
"src/*-plugins/**",
"src/prebuilt/css",
"src/prebuilt/js",
"*.md",
"!build-types/**/*.d.mts.map",
"!build-types/**/*.d.ts.map",
"!**/stories/**",
"!**/test/**"
],
"type": "module",
"module": "build-module/index.mjs",
Expand Down Expand Up @@ -67,7 +73,6 @@
"./package.json": "./package.json"
},
"wpScript": true,
"types": "build-types",
"sideEffects": [
"src/prebuilt/css/design-tokens.css"
],
Expand All @@ -91,6 +96,7 @@
"@testing-library/user-event": "^14.6.1",
"@types/jest": "^29.5.14",
"@types/node": "^20.19.39",
"@wordpress/validation-tools": "file:../../tools/validation",
"esbuild": "^0.27.2",
"esbuild-esm-loader": "^0.3.3",
"prettier": "npm:wp-prettier@^3.0.3",
Expand Down Expand Up @@ -130,6 +136,7 @@
"build:default-ramps": "node --import=esbuild-esm-loader/register bin/generate-default-ramps/index.ts",
"build:tokens": "node --import=esbuild-esm-loader/register bin/generate-primitive-tokens/index.ts && node --import=esbuild-esm-loader/register bin/build-design-tokens/index.ts",
"build": "npm run build:tokens && npm run build:default-ramps",
"lint:package-contents": "wp-validate-package-contents .",
"postbuild": "prettier --write tokens/color.json tokens/modes src/prebuilt src/color-ramps/lib/default-ramps.ts docs ../base-styles/internal/_wpds-token-fallbacks.scss"
}
}
3 changes: 3 additions & 0 deletions tools/validation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
"url": "https://github.com/WordPress/gutenberg/issues"
},
"type": "module",
"bin": {
"wp-validate-package-contents": "./validate-package-contents.mjs"
},
"devDependencies": {
"@mawesome/dependency-audit": "^0.4.5",
"@wordpress/create-block": "file:../../packages/create-block",
Expand Down
125 changes: 125 additions & 0 deletions tools/validation/validate-package-contents.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#!/usr/bin/env node

import { spawnSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';

const [ packagePath, ...extraArgs ] = process.argv.slice( 2 );

if ( ! packagePath || extraArgs.length ) {
console.error(
'Usage: node tools/validation/validate-package-contents.mjs <package-directory>'
);
process.exit( 1 );
}

const packageRoot = resolve( process.cwd(), packagePath );
const packageJson = JSON.parse(
readFileSync( join( packageRoot, 'package.json' ), 'utf8' )
);
const npmExecPath = process.env.npm_execpath;
const packResult = spawnSync(
npmExecPath ? process.execPath : 'npm',
[
...( npmExecPath ? [ npmExecPath ] : [] ),
'pack',
'--dry-run',
'--json',
],
{
cwd: packageRoot,
encoding: 'utf8',
env: {
...process.env,
npm_config_cache:
process.env.WORDPRESS_PACKAGE_NPM_CACHE ??
join( tmpdir(), 'wordpress-package-npm-cache' ),
},
shell: ! npmExecPath && process.platform === 'win32',
}
);

if ( packResult.error ) {
throw packResult.error;
}

if ( packResult.status !== 0 ) {
process.stderr.write( packResult.stdout ?? '' );
process.stderr.write( packResult.stderr ?? '' );
process.exit( packResult.status ?? 1 );
}

const [ pack ] = JSON.parse( packResult.stdout );
const packedPaths = pack.files.map( ( { path } ) => path );
const packedPathSet = new Set( packedPaths );
const disallowedPathPatterns = [
/(^|\/)(__fixtures__|__snapshots__|__tests__|fixtures|stories|test|tests)(\/|$)/,
/(^|\/)[^/]+\.(spec|test)\.[^/]+$/,
/(^|\/)[^/]+\.stor(?:y|ies)\.[^/]+$/,
];
Comment on lines +56 to +60

@aduth aduth Jul 13, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should work toward src being a disallowed path. I expect we have some references that we need it for now, but it feels like if we're building something from src, we should always be referencing its built output in the runtime usage. And if something truly doesn't need to be transpiled at all (e.g. plugins) then we shouldn't go through the effort of building them and they could live outside the source folder.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this direction. For @wordpress/theme, most of these src paths already have built equivalents: the plugin and design-token JavaScript under build-module, and the CSS under build-style. The remaining complication is the hand-authored plugin declarations currently shipped from src.

I think the clean follow-up is to generate those declarations into build-types, point all runtime and style exports at the built directories, and then remove src from the package entirely. I wouldn't make src globally disallowed here because other Gutenberg packages intentionally publish source assets, but we can enforce it specifically for @wordpress/theme once that cleanup lands?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened a draft #80213 , will iterate on it

const disallowedPaths = packedPaths.filter( ( path ) =>
disallowedPathPatterns.some( ( pattern ) => pattern.test( path ) )
);

/**
* @param {unknown} value Export map value.
* @return {string[]} Concrete local export target paths.
*/
function getLocalExportTargets( value ) {
if ( typeof value === 'string' ) {
return value.startsWith( './' ) && ! value.includes( '*' )
? [ value.slice( 2 ) ]
: [];
}

if ( Array.isArray( value ) ) {
return value.flatMap( getLocalExportTargets );
}

if ( ! value || typeof value !== 'object' ) {
return [];
}

return Object.values( value ).flatMap( getLocalExportTargets );
}

const missingExportTargets = getLocalExportTargets(
packageJson.exports
).filter( ( path ) => ! packedPathSet.has( path ) );

if ( packedPaths.length === 0 ) {
console.error( 'The package tarball does not include any files.' );
}

if ( disallowedPaths.length ) {
console.error(
[
'The package tarball includes disallowed files:',
...disallowedPaths.map( ( path ) => `- ${ path }` ),
].join( '\n' )
);
}

if ( missingExportTargets.length ) {
console.error(
[
'The package tarball is missing exported targets:',
...missingExportTargets.map( ( path ) => `- ${ path }` ),
].join( '\n' )
);
}

if (
packedPaths.length === 0 ||
disallowedPaths.length ||
missingExportTargets.length
) {
process.exit( 1 );
}

console.log(
`Validated ${ packedPaths.length } packed files for ${
packageJson.name ?? packageRoot
}.`
);
131 changes: 131 additions & 0 deletions tools/validation/validate-package-contents.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/* global afterEach, expect, test */

/**
* External dependencies
*/
const { spawnSync } = require( 'node:child_process' );
const { mkdtempSync, mkdirSync, rmSync, writeFileSync } = require( 'node:fs' );
const { tmpdir } = require( 'node:os' );
const { dirname, join } = require( 'node:path' );

const validatorPath = join( __dirname, 'validate-package-contents.mjs' );
const temporaryRoots = [];

afterEach( () => {
for ( const root of temporaryRoots.splice( 0 ) ) {
rmSync( root, { force: true, recursive: true } );
}
} );

function createPackage( { files, packageJson } ) {
const root = mkdtempSync( join( tmpdir(), 'validate-package-contents-' ) );
temporaryRoots.push( root );

for ( const [ path, contents ] of Object.entries( files ) ) {
const filePath = join( root, path );
mkdirSync( dirname( filePath ), { recursive: true } );
writeFileSync( filePath, contents );
}

writeFileSync(
join( root, 'package.json' ),
JSON.stringify(
{
name: 'test-package',
version: '1.0.0',
...packageJson,
},
null,
'\t'
) + '\n'
);

return root;
}

function runValidator( packageRoot ) {
return spawnSync( process.execPath, [ validatorPath, packageRoot ], {
encoding: 'utf8',
env: {
...process.env,
WORDPRESS_PACKAGE_NPM_CACHE: join(
tmpdir(),
'wordpress-package-npm-cache'
),
},
} );
}

test( 'passes for a package with clean packed contents', () => {
const packageRoot = createPackage( {
files: {
'index.js': "export const value = 'ok';\n",
'index.d.ts': 'export declare const value: string;\n',
'styles.css': '.test {}\n',
},
packageJson: {
files: [ 'index.js', 'index.d.ts', 'styles.css' ],
exports: {
'.': {
types: './index.d.ts',
default: './index.js',
},
'./styles.css': './styles.css',
},
},
} );

const result = runValidator( packageRoot );

expect( result.status ).toBe( 0 );
expect( result.stdout ).toMatch(
/Validated \d+ packed files for test-package\./
);
} );

test.each( [ 'index.test.js', 'index.story.js' ] )(
'fails when packed contents include %s',
( disallowedPath ) => {
const packageRoot = createPackage( {
files: {
'index.js': "export const value = 'ok';\n",
[ disallowedPath ]: "import './index.js';\n",
},
packageJson: {
files: [ 'index.js', disallowedPath ],
exports: './index.js',
},
} );

const result = runValidator( packageRoot );

expect( result.status ).not.toBe( 0 );
expect( result.stderr ).toContain(
`The package tarball includes disallowed files:\n- ${ disallowedPath }`
);
}
);

test( 'fails when an exported target is missing from the package', () => {
const packageRoot = createPackage( {
files: {
'index.js': "export const value = 'ok';\n",
},
packageJson: {
files: [ 'index.js' ],
exports: {
'.': {
types: './index.d.ts',
default: './index.js',
},
},
},
} );

const result = runValidator( packageRoot );

expect( result.status ).not.toBe( 0 );
expect( result.stderr ).toMatch(
/The package tarball is missing exported targets:\n- index\.d\.ts/
);
} );
Loading