@wordpress/build 0.2.0 → 0.3.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 -1
- package/CHANGELOG.md +2 -0
- package/README.md +95 -0
- package/package.json +2 -2
- package/src/build.mjs +501 -107
- package/src/dependency-graph.mjs +110 -12
- package/src/package-utils.mjs +5 -0
- package/src/php-generator.mjs +36 -12
- package/src/route-utils.mjs +132 -0
- package/src/wordpress-externals-plugin.mjs +27 -10
- package/templates/index.php.template +12 -0
- package/templates/page-wp-admin.php.template +252 -0
- package/templates/page.php.template +311 -0
- package/templates/pages.php.template +9 -0
- package/templates/route-registry.php.template +11 -0
- package/templates/routes-registration.php.template +90 -0
package/src/build.mjs
CHANGED
|
@@ -3,19 +3,15 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* External dependencies
|
|
5
5
|
*/
|
|
6
|
-
import { readFile, writeFile, copyFile, mkdir } from 'fs/promises';
|
|
7
|
-
import { readdirSync } from 'fs';
|
|
6
|
+
import { readFile, writeFile, copyFile, mkdir, unlink } from 'fs/promises';
|
|
8
7
|
import path from 'path';
|
|
9
8
|
import { parseArgs } from 'node:util';
|
|
10
9
|
import esbuild from 'esbuild';
|
|
11
10
|
import glob from 'fast-glob';
|
|
12
11
|
import chokidar from 'chokidar';
|
|
13
|
-
// See https://github.com/WordPress/gutenberg/issues/72136
|
|
14
|
-
// eslint-disable-next-line import/no-unresolved
|
|
15
12
|
import browserslistToEsbuild from 'browserslist-to-esbuild';
|
|
16
|
-
import { sassPlugin } from 'esbuild-sass-plugin';
|
|
13
|
+
import { sassPlugin, postcssModules } from 'esbuild-sass-plugin';
|
|
17
14
|
import postcss from 'postcss';
|
|
18
|
-
import postcssModulesPlugin from 'postcss-modules';
|
|
19
15
|
import autoprefixer from 'autoprefixer';
|
|
20
16
|
import rtlcss from 'rtlcss';
|
|
21
17
|
import cssnano from 'cssnano';
|
|
@@ -25,28 +21,42 @@ import { camelCase } from 'change-case';
|
|
|
25
21
|
/**
|
|
26
22
|
* Internal dependencies
|
|
27
23
|
*/
|
|
28
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
groupByDepth,
|
|
26
|
+
findScriptsToRebundle,
|
|
27
|
+
findRoutesToRebuild,
|
|
28
|
+
} from './dependency-graph.mjs';
|
|
29
29
|
import {
|
|
30
30
|
generatePhpFromTemplate,
|
|
31
31
|
getPhpReplacements,
|
|
32
32
|
} from './php-generator.mjs';
|
|
33
33
|
import { getPackageInfo, getPackageInfoFromFile } from './package-utils.mjs';
|
|
34
34
|
import { createWordpressExternalsPlugin } from './wordpress-externals-plugin.mjs';
|
|
35
|
+
import {
|
|
36
|
+
getAllRoutes,
|
|
37
|
+
getRouteFiles,
|
|
38
|
+
getRouteMetadata,
|
|
39
|
+
generateContentEntryPoint,
|
|
40
|
+
} from './route-utils.mjs';
|
|
35
41
|
|
|
36
42
|
const ROOT_DIR = process.cwd();
|
|
37
43
|
const PACKAGES_DIR = path.join( ROOT_DIR, 'packages' );
|
|
38
44
|
const BUILD_DIR = path.join( ROOT_DIR, 'build' );
|
|
39
45
|
|
|
40
46
|
const SOURCE_EXTENSIONS = '{js,ts,tsx}';
|
|
47
|
+
const ASSET_EXTENSIONS = 'json';
|
|
41
48
|
const IGNORE_PATTERNS = [
|
|
42
49
|
'**/benchmark/**',
|
|
43
50
|
'**/{__mocks__,__tests__,test}/**',
|
|
44
51
|
'**/{storybook,stories}/**',
|
|
45
52
|
'**/*.native.*',
|
|
53
|
+
'**/*.ios.*',
|
|
54
|
+
'**/*.android.*',
|
|
46
55
|
];
|
|
47
56
|
const TEST_FILE_PATTERNS = [
|
|
48
57
|
/\/(benchmark|__mocks__|__tests__|test|storybook|stories)\/.+/,
|
|
49
58
|
/\.(spec|test)\.(js|ts|tsx)$/,
|
|
59
|
+
/\.(native|ios|android)\.(js|ts|tsx)$/,
|
|
50
60
|
];
|
|
51
61
|
|
|
52
62
|
/**
|
|
@@ -55,19 +65,23 @@ const TEST_FILE_PATTERNS = [
|
|
|
55
65
|
* @return {string[]} Array of package names.
|
|
56
66
|
*/
|
|
57
67
|
function getAllPackages() {
|
|
58
|
-
return
|
|
59
|
-
.
|
|
60
|
-
.map( (
|
|
68
|
+
return glob
|
|
69
|
+
.sync( normalizePath( path.join( PACKAGES_DIR, '*', 'package.json' ) ) )
|
|
70
|
+
.map( ( packageJsonPath ) =>
|
|
71
|
+
path.basename( path.dirname( packageJsonPath ) )
|
|
72
|
+
);
|
|
61
73
|
}
|
|
62
74
|
|
|
63
75
|
const PACKAGES = getAllPackages();
|
|
64
|
-
const ROOT_PACKAGE_JSON = getPackageInfoFromFile(
|
|
76
|
+
const ROOT_PACKAGE_JSON = getPackageInfoFromFile(
|
|
77
|
+
path.join( ROOT_DIR, 'package.json' )
|
|
78
|
+
);
|
|
65
79
|
const WP_PLUGIN_CONFIG = ROOT_PACKAGE_JSON.wpPlugin || {};
|
|
66
80
|
const SCRIPT_GLOBAL = WP_PLUGIN_CONFIG.scriptGlobal;
|
|
67
81
|
const PACKAGE_NAMESPACE = WP_PLUGIN_CONFIG.packageNamespace;
|
|
68
82
|
const HANDLE_PREFIX = WP_PLUGIN_CONFIG.handlePrefix || PACKAGE_NAMESPACE;
|
|
69
83
|
const EXTERNAL_NAMESPACES = WP_PLUGIN_CONFIG.externalNamespaces || {};
|
|
70
|
-
|
|
84
|
+
const PAGES = WP_PLUGIN_CONFIG.pages || [];
|
|
71
85
|
|
|
72
86
|
const baseDefine = {
|
|
73
87
|
'globalThis.IS_GUTENBERG_PLUGIN': JSON.stringify(
|
|
@@ -92,20 +106,26 @@ const wordpressExternalsPlugin = createWordpressExternalsPlugin(
|
|
|
92
106
|
HANDLE_PREFIX
|
|
93
107
|
);
|
|
94
108
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
const styleBundlingPlugins = [
|
|
110
|
+
// Handle CSS modules (.module.css and .module.scss)
|
|
111
|
+
sassPlugin( {
|
|
112
|
+
embedded: true,
|
|
113
|
+
filter: /\.module\.(css|scss)$/,
|
|
114
|
+
transform: postcssModules( {
|
|
115
|
+
generateScopedName: '[name]__[local]__[hash:base64:5]',
|
|
116
|
+
} ),
|
|
117
|
+
type: 'style',
|
|
118
|
+
loadPaths: [ 'node_modules', path.join( PACKAGES_DIR, 'base-styles' ) ],
|
|
119
|
+
} ),
|
|
120
|
+
// Handle regular CSS/SCSS files
|
|
121
|
+
// Note: .module.css and .module.scss already handled by plugin above
|
|
122
|
+
sassPlugin( {
|
|
123
|
+
embedded: true,
|
|
124
|
+
filter: /\.(css|scss)$/,
|
|
125
|
+
type: 'style',
|
|
126
|
+
loadPaths: [ 'node_modules', path.join( PACKAGES_DIR, 'base-styles' ) ],
|
|
127
|
+
} ),
|
|
128
|
+
];
|
|
109
129
|
|
|
110
130
|
/**
|
|
111
131
|
* Normalize path separators for cross-platform compatibility.
|
|
@@ -278,7 +298,9 @@ async function bundlePackage( packageName ) {
|
|
|
278
298
|
|
|
279
299
|
// Check if package matches the namespace and should expose a global
|
|
280
300
|
const packageFullName = packageJson.name;
|
|
281
|
-
const matchesNamespace = packageFullName.startsWith(
|
|
301
|
+
const matchesNamespace = packageFullName.startsWith(
|
|
302
|
+
`@${ PACKAGE_NAMESPACE }/`
|
|
303
|
+
);
|
|
282
304
|
const shouldExposeGlobal = matchesNamespace && SCRIPT_GLOBAL !== false;
|
|
283
305
|
|
|
284
306
|
const globalName = shouldExposeGlobal
|
|
@@ -761,6 +783,132 @@ async function generateMainIndexPhp( replacements ) {
|
|
|
761
783
|
);
|
|
762
784
|
}
|
|
763
785
|
|
|
786
|
+
/**
|
|
787
|
+
* Generate global route registry file.
|
|
788
|
+
* Creates a single registry with all routes including page metadata.
|
|
789
|
+
*
|
|
790
|
+
* @param {Array} routes Array of route objects.
|
|
791
|
+
* @param {Record<string, string>} replacements PHP template replacements.
|
|
792
|
+
*/
|
|
793
|
+
async function generateRoutesRegistry( routes, replacements ) {
|
|
794
|
+
if ( routes.length === 0 ) {
|
|
795
|
+
// No routes to register, skip generating routes registry
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Generate PHP array entries with page metadata
|
|
800
|
+
const routeEntries = routes
|
|
801
|
+
.map( ( route ) => {
|
|
802
|
+
const hasRouteStr = route.hasRoute ? 'true' : 'false';
|
|
803
|
+
const hasContentStr = route.hasContent ? 'true' : 'false';
|
|
804
|
+
return `\tarray(
|
|
805
|
+
'name' => '${ route.name }',
|
|
806
|
+
'path' => '${ route.path }',
|
|
807
|
+
'page' => '${ route.page }',
|
|
808
|
+
'has_route' => ${ hasRouteStr },
|
|
809
|
+
'has_content' => ${ hasContentStr },
|
|
810
|
+
)`;
|
|
811
|
+
} )
|
|
812
|
+
.join( ',\n' );
|
|
813
|
+
|
|
814
|
+
// Generate single global registry at build/routes/index.php
|
|
815
|
+
await generatePhpFromTemplate(
|
|
816
|
+
'route-registry.php.template',
|
|
817
|
+
path.join( BUILD_DIR, 'routes', 'index.php' ),
|
|
818
|
+
{ ...replacements, '{{ROUTES}}': routeEntries }
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Generate routes.php file with route registration logic.
|
|
824
|
+
* Uses registry pattern with loop-based registration on page init hooks.
|
|
825
|
+
*
|
|
826
|
+
* @param {Array} routes Array of route objects.
|
|
827
|
+
* @param {Record<string, string>} replacements PHP template replacements.
|
|
828
|
+
*/
|
|
829
|
+
async function generateRoutesPhp( routes, replacements ) {
|
|
830
|
+
if ( routes.length === 0 ) {
|
|
831
|
+
// No routes to register, skip generating routes.php
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Generate routes.php from template
|
|
836
|
+
await generatePhpFromTemplate(
|
|
837
|
+
'routes-registration.php.template',
|
|
838
|
+
path.join( BUILD_DIR, 'routes.php' ),
|
|
839
|
+
{ ...replacements, '{{HANDLE_PREFIX}}': HANDLE_PREFIX }
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Generate page-specific PHP files for all pages.
|
|
845
|
+
*
|
|
846
|
+
* @param {Array} pageData Array of page objects with routes.
|
|
847
|
+
* @param {Record<string, string>} replacements PHP template replacements.
|
|
848
|
+
*/
|
|
849
|
+
async function generatePagesPhp( pageData, replacements ) {
|
|
850
|
+
if ( pageData.length === 0 ) {
|
|
851
|
+
// No pages to generate
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Generate files for each page
|
|
856
|
+
const pageGenerationPromises = pageData.map( async ( page ) => {
|
|
857
|
+
// Skip pages with no routes
|
|
858
|
+
if ( page.routes.length === 0 ) {
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const pageSlugUnderscore = page.slug.replace( /-/g, '_' );
|
|
863
|
+
const prefixUnderscore = replacements[ '{{PREFIX}}' ].replace(
|
|
864
|
+
/-/g,
|
|
865
|
+
'_'
|
|
866
|
+
);
|
|
867
|
+
|
|
868
|
+
const templateReplacements = {
|
|
869
|
+
...replacements,
|
|
870
|
+
'{{PAGE_SLUG}}': page.slug,
|
|
871
|
+
'{{PAGE_SLUG_UNDERSCORE}}': pageSlugUnderscore,
|
|
872
|
+
'{{PREFIX}}': prefixUnderscore,
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
// Generate both page.php and page-wp-admin.php
|
|
876
|
+
await Promise.all( [
|
|
877
|
+
generatePhpFromTemplate(
|
|
878
|
+
'page.php.template',
|
|
879
|
+
path.join( BUILD_DIR, 'pages', page.slug, 'page.php' ),
|
|
880
|
+
templateReplacements
|
|
881
|
+
),
|
|
882
|
+
generatePhpFromTemplate(
|
|
883
|
+
'page-wp-admin.php.template',
|
|
884
|
+
path.join( BUILD_DIR, 'pages', page.slug, 'page-wp-admin.php' ),
|
|
885
|
+
templateReplacements
|
|
886
|
+
),
|
|
887
|
+
] );
|
|
888
|
+
|
|
889
|
+
// Generate empty loader.js (dummy module for dependencies)
|
|
890
|
+
await writeFile(
|
|
891
|
+
path.join( BUILD_DIR, 'pages', page.slug, 'loader.js' ),
|
|
892
|
+
'// Empty module loader for page dependencies\n'
|
|
893
|
+
);
|
|
894
|
+
} );
|
|
895
|
+
|
|
896
|
+
await Promise.all( pageGenerationPromises );
|
|
897
|
+
|
|
898
|
+
// Generate pages.php loader
|
|
899
|
+
const pageIncludes = pageData
|
|
900
|
+
.map( ( page ) => {
|
|
901
|
+
return `require_once __DIR__ . '/pages/${ page.slug }/page.php';\nrequire_once __DIR__ . '/pages/${ page.slug }/page-wp-admin.php';`;
|
|
902
|
+
} )
|
|
903
|
+
.join( '\n' );
|
|
904
|
+
|
|
905
|
+
await generatePhpFromTemplate(
|
|
906
|
+
'pages.php.template',
|
|
907
|
+
path.join( BUILD_DIR, 'pages.php' ),
|
|
908
|
+
{ ...replacements, '{{PAGE_INCLUDES}}': pageIncludes }
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
|
|
764
912
|
/**
|
|
765
913
|
* Transpile a single package's source files and copy JSON files.
|
|
766
914
|
*
|
|
@@ -789,8 +937,10 @@ async function transpilePackage( packageName ) {
|
|
|
789
937
|
}
|
|
790
938
|
);
|
|
791
939
|
|
|
792
|
-
const
|
|
793
|
-
normalizePath(
|
|
940
|
+
const assetFiles = await glob(
|
|
941
|
+
normalizePath(
|
|
942
|
+
path.join( packageDir, `src/**/*.${ ASSET_EXTENSIONS }` )
|
|
943
|
+
),
|
|
794
944
|
{
|
|
795
945
|
ignore: IGNORE_PATTERNS,
|
|
796
946
|
}
|
|
@@ -806,7 +956,37 @@ async function transpilePackage( packageName ) {
|
|
|
806
956
|
// Check if this is the components package that needs emotion babel plugin.
|
|
807
957
|
// Ideally we should remove this exception and move away from emotion.
|
|
808
958
|
const needsEmotionPlugin = packageName === 'components';
|
|
809
|
-
const
|
|
959
|
+
const emotionPlugin = babel( {
|
|
960
|
+
filter: /\.[jt]sx?$/,
|
|
961
|
+
config: {
|
|
962
|
+
plugins: [ '@emotion/babel-plugin' ],
|
|
963
|
+
},
|
|
964
|
+
} );
|
|
965
|
+
const externalizeAllExceptCssPlugin = {
|
|
966
|
+
name: 'externalize-except-css',
|
|
967
|
+
setup( build ) {
|
|
968
|
+
// Externalize all non-CSS imports
|
|
969
|
+
build.onResolve( { filter: /.*/ }, ( args ) => {
|
|
970
|
+
// Skip entry points
|
|
971
|
+
if ( args.kind === 'entry-point' ) {
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Let CSS/SCSS files be processed by sassPlugin
|
|
976
|
+
if ( args.path.match( /\.(css|scss)$/ ) ) {
|
|
977
|
+
return null;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Externalize everything else (keep imports as-is)
|
|
981
|
+
return { path: args.path, external: true };
|
|
982
|
+
} );
|
|
983
|
+
},
|
|
984
|
+
};
|
|
985
|
+
const plugins = [
|
|
986
|
+
needsEmotionPlugin && emotionPlugin,
|
|
987
|
+
externalizeAllExceptCssPlugin,
|
|
988
|
+
...styleBundlingPlugins,
|
|
989
|
+
].filter( Boolean );
|
|
810
990
|
|
|
811
991
|
if ( packageJson.main ) {
|
|
812
992
|
builds.push(
|
|
@@ -814,7 +994,7 @@ async function transpilePackage( packageName ) {
|
|
|
814
994
|
entryPoints: srcFiles,
|
|
815
995
|
outdir: buildDir,
|
|
816
996
|
outbase: srcDir,
|
|
817
|
-
bundle:
|
|
997
|
+
bundle: true,
|
|
818
998
|
platform: 'node',
|
|
819
999
|
format: 'cjs',
|
|
820
1000
|
sourcemap: true,
|
|
@@ -828,13 +1008,13 @@ async function transpilePackage( packageName ) {
|
|
|
828
1008
|
} )
|
|
829
1009
|
);
|
|
830
1010
|
|
|
831
|
-
for ( const
|
|
832
|
-
const relativePath = path.relative( srcDir,
|
|
1011
|
+
for ( const assetFile of assetFiles ) {
|
|
1012
|
+
const relativePath = path.relative( srcDir, assetFile );
|
|
833
1013
|
const destPath = path.join( buildDir, relativePath );
|
|
834
1014
|
const destDir = path.dirname( destPath );
|
|
835
1015
|
builds.push(
|
|
836
1016
|
mkdir( destDir, { recursive: true } ).then( () =>
|
|
837
|
-
copyFile(
|
|
1017
|
+
copyFile( assetFile, destPath )
|
|
838
1018
|
)
|
|
839
1019
|
);
|
|
840
1020
|
}
|
|
@@ -846,7 +1026,7 @@ async function transpilePackage( packageName ) {
|
|
|
846
1026
|
entryPoints: srcFiles,
|
|
847
1027
|
outdir: buildModuleDir,
|
|
848
1028
|
outbase: srcDir,
|
|
849
|
-
bundle:
|
|
1029
|
+
bundle: true,
|
|
850
1030
|
platform: 'neutral',
|
|
851
1031
|
format: 'esm',
|
|
852
1032
|
sourcemap: true,
|
|
@@ -860,7 +1040,7 @@ async function transpilePackage( packageName ) {
|
|
|
860
1040
|
} )
|
|
861
1041
|
);
|
|
862
1042
|
|
|
863
|
-
for ( const jsonFile of
|
|
1043
|
+
for ( const jsonFile of assetFiles ) {
|
|
864
1044
|
const relativePath = path.relative( srcDir, jsonFile );
|
|
865
1045
|
const destPath = path.join( buildModuleDir, relativePath );
|
|
866
1046
|
const destDir = path.dirname( destPath );
|
|
@@ -882,9 +1062,9 @@ async function transpilePackage( packageName ) {
|
|
|
882
1062
|
/**
|
|
883
1063
|
* Compile styles for a single package.
|
|
884
1064
|
*
|
|
885
|
-
* Discovers and compiles SCSS entry points based on package configuration
|
|
886
|
-
*
|
|
887
|
-
*
|
|
1065
|
+
* Discovers and compiles SCSS entry points based on package configuration,
|
|
1066
|
+
* supporting wpStyleEntryPoints in package.json for custom entry point
|
|
1067
|
+
* patterns.
|
|
888
1068
|
*
|
|
889
1069
|
* @param {string} packageName Package name.
|
|
890
1070
|
* @return {Promise<number|null>} Build time in milliseconds, or null if no styles.
|
|
@@ -907,13 +1087,7 @@ async function compileStyles( packageName ) {
|
|
|
907
1087
|
)
|
|
908
1088
|
);
|
|
909
1089
|
|
|
910
|
-
|
|
911
|
-
const cssModuleEntries = await glob(
|
|
912
|
-
normalizePath( path.join( packageDir, 'src/**/*.module.css' ) ),
|
|
913
|
-
{ ignore: IGNORE_PATTERNS }
|
|
914
|
-
);
|
|
915
|
-
|
|
916
|
-
if ( scssEntries.length === 0 && cssModuleEntries.length === 0 ) {
|
|
1090
|
+
if ( scssEntries.length === 0 ) {
|
|
917
1091
|
return null;
|
|
918
1092
|
}
|
|
919
1093
|
|
|
@@ -921,64 +1095,6 @@ async function compileStyles( packageName ) {
|
|
|
921
1095
|
const buildStyleDir = path.join( packageDir, 'build-style' );
|
|
922
1096
|
const srcDir = path.join( packageDir, 'src' );
|
|
923
1097
|
|
|
924
|
-
// Process .module.css files and generate JS modules
|
|
925
|
-
const cssResults = await Promise.all(
|
|
926
|
-
cssModuleEntries.map( async ( styleEntryPath ) => {
|
|
927
|
-
const buildDir = path.join( packageDir, 'build' );
|
|
928
|
-
const buildModuleDir = path.join( packageDir, 'build-module' );
|
|
929
|
-
|
|
930
|
-
const cssContent = await readFile( styleEntryPath, 'utf8' );
|
|
931
|
-
const relativePath = path.relative( srcDir, styleEntryPath );
|
|
932
|
-
|
|
933
|
-
let mappings = {};
|
|
934
|
-
const result = await postcss( [
|
|
935
|
-
postcssModulesPlugin( {
|
|
936
|
-
getJSON: ( _, json ) => ( mappings = json ),
|
|
937
|
-
} ),
|
|
938
|
-
] ).process( cssContent, { from: styleEntryPath } );
|
|
939
|
-
|
|
940
|
-
// Write processed CSS to build-style (preserving directory structure)
|
|
941
|
-
const cssOutPath = path.join(
|
|
942
|
-
buildStyleDir,
|
|
943
|
-
relativePath.replace( '.module.css', '.css' )
|
|
944
|
-
);
|
|
945
|
-
await mkdir( path.dirname( cssOutPath ), { recursive: true } );
|
|
946
|
-
await writeFile( cssOutPath, result.css );
|
|
947
|
-
|
|
948
|
-
// Generate JS modules with class name mappings (preserving directory structure)
|
|
949
|
-
const jsExport = JSON.stringify( mappings );
|
|
950
|
-
const jsPath = `${ relativePath }.js`;
|
|
951
|
-
await Promise.all( [
|
|
952
|
-
mkdir( path.dirname( path.join( buildDir, jsPath ) ), {
|
|
953
|
-
recursive: true,
|
|
954
|
-
} ),
|
|
955
|
-
mkdir( path.dirname( path.join( buildModuleDir, jsPath ) ), {
|
|
956
|
-
recursive: true,
|
|
957
|
-
} ),
|
|
958
|
-
] );
|
|
959
|
-
await Promise.all( [
|
|
960
|
-
writeFile(
|
|
961
|
-
path.join( buildDir, jsPath ),
|
|
962
|
-
`"use strict";\nmodule.exports = ${ jsExport };\n`
|
|
963
|
-
),
|
|
964
|
-
writeFile(
|
|
965
|
-
path.join( buildModuleDir, jsPath ),
|
|
966
|
-
`export default ${ jsExport };\n`
|
|
967
|
-
),
|
|
968
|
-
] );
|
|
969
|
-
|
|
970
|
-
// Return the processed CSS for combining
|
|
971
|
-
return result.css;
|
|
972
|
-
} )
|
|
973
|
-
);
|
|
974
|
-
|
|
975
|
-
// Generate combined stylesheet from all CSS modules
|
|
976
|
-
if ( cssResults.length > 0 ) {
|
|
977
|
-
const combinedCss = cssResults.join( '\n' );
|
|
978
|
-
await mkdir( buildStyleDir, { recursive: true } );
|
|
979
|
-
await writeFile( path.join( buildStyleDir, 'style.css' ), combinedCss );
|
|
980
|
-
}
|
|
981
|
-
|
|
982
1098
|
// Process SCSS files
|
|
983
1099
|
await Promise.all(
|
|
984
1100
|
scssEntries.map( async ( styleEntryPath ) => {
|
|
@@ -1097,6 +1213,135 @@ function getPackageName( filename ) {
|
|
|
1097
1213
|
return null;
|
|
1098
1214
|
}
|
|
1099
1215
|
|
|
1216
|
+
/**
|
|
1217
|
+
* Build a single route's files.
|
|
1218
|
+
*
|
|
1219
|
+
* @param {string} routeName Route name.
|
|
1220
|
+
* @return {Promise<number>} Build time in milliseconds.
|
|
1221
|
+
*/
|
|
1222
|
+
async function buildRoute( routeName ) {
|
|
1223
|
+
const startTime = Date.now();
|
|
1224
|
+
const routeDir = path.join( ROOT_DIR, 'routes', routeName );
|
|
1225
|
+
const outputDir = path.join( BUILD_DIR, 'routes', routeName );
|
|
1226
|
+
|
|
1227
|
+
// Ensure output directory exists
|
|
1228
|
+
await mkdir( outputDir, { recursive: true } );
|
|
1229
|
+
|
|
1230
|
+
// Copy package.json
|
|
1231
|
+
await copyFile(
|
|
1232
|
+
path.join( routeDir, 'package.json' ),
|
|
1233
|
+
path.join( outputDir, 'package.json' )
|
|
1234
|
+
);
|
|
1235
|
+
|
|
1236
|
+
const files = getRouteFiles( routeDir );
|
|
1237
|
+
|
|
1238
|
+
// Build route.js if it exists
|
|
1239
|
+
if ( files.hasRoute ) {
|
|
1240
|
+
const routeEntryPoints = await glob( `route.${ SOURCE_EXTENSIONS }`, {
|
|
1241
|
+
cwd: routeDir,
|
|
1242
|
+
absolute: true,
|
|
1243
|
+
} );
|
|
1244
|
+
|
|
1245
|
+
if ( routeEntryPoints.length > 0 ) {
|
|
1246
|
+
const routePlugins = [
|
|
1247
|
+
wordpressExternalsPlugin( 'route.min', 'esm' ),
|
|
1248
|
+
];
|
|
1249
|
+
|
|
1250
|
+
// Build both minified and non-minified versions in parallel
|
|
1251
|
+
await Promise.all( [
|
|
1252
|
+
esbuild.build( {
|
|
1253
|
+
entryPoints: routeEntryPoints,
|
|
1254
|
+
outfile: path.join( outputDir, 'route.min.js' ),
|
|
1255
|
+
bundle: true,
|
|
1256
|
+
format: 'esm',
|
|
1257
|
+
target: browserslistToEsbuild(),
|
|
1258
|
+
minify: true,
|
|
1259
|
+
define: getDefine( false ),
|
|
1260
|
+
plugins: routePlugins,
|
|
1261
|
+
} ),
|
|
1262
|
+
esbuild.build( {
|
|
1263
|
+
entryPoints: routeEntryPoints,
|
|
1264
|
+
outfile: path.join( outputDir, 'route.js' ),
|
|
1265
|
+
bundle: true,
|
|
1266
|
+
format: 'esm',
|
|
1267
|
+
target: browserslistToEsbuild(),
|
|
1268
|
+
minify: false,
|
|
1269
|
+
define: getDefine( true ),
|
|
1270
|
+
plugins: routePlugins,
|
|
1271
|
+
} ),
|
|
1272
|
+
] );
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// Build content.js if stage or inspector exists
|
|
1277
|
+
if ( files.hasStage || files.hasInspector ) {
|
|
1278
|
+
// Create synthetic entry point
|
|
1279
|
+
const syntheticEntry = generateContentEntryPoint( files );
|
|
1280
|
+
const tempEntryPath = path.join( routeDir, '.content-entry.js' );
|
|
1281
|
+
|
|
1282
|
+
// Write temporary entry file
|
|
1283
|
+
await writeFile( tempEntryPath, syntheticEntry );
|
|
1284
|
+
|
|
1285
|
+
const contentPlugins = [
|
|
1286
|
+
wordpressExternalsPlugin( 'content.min', 'esm' ),
|
|
1287
|
+
...styleBundlingPlugins,
|
|
1288
|
+
];
|
|
1289
|
+
|
|
1290
|
+
// Build both minified and non-minified versions in parallel
|
|
1291
|
+
await Promise.all( [
|
|
1292
|
+
esbuild.build( {
|
|
1293
|
+
entryPoints: [ tempEntryPath ],
|
|
1294
|
+
outfile: path.join( outputDir, 'content.min.js' ),
|
|
1295
|
+
bundle: true,
|
|
1296
|
+
format: 'esm',
|
|
1297
|
+
target: browserslistToEsbuild(),
|
|
1298
|
+
minify: true,
|
|
1299
|
+
define: getDefine( false ),
|
|
1300
|
+
plugins: contentPlugins,
|
|
1301
|
+
} ),
|
|
1302
|
+
esbuild.build( {
|
|
1303
|
+
entryPoints: [ tempEntryPath ],
|
|
1304
|
+
outfile: path.join( outputDir, 'content.js' ),
|
|
1305
|
+
bundle: true,
|
|
1306
|
+
format: 'esm',
|
|
1307
|
+
target: browserslistToEsbuild(),
|
|
1308
|
+
minify: false,
|
|
1309
|
+
define: getDefine( true ),
|
|
1310
|
+
plugins: contentPlugins,
|
|
1311
|
+
} ),
|
|
1312
|
+
] );
|
|
1313
|
+
|
|
1314
|
+
await unlink( tempEntryPath );
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
return Date.now() - startTime;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Build all discovered routes.
|
|
1322
|
+
*
|
|
1323
|
+
* @return {Promise<void>}
|
|
1324
|
+
*/
|
|
1325
|
+
async function buildAllRoutes() {
|
|
1326
|
+
console.log( '\n🚦 Phase 3: Building routes...\n' );
|
|
1327
|
+
|
|
1328
|
+
const routes = getAllRoutes( ROOT_DIR );
|
|
1329
|
+
|
|
1330
|
+
if ( routes.length === 0 ) {
|
|
1331
|
+
console.log( ' No routes found, skipping.\n' );
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
await Promise.all(
|
|
1336
|
+
routes.map( async ( routeName ) => {
|
|
1337
|
+
const buildTime = await buildRoute( routeName );
|
|
1338
|
+
console.log(
|
|
1339
|
+
` ✔ Built route ${ routeName } (${ buildTime }ms)`
|
|
1340
|
+
);
|
|
1341
|
+
} )
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1100
1345
|
/**
|
|
1101
1346
|
* Main build function.
|
|
1102
1347
|
*/
|
|
@@ -1159,6 +1404,32 @@ async function buildAll() {
|
|
|
1159
1404
|
} )
|
|
1160
1405
|
);
|
|
1161
1406
|
|
|
1407
|
+
// Build routes
|
|
1408
|
+
await buildAllRoutes();
|
|
1409
|
+
|
|
1410
|
+
// Collect route and page data for PHP generation
|
|
1411
|
+
const routes = getAllRoutes( ROOT_DIR ).map( ( routeName ) => {
|
|
1412
|
+
const metadata = getRouteMetadata( ROOT_DIR, routeName );
|
|
1413
|
+
const routeFiles = getRouteFiles(
|
|
1414
|
+
path.join( ROOT_DIR, 'routes', routeName )
|
|
1415
|
+
);
|
|
1416
|
+
return {
|
|
1417
|
+
name: routeName,
|
|
1418
|
+
path: metadata?.path,
|
|
1419
|
+
page: metadata?.page,
|
|
1420
|
+
hasRoute: routeFiles.hasRoute,
|
|
1421
|
+
hasContent: routeFiles.hasStage || routeFiles.hasInspector,
|
|
1422
|
+
};
|
|
1423
|
+
} );
|
|
1424
|
+
|
|
1425
|
+
const pageData = PAGES.map( ( pageSlug ) => {
|
|
1426
|
+
const pageRoutes = routes.filter( ( r ) => r.page === pageSlug );
|
|
1427
|
+
return {
|
|
1428
|
+
slug: pageSlug,
|
|
1429
|
+
routes: pageRoutes,
|
|
1430
|
+
};
|
|
1431
|
+
} );
|
|
1432
|
+
|
|
1162
1433
|
console.log( '\n📄 Generating PHP registration files...\n' );
|
|
1163
1434
|
const phpReplacements = await getPhpReplacements( ROOT_DIR );
|
|
1164
1435
|
await Promise.all( [
|
|
@@ -1167,6 +1438,9 @@ async function buildAll() {
|
|
|
1167
1438
|
generateScriptRegistrationPhp( scripts, phpReplacements ),
|
|
1168
1439
|
generateStyleRegistrationPhp( styles, phpReplacements ),
|
|
1169
1440
|
generateVersionPhp( phpReplacements ),
|
|
1441
|
+
generateRoutesRegistry( routes, phpReplacements ),
|
|
1442
|
+
generateRoutesPhp( routes, phpReplacements ),
|
|
1443
|
+
generatePagesPhp( pageData, phpReplacements ),
|
|
1170
1444
|
] );
|
|
1171
1445
|
console.log( ' ✔ Generated build/modules.php' );
|
|
1172
1446
|
console.log( ' ✔ Generated build/modules/index.php' );
|
|
@@ -1175,6 +1449,16 @@ async function buildAll() {
|
|
|
1175
1449
|
console.log( ' ✔ Generated build/styles.php' );
|
|
1176
1450
|
console.log( ' ✔ Generated build/styles/index.php' );
|
|
1177
1451
|
console.log( ' ✔ Generated build/version.php' );
|
|
1452
|
+
console.log( ' ✔ Generated build/routes.php' );
|
|
1453
|
+
if ( PAGES.length > 0 ) {
|
|
1454
|
+
console.log( ' ✔ Generated build/pages.php' );
|
|
1455
|
+
for ( const page of PAGES ) {
|
|
1456
|
+
console.log( ` ✔ Generated build/pages/${ page }/page.php` );
|
|
1457
|
+
console.log(
|
|
1458
|
+
` ✔ Generated build/pages/${ page }/page-wp-admin.php`
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1178
1462
|
console.log( ' ✔ Generated build/index.php' );
|
|
1179
1463
|
|
|
1180
1464
|
const totalTime = Date.now() - startTime;
|
|
@@ -1202,6 +1486,9 @@ async function watchMode() {
|
|
|
1202
1486
|
}
|
|
1203
1487
|
const allFullNames = Array.from( shortToFull.values() );
|
|
1204
1488
|
|
|
1489
|
+
// Get all routes for dependency tracking
|
|
1490
|
+
const allRoutes = getAllRoutes( ROOT_DIR );
|
|
1491
|
+
|
|
1205
1492
|
/**
|
|
1206
1493
|
* Rebuild a package and any affected scripts/modules.
|
|
1207
1494
|
*
|
|
@@ -1238,22 +1525,69 @@ async function watchMode() {
|
|
|
1238
1525
|
);
|
|
1239
1526
|
}
|
|
1240
1527
|
}
|
|
1528
|
+
|
|
1529
|
+
// Find and rebuild affected routes
|
|
1530
|
+
const affectedRoutes = findRoutesToRebuild(
|
|
1531
|
+
fullName,
|
|
1532
|
+
allFullNames,
|
|
1533
|
+
ROOT_DIR,
|
|
1534
|
+
allRoutes
|
|
1535
|
+
);
|
|
1536
|
+
|
|
1537
|
+
for ( const route of affectedRoutes ) {
|
|
1538
|
+
try {
|
|
1539
|
+
const rebuildStartTime = Date.now();
|
|
1540
|
+
await buildRoute( route );
|
|
1541
|
+
const rebuildTime = Date.now() - rebuildStartTime;
|
|
1542
|
+
console.log(
|
|
1543
|
+
`✅ routes/${ route } (rebuilt) (${ rebuildTime }ms)`
|
|
1544
|
+
);
|
|
1545
|
+
} catch ( error ) {
|
|
1546
|
+
console.log(
|
|
1547
|
+
`❌ routes/${ route } - Rebuild error: ${ error.message }`
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1241
1551
|
} catch ( error ) {
|
|
1242
1552
|
console.log( `❌ ${ packageName } - Error: ${ error.message }` );
|
|
1243
1553
|
}
|
|
1244
1554
|
}
|
|
1245
1555
|
|
|
1556
|
+
/**
|
|
1557
|
+
* Rebuild a route.
|
|
1558
|
+
*
|
|
1559
|
+
* @param {string} routeName Route to rebuild.
|
|
1560
|
+
*/
|
|
1561
|
+
async function rebuildRoute( routeName ) {
|
|
1562
|
+
try {
|
|
1563
|
+
const startTime = Date.now();
|
|
1564
|
+
await buildRoute( routeName );
|
|
1565
|
+
const buildTime = Date.now() - startTime;
|
|
1566
|
+
console.log( `✅ routes/${ routeName } (${ buildTime }ms)` );
|
|
1567
|
+
} catch ( error ) {
|
|
1568
|
+
console.log(
|
|
1569
|
+
`❌ routes/${ routeName } - Error: ${ error.message }`
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1246
1574
|
async function processNextRebuild() {
|
|
1247
1575
|
if ( needsRebuild.size === 0 ) {
|
|
1248
1576
|
isRebuilding = false;
|
|
1249
1577
|
return;
|
|
1250
1578
|
}
|
|
1251
1579
|
|
|
1252
|
-
const
|
|
1580
|
+
const itemsToRebuild = Array.from( needsRebuild );
|
|
1253
1581
|
needsRebuild.clear();
|
|
1254
1582
|
|
|
1255
|
-
for ( const
|
|
1256
|
-
|
|
1583
|
+
for ( const item of itemsToRebuild ) {
|
|
1584
|
+
// Check if it's a route (prefixed with 'route:')
|
|
1585
|
+
if ( item.startsWith( 'route:' ) ) {
|
|
1586
|
+
const routeName = item.slice( 6 ); // Remove 'route:' prefix
|
|
1587
|
+
await rebuildRoute( routeName );
|
|
1588
|
+
} else {
|
|
1589
|
+
await rebuildPackage( item );
|
|
1590
|
+
}
|
|
1257
1591
|
}
|
|
1258
1592
|
|
|
1259
1593
|
await processNextRebuild();
|
|
@@ -1315,6 +1649,66 @@ async function watchMode() {
|
|
|
1315
1649
|
watcher.on( 'change', handleFileChange );
|
|
1316
1650
|
watcher.on( 'add', handleFileChange );
|
|
1317
1651
|
watcher.on( 'unlink', handleFileChange );
|
|
1652
|
+
|
|
1653
|
+
// Watch route files if routes exist
|
|
1654
|
+
if ( allRoutes.length > 0 ) {
|
|
1655
|
+
const routeWatchPaths = allRoutes.map( ( routeName ) =>
|
|
1656
|
+
path.join( ROOT_DIR, 'routes', routeName )
|
|
1657
|
+
);
|
|
1658
|
+
|
|
1659
|
+
const routeWatcher = chokidar.watch( routeWatchPaths, {
|
|
1660
|
+
persistent: true,
|
|
1661
|
+
ignoreInitial: true,
|
|
1662
|
+
ignored: ( filepath ) => {
|
|
1663
|
+
const basename = path.basename( filepath );
|
|
1664
|
+
// Ignore .content-entry.js temporary build files
|
|
1665
|
+
if ( basename === '.content-entry.js' ) {
|
|
1666
|
+
return true;
|
|
1667
|
+
}
|
|
1668
|
+
// Ignore node_modules directories and contents
|
|
1669
|
+
if ( filepath.includes( 'node_modules' ) ) {
|
|
1670
|
+
return true;
|
|
1671
|
+
}
|
|
1672
|
+
return false;
|
|
1673
|
+
},
|
|
1674
|
+
useFsEvents: true,
|
|
1675
|
+
depth: 10,
|
|
1676
|
+
awaitWriteFinish: {
|
|
1677
|
+
stabilityThreshold: 100,
|
|
1678
|
+
pollInterval: 50,
|
|
1679
|
+
},
|
|
1680
|
+
} );
|
|
1681
|
+
|
|
1682
|
+
routeWatcher.on( 'error', ( error ) => {
|
|
1683
|
+
console.error( '❌ Route watcher error:', error );
|
|
1684
|
+
} );
|
|
1685
|
+
|
|
1686
|
+
const handleRouteFileChange = async ( filename ) => {
|
|
1687
|
+
// Extract route name from path: routes/{routeName}/...
|
|
1688
|
+
const routeMatch = filename.match( /routes[/\\]([^/\\]+)[/\\]/ );
|
|
1689
|
+
if ( ! routeMatch ) {
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
const routeName = routeMatch[ 1 ];
|
|
1694
|
+
if ( ! allRoutes.includes( routeName ) ) {
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
if ( isRebuilding ) {
|
|
1699
|
+
needsRebuild.add( `route:${ routeName }` );
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
isRebuilding = true;
|
|
1704
|
+
await rebuildRoute( routeName );
|
|
1705
|
+
await processNextRebuild();
|
|
1706
|
+
};
|
|
1707
|
+
|
|
1708
|
+
routeWatcher.on( 'change', handleRouteFileChange );
|
|
1709
|
+
routeWatcher.on( 'add', handleRouteFileChange );
|
|
1710
|
+
routeWatcher.on( 'unlink', handleRouteFileChange );
|
|
1711
|
+
}
|
|
1318
1712
|
}
|
|
1319
1713
|
|
|
1320
1714
|
/**
|