From 4c1171f2ed62584211250df0af8302d34c04621a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82=C4=99biowski-Owczarek?= Date: Mon, 21 Nov 2022 23:23:39 +0100 Subject: [PATCH] Selector: Re-introduce selector-native.js Re-introduce the `selector-native` similar to the one on the `3.x-stable` branch. One difference is since the `main` branch inlined Sizzle, some selector utils can be shared between the main `selector` module and `selector-native`. The main `selector` module can be disabled in favor of `selector-native` via: grunt custom:-selector Other changes: * Tests: Fix Safari detection - Chrome Headless has a different user agent than Safari and a browser check in selector tests didn't take that into account. * Tests: Run selector-native tests in `npm test` * Selector: Fix querying on document fragments Ref gh-4395 Closes gh-5085 --- .github/workflows/node.js.yml | 4 ++ Gruntfile.js | 6 +-- README.md | 6 +++ build/tasks/build.js | 30 ++++++++---- package.json | 3 +- src/selector-native.js | 88 +++++++++++++++++++++++++++++++++++ test/data/testinit.js | 23 ++++----- test/unit/selector.js | 19 +++++++- test/unit/support.js | 9 +++- 9 files changed, 161 insertions(+), 27 deletions(-) create mode 100644 src/selector-native.js diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 4ea1e23cc..c2c02ce9f 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -25,6 +25,10 @@ jobs: NODE_VERSION: "16.x" NPM_SCRIPT: "test:no-deprecated" BROWSERS: "ChromeHeadless" + - NAME: "Browser tests: selector-native build, Chrome stable" + NODE_VERSION: "16.x" + NPM_SCRIPT: "test:selector-native" + BROWSERS: "ChromeHeadless" - NAME: "Browser tests: ES modules build, Chrome stable" NODE_VERSION: "16.x" NPM_SCRIPT: "test:esmodules" diff --git a/Gruntfile.js b/Gruntfile.js index 687b3215c..3a56cb4e4 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -61,8 +61,7 @@ module.exports = function( grunt ) { all: { dest: "dist/jquery.js", minimum: [ - "core", - "selector" + "core" ], // Exclude specified modules if the module matching the key is removed @@ -75,7 +74,8 @@ module.exports = function( grunt ) { remove: [ "ajax", "effects", "queue", "core/ready" ], include: [ "core/ready-no-deferred" ] }, - event: [ "deprecated/ajax-event-alias", "deprecated/event" ] + event: [ "deprecated/ajax-event-alias", "deprecated/event" ], + selector: [ "css/hiddenVisibleSelectors", "effects/animatedSelector" ] } } }, diff --git a/README.md b/README.md index 92092013d..4b4cb873f 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,12 @@ Some example modules that can be excluded are: - **exports/global**: Exclude the attachment of global jQuery variables ($ and jQuery) to the window. - **exports/amd**: Exclude the AMD definition. +As a special case, you may also replace the full jQuery `selector` module by using a special flag `grunt custom:-selector`. + +- **selector**: The full jQuery selector engine. When this module is excluded, it is replaced by a rudimentary selector engine based on the browser's `querySelectorAll` method that does not support jQuery selector extensions or enhanced semantics. See the [selector-native.js](https://github.com/jquery/jquery/blob/main/src/selector-native.js) file for details. + +*Note*: Excluding the full `selector` module will also exclude all jQuery selector extensions (such as `effects/animatedSelector` and `css/hiddenVisibleSelectors`). + The build process shows a message for each dependent module it excludes or includes. ##### AMD name diff --git a/build/tasks/build.js b/build/tasks/build.js index e344f991b..61ec670fb 100644 --- a/build/tasks/build.js +++ b/build/tasks/build.js @@ -128,11 +128,11 @@ module.exports = function( grunt ) { * Adds the specified module to the excluded or included list, depending on the flag * @param {String} flag A module path relative to * the src directory starting with + or - to indicate - * whether it should included or excluded + * whether it should be included or excluded */ const excluder = flag => { let additional; - const m = /^(\+|\-|)([\w\/-]+)$/.exec( flag ); + const m = /^(\+|-|)([\w\/-]+)$/.exec( flag ); const exclude = m[ 1 ] === "-"; const module = m[ 2 ]; @@ -150,10 +150,16 @@ module.exports = function( grunt ) { // These are the removable dependencies // It's fine if the directory is not there try { - excludeList( - fs.readdirSync( `${ srcFolder }/${ module }` ), - module - ); + + // `selector` is a special case as we don't just remove + // the module, but we replace it with `selector-native` + // which re-uses parts of the `src/selector` folder. + if ( module !== "selector" ) { + excludeList( + fs.readdirSync( `${ srcFolder }/${ module }` ), + module + ); + } } catch ( e ) { grunt.verbose.writeln( e ); } @@ -232,14 +238,14 @@ module.exports = function( grunt ) { // Remove the comma for anonymous defines setOverride( `${ srcFolder }/exports/amd.js`, read( "exports/amd.js" ) - .replace( /(\s*)"jquery"(\,\s*)/, + .replace( /(\s*)"jquery"(,\s*)/, amdName ? "$1\"" + amdName + "\"$2" : "" ) ); } grunt.verbose.writeflags( excluded, "Excluded" ); grunt.verbose.writeflags( included, "Included" ); - // Indicate a Slim build without listing all of the exclusions + // Indicate a Slim build without listing all the exclusions // to save space. if ( isPureSlim ) { version += " slim"; @@ -260,7 +266,13 @@ module.exports = function( grunt ) { // Replace excluded modules with empty sources. for ( const module of excluded ) { - setOverride( `${ srcFolder }/${ module }.js`, "" ); + setOverride( + `${ srcFolder }/${ module }.js`, + + // The `selector` module is not removed, but replaced + // with `selector-native`. + module === "selector" ? read( "selector-native.js" ) : "" + ); } } diff --git a/package.json b/package.json index 9be880bdd..84a375ac6 100644 --- a/package.json +++ b/package.json @@ -77,8 +77,9 @@ "test:esmodules": "grunt && grunt karma:esmodules", "test:amd": "grunt && grunt karma:amd", "test:no-deprecated": "grunt test:prepare && grunt custom:-deprecated && grunt karma:main", + "test:selector-native": "grunt test:prepare && grunt custom:-selector && grunt karma:main", "test:slim": "grunt test:prepare && grunt custom:slim && grunt karma:main", - "test": "npm run test:slim && npm run test:no-deprecated && grunt && grunt test:slow && grunt karma:main && grunt karma:esmodules && grunt karma:amd", + "test": "npm run test:slim && npm run test:no-deprecated && npm run test:selector-native && grunt && grunt test:slow && grunt karma:main && grunt karma:esmodules && grunt karma:amd", "jenkins": "npm run test:browserless" }, "commitplease": { diff --git a/src/selector-native.js b/src/selector-native.js new file mode 100644 index 000000000..0e2e48d81 --- /dev/null +++ b/src/selector-native.js @@ -0,0 +1,88 @@ +/* + * Optional limited selector module for custom builds. + * + * Note that this DOES NOT SUPPORT many documented jQuery + * features in exchange for its smaller size: + * + * * Attribute not equal selector (!=) + * * Positional selectors (:first; :eq(n); :odd; etc.) + * * Type selectors (:input; :checkbox; :button; etc.) + * * State-based selectors (:animated; :visible; :hidden; etc.) + * * :has(selector) + * * :not(complex selector) + * * custom selectors via jQuery extensions + * * Leading combinators (e.g., $collection.find("> *")) + * * Reliable functionality on XML fragments + * * Requiring all parts of a selector to match elements under context + * (e.g., $div.find("div > *") now matches children of $div) + * * Matching against non-elements + * * Reliable sorting of disconnected nodes + * * querySelectorAll bug fixes (e.g., unreliable :focus on WebKit) + * + * If any of these are unacceptable tradeoffs, either use the full + * selector engine or customize this stub for the project's specific + * needs. + */ + +import jQuery from "./core.js"; +import document from "./var/document.js"; +import documentElement from "./var/documentElement.js"; +import whitespace from "./var/whitespace.js"; + +// The following utils are attached directly to the jQuery object. +import "./selector/contains.js"; +import "./selector/escapeSelector.js"; +import "./selector/uniqueSort.js"; + +// Support: IE 9 - 11+ +// IE requires a prefix. +var matches = documentElement.matches || documentElement.msMatchesSelector; + +jQuery.extend( { + find: function( selector, context, results, seed ) { + var elem, nodeType, + i = 0; + + results = results || []; + context = context || document; + + // Same basic safeguard as in the full selector module + if ( !selector || typeof selector !== "string" ) { + return results; + } + + // Early return if context is not an element, document or document fragment + if ( ( nodeType = context.nodeType ) !== 1 && nodeType !== 9 && nodeType !== 11 ) { + return []; + } + + if ( seed ) { + while ( ( elem = seed[ i++ ] ) ) { + if ( jQuery.find.matchesSelector( elem, selector ) ) { + results.push( elem ); + } + } + } else { + jQuery.merge( results, context.querySelectorAll( selector ) ); + } + + return results; + }, + expr: { + attrHandle: {}, + match: { + bool: new RegExp( "^(?:checked|selected|async|autofocus|autoplay|controls|defer" + + "|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped)$", "i" ), + needsContext: new RegExp( "^" + whitespace + "*[>+~]" ) + } + } +} ); + +jQuery.extend( jQuery.find, { + matches: function( expr, elements ) { + return jQuery.find( expr, null, null, elements ); + }, + matchesSelector: function( elem, expr ) { + return matches.call( elem, expr ); + } +} ); diff --git a/test/data/testinit.js b/test/data/testinit.js index 650042b2a..6503b70a5 100644 --- a/test/data/testinit.js +++ b/test/data/testinit.js @@ -301,17 +301,6 @@ if ( !window.__karma__ ) { QUnit.isSwarm = ( QUnit.urlParams.swarmURL + "" ).indexOf( "http" ) === 0; QUnit.basicTests = ( QUnit.urlParams.module + "" ) === "basic"; -// Says whether jQuery positional selector extensions are supported. -// A full selector engine is required to support them as they need to be evaluated -// left-to-right. Remove that property when support for positional selectors is dropped. -QUnit.jQuerySelectorsPos = true; - -// Says whether jQuery selector extensions are supported. Change that to `false` -// if your custom jQuery versions relies more on native qSA. -// This doesn't include support for positional selectors (see above). -// TODO do we want to keep this or just assume support for jQuery extensions? -QUnit.jQuerySelectors = true; - // Support: IE 11+ // A variable to make it easier to skip specific tests in IE, mostly // testing integrations with newer Web features not supported by it. @@ -388,6 +377,18 @@ this.loadTests = function() { // Get testSubproject from testrunner first require( [ parentUrl + "test/data/testrunner.js" ], function() { + + // Says whether jQuery positional selector extensions are supported. + // A full selector engine is required to support them as they need to + // be evaluated left-to-right. Remove that property when support for + // positional selectors is dropped. + QUnit.jQuerySelectorsPos = includesModule( "selector" ); + + // Says whether jQuery selector extensions are supported. Change that + // to `false` if your custom jQuery versions relies more on native qSA. + // This doesn't include support for positional selectors (see above). + QUnit.jQuerySelectors = includesModule( "selector" ); + var i = 0, tests = [ // A special module with basic tests, meant for not fully diff --git a/test/unit/selector.js b/test/unit/selector.js index 47837d6ce..4f50412e7 100644 --- a/test/unit/selector.js +++ b/test/unit/selector.js @@ -1,7 +1,7 @@ QUnit.module( "selector", { beforeEach: function() { this.safari = /\bsafari\b/i.test( navigator.userAgent ) && - !/\bchrome\b/i.test( navigator.userAgent ); + !/\b(?:headless)?chrome\b/i.test( navigator.userAgent ); }, afterEach: moduleTeardown } ); @@ -1908,6 +1908,21 @@ QUnit.testUnlessIE( "jQuery.contains within