@wordpress/build 0.2.0
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/.cache/tsconfig.tsbuildinfo +1 -0
- package/.eslintrc.cjs +8 -0
- package/CHANGELOG.md +14 -0
- package/LICENSE.md +788 -0
- package/README.md +265 -0
- package/package.json +58 -0
- package/src/build.mjs +1345 -0
- package/src/dependency-graph.mjs +271 -0
- package/src/package-utils.mjs +71 -0
- package/src/php-generator.mjs +72 -0
- package/src/wordpress-externals-plugin.mjs +327 -0
- package/templates/index.php.template +31 -0
- package/templates/module-registration.php.template +44 -0
- package/templates/module-registry.php.template +11 -0
- package/templates/script-registration.php.template +87 -0
- package/templates/script-registry.php.template +11 -0
- package/templates/style-registration.php.template +73 -0
- package/templates/style-registry.php.template +11 -0
- package/templates/version.php.template +11 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency graph utilities for WordPress packages.
|
|
3
|
+
*
|
|
4
|
+
* This module provides functions to analyze dependencies between @wordpress/* packages
|
|
5
|
+
* and determine the correct build order using topological sorting.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* External dependencies
|
|
10
|
+
*/
|
|
11
|
+
import toposort from 'toposort';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Internal dependencies
|
|
15
|
+
*/
|
|
16
|
+
import { getPackageInfo } from './package-utils.mjs';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if a package is a script or script module.
|
|
20
|
+
* A package is a script if it has wpScript or wpScriptModuleExports.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} packageName The full package name (e.g., '@wordpress/blocks').
|
|
23
|
+
* @return {boolean} True if the package is a script or script module.
|
|
24
|
+
*/
|
|
25
|
+
function isScriptOrModule( packageName ) {
|
|
26
|
+
const packageJson = getPackageInfo( packageName );
|
|
27
|
+
if ( ! packageJson ) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return !! ( packageJson.wpScript || packageJson.wpScriptModuleExports );
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get WordPress package dependencies from a package.json file.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} packageName The full package name (e.g., '@wordpress/blocks').
|
|
37
|
+
* @return {string[]} Array of WordPress package names this package depends on.
|
|
38
|
+
*/
|
|
39
|
+
function getWordPressDependencies( packageName ) {
|
|
40
|
+
const packageJson = getPackageInfo( packageName );
|
|
41
|
+
if ( ! packageJson ) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const deps = packageJson.dependencies || {};
|
|
46
|
+
|
|
47
|
+
// Extract @wordpress/* package names (keep full names)
|
|
48
|
+
return Object.keys( deps ).filter( ( dep ) =>
|
|
49
|
+
dep.startsWith( '@wordpress/' )
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build a dependency graph for the given packages.
|
|
55
|
+
*
|
|
56
|
+
* @param {string[]} packages Array of full package names to analyze.
|
|
57
|
+
* @return {Array<[string, string]>} Array of [dependent, dependency] edges.
|
|
58
|
+
*/
|
|
59
|
+
function buildDependencyGraph( packages ) {
|
|
60
|
+
/** @type {Array<[string, string]>} */
|
|
61
|
+
const edges = [];
|
|
62
|
+
const packagesSet = new Set( packages );
|
|
63
|
+
|
|
64
|
+
for ( const packageName of packages ) {
|
|
65
|
+
const deps = getWordPressDependencies( packageName );
|
|
66
|
+
|
|
67
|
+
// Only include edges where both packages are in our list
|
|
68
|
+
for ( const dep of deps ) {
|
|
69
|
+
if ( packagesSet.has( dep ) ) {
|
|
70
|
+
edges.push( [ packageName, dep ] );
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// If package has no dependencies in our list, add a self-reference
|
|
75
|
+
// This ensures it appears in the sorted output
|
|
76
|
+
if ( deps.filter( ( dep ) => packagesSet.has( dep ) ).length === 0 ) {
|
|
77
|
+
edges.push( [ packageName, packageName ] );
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return edges;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Sort packages in topological order based on their dependencies.
|
|
86
|
+
*
|
|
87
|
+
* @param {string[]} packages Array of full package names to sort.
|
|
88
|
+
* @return {string[]} Sorted array where dependencies come before dependents.
|
|
89
|
+
*/
|
|
90
|
+
function topologicalSort( packages ) {
|
|
91
|
+
const edges = buildDependencyGraph( packages );
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
// toposort returns dependencies first, then dependents
|
|
95
|
+
/** @type {Array<string>} */
|
|
96
|
+
const sorted = toposort( edges );
|
|
97
|
+
|
|
98
|
+
// Filter to only include packages in our input list
|
|
99
|
+
// (toposort might include extra nodes)
|
|
100
|
+
const packagesSet = new Set( packages );
|
|
101
|
+
return sorted.filter( ( pkg ) => packagesSet.has( pkg ) );
|
|
102
|
+
} catch ( error ) {
|
|
103
|
+
if ( error instanceof Error && error.message.includes( 'cyclic' ) ) {
|
|
104
|
+
console.error(
|
|
105
|
+
'❌ Cyclic dependency detected in packages:',
|
|
106
|
+
error.message
|
|
107
|
+
);
|
|
108
|
+
throw new Error(
|
|
109
|
+
'Cannot build packages due to cyclic dependencies'
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Group packages by dependency depth level.
|
|
118
|
+
* Packages at the same depth level can be built in parallel.
|
|
119
|
+
*
|
|
120
|
+
* @param {string[]} packages Array of full package names to group.
|
|
121
|
+
* @return {string[][]} Array of arrays, where each inner array is a depth level.
|
|
122
|
+
*/
|
|
123
|
+
function groupByDepth( packages ) {
|
|
124
|
+
const packagesSet = new Set( packages );
|
|
125
|
+
const depths = new Map();
|
|
126
|
+
const visited = new Set();
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Calculate depth for a package recursively.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} packageName Package name to calculate depth for.
|
|
132
|
+
* @return {number} Depth level (0 = no dependencies).
|
|
133
|
+
*/
|
|
134
|
+
function calculateDepth( packageName ) {
|
|
135
|
+
if ( depths.has( packageName ) ) {
|
|
136
|
+
return depths.get( packageName );
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Prevent infinite loops in case of circular dependencies
|
|
140
|
+
if ( visited.has( packageName ) ) {
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
visited.add( packageName );
|
|
145
|
+
|
|
146
|
+
const deps = getWordPressDependencies( packageName );
|
|
147
|
+
const relevantDeps = deps.filter( ( dep ) => packagesSet.has( dep ) );
|
|
148
|
+
|
|
149
|
+
if ( relevantDeps.length === 0 ) {
|
|
150
|
+
depths.set( packageName, 0 );
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const maxDepth = Math.max(
|
|
155
|
+
...relevantDeps.map( ( dep ) => calculateDepth( dep ) )
|
|
156
|
+
);
|
|
157
|
+
const depth = maxDepth + 1;
|
|
158
|
+
depths.set( packageName, depth );
|
|
159
|
+
|
|
160
|
+
return depth;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Calculate depth for all packages
|
|
164
|
+
for ( const packageName of packages ) {
|
|
165
|
+
calculateDepth( packageName );
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Group by depth
|
|
169
|
+
const levels = [];
|
|
170
|
+
const maxDepth = Math.max( ...depths.values() );
|
|
171
|
+
|
|
172
|
+
for ( let depth = 0; depth <= maxDepth; depth++ ) {
|
|
173
|
+
const packagesAtDepth = packages.filter(
|
|
174
|
+
( pkg ) => depths.get( pkg ) === depth
|
|
175
|
+
);
|
|
176
|
+
if ( packagesAtDepth.length > 0 ) {
|
|
177
|
+
levels.push( packagesAtDepth );
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return levels;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get packages that depend on a given package (reverse dependencies).
|
|
186
|
+
*
|
|
187
|
+
* @param {string} packageName The full package name to find dependents of.
|
|
188
|
+
* @param {string[]} allPackages Array of all full package names to search.
|
|
189
|
+
* @return {string[]} Array of package names that depend on the given package.
|
|
190
|
+
*/
|
|
191
|
+
function getReverseDependencies( packageName, allPackages ) {
|
|
192
|
+
const dependents = [];
|
|
193
|
+
|
|
194
|
+
for ( const pkg of allPackages ) {
|
|
195
|
+
const deps = getWordPressDependencies( pkg );
|
|
196
|
+
if ( deps.includes( packageName ) ) {
|
|
197
|
+
dependents.push( pkg );
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return dependents;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Find scripts/script-modules that need to be rebundled when a bundled package changes.
|
|
206
|
+
* Uses BFS to traverse reverse dependencies, stopping at script/module boundaries.
|
|
207
|
+
*
|
|
208
|
+
* When a bundled package (no wpScript/wpScriptModuleExports) changes, we need to
|
|
209
|
+
* rebundle any scripts/modules that depend on it through a chain of bundled packages.
|
|
210
|
+
* We stop at script/module boundaries because they handle their own bundling.
|
|
211
|
+
*
|
|
212
|
+
* Example:
|
|
213
|
+
* - A (bundled) changes
|
|
214
|
+
* - B (bundled) depends on A
|
|
215
|
+
* - C (script) depends on B
|
|
216
|
+
* - D (script) depends on C
|
|
217
|
+
* Result: Only C needs rebundling (D stops at C boundary)
|
|
218
|
+
*
|
|
219
|
+
* @param {string} changedPackage The full package name that changed.
|
|
220
|
+
* @param {string[]} allPackages Array of all full package names.
|
|
221
|
+
* @return {string[]} Array of script/module package names to rebundle.
|
|
222
|
+
*/
|
|
223
|
+
function findScriptsToRebundle( changedPackage, allPackages ) {
|
|
224
|
+
// If the changed package itself is a script/module, no need to find others
|
|
225
|
+
// (it will be rebuilt by the regular watch logic)
|
|
226
|
+
if ( isScriptOrModule( changedPackage ) ) {
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const scriptsToRebundle = new Set();
|
|
231
|
+
const visited = new Set();
|
|
232
|
+
const queue = [ changedPackage ];
|
|
233
|
+
|
|
234
|
+
while ( queue.length > 0 ) {
|
|
235
|
+
const currentPackage = queue.shift();
|
|
236
|
+
|
|
237
|
+
if ( ! currentPackage || visited.has( currentPackage ) ) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
visited.add( currentPackage );
|
|
241
|
+
|
|
242
|
+
// Get all packages that depend on the current package
|
|
243
|
+
const dependents = getReverseDependencies(
|
|
244
|
+
currentPackage,
|
|
245
|
+
allPackages
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
for ( const dependent of dependents ) {
|
|
249
|
+
// If this dependent is a script/module, add it to the result
|
|
250
|
+
// but don't traverse further (stop at script boundaries)
|
|
251
|
+
if ( isScriptOrModule( dependent ) ) {
|
|
252
|
+
scriptsToRebundle.add( dependent );
|
|
253
|
+
} else if ( ! visited.has( dependent ) ) {
|
|
254
|
+
// If it's a bundled package, continue traversing
|
|
255
|
+
queue.push( dependent );
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return Array.from( scriptsToRebundle );
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export {
|
|
264
|
+
getWordPressDependencies,
|
|
265
|
+
buildDependencyGraph,
|
|
266
|
+
topologicalSort,
|
|
267
|
+
groupByDepth,
|
|
268
|
+
isScriptOrModule,
|
|
269
|
+
getReverseDependencies,
|
|
270
|
+
findScriptsToRebundle,
|
|
271
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Shared cache for package.json files to avoid redundant reads.
|
|
9
|
+
* Cache is keyed by the full package name from package.json's name field.
|
|
10
|
+
*/
|
|
11
|
+
const packageJsonCache = new Map();
|
|
12
|
+
const packagePathCache = new Map();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} PackageJson
|
|
16
|
+
*
|
|
17
|
+
* @property {string} name Package name.
|
|
18
|
+
* @property {string} version Package version.
|
|
19
|
+
* @property {string} [description] Package description.
|
|
20
|
+
* @property {string} [author] Package author.
|
|
21
|
+
* @property {string} [license] Package license.
|
|
22
|
+
* @property {string} [main] Main entry point.
|
|
23
|
+
* @property {string} [module] ES module entry point.
|
|
24
|
+
* @property {string} [react-native] React Native entry point.
|
|
25
|
+
* @property {Record<string, string>} [dependencies] Runtime dependencies.
|
|
26
|
+
* @property {Record<string, string>} [devDependencies] Development dependencies.
|
|
27
|
+
* @property {Record<string, string>} [peerDependencies] Peer dependencies.
|
|
28
|
+
* @property {string[]} [wpScript] WordPress script handles for dependency extraction.
|
|
29
|
+
* @property {Record<string, string>} [wpScriptModuleExports] WordPress script module exports.
|
|
30
|
+
* @property {Object} [sideEffects] Side effects configuration for tree shaking.
|
|
31
|
+
* @property {string} [publishConfig] NPM publish configuration.
|
|
32
|
+
* @property {Record<string, string>} [scripts] NPM scripts.
|
|
33
|
+
* @property {string[]} [files] Files to include in package.
|
|
34
|
+
* @property {string} [repository] Repository URL.
|
|
35
|
+
* @property {string[]} [keywords] Package keywords.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get package.json info using Node's module resolution.
|
|
40
|
+
* Resolves the package using import.meta.resolve and reads its package.json.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} fullPackageName The full package name (e.g., '@wordpress/blocks').
|
|
43
|
+
* @return {PackageJson|null} Package.json object or null if not found.
|
|
44
|
+
*/
|
|
45
|
+
export function getPackageInfo( fullPackageName ) {
|
|
46
|
+
if ( packageJsonCache.has( fullPackageName ) ) {
|
|
47
|
+
return packageJsonCache.get( fullPackageName );
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const resolved = import.meta.resolve( `${ fullPackageName }/package.json` );
|
|
51
|
+
const result = getPackageInfoFromFile( fileURLToPath( resolved ) );
|
|
52
|
+
packageJsonCache.set( fullPackageName, result );
|
|
53
|
+
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get package.json info from an explicit file path.
|
|
59
|
+
* Reads the package.json file and caches it by its name field.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} packageJsonPath Absolute path to package.json file.
|
|
62
|
+
* @return {PackageJson|null} Package.json object or null if not found.
|
|
63
|
+
*/
|
|
64
|
+
export function getPackageInfoFromFile( packageJsonPath ) {
|
|
65
|
+
if ( packagePathCache.has( packageJsonPath ) ) {
|
|
66
|
+
return packagePathCache.get( packageJsonPath );
|
|
67
|
+
}
|
|
68
|
+
const packageJson = JSON.parse( readFileSync( packageJsonPath, 'utf8' ) );
|
|
69
|
+
packagePathCache.set( packageJsonPath, packageJson );
|
|
70
|
+
return packageJson;
|
|
71
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Internal dependencies
|
|
10
|
+
*/
|
|
11
|
+
import { getPackageInfoFromFile } from './package-utils.mjs';
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname( fileURLToPath( import.meta.url ) );
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get PHP replacements from root package.json.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} rootDir Root directory path.
|
|
19
|
+
* @return {Promise<Record<string, string>>} Replacements object with {{PREFIX}}, {{VERSION}}, {{VERSION_CONSTANT}}.
|
|
20
|
+
*/
|
|
21
|
+
export async function getPhpReplacements( rootDir ) {
|
|
22
|
+
const rootPackageJson = getPackageInfoFromFile(
|
|
23
|
+
path.join( rootDir, 'package.json' )
|
|
24
|
+
);
|
|
25
|
+
if ( ! rootPackageJson ) {
|
|
26
|
+
throw new Error( 'Could not read root package.json' );
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// @ts-expect-error specific override to package.json
|
|
30
|
+
const name = rootPackageJson.wpPlugin?.name || 'gutenberg';
|
|
31
|
+
const version = rootPackageJson.version;
|
|
32
|
+
const versionConstant =
|
|
33
|
+
name.toUpperCase().replace( /-/g, '_' ) + '_VERSION';
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
'{{PREFIX}}': name,
|
|
37
|
+
'{{VERSION}}': version,
|
|
38
|
+
'{{VERSION_CONSTANT}}': versionConstant,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Generate a PHP file from a template with replacements.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} templateName Template file name.
|
|
46
|
+
* @param {string} outputPath Full output path.
|
|
47
|
+
* @param {Record<string, string>} replacements Replacements object (e.g. {'{{PREFIX}}': 'gutenberg'}).
|
|
48
|
+
*/
|
|
49
|
+
export async function generatePhpFromTemplate(
|
|
50
|
+
templateName,
|
|
51
|
+
outputPath,
|
|
52
|
+
replacements
|
|
53
|
+
) {
|
|
54
|
+
// Templates directory
|
|
55
|
+
const templatesDir = path.join( __dirname, '..', 'templates' );
|
|
56
|
+
|
|
57
|
+
// Read template
|
|
58
|
+
const template = await readFile(
|
|
59
|
+
path.join( templatesDir, templateName ),
|
|
60
|
+
'utf8'
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Apply all replacements
|
|
64
|
+
let content = template;
|
|
65
|
+
for ( const [ placeholder, value ] of Object.entries( replacements ) ) {
|
|
66
|
+
content = content.replaceAll( placeholder, value );
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Write output file
|
|
70
|
+
await mkdir( path.dirname( outputPath ), { recursive: true } );
|
|
71
|
+
await writeFile( outputPath, content );
|
|
72
|
+
}
|