@wordpress/build 0.2.0 → 0.2.1-next.2f1c7c01b.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/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,43 @@ import { camelCase } from 'change-case';
25
21
  /**
26
22
  * Internal dependencies
27
23
  */
28
- import { groupByDepth, findScriptsToRebundle } from './dependency-graph.mjs';
24
+ import {
25
+ groupByDepth,
26
+ findScriptsToRebundle,
27
+ findRoutesToRebuild,
28
+ } from './dependency-graph.mjs';
29
29
  import {
30
30
  generatePhpFromTemplate,
31
31
  getPhpReplacements,
32
+ generateRoutesPhp,
33
+ generateRoutesRegistry,
32
34
  } from './php-generator.mjs';
33
35
  import { getPackageInfo, getPackageInfoFromFile } from './package-utils.mjs';
34
36
  import { createWordpressExternalsPlugin } from './wordpress-externals-plugin.mjs';
37
+ import {
38
+ getAllRoutes,
39
+ getRouteFiles,
40
+ generateContentEntryPoint,
41
+ } from './route-utils.mjs';
35
42
 
36
43
  const ROOT_DIR = process.cwd();
37
44
  const PACKAGES_DIR = path.join( ROOT_DIR, 'packages' );
38
45
  const BUILD_DIR = path.join( ROOT_DIR, 'build' );
39
46
 
40
47
  const SOURCE_EXTENSIONS = '{js,ts,tsx}';
48
+ const ASSET_EXTENSIONS = 'json';
41
49
  const IGNORE_PATTERNS = [
42
50
  '**/benchmark/**',
43
51
  '**/{__mocks__,__tests__,test}/**',
44
52
  '**/{storybook,stories}/**',
45
53
  '**/*.native.*',
54
+ '**/*.ios.*',
55
+ '**/*.android.*',
46
56
  ];
47
57
  const TEST_FILE_PATTERNS = [
48
58
  /\/(benchmark|__mocks__|__tests__|test|storybook|stories)\/.+/,
49
59
  /\.(spec|test)\.(js|ts|tsx)$/,
60
+ /\.(native|ios|android)\.(js|ts|tsx)$/,
50
61
  ];
51
62
 
52
63
  /**
@@ -55,20 +66,23 @@ const TEST_FILE_PATTERNS = [
55
66
  * @return {string[]} Array of package names.
56
67
  */
57
68
  function getAllPackages() {
58
- return readdirSync( PACKAGES_DIR, { withFileTypes: true } )
59
- .filter( ( dirent ) => dirent.isDirectory() )
60
- .map( ( dirent ) => dirent.name );
69
+ return glob
70
+ .sync( normalizePath( path.join( PACKAGES_DIR, '*', 'package.json' ) ) )
71
+ .map( ( packageJsonPath ) =>
72
+ path.basename( path.dirname( packageJsonPath ) )
73
+ );
61
74
  }
62
75
 
63
76
  const PACKAGES = getAllPackages();
64
- const ROOT_PACKAGE_JSON = getPackageInfoFromFile( path.join( ROOT_DIR, 'package.json' ) );
77
+ const ROOT_PACKAGE_JSON = getPackageInfoFromFile(
78
+ path.join( ROOT_DIR, 'package.json' )
79
+ );
65
80
  const WP_PLUGIN_CONFIG = ROOT_PACKAGE_JSON.wpPlugin || {};
66
81
  const SCRIPT_GLOBAL = WP_PLUGIN_CONFIG.scriptGlobal;
67
82
  const PACKAGE_NAMESPACE = WP_PLUGIN_CONFIG.packageNamespace;
68
83
  const HANDLE_PREFIX = WP_PLUGIN_CONFIG.handlePrefix || PACKAGE_NAMESPACE;
69
84
  const EXTERNAL_NAMESPACES = WP_PLUGIN_CONFIG.externalNamespaces || {};
70
85
 
