diff --git a/Gruntfile.js b/Gruntfile.js index fba4a6153f3c0..0a9270f436847 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -12,6 +12,15 @@ module.exports = function(grunt) { SOURCE_DIR = 'src/', BUILD_DIR = 'build/', WORKING_DIR = grunt.option( 'dev' ) ? SOURCE_DIR : BUILD_DIR, + /* + * With --symlink, when building into /src, the wp-admin and wp-includes scripts that are copied unchanged from + * /src/js/_enqueues are symlinked instead, so that edits to them take effect without a rebuild. + * + * This is opt-in because Docker Desktop's file sharing can keep serving the previous entry for a path whose + * file is replaced, by a symlink or by a file in place of one, failing to read it until the containers are + * restarted (`npm run env:restart`). For the same reason, the links in place are kept by later builds. + */ + linkScripts = WORKING_DIR === SOURCE_DIR && grunt.option( 'symlink' ) === true, BANNER_TEXT = '/*! This file is auto-generated */', autoprefixer = require( 'autoprefixer' ), sass = require( 'sass' ), @@ -140,8 +149,7 @@ module.exports = function(grunt) { 'copy', 'cssmin', 'jshint', - 'uglify', - 'watch' + 'uglify' ], 'standard': [ 'banner', @@ -261,14 +269,28 @@ module.exports = function(grunt) { css: cssFiles.map( function( file ) { return setFilePath( WORKING_DIR, file ); } ), - js: jsFiles.map( function( file ) { + js: linkScripts ? { + // Clean the files in wp-admin/js one by one, so that the links in place are kept. See the link task. + src: jsFiles.map( function( file ) { + return setFilePath( WORKING_DIR, 'wp-admin/js/' === file ? 'wp-admin/js/**' : file ); + } ), + filter: function( filepath ) { + return ! isLinkedScriptPath( filepath ); + } + } : jsFiles.map( function( file ) { return setFilePath( WORKING_DIR, file ); } ), // Clean files built by Webpack. - 'webpack-assets': webpackFiles.map( function( file ) { - return setFilePath( WORKING_DIR, file ); - } ), + 'webpack-assets': { + src: webpackFiles.map( function( file ) { + return setFilePath( WORKING_DIR, file ); + } ), + // Keep the link to media-editor.js, which media-*.js matches. See the link task. + filter: function( filepath ) { + return ! linkScripts || ! isLinkedScriptPath( filepath ); + } + }, /* * Clean files sourced from the downloaded zip file built by the Gutenberg repository. @@ -1648,29 +1670,37 @@ module.exports = function(grunt) { }, _watch: { options: { - interval: 2000 + // Directories that are never watched, so that they are not searched. + ignored: [ + '**/.{svn,git}/**', + '**/node_modules/**', + 'vendor/**', + BUILD_DIR + '**', + // Third-party plugins, other than those that the build copies. + SOURCE_DIR + 'wp-content/plugins/!(index.php|hello.php|akismet)/**', + // Third-party themes. + SOURCE_DIR + 'wp-content/themes/!(twenty*|ipsum|index.php)/**', + // Uploads, translations, and other content that is not part of the build. + SOURCE_DIR + 'wp-content/!(index.php|plugins|themes)/**' + ] }, + /* + * Copies changed files from /src to /build. Its files are those of the copy:files task, set below, so + * that the same files are copied as by the build. When building into /src with --dev, there is nothing to + * copy, so this target is removed. + */ all: { - files: [ - SOURCE_DIR + '**', - '!' + SOURCE_DIR + 'js/**/*.js', - // Ignore version control directories. - '!' + SOURCE_DIR + '**/.{svn,git}/**', - // Ignore third-party plugins. - '!' + SOURCE_DIR + 'wp-content/plugins/**' - ], - tasks: ['clean:dynamic', 'copy:dynamic'], + files: [], + tasks: ['watch-dynamic:all', 'clean:dynamic', 'copy:dynamic', 'copy:dynamic-js'], options: { - dot: true, - spawn: false + dot: true } }, 'js-enqueues': { files: [SOURCE_DIR + 'js/_enqueues/**/*.js'], - tasks: ['clean:dynamic', 'copy:dynamic-js', 'uglify:dynamic'], + tasks: ['watch-dynamic:js-enqueues', 'clean:dynamic', 'copy:dynamic-js', 'uglify:dynamic', 'watch-uglify'], options: { - dot: true, - spawn: false + dot: true } }, 'js-webpack': { @@ -1679,31 +1709,33 @@ module.exports = function(grunt) { '!' + SOURCE_DIR + 'js/_enqueues/**/*.js', 'webpack-dev.config.js' ], - tasks: ['clean:dynamic', 'webpack:dev', 'uglify:dynamic'], + tasks: ['watch-dynamic:js-webpack', 'clean:dynamic', 'webpack:dev'], options: { - dot: true, - spawn: false + dot: true } }, config: { files: [ 'Gruntfile.js', 'webpack.config.js' - ] + ], + options: { + reload: true + } }, colors: { - files: [SOURCE_DIR + 'wp-admin/css/colors/**'], + files: [SOURCE_DIR + 'wp-admin/css/colors/**/*.scss'], tasks: ['sass:colors'] }, rtl: { files: [ SOURCE_DIR + 'wp-admin/css/*.css', - SOURCE_DIR + 'wp-includes/css/*.css' + SOURCE_DIR + 'wp-includes/css/*.css', + // Built stylesheets, written next to the sources with --dev. + '!' + SOURCE_DIR + '{wp-admin,wp-includes}/css/*.min.css', + '!' + SOURCE_DIR + '{wp-admin,wp-includes}/css/*-rtl*.css' ], - tasks: ['rtlcss:dynamic'], - options: { - spawn: false - } + tasks: ['watch-dynamic:rtl', 'rtlcss:dynamic'] }, test: { files: [ @@ -1722,6 +1754,35 @@ module.exports = function(grunt) { grunt.config.set( 'copy.files.files', copyFilesOptions ); } + if ( WORKING_DIR === SOURCE_DIR ) { + // When building into /src, there is nothing for the `all` watch target to copy, so its files are not watched. + delete grunt.config.data._watch.all; + } else { + /* + * Watch the files that the copy:files task copies, so that a change is copied as by the build, including + * the sources it copies to other paths, such as _index.php, and wp-config-sample.php from the root. + */ + grunt.config( [ '_watch', 'all', 'files' ], grunt.config( 'copy.files.files' )[0].src.map( function( pattern ) { + return pattern.charAt( 0 ) === '!' ? '!' + SOURCE_DIR + pattern.slice( 1 ) : SOURCE_DIR + pattern; + } ).concat( Object.keys( getMappedBuildFiles() ).map( function( dest ) { + return getMappedBuildFiles()[ dest ]; + } ) ) ); + } + + /* + * The `phpunit` watch target, also added here rather than only by the `watch` task, so that the watch task has + * it after restarting for a change to this file. + */ + if ( /^_?watch:phpunit$/.test( grunt.cli.tasks[ 0 ] ) || 'undefined' !== typeof grunt.option( 'phpunit' ) ) { + grunt.config( [ '_watch', 'phpunit' ], { + files: [ + SOURCE_DIR + '**/*.php', + 'tests/phpunit/**/*.php' + ], + tasks: [ 'phpunit:default' ] + } ); + } + // Register tasks. @@ -1800,20 +1861,13 @@ module.exports = function(grunt) { } ); } ); - grunt.renameTask( 'watch', '_watch' ); + grunt.registerTask( '_watch', 'Runs the tasks of the watch targets whose files change.', require( './tools/watch/task.js' )( grunt ) ); grunt.registerTask( 'watch', 'Watches files for changes and runs the related tasks. Use watch:phpunit or --phpunit to also run PHPUnit tests.', function() { if ( ! this.args.length || this.args.indexOf( 'webpack' ) > -1 ) { grunt.task.run( 'build' ); } - if ( 'watch:phpunit' === grunt.cli.tasks[ 0 ] || 'undefined' !== typeof grunt.option( 'phpunit' ) ) { - grunt.config.data._watch.phpunit = { - files: [ '**/*.php' ], - tasks: [ 'phpunit:default' ] - }; - } - grunt.task.run( '_' + this.nameArgs ); } ); @@ -1988,10 +2042,123 @@ module.exports = function(grunt) { grunt.registerTask( 'copy:js', 'Copies JavaScript files from npm packages, vendor libraries, wp-admin, and wp-includes.', [ 'copy:npm-packages', 'copy:vendor-js', - 'copy:admin-js', - 'copy:includes-js' + linkScripts ? 'link:admin-js' : 'copy:admin-js', + linkScripts ? 'link:includes-js' : 'copy:includes-js' ] ); + /** + * Checks whether a path is a symlink, including one whose target no longer exists. + * + * @param {string} filepath Path to check. + * @return {boolean} Whether the path is a symlink. + */ + function isSymlink( filepath ) { + try { + return fs.lstatSync( filepath ).isSymbolicLink(); + } catch ( e ) { + return false; + } + } + + // The scripts' symlinks in place before the build, and the directories that hold them, keyed by path. + var linkedScriptPaths = null; + + /** + * Checks whether a path is a script's symlink as created by the link task, or a directory that holds one. + * + * @param {string} filepath Path to check. + * @return {boolean} Whether the path is or holds a script's symlink. + */ + function isLinkedScriptPath( filepath ) { + var files, dest; + + // Collect the links and the directories that hold them once, as the clean tasks check each path. + if ( ! linkedScriptPaths ) { + linkedScriptPaths = {}; + files = Object.assign( {}, + grunt.config( [ 'copy', 'admin-js', 'files' ] ), + grunt.config( [ 'copy', 'includes-js', 'files' ] ) + ); + Object.keys( files ).forEach( function( linkPath ) { + if ( ! isSymlink( linkPath ) || fs.readlinkSync( linkPath ) !== path.relative( path.dirname( linkPath ), files[ linkPath ][ 0 ] ) ) { + return; + } + linkedScriptPaths[ linkPath ] = true; + for ( dest = path.dirname( linkPath ); dest !== '.' && ! linkedScriptPaths[ dest ]; dest = path.dirname( dest ) ) { + linkedScriptPaths[ dest ] = true; + } + } ); + } + + return !! linkedScriptPaths[ filepath.replace( /\/$/, '' ) ]; + } + + grunt.registerTask( 'link', 'Symlinks the files of a copy task to their sources, falling back to copying them where symlinks cannot be created.', function( target ) { + var files = grunt.config( [ 'copy', target, 'files' ] ); + var linked = 0; + var unchanged = 0; + var copied = 0; + var canLink = true; + + if ( ! files ) { + grunt.fail.warn( 'The copy:' + target + ' task has no files to link.' ); + return; + } + + Object.keys( files ).forEach( function( dest ) { + var src = files[ dest ][ 0 ]; + var linkTarget = path.relative( path.dirname( dest ), src ); + + /* + * Keep a link that is already in place. Docker Desktop's file sharing can keep serving the previous + * entry for a path that is replaced, failing to read it until the containers are restarted. + */ + if ( isSymlink( dest ) && fs.readlinkSync( dest ) === linkTarget && grunt.file.exists( src ) ) { + unchanged++; + return; + } + + // Remove the previous copy or link, so that neither is written through. + if ( isSymlink( dest ) || grunt.file.exists( dest ) ) { + fs.unlinkSync( dest ); + } + + // Like the copy task, skip a source that does not exist. + if ( ! grunt.file.exists( src ) ) { + grunt.verbose.writeln( 'Source file ' + src + ' not found.' ); + return; + } + grunt.file.mkdir( path.dirname( dest ) ); + + if ( canLink ) { + try { + // A relative link also resolves inside the Docker containers, which mount the repository elsewhere. + fs.symlinkSync( linkTarget, dest, 'file' ); + linked++; + return; + } catch ( e ) { + // Creating symlinks on Windows requires Developer Mode or administrator rights. + if ( [ 'EPERM', 'EACCES', 'ENOTSUP', 'ENOSYS' ].indexOf( e.code ) === -1 ) { + throw e; + } + canLink = false; + grunt.log.writeln( 'Symlinks cannot be created (' + e.code + '), so the files are copied instead.' ); + } + } + + grunt.file.copy( src, dest ); + copied++; + } ); + + grunt.log.ok( 'Linked ' + linked + ' ' + grunt.util.pluralize( linked, 'file/files' ) + + ( unchanged ? ', kept ' + unchanged + ' existing ' + grunt.util.pluralize( unchanged, 'link/links' ) : '' ) + + ( copied ? ', copied ' + copied + ' ' + grunt.util.pluralize( copied, 'file/files' ) : '' ) + '.' ); + + if ( linked ) { + grunt.log.writeln( 'If the local environment cannot read the linked files, restart it with `npm run env:restart`.' ); + } + } ); + grunt.registerTask( 'uglify:all', 'Minifies all JavaScript files.', [ 'uglify:core', 'uglify:emoji-loader', @@ -2498,104 +2665,227 @@ module.exports = function(grunt) { grunt.registerTask('default', 'Runs the build task.', ['build']); /* - * Automatically updates the `:dynamic` configurations - * so that only the changed files are updated. + * Changes seen by each watch target since its tasks last ran, keyed by file path. + * + * The watch task runs the tasks of a target once for all the changes made within its debounce delay, so the + * changes are collected here and the `:dynamic` configurations are set from all of them when the tasks run, + * by the `watch-dynamic` task, which each target runs first. */ - grunt.event.on( 'watch', function( action, filepath, target ) { - var src; + var watchedChanges = {}; - // Only configure the dynamic tasks based on known targets. - if ( [ 'all', 'rtl', 'webpack', 'js-enqueues', 'js-webpack' ].indexOf( target ) === -1 ) { + // The uglify targets to run for the changed scripts that the uglify:core task does not minify. + var watchedUglifyTargets = []; + + grunt.event.on( 'watch', function( action, filepath, target ) { + if ( [ 'all', 'rtl', 'js-enqueues', 'js-webpack' ].indexOf( target ) === -1 ) { return; } + watchedChanges[ target ] = watchedChanges[ target ] || {}; + // Normalize filepath for Windows. - filepath = filepath.replace( /\\/g, '/' ); - - // If the target is a file in the restructured js src. - if ( target === 'js-enqueues' ) { - var files = {}; - var configs, dest; - - // If it's a vendor file which are configured with glob matchers. - if ( filepath.indexOf( SOURCE_DIR + 'js/_enqueues/vendor/' ) === 0 ) { - // Grab the glob matchers from the copy task. - configs = grunt.config( [ 'copy', 'vendor-js', 'files' ] ); - - // For each glob matcher check if it matches and if so set the variables for our dynamic tasks. - for ( var i = 0; i < configs.length; i++ ) { - var config = configs[ i ]; - var relative = path.relative( config.cwd, filepath ); - var minimatch = require('minimatch'); - - if ( minimatch.match( config.src, relative, {} ) ) { - dest = config.dest + relative; - src = [ path.relative( WORKING_DIR, dest ) ]; - files[ dest ] = [ filepath ]; - break; - } + watchedChanges[ target ][ filepath.replace( /\\/g, '/' ) ] = action; + } ); + + /** + * Gets the destination of a file in the restructured js src, relative to the working directory. + * + * @param {string} filepath Path of the source file. + * @return {?{dest: string, src: string}} The destination and source of the copy, or null if the file is not copied. + */ + function getEnqueuedScriptCopy( filepath ) { + var configs, i, dest, relative; + + // If it's a vendor file which are configured with glob matchers. + if ( filepath.indexOf( SOURCE_DIR + 'js/_enqueues/vendor/' ) === 0 ) { + configs = grunt.config( [ 'copy', 'vendor-js', 'files' ] ); + + // Find the glob matcher that copies it. The other entries map files from node_modules. + for ( i = 0; i < configs.length; i++ ) { + if ( ! configs[ i ].cwd ) { + continue; } - // Or if it's another file which has a straight mapping. - } else { - configs = Object.assign( {}, - grunt.config( [ 'copy', 'admin-js', 'files' ] ), - grunt.config( [ 'copy', 'includes-js', 'files' ] ) - ); + relative = path.relative( configs[ i ].cwd, filepath ).replace( /\\/g, '/' ); - for ( dest in configs ) { - // If a file in the mapping matches then set the variables for our dynamic tasks. - if ( dest && configs.hasOwnProperty( dest ) && configs[ dest ][0] === './' + filepath ) { - files[ dest ] = configs[ dest ]; - src = [ path.relative( WORKING_DIR, dest ) ]; - break; - } + if ( relative.indexOf( '../' ) !== 0 && grunt.file.isMatch( configs[ i ].src, relative ) ) { + return { dest: configs[ i ].dest + relative, src: filepath }; } } + return null; + } + + // Or if it's another file which has a straight mapping. + configs = Object.assign( {}, + grunt.config( [ 'copy', 'admin-js', 'files' ] ), + grunt.config( [ 'copy', 'includes-js', 'files' ] ) + ); - // Configure our dynamic-js copy task which uses a file mapping rather than simply copying from src to build. - if ( action !== 'deleted' ) { - grunt.config( [ 'copy', 'dynamic-js', 'files' ], files ); + for ( dest in configs ) { + if ( configs.hasOwnProperty( dest ) && configs[ dest ][ 0 ] === './' + filepath ) { + return { dest: dest, src: configs[ dest ][ 0 ] }; } - // For the webpack builds configure the task to only check those files built by webpack. - } else if ( target === 'js-webpack' ) { - src = [ - 'wp-includes/js/media-audiovideo.js', - 'wp-includes/js/media-grid.js', - 'wp-includes/js/media-models.js', - 'wp-includes/js/media-views.js' - ]; - // Else simply use the path relative to the source directory. - } else { - src = [ path.relative( SOURCE_DIR, filepath ) ]; } + return null; + } - if ( ! src ) { - grunt.warn( 'Failed to determine the destination file.' ); - return; - } + /** + * Gets the files that the copy:files task copies other than by its glob patterns in /src: those it copies to + * another path, such as _index.php to index.php, and wp-config-sample.php from the root. + * + * @return {Object} Source paths, keyed by their destinations. + */ + function getMappedBuildFiles() { + var mapped = {}; - if ( action === 'deleted' ) { - // Clean up only those files that were deleted. - grunt.config( [ 'clean', 'dynamic', 'src' ], src ); - } else { - if ( ! grunt.option( 'dev' ) ) { - // Otherwise copy over only the changed file. - grunt.config(['copy', 'dynamic', 'src'], src); + grunt.config( 'copy.files.files' ).forEach( function( files ) { + // Skip the entries of glob patterns, which have a cwd. + if ( files.cwd ) { + return; } + // A single file copied into a directory, such as wp-config-sample.php. + if ( files.src ) { + [].concat( files.src ).forEach( function( src ) { + mapped[ files.dest + path.basename( src ) ] = src; + } ); + return; + } + Object.keys( files ).forEach( function( dest ) { + if ( 0 === files[ dest ][ 0 ].indexOf( SOURCE_DIR ) ) { + mapped[ dest ] = files[ dest ][ 0 ]; + } + } ); + } ); - // For javascript also minify and validate the changed file. - if ( target === 'js-enqueues' ) { - grunt.config( [ 'uglify', 'dynamic', 'src' ], src ); - grunt.config( [ 'dynamic', 'files', 'src' ], src.map( function( dir ) { return WORKING_DIR + dir; } ) ); + return mapped; + } + + /** + * Gets the uglify target that minifies a script in the build. + * + * @param {string} dest Path of the script, relative to the working directory. + * @return {?string} The name of the uglify target, or null if the script is not minified. + */ + function getUglifyTarget( dest ) { + var targets = grunt.config( 'uglify' ); + + return Object.keys( targets ).filter( function( name ) { + var target = targets[ name ]; + var relative; + + if ( 'options' === name || 'dynamic' === name ) { + return false; } - // For webpack only validate the file, minification is handled by webpack itself. - if ( target === 'js-webpack' ) { - grunt.config( [ 'dynamic', 'files', 'src' ], src.map( function( dir ) { return WORKING_DIR + dir; } ) ); + if ( ! target.expand ) { + return target.src === WORKING_DIR + dest; } - // For css run the rtl task on just the changed file. - if ( target === 'rtl' ) { - grunt.config( [ 'rtlcss', 'dynamic', 'src' ], src ); + relative = path.relative( target.cwd, WORKING_DIR + dest ).replace( /\\/g, '/' ); + return relative.indexOf( '../' ) !== 0 && grunt.file.isMatch( target.src, relative ); + } )[ 0 ] || null; + } + + /* + * Updates the `:dynamic` configurations so that only the changed files are updated. + * + * Every configuration a target uses is set each time, so that none is left over from an earlier run. + */ + grunt.registerTask( 'watch-dynamic', 'Configures the dynamic tasks of a watch target for the files changed since they last ran.', function( target ) { + var changes = watchedChanges[ target ] || {}; + var mappedBuildFiles = getMappedBuildFiles(); + var deleted = []; + var copyFiles = {}; + var minify = []; + var uglifyTargets = []; + var changed = []; + + delete watchedChanges[ target ]; + + Object.keys( changes ).forEach( function( filepath ) { + var action = changes[ filepath ]; + var copy, dest, uglifyTarget; + + if ( target === 'js-enqueues' ) { + copy = getEnqueuedScriptCopy( filepath ); + if ( ! copy ) { + grunt.log.writeln( 'Not built: ' + filepath ); + return; + } + dest = path.relative( WORKING_DIR, copy.dest ).replace( /\\/g, '/' ); + + if ( action === 'deleted' ) { + // A symlink to the deleted file no longer exists for the clean task, so it is removed here. + if ( isSymlink( copy.dest ) ) { + fs.unlinkSync( copy.dest ); + } else { + deleted.push( dest ); + } + // Every uglify target writes the minified script next to the script, with a .min.js extension. + if ( getUglifyTarget( dest ) ) { + deleted.push( dest.replace( /\.js$/, '.min.js' ) ); + } + return; + } + + // A symlink already has the changes, and copying onto it would write the file onto itself. + if ( ! isSymlink( copy.dest ) ) { + copyFiles[ copy.dest ] = [ copy.src ]; + } + + // Minify the script as the build does: with uglify:core, or by running the target that minifies it. + uglifyTarget = getUglifyTarget( dest ); + if ( 'core' === uglifyTarget ) { + minify.push( dest ); + } else if ( uglifyTarget && uglifyTargets.indexOf( uglifyTarget ) === -1 ) { + uglifyTargets.push( uglifyTarget ); + } + } else if ( target === 'js-webpack' ) { + // Check only the files built by webpack. + if ( action === 'deleted' ) { + deleted = [ + 'wp-includes/js/media-audiovideo.js', + 'wp-includes/js/media-grid.js', + 'wp-includes/js/media-models.js', + 'wp-includes/js/media-views.js' + ]; + } + } else if ( target === 'all' && Object.keys( mappedBuildFiles ).some( function( mappedDest ) { + return mappedBuildFiles[ mappedDest ] === filepath; + } ) ) { + // Copy a file that the build copies to another path, such as _index.php to index.php, or delete the copy. + Object.keys( mappedBuildFiles ).forEach( function( mappedDest ) { + if ( mappedBuildFiles[ mappedDest ] !== filepath ) { + return; + } + if ( action === 'deleted' ) { + deleted.push( path.relative( WORKING_DIR, mappedDest ).replace( /\\/g, '/' ) ); + } else { + copyFiles[ mappedDest ] = [ filepath ]; + } + } ); + } else if ( action === 'deleted' ) { + deleted.push( path.relative( SOURCE_DIR, filepath ) ); + } else { + changed.push( path.relative( SOURCE_DIR, filepath ) ); } + } ); + + grunt.config( [ 'clean', 'dynamic', 'src' ], deleted ); + + if ( target === 'all' ) { + grunt.config( [ 'copy', 'dynamic', 'src' ], changed ); + grunt.config( [ 'copy', 'dynamic-js', 'files' ], copyFiles ); + } else if ( target === 'rtl' ) { + grunt.config( [ 'rtlcss', 'dynamic', 'src' ], changed ); + } else if ( target === 'js-enqueues' ) { + grunt.config( [ 'copy', 'dynamic-js', 'files' ], copyFiles ); + grunt.config( [ 'uglify', 'dynamic', 'src' ], minify ); + watchedUglifyTargets = uglifyTargets; } - }); + } ); + + grunt.registerTask( 'watch-uglify', 'Runs the uglify targets for the changed scripts that uglify:core does not minify.', function() { + grunt.task.run( watchedUglifyTargets.map( function( name ) { + return 'uglify:' + name; + } ) ); + watchedUglifyTargets = []; + } ); }; diff --git a/README.md b/README.md index 0f41da84643bd..a0fd93673885f 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,18 @@ npm run dev To stop the watcher, press `ctrl+c`. +The watcher relies on the file system events of your operating system. Where these are not delivered, such as on a network drive or for a Windows drive accessed from WSL, set the `CHOKIDAR_USEPOLLING=1` environment variable to check the files for changes periodically instead. + +The JavaScript files in `src/wp-admin/js` and `src/wp-includes/js` that are copied unchanged from `src/js/_enqueues` can be symlinked instead, so that changes to them take effect without the watcher: + +``` +npm run build:dev -- --symlink +``` + +The option also applies to the build that the watcher runs when it starts, as in `npm run dev -- --symlink`. Later builds with the option keep the links in place, while a build without it replaces them with copies again. + +If the local environment then cannot load these files, which can happen with Docker Desktop after switching between links and copies, restart it with `npm run env:restart`. + #### To run a [WP-CLI](https://make.wordpress.org/cli/handbook/) command ``` diff --git a/package-lock.json b/package-lock.json index 5b497251c419d..c327ba65167fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,6 +57,7 @@ "@wordpress/prettier-config": "4.51.0", "@wordpress/scripts": "33.0.0", "autoprefixer": "10.5.3", + "chokidar": "4.0.3", "dotenv": "17.4.2", "dotenv-expand": "13.0.0", "grunt": "1.6.2", @@ -67,7 +68,6 @@ "grunt-contrib-cssmin": "~5.0.0", "grunt-contrib-jshint": "3.2.0", "grunt-contrib-uglify": "~5.2.2", - "grunt-contrib-watch": "~1.1.0", "grunt-file-append": "0.0.7", "grunt-jsdoc": "2.4.1", "grunt-legacy-util": "^2.0.2", @@ -3013,18 +3013,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@jest/core/node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/core/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3059,18 +3047,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/@jest/core/node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/core/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3080,15 +3056,6 @@ "node": ">=8" } }, - "node_modules/@jest/core/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/@jest/core/node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -3127,18 +3094,6 @@ "node": ">=8" } }, - "node_modules/@jest/core/node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/@jest/environment": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", @@ -3802,18 +3757,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@jest/transform/node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/transform/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3854,18 +3797,6 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, - "node_modules/@jest/transform/node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/transform/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3875,15 +3806,6 @@ "node": ">=8" } }, - "node_modules/@jest/transform/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/@jest/transform/node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -3910,18 +3832,6 @@ "node": ">=8" } }, - "node_modules/@jest/transform/node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/@jest/types": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", @@ -4789,45 +4699,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher/node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@parcel/watcher/node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@parcel/watcher/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/@parcel/watcher/node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -4843,20 +4714,6 @@ "node": ">=8.6" } }, - "node_modules/@parcel/watcher/node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/@paulirish/trace_engine": { "version": "0.0.39", "resolved": "https://registry.npmjs.org/@paulirish/trace_engine/-/trace_engine-0.0.39.tgz", @@ -9316,15 +9173,6 @@ "node": ">=8" } }, - "node_modules/async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "dev": true, - "dependencies": { - "lodash": "^4.17.14" - } - }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -9958,12 +9806,16 @@ } }, "node_modules/binary-extensions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", - "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/bluebird": { @@ -9972,18 +9824,6 @@ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "dev": true }, - "node_modules/body": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/body/-/body-5.1.0.tgz", - "integrity": "sha1-5LoM5BCkaTYyM2dgnstOZVMSUGk=", - "dev": true, - "dependencies": { - "continuable-cache": "^0.3.1", - "error": "^7.0.0", - "raw-body": "~1.1.0", - "safe-json-parse": "~1.0.1" - } - }, "node_modules/body-parser": { "version": "1.20.6", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", @@ -10070,33 +9910,6 @@ "node": ">= 0.8" } }, - "node_modules/body/node_modules/bytes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz", - "integrity": "sha512-/x68VkHLeTl3/Ll8IvxdwzhrT+IyKc52e/oyHhA2RwqPqswSnjVbSddfPRwAsJtbilMAPSRWwAlpxdYsSWOTKQ==", - "dev": true - }, - "node_modules/body/node_modules/raw-body": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz", - "integrity": "sha512-WmJJU2e9Y6M5UzTOkHaM7xJGAPQD8PNzx3bAd2+uhZAim6wDk6dAZxPVYLF67XhbR4hmKGh33Lpmh4XWrCH5Mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "1", - "string_decoder": "0.10" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/body/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", - "dev": true, - "license": "MIT" - }, "node_modules/bonjour-service": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", @@ -10124,6 +9937,19 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.28.6", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", @@ -10521,73 +10347,19 @@ } }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chokidar/node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chokidar/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/chokidar/node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" } }, "node_modules/chrome-launcher": { @@ -11033,12 +10805,6 @@ "node": ">= 0.6" } }, - "node_modules/continuable-cache": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/continuable-cache/-/continuable-cache-0.3.1.tgz", - "integrity": "sha1-vXJ6f67XfnH/OYWskzUakSczrQ8=", - "dev": true - }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -12289,15 +12055,6 @@ "node": ">=4" } }, - "node_modules/error": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/error/-/error-7.2.1.tgz", - "integrity": "sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA==", - "dev": true, - "dependencies": { - "string-template": "~0.2.1" - } - }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -13679,63 +13436,18 @@ "node": ">=8.6.0" } }, - "node_modules/fast-glob/node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "node_modules/fast-glob/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.1.1" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { - "node": ">=8" - } - }, - "node_modules/fast-glob/node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fast-glob/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/fast-glob/node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/fast-glob/node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" + "node": ">=8.6" } }, "node_modules/fast-json-stable-stringify": { @@ -13786,19 +13498,6 @@ "reusify": "^1.0.4" } }, - "node_modules/faye-websocket": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", - "integrity": "sha512-Xhj93RXbMSq8urNCUq4p9l0P6hnySJ/7YNRhYNug0bLOuii7pKO7xQFb5mx9xZXWCar88pLPb805PvUkwrLZpQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -13876,6 +13575,19 @@ "integrity": "sha1-peeo/7+kk7Q7kju9TKiaU7Y7YSs=", "dev": true }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/finalhandler": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", @@ -14065,18 +13777,6 @@ "node": ">= 10.13.0" } }, - "node_modules/findup-sync/node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/findup-sync/node_modules/expand-tilde": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", @@ -14089,18 +13789,6 @@ "node": ">=0.10.0" } }, - "node_modules/findup-sync/node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/findup-sync/node_modules/global-modules": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", @@ -14131,15 +13819,6 @@ "node": ">=0.10.0" } }, - "node_modules/findup-sync/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/findup-sync/node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -14167,18 +13846,6 @@ "node": ">=0.10.0" } }, - "node_modules/findup-sync/node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/findup-sync/node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -14505,18 +14172,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gaze": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", - "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", - "dev": true, - "dependencies": { - "globule": "^1.0.0" - }, - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -14831,33 +14486,6 @@ "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", "dev": true }, - "node_modules/globule": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.4.tgz", - "integrity": "sha512-OPTIfhMBh7JbBYDpa5b+Q5ptmMWKwcNcFSR/0c6t8V4f3ZAVBEsKNY37QdVqmLRYSMhOUGYrY0QhSoEpzGr/Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob": "~7.1.1", - "lodash": "^4.17.21", - "minimatch": "~3.0.2" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/globule/node_modules/minimatch": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", - "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/good-listener": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", @@ -15421,21 +15049,6 @@ "node": ">=8" } }, - "node_modules/grunt-contrib-watch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-watch/-/grunt-contrib-watch-1.1.0.tgz", - "integrity": "sha512-yGweN+0DW5yM+oo58fRu/XIRrPcn3r4tQx+nL7eMRwjpvk+rQY6R8o94BPK0i2UhTg9FN21hS+m8vR8v9vXfeg==", - "dev": true, - "dependencies": { - "async": "^2.6.0", - "gaze": "^1.1.0", - "lodash": "^4.17.10", - "tiny-lr": "^1.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/grunt-file-append": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/grunt-file-append/-/grunt-file-append-0.0.7.tgz", @@ -16578,39 +16191,6 @@ } } }, - "node_modules/http-proxy-middleware/node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/http-proxy-middleware/node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/http-proxy-middleware/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", @@ -16637,18 +16217,6 @@ "node": ">=8.6" } }, - "node_modules/http-proxy-middleware/node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -17079,6 +16647,7 @@ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -17284,6 +16853,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-number-object": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", @@ -18139,18 +17718,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/jest-config/node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-config/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -18185,18 +17752,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/jest-config/node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-config/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -18206,15 +17761,6 @@ "node": ">=8" } }, - "node_modules/jest-config/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/jest-config/node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -18241,18 +17787,6 @@ "node": ">=8" } }, - "node_modules/jest-config/node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/jest-diff": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", @@ -18780,30 +18314,6 @@ "fsevents": "^2.3.2" } }, - "node_modules/jest-haste-map/node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-haste-map/node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-haste-map/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -18813,15 +18323,6 @@ "node": ">=8" } }, - "node_modules/jest-haste-map/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/jest-haste-map/node_modules/jest-worker": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", @@ -18866,18 +18367,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/jest-haste-map/node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/jest-leak-detector": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", @@ -19011,18 +18500,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/jest-message-util/node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-message-util/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -19057,18 +18534,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/jest-message-util/node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-message-util/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -19078,15 +18543,6 @@ "node": ">=8" } }, - "node_modules/jest-message-util/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/jest-message-util/node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -19113,18 +18569,6 @@ "node": ">=8" } }, - "node_modules/jest-message-util/node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/jest-mock": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", @@ -20506,18 +19950,6 @@ "node": ">=10" } }, - "node_modules/liftup/node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/liftup/node_modules/expand-tilde": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", @@ -20531,18 +19963,6 @@ "node": ">=0.10.0" } }, - "node_modules/liftup/node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/liftup/node_modules/findup-sync": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz", @@ -20589,15 +20009,6 @@ "node": ">=0.10.0" } }, - "node_modules/liftup/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/liftup/node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -20638,18 +20049,6 @@ "node": ">=0.10.0" } }, - "node_modules/liftup/node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/liftup/node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -20774,12 +20173,6 @@ "uc.micro": "^1.0.1" } }, - "node_modules/livereload-js": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.4.0.tgz", - "integrity": "sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw==", - "dev": true - }, "node_modules/loader-runner": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", @@ -24284,15 +23677,17 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, + "license": "MIT", "engines": { - "node": ">=8.10.0" + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/rechoir": { @@ -24876,12 +24271,6 @@ } ] }, - "node_modules/safe-json-parse": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-1.0.1.tgz", - "integrity": "sha1-PnZyPjjf3aE8mx0poeB//uSzC1c=", - "dev": true - }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -25939,12 +25328,6 @@ "node": ">=8" } }, - "node_modules/string-template": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", - "integrity": "sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=", - "dev": true - }, "node_modules/string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", @@ -26347,19 +25730,6 @@ "dev": true, "license": "MIT" }, - "node_modules/stylelint/node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/stylelint/node_modules/cosmiconfig": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", @@ -26418,19 +25788,6 @@ "flat-cache": "^6.1.23" } }, - "node_modules/stylelint/node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/stylelint/node_modules/flat-cache": { "version": "6.1.23", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", @@ -26463,16 +25820,6 @@ "node": ">=8" } }, - "node_modules/stylelint/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/stylelint/node_modules/js-yaml": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", @@ -26595,19 +25942,6 @@ "node": ">=8" } }, - "node_modules/stylelint/node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/stylelint/node_modules/write-file-atomic": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", @@ -27089,29 +26423,6 @@ "globrex": "^0.1.2" } }, - "node_modules/tiny-lr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.1.1.tgz", - "integrity": "sha512-44yhA3tsaRoMOjQQ+5v5mVdqef+kH6Qze9jTpqtVufgYjYt08zyZAwNwwVBj3i1rJMnR52IxOW0LK0vBzgAkuA==", - "dev": true, - "dependencies": { - "body": "^5.1.0", - "debug": "^3.1.0", - "faye-websocket": "~0.10.0", - "livereload-js": "^2.3.0", - "object-assign": "^4.1.0", - "qs": "^6.4.0" - } - }, - "node_modules/tiny-lr/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -27189,6 +26500,19 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -28326,12 +27650,50 @@ "ajv": "^8.8.2" } }, + "node_modules/webpack-dev-server/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/webpack-dev-server/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, + "node_modules/webpack-dev-server/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/webpack-dev-server/node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", diff --git a/package.json b/package.json index 4b29c829fb5a0..6f429eb4c7504 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@wordpress/prettier-config": "4.51.0", "@wordpress/scripts": "33.0.0", "autoprefixer": "10.5.3", + "chokidar": "4.0.3", "dotenv": "17.4.2", "dotenv-expand": "13.0.0", "grunt": "1.6.2", @@ -65,7 +66,6 @@ "grunt-contrib-cssmin": "~5.0.0", "grunt-contrib-jshint": "3.2.0", "grunt-contrib-uglify": "~5.2.2", - "grunt-contrib-watch": "~1.1.0", "grunt-file-append": "0.0.7", "grunt-jsdoc": "2.4.1", "grunt-legacy-util": "^2.0.2", diff --git a/tools/watch/task.js b/tools/watch/task.js new file mode 100644 index 0000000000000..525036b6fe164 --- /dev/null +++ b/tools/watch/task.js @@ -0,0 +1,430 @@ +/** + * Watch Task + * + * Runs the tasks of the `_watch` targets whose files change. It replaces + * grunt-contrib-watch, which missed changes: it polls each watched file through + * gaze, which finds every file in the directories that a target's patterns + * exclude before filtering them out; it recreates its watchers after each run, + * missing the changes made meanwhile; and it leaves the changes seen while + * another target's tasks run waiting for the next change. + * + * Files are watched with chokidar, through the native file system events of + * each platform. Set `CHOKIDAR_USEPOLLING=1` to poll instead, where the events + * are not delivered, as on network drives or for Windows drives under WSL. + * + * Each target takes `files` patterns, the `tasks` to run when one of them + * changes, and the options `dot`, to match files whose names begin with a dot, + * and `reload`, to restart the watch task, as for a change to the Gruntfile. + * The `ignored` option of the task lists the patterns of the directories that + * are never watched. + * + * The tasks run in the watch process, and a `watch` event is emitted for each + * change of a target before they run, so that a listener can configure them for + * the changed files. Changes made while tasks run are run once they finish. When + * a task fails, the rest of its target's tasks are skipped, and those of the + * other targets still run. A reload happens after the tasks of the other + * changes made with it, or while its tasks ran, have run. The new process is + * handed the changes made while it starts, which the previous one still watches. + * + * A target's tasks must not write files that match its own patterns, which + * would run them again without end. + * + * @package WordPress + */ + +const path = require( 'path' ); +const { spawn } = require( 'child_process' ); +const chokidar = require( 'chokidar' ); + +/** + * Time to wait for more changes before running the tasks, in milliseconds. + * + * An editor or a version control checkout often changes several files at once. + */ +const DEBOUNCE_DELAY = 250; + +/** + * Environment variable set for a watch process started by a reload, which takes over the changes that the + * previous process saw while it started. + */ +const HAND_OFF_ENV = 'WP_WATCH_HAND_OFF'; + +/** + * Change actions, keyed by the chokidar event names. + */ +const ACTIONS = { + add: 'added', + change: 'changed', + unlink: 'deleted', +}; + +/** + * Gets the directory that a pattern searches, before any part with glob syntax. + * + * @param {string} pattern The pattern. + * + * @return {string} The directory, or the file when the pattern has no glob syntax. + */ +function getPatternBase( pattern ) { + const segments = []; + + for ( const segment of pattern.split( '/' ) ) { + if ( /[*?[\]{}()!+@]/.test( segment ) ) { + break; + } + segments.push( segment ); + } + + return segments.join( '/' ) || '.'; +} + +/** + * Gets the paths to watch for the patterns, without those inside another one. + * + * @param {string[]} patterns The patterns. + * + * @return {string[]} The paths to watch. + */ +function getWatchPaths( patterns ) { + const bases = [ ...new Set( + patterns + .filter( ( pattern ) => ! pattern.startsWith( '!' ) ) + .map( getPatternBase ) + ) ]; + const isInside = ( child, parent ) => parent === '.' || child.startsWith( parent + '/' ); + + return bases.filter( ( base ) => ! bases.some( ( other ) => other !== base && isInside( base, other ) ) ); +} + +/** + * Builds the watch task. + * + * @param {Object} grunt The grunt instance. + * + * @return {Function} The task function. + */ +module.exports = ( grunt ) => { + let watcher = null; + let taskName = '_watch'; + let targetNames = []; + let nameArgs = ''; + let resume = null; + let timer = null; + let reload = false; + let restartPending = false; + let startedAt = null; + let currentTarget = null; + let failureReported = false; + + // The changes seen while restarting, as chokidar event names and paths, to hand to the new process. + let handOffChanges = null; + + // Changed files since the tasks last ran, keyed by path, and the targets to run for them. + const changes = new Map(); + const queuedTargets = new Set(); + + // The targets whose tasks are queued in the current run, until each one's tasks finish. + let runningTargets = []; + + grunt.registerTask( '_watch-target', 'Marks the tasks of a watch target as started or finished.', ( step, name ) => { + failureReported = false; + if ( 'start' === step ) { + currentTarget = name; + } else { + currentTarget = null; + runningTargets = runningTargets.filter( ( running ) => running !== name ); + } + } ); + + /** + * Gets the configuration of a target. + * + * @param {string} name The target name. + * + * @return {{files: string[], tasks: string[], options: Object}} The target configuration. + */ + function getTarget( name ) { + const target = grunt.config( [ taskName, name ] ); + + return { + files: [].concat( target.files || [] ).map( ( pattern ) => grunt.config.process( pattern ) ), + tasks: [].concat( target.tasks || [] ), + options: target.options || {}, + }; + } + + /** + * Gets the tasks to run for targets, each between the tasks that mark it as started and finished. + * + * @param {string[]} names The target names. + * + * @return {string[]} The tasks. + */ + function getTasks( names ) { + return names.flatMap( ( name ) => [ '_watch-target:start:' + name ].concat( + getTarget( name ).tasks, + '_watch-target:done:' + name + ) ); + } + + /** + * Records a change, for each target whose patterns match the file. + * + * @param {string} event The chokidar event name. + * @param {string} filepath The path of the changed file. + */ + function recordChange( event, filepath ) { + const action = ACTIONS[ event ]; + let matched = false; + + if ( ! action ) { + return; + } + + // Once restarting, the changes are for the new process, which is handed them when it is ready. + if ( handOffChanges ) { + handOffChanges.push( [ event, filepath ] ); + return; + } + + filepath = path.relative( process.cwd(), filepath ).replace( /\\/g, '/' ); + + targetNames.forEach( ( name ) => { + const target = getTarget( name ); + + if ( ! grunt.file.isMatch( { dot: !! target.options.dot }, target.files, filepath ) ) { + return; + } + + matched = true; + queuedTargets.add( name ); + if ( target.options.reload ) { + reload = true; + } + + grunt.event.emit( 'watch', action, filepath, name ); + } ); + + if ( matched ) { + changes.set( filepath, action ); + clearTimeout( timer ); + timer = setTimeout( runQueued, DEBOUNCE_DELAY ); + } + } + + /** + * Runs the tasks of the targets that have changes, unless tasks are running. + * + * The changes are then kept until the tasks finish and the watch task resumes. + */ + function runQueued() { + const done = resume; + + timer = null; + if ( ! done ) { + return; + } + + changes.forEach( ( action, filepath ) => { + grunt.log.ok( 'File "' + filepath + '" ' + action + '.' ); + } ); + + // Restart once the tasks for the other changes have run, as the new process does not see those changes. + if ( reload ) { + restartPending = true; + reload = false; + } + + runningTargets = targetNames.filter( ( name ) => queuedTargets.has( name ) ); + changes.clear(); + queuedTargets.clear(); + resume = null; + startedAt = Date.now(); + + // Run the watch task again after the tasks, to wait for the next changes. + grunt.task.run( getTasks( runningTargets ).concat( nameArgs ) ); + done(); + } + + /** + * Restarts the watch task in a new process, so that it loads the changed configuration. + */ + function restart() { + grunt.log.writeln( 'Reloading watch config...'.cyan ); + + /* + * Keep watching until the new process is ready, which can take several seconds for its initial scan, and + * hand it the changes seen meanwhile, which it would otherwise miss. + */ + handOffChanges = []; + + const child = spawn( + process.execPath, + process.execArgv.concat( process.argv[ 1 ], nameArgs, grunt.option.flags() ), + { + stdio: [ 'inherit', 'inherit', 'inherit', 'ipc' ], + env: { ...process.env, [ HAND_OFF_ENV ]: '1' }, + } + ); + child.on( 'message', ( message ) => { + if ( ! message || 'watch-ready' !== message.type ) { + return; + } + watcher.close(); + child.send( { type: 'watch-changes', changes: handOffChanges }, () => child.disconnect() ); + handOffChanges = []; + } ); + child.on( 'exit', ( code ) => process.exit( code === null ? 1 : code ) ); + + // Stop the new process with this one, which only waits for it. + [ 'SIGINT', 'SIGTERM', 'SIGHUP' ].forEach( ( signal ) => { + process.on( signal, () => child.kill( signal ) ); + } ); + } + + /** + * Takes over the changes that the previous watch process saw while this one started, when started by a reload. + * + * The previous process keeps watching until this one reports that it is ready, then hands over its changes. + */ + function takeOverChanges() { + if ( ! process.send || ! process.env[ HAND_OFF_ENV ] ) { + return; + } + + process.on( 'message', ( message ) => { + if ( message && 'watch-changes' === message.type ) { + message.changes.forEach( ( [ event, filepath ] ) => recordChange( event, filepath ) ); + } + } ); + process.send( { type: 'watch-ready' } ); + } + + /** + * Keeps the watch task running when a task fails, instead of exiting. + * + * The failure is logged, and the watch task resumes, waiting for the next changes. + */ + function keepWatchingOnFailure() { + /* + * Exit once the output can no longer be written, as when the terminal is closed. Otherwise, the error + * of each attempt to log would be reported as another failure, without end. + */ + [ process.stdout, process.stderr ].forEach( ( stream ) => { + stream.on( 'error', ( error ) => { + if ( error.code === 'EPIPE' ) { + process.exit( 1 ); + } + } ); + } ); + + /* + * Skip the rest of the failed target's tasks, but still run those of the other targets in the run. + */ + function rerun() { + runningTargets = runningTargets.filter( ( running ) => running !== currentTarget ); + currentTarget = null; + grunt.task.clearQueue(); + grunt.task.run( getTasks( runningTargets ).concat( nameArgs ) ); + } + + /* + * Unlike grunt's own handlers, which exit, these return to the failed task, which can then fail again on + * the result it did not get, as grunt-contrib-uglify does after "Uglification failed.". Only the first + * failure is reported, until the next task starts. + */ + function reportFailure( prefix, error ) { + const message = prefix + ( typeof error === 'string' ? error : error.message ); + + if ( failureReported ) { + grunt.verbose.writeln( message ); + return; + } + failureReported = true; + grunt.log.writeln( 'Fatal error: ' === prefix ? message.red : message.yellow ); + rerun(); + } + + grunt.warn = grunt.fail.warn = ( error ) => { + if ( grunt.option( 'force' ) ) { + grunt.log.writeln( ( 'Warning: ' + ( typeof error === 'string' ? error : error.message ) ).yellow ); + return; + } + reportFailure( 'Warning: ', error ); + }; + grunt.fatal = grunt.fail.fatal = ( error ) => { + reportFailure( 'Fatal error: ', error ); + }; + } + + /** + * Starts watching the files of the targets. + * + * @param {Function} onReady Called once the initial scan is complete. + */ + function startWatching( onReady ) { + // Compiled once, as every path found is checked against them. + const ignored = ( grunt.config( [ taskName, 'options', 'ignored' ] ) || [] ) + .flatMap( + // Match each ignored directory itself, so that it is not searched. + ( pattern ) => pattern.endsWith( '/**' ) ? [ pattern, pattern.slice( 0, -3 ) ] : [ pattern ] + ) + // Grunt's own minimatch, which it provides for its file matching. + .map( ( pattern ) => new grunt.file.minimatch.Minimatch( pattern, { dot: true } ) ); + const patterns = targetNames.flatMap( ( name ) => getTarget( name ).files ); + + watcher = chokidar.watch( getWatchPaths( patterns ), { + ignoreInitial: true, + ignored: ( filepath ) => { + const relative = path.relative( process.cwd(), filepath ).replace( /\\/g, '/' ); + return ignored.some( ( matcher ) => matcher.match( relative ) ); + }, + } ); + + watcher.on( 'all', recordChange ); + watcher.on( 'error', ( error ) => grunt.log.error( 'Watch error: ' + error.message ) ); + watcher.once( 'ready', onReady ); + } + + return function( target ) { + const config = grunt.config( this.name ) || {}; + + taskName = this.name; + nameArgs = this.nameArgs; + resume = this.async(); + failureReported = false; + + if ( watcher ) { + if ( startedAt ) { + grunt.log.writeln( ( 'Completed in ' + ( ( Date.now() - startedAt ) / 1000 ).toFixed( 3 ) + 's at ' + new Date().toString() ).cyan + ' - Waiting...' ); + startedAt = null; + } + + // Run the changes made while the tasks ran, now or once the debounce timer fires, before any restart. + if ( changes.size ) { + if ( ! timer ) { + runQueued(); + } + return; + } + + if ( restartPending ) { + restart(); + } + return; + } + + targetNames = target ? [ target ] : Object.keys( config ).filter( ( name ) => name !== 'options' ); + const missing = targetNames.filter( ( name ) => ! config[ name ] ); + if ( missing.length ) { + grunt.fail.warn( 'No "' + this.name + '" targets found: ' + missing.join( ', ' ) ); + return; + } + + keepWatchingOnFailure(); + startWatching( () => { + grunt.log.writeln( 'Waiting...' ); + takeOverChanges(); + } ); + }; +};