71
-
72
86
  const baseDefine = {
73
87
  'globalThis.IS_GUTENBERG_PLUGIN': JSON.stringify(
74
88
  Boolean( process.env.npm_package_config_IS_GUTENBERG_PLUGIN )
@@ -92,20 +106,26 @@ const wordpressExternalsPlugin = createWordpressExternalsPlugin(
92
106
  HANDLE_PREFIX
93
107
  );
94
108
 
95
- /**
96
- * Create emotion babel plugin for esbuild.
97
- * This plugin enables emotion's babel transformations for proper CSS-in-JS handling.
98
- *
99
- * @return {Object} esbuild plugin.
100
- */
101
- function emotionBabelPlugin() {
102
- return babel( {
103
- filter: /\.[jt]sx?$/,
104
- config: {
105
- plugins: [ '@emotion/babel-plugin' ],
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( `@${ PACKAGE_NAMESPACE }/` );
301
+ const matchesNamespace = packageFullName.startsWith(
302
+ `@${ PACKAGE_NAMESPACE }/`
303
+ );
282
304
  const shouldExposeGlobal = matchesNamespace && SCRIPT_GLOBAL !== false;
283
305
 
284
306
  const globalName = shouldExposeGlobal
@@ -789,8 +811,10 @@ async function transpilePackage( packageName ) {
789
811
  }
790
812
  );
791
813
 
792
- const jsonFiles = await glob(
793
- normalizePath( path.join( packageDir, 'src/**/*.json' ) ),
814
+ const assetFiles = await glob(
815
+ normalizePath(
816
+ path.join( packageDir, `src/**/*.${ ASSET_EXTENSIONS }` )
817
+ ),
794
818
  {
795
819
  ignore: IGNORE_PATTERNS,
796
820
  }
@@ -806,7 +830,37 @@ async function transpilePackage( packageName ) {
806
830
  // Check if this is the components package that needs emotion babel plugin.
807
831
  // Ideally we should remove this exception and move away from emotion.
808
832
  const needsEmotionPlugin = packageName === 'components';
809
- const plugins = needsEmotionPlugin ? [ emotionBabelPlugin() ] : [];
833
+ const emotionPlugin = babel( {
834
+ filter: /\.[jt]sx?$/,
835
+ config: {
836
+ plugins: [ '@emotion/babel-plugin' ],
837
+ },
838
+ } );
839
+ const externalizeAllExceptCssPlugin = {
840
+ name: 'externalize-except-css',
841
+ setup( build ) {
842
+ // Externalize all non-CSS imports
843
+ build.onResolve( { filter: /.*/ }, ( args ) => {
844
+ // Skip entry points
845
+ if ( args.kind === 'entry-point' ) {
846
+ return null;
847
+ }
848
+
849
+ // Let CSS/SCSS files be processed by sassPlugin
850
+ if ( args.path.match( /\.(css|scss)$/ ) ) {
851
+ return null;
852
+ }
853
+
854
+ // Externalize everything else (keep imports as-is)
855
+ return { path: args.path, external: true };
856
+ } );
857
+ },
858
+ };
859
+ const plugins = [
860
+ needsEmotionPlugin && emotionPlugin,
861
+ externalizeAllExceptCssPlugin,
862
+ ...styleBundlingPlugins,
863
+ ].filter( Boolean );
810
864
 
811
865
  if ( packageJson.main ) {
812
866
  builds.push(
@@ -814,7 +868,7 @@ async function transpilePackage( packageName ) {
814
868
  entryPoints: srcFiles,
815
869
  outdir: buildDir,
816
870
  outbase: srcDir,
817
- bundle: false,
871
+ bundle: true,
818
872
  platform: 'node',
819
873
  format: 'cjs',
820
874
  sourcemap: true,
@@ -828,13 +882,13 @@ async function transpilePackage( packageName ) {
828
882
  } )
829
883
  );
830
884
 
831
- for ( const jsonFile of jsonFiles ) {
832
- const relativePath = path.relative( srcDir, jsonFile );
885
+ for ( const assetFile of assetFiles ) {
886
+ const relativePath = path.relative( srcDir, assetFile );
833
887
  const destPath = path.join( buildDir, relativePath );
834
888
  const destDir = path.dirname( destPath );
835
889
  builds.push(
836
890
  mkdir( destDir, { recursive: true } ).then( () =>
837
- copyFile( jsonFile, destPath )
891
+ copyFile( assetFile, destPath )
838
892
  )
839
893
  );
840
894
  }
@@ -846,7 +900,7 @@ async function transpilePackage( packageName ) {
846
900
  entryPoints: srcFiles,
847
901
  outdir: buildModuleDir,
848
902
  outbase: srcDir,
849
- bundle: false,
903
+ bundle: true,
850
904
  platform: 'neutral',
851
905
  format: 'esm',
852
906
  sourcemap: true,
@@ -860,7 +914,7 @@ async function transpilePackage( packageName ) {
860
914
  } )
861
915
  );
862
916
 
863
- for ( const jsonFile of jsonFiles ) {
917
+ for ( const jsonFile of assetFiles ) {
864
918
  const relativePath = path.relative( srcDir, jsonFile );
865
919
  const destPath = path.join( buildModuleDir, relativePath );
866
920
  const destDir = path.dirname( destPath );
@@ -882,9 +936,9 @@ async function transpilePackage( packageName ) {
882
936
  /**
883
937
  * Compile styles for a single package.
884
938
  *
885
- * Discovers and compiles SCSS entry points based on package configuration
886
- * (supporting wpStyleEntryPoints in package.json for custom entry point patterns),
887
- * and all .module.css files in src/ directory.
939
+ * Discovers and compiles SCSS entry points based on package configuration,
940
+ * supporting wpStyleEntryPoints in package.json for custom entry point
941
+ * patterns.
888
942
  *
889
943
  * @param {string} packageName Package name.
890
944
  * @return {Promise<number|null>} Build time in milliseconds, or null if no styles.
@@ -907,13 +961,7 @@ async function compileStyles( packageName ) {
907
961
  )
908
962
  );
909
963
 
910
- // Get CSS modules from anywhere in src/
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 ) {
964
+ if ( scssEntries.length === 0 ) {
917
965
  return null;
918
966
  }
919
967
 
@@ -921,64 +969,6 @@ async function compileStyles( packageName ) {
921
969
  const buildStyleDir = path.join( packageDir, 'build-style' );
922
970
  const srcDir = path.join( packageDir, 'src' );
923
971
 
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
972
  // Process SCSS files
983
973
  await Promise.all(
984
974
  scssEntries.map( async ( styleEntryPath ) => {
@@ -1097,6 +1087,135 @@ function getPackageName( filename ) {
1097
1087
  return null;
1098
1088
  }
1099
1089
 
1090
+ /**
1091
+ * Build a single route's files.
1092
+ *
1093
+ * @param {string} routeName Route name.
1094
+ * @return {Promise<number>} Build time in milliseconds.
1095
+ */
1096
+ async function buildRoute( routeName ) {
1097
+ const startTime = Date.now();
1098
+ const routeDir = path.join( ROOT_DIR, 'routes', routeName );
1099
+ const outputDir = path.join( BUILD_DIR, 'routes', routeName );
1100
+
1101
+ // Ensure output directory exists
1102
+ await mkdir( outputDir, { recursive: true } );
1103
+
1104
+ // Copy package.json
1105
+ await copyFile(
1106
+ path.join( routeDir, 'package.json' ),
1107
+ path.join( outputDir, 'package.json' )
1108
+ );
1109
+
1110
+ const files = getRouteFiles( routeDir );
1111
+
1112
+ // Build route.js if it exists
1113
+ if ( files.hasRoute ) {
1114
+ const routeEntryPoints = await glob( `route.${ SOURCE_EXTENSIONS }`, {
1115
+ cwd: routeDir,
1116
+ absolute: true,
1117
+ } );
1118
+
1119
+ if ( routeEntryPoints.length > 0 ) {
1120
+ const routePlugins = [
1121
+ wordpressExternalsPlugin( 'route.min', 'esm' ),
1122
+ ];
1123
+
1124
+ // Build both minified and non-minified versions in parallel
1125
+ await Promise.all( [
1126
+ esbuild.build( {
1127
+ entryPoints: routeEntryPoints,
1128
+ outfile: path.join( outputDir, 'route.min.js' ),
1129
+ bundle: true,
1130
+ format: 'esm',
1131
+ target: browserslistToEsbuild(),
1132
+ minify: true,
1133
+ define: getDefine( false ),
1134
+ plugins: routePlugins,
1135
+ } ),
1136
+ esbuild.build( {
1137
+ entryPoints: routeEntryPoints,
1138
+ outfile: path.join( outputDir, 'route.js' ),
1139
+ bundle: true,
1140
+ format: 'esm',
1141
+ target: browserslistToEsbuild(),
1142
+ minify: false,
1143
+ define: getDefine( true ),
1144
+ plugins: routePlugins,
1145
+ } ),
1146
+ ] );
1147
+ }
1148
+ }
1149
+
1150
+ // Build content.js if stage or inspector exists
1151
+ if ( files.hasStage || files.hasInspector ) {
1152
+ // Create synthetic entry point
1153
+ const syntheticEntry = generateContentEntryPoint( files );
1154
+ const tempEntryPath = path.join( routeDir, '.content-entry.js' );
1155
+
1156
+ // Write temporary entry file
1157
+ await writeFile( tempEntryPath, syntheticEntry );
1158
+
1159
+ const contentPlugins = [
1160
+ wordpressExternalsPlugin( 'content.min', 'esm' ),
1161
+ ...styleBundlingPlugins,
1162
+ ];
1163
+
1164
+ // Build both minified and non-minified versions in parallel
1165
+ await Promise.all( [
1166
+ esbuild.build( {
1167
+ entryPoints: [ tempEntryPath ],
1168
+ outfile: path.join( outputDir, 'content.min.js' ),
1169
+ bundle: true,
1170
+ format: 'esm',
1171
+ target: browserslistToEsbuild(),
1172
+ minify: true,
1173
+ define: getDefine( false ),
1174
+ plugins: contentPlugins,
1175
+ } ),
1176
+ esbuild.build( {
1177
+ entryPoints: [ tempEntryPath ],
1178
+ outfile: path.join( outputDir, 'content.js' ),
1179
+ bundle: true,
1180
+ format: 'esm',
1181
+ target: browserslistToEsbuild(),
1182
+ minify: false,
1183
+ define: getDefine( true ),
1184
+ plugins: contentPlugins,
1185
+ } ),
1186
+ ] );
1187
+
1188
+ await unlink( tempEntryPath );
1189
+ }
1190
+
1191
+ return Date.now() - startTime;
1192
+ }
1193
+
1194
+ /**
1195
+ * Build all discovered routes.
1196
+ *
1197
+ * @return {Promise<void>}
1198
+ */
1199
+ async function buildAllRoutes() {
1200
+ console.log( '\n🚦 Phase 3: Building routes...\n' );
1201
+
1202
+ const routes = getAllRoutes( ROOT_DIR );
1203
+
1204
+ if ( routes.length === 0 ) {
1205
+ console.log( ' No routes found, skipping.\n' );
1206
+ return;
1207
+ }
1208
+
1209
+ await Promise.all(
1210
+ routes.map( async ( routeName ) => {
1211
+ const buildTime = await buildRoute( routeName );
1212
+ console.log(
1213
+ ` ✔ Built route ${ routeName } (${ buildTime }ms)`
1214
+ );
1215
+ } )
1216
+ );
1217
+ }
1218
+
1100
1219
  /**
1101
1220
  * Main build function.
1102
1221
  */
@@ -1159,6 +1278,9 @@ async function buildAll() {
1159
1278
  } )
1160
1279
  );
1161
1280
 
1281
+ // Build routes
1282
+ await buildAllRoutes();
1283
+
1162
1284
  console.log( '\n📄 Generating PHP registration files...\n' );
1163
1285
  const phpReplacements = await getPhpReplacements( ROOT_DIR );
1164
1286
  await Promise.all( [
@@ -1167,6 +1289,17 @@ async function buildAll() {
1167
1289
  generateScriptRegistrationPhp( scripts, phpReplacements ),
1168
1290
  generateStyleRegistrationPhp( styles, phpReplacements ),
1169
1291
  generateVersionPhp( phpReplacements ),
1292
+ generateRoutesRegistry(
1293
+ ROOT_DIR,
1294
+ BUILD_DIR,
1295
+ phpReplacements[ '{{PREFIX}}' ]
1296
+ ),
1297
+ generateRoutesPhp(
1298
+ ROOT_DIR,
1299
+ BUILD_DIR,
1300
+ HANDLE_PREFIX,
1301
+ phpReplacements[ '{{PREFIX}}' ]
1302
+ ),
1170
1303
  ] );
1171
1304
  console.log( ' ✔ Generated build/modules.php' );
1172
1305
  console.log( ' ✔ Generated build/modules/index.php' );
@@ -1175,6 +1308,7 @@ async function buildAll() {
1175
1308
  console.log( ' ✔ Generated build/styles.php' );
1176
1309
  console.log( ' ✔ Generated build/styles/index.php' );
1177
1310
  console.log( ' ✔ Generated build/version.php' );
1311
+ console.log( ' ✔ Generated build/routes.php' );
1178
1312
  console.log( ' ✔ Generated build/index.php' );
1179
1313
 
1180
1314
  const totalTime = Date.now() - startTime;
@@ -1202,6 +1336,9 @@ async function watchMode() {
1202
1336
  }
1203
1337
  const allFullNames = Array.from( shortToFull.values() );
1204
1338
 
1339
+ // Get all routes for dependency tracking
1340
+ const allRoutes = getAllRoutes( ROOT_DIR );
1341
+
1205
1342
  /**
1206
1343
  * Rebuild a package and any affected scripts/modules.
1207
1344
  *
@@ -1238,22 +1375,69 @@ async function watchMode() {
1238
1375
  );
1239
1376
  }
1240
1377
  }
1378
+
1379
+ // Find and rebuild affected routes
1380
+ const affectedRoutes = findRoutesToRebuild(
1381
+ fullName,
1382
+ allFullNames,
1383
+ ROOT_DIR,
1384
+ allRoutes
1385
+ );
1386
+
1387
+ for ( const route of affectedRoutes ) {
1388
+ try {
1389
+ const rebuildStartTime = Date.now();
1390
+ await buildRoute( route );
1391
+ const rebuildTime = Date.now() - rebuildStartTime;
1392
+ console.log(
1393
+ `✅ routes/${ route } (rebuilt) (${ rebuildTime }ms)`
1394
+ );
1395
+ } catch ( error ) {
1396
+ console.log(
1397
+ `❌ routes/${ route } - Rebuild error: ${ error.message }`
1398
+ );
1399
+ }
1400
+ }
1241
1401
  } catch ( error ) {
1242
1402
  console.log( `❌ ${ packageName } - Error: ${ error.message }` );
1243
1403
  }
1244
1404
  }
1245
1405
 
1406
+ /**
1407
+ * Rebuild a route.
1408
+ *
1409
+ * @param {string} routeName Route to rebuild.
1410
+ */
1411
+ async function rebuildRoute( routeName ) {
1412
+ try {
1413
+ const startTime = Date.now();
1414
+ await buildRoute( routeName );
1415
+ const buildTime = Date.now() - startTime;
1416
+ console.log( `✅ routes/${ routeName } (${ buildTime }ms)` );
1417
+ } catch ( error ) {
1418
+ console.log(
1419
+ `❌ routes/${ routeName } - Error: ${ error.message }`
1420
+ );
1421
+ }
1422
+ }
1423
+
1246
1424
  async function processNextRebuild() {
1247
1425
  if ( needsRebuild.size === 0 ) {
1248
1426
  isRebuilding = false;
1249
1427
  return;
1250
1428
  }
1251
1429
 
1252
- const packagesToRebuild = Array.from( needsRebuild );
1430
+ const itemsToRebuild = Array.from( needsRebuild );
1253
1431
  needsRebuild.clear();
1254
1432
 
1255
- for ( const packageName of packagesToRebuild ) {
1256
- await rebuildPackage( packageName );
1433
+ for ( const item of itemsToRebuild ) {
1434
+ // Check if it's a route (prefixed with 'route:')
1435
+ if ( item.startsWith( 'route:' ) ) {
1436
+ const routeName = item.slice( 6 ); // Remove 'route:' prefix
1437
+ await rebuildRoute( routeName );
1438
+ } else {
1439
+ await rebuildPackage( item );
1440
+ }
1257
1441
  }
1258
1442
 
1259
1443
  await processNextRebuild();
@@ -1315,6 +1499,54 @@ async function watchMode() {
1315
1499
  watcher.on( 'change', handleFileChange );
1316
1500
  watcher.on( 'add', handleFileChange );
1317
1501
  watcher.on( 'unlink', handleFileChange );
1502
+
1503
+ // Watch route files if routes exist
1504
+ if ( allRoutes.length > 0 ) {
1505
+ const routeWatchPaths = allRoutes.map( ( routeName ) =>
1506
+ path.join( ROOT_DIR, 'routes', routeName )
1507
+ );
1508
+
1509
+ const routeWatcher = chokidar.watch( routeWatchPaths, {
1510
+ persistent: true,
1511
+ ignoreInitial: true,
1512
+ useFsEvents: true,
1513
+ depth: 10,
1514
+ awaitWriteFinish: {
1515
+ stabilityThreshold: 100,
1516
+ pollInterval: 50,
1517
+ },
1518
+ } );
1519
+
1520
+ routeWatcher.on( 'error', ( error ) => {
1521
+ console.error( '❌ Route watcher error:', error );
1522
+ } );
1523
+
1524
+ const handleRouteFileChange = async ( filename ) => {
1525
+ // Extract route name from path: routes/{routeName}/...
1526
+ const routeMatch = filename.match( /routes[/\\]([^/\\]+)[/\\]/ );
1527
+ if ( ! routeMatch ) {
1528
+ return;
1529
+ }
1530
+
1531
+ const routeName = routeMatch[ 1 ];
1532
+ if ( ! allRoutes.includes( routeName ) ) {
1533
+ return;
1534
+ }
1535
+
1536
+ if ( isRebuilding ) {
1537
+ needsRebuild.add( `route:${ routeName }` );
1538
+ return;
1539
+ }
1540
+
1541
+ isRebuilding = true;
1542
+ await rebuildRoute( routeName );
1543
+ await processNextRebuild();
1544
+ };
1545
+
1546
+ routeWatcher.on( 'change', handleRouteFileChange );
1547
+ routeWatcher.on( 'add', handleRouteFileChange );
1548
+ routeWatcher.on( 'unlink', handleRouteFileChange );
1549
+ }
1318
1550
  }
1319
1551
 
1320
1552
  /**