diff --git a/tests/lib/feature-query.js b/tests/lib/feature-query.js index 3f49b14..09c6938 100644 --- a/tests/lib/feature-query.js +++ b/tests/lib/feature-query.js @@ -1,5 +1,5 @@ import test from 'ava'; -import { FeatureQuery } from '../../tools/lib/feature-query.js'; +import { FeatureQuery, FeatureQueryModern } from '../../tools/lib/feature-query.js'; test('most stable channel is preferred', (t) => { const featureQuery = new FeatureQuery({}); @@ -25,6 +25,28 @@ test('extension features are preferred', (t) => { ); }); +test('extension_types "all" is preferred like an explicit list', (t) => { + const featureQuery = new FeatureQuery({}); + + // Chrome's feature files may give extension_types as the string "all" rather than an array + // (see feature_compiler.py's `allow_all`). That still has to count as including "extension". + t.deepEqual( + featureQuery.mergeComplexFeature([ + { channel: 'stable', extension_types: ['hosted_app'] }, + { channel: 'stable', min_manifest_version: 3, extension_types: 'all' } + ]), + { channel: 'stable', min_manifest_version: 3, extension_types: 'all' } + ); +}); + +test('MV3+ filter keeps extension_types "all"', (t) => { + // This is the site that dropped runtime.lastError: extension_types: "all" has to pass the + // same as an explicit array containing "extension". + const featureQuery = new FeatureQueryModern({}); + + t.assert(featureQuery.filter({ channel: 'stable', extension_types: 'all' })); +}); + test('non-location specific features are preferred', (t) => { const featureQuery = new FeatureQuery({}); diff --git a/tools/lib/feature-query.js b/tools/lib/feature-query.js index e612112..89d358c 100644 --- a/tools/lib/feature-query.js +++ b/tools/lib/feature-query.js @@ -70,7 +70,7 @@ export class FeatureQuery { } // Filter by extension (prefer). - const extensionFilter = q.filter(({ extension_types }) => extension_types?.includes('extension')); + const extensionFilter = q.filter(({ extension_types }) => extensionTypesInclude(extension_types, 'extension')); if (extensionFilter.length === 1) { return extensionFilter[0]; } @@ -214,6 +214,46 @@ export class FeatureQuery { } +/** + * Checks whether an `extension_types` value includes the given type. Chrome's feature compiler + * allows this field to be the literal string "all" instead of a list, meaning every extension + * type is allowed, so a plain `includes` call would run as a substring test and misread "all" as + * excluding everything. + * + * @param {chromeTypes.MinArray|chromeTypes.All|undefined} extensionTypes + * @param {chromeTypes.ExtensionType} type + * @return {boolean} + */ +export function extensionTypesInclude(extensionTypes, type) { + return extensionTypes === 'all' || (extensionTypes?.includes(type) ?? false); +} + +/** + * A FeatureQuery that only allows APIs available in MV3+, used to render the main chrome-types + * bundle (everything except the `-a`/`--all` output, which keeps MV2 and Platform Apps too). + */ +export class FeatureQueryModern extends FeatureQuery { + + /** + * @param {chromeTypes.FeatureSpec} f + * @return {boolean} + */ + filter(f) { + // Don't show anything that maxes out before MV3. + if (f.max_manifest_version && f.max_manifest_version < 3) { + return false; + } + + // Remove non-extension APIs. + if (f.extension_types && !extensionTypesInclude(f.extension_types, 'extension')) { + return false; + } + + return super.filter(f); + } + +} + /** * @param {chromeTypes.FeatureSpec} f * @return {boolean} @@ -243,10 +283,10 @@ function basicFilter(f) { } // Only allow a limited set of extension types. - if (Array.isArray(f.extension_types)) { + if (f.extension_types !== undefined) { /** @type {chromeTypes.ExtensionType[]} */ const allowedTypes = ['extension', 'platform_app']; - const ok = allowedTypes.some((check) => f.extension_types?.includes(check)); + const ok = allowedTypes.some((check) => extensionTypesInclude(f.extension_types, check)); if (!ok) { return false; } diff --git a/tools/override.js b/tools/override.js index 99a6964..a587bf4 100644 --- a/tools/override.js +++ b/tools/override.js @@ -24,7 +24,7 @@ import * as chromeTypes from '../types/chrome.js'; import * as overrideTypes from '../types/override.js'; import { leastReleasedChannel, mostReleasedChannel } from './lib/channel.js'; import { buildNamespaceAwareMarkdownRewrite } from './lib/comment.js'; -import { FeatureQuery } from './lib/feature-query.js'; +import { FeatureQuery, extensionTypesInclude } from './lib/feature-query.js'; import { namespaceNameFromId, parentId } from './lib/traverse.js'; import { isDeepEqual } from './lib/equal.js'; @@ -182,7 +182,7 @@ export class RenderOverride extends EmptyRenderOverride { * @param {string} id */ isPlatformAppsOnly(id) { - return !this.#fq.checkFeature(id, (f) => !f.extension_types || [...f.extension_types].some((t) => t !== "platform_app")); + return !this.#fq.checkFeature(id, (f) => !f.extension_types || f.extension_types === 'all' || f.extension_types.some((t) => t !== "platform_app")); } /** @@ -546,7 +546,7 @@ export class RenderOverride extends EmptyRenderOverride { (f.platforms ?? []).forEach((platform) => platforms.add(platform)); - if (f.extension_types && !f.extension_types.includes('extension') && f.extension_types.includes('platform_app')) { + if (f.extension_types && !extensionTypesInclude(f.extension_types, 'extension') && extensionTypesInclude(f.extension_types, 'platform_app')) { isOnlyPlatformApps = true; } diff --git a/tools/render-tsd.js b/tools/render-tsd.js index fc00089..d9b8327 100755 --- a/tools/render-tsd.js +++ b/tools/render-tsd.js @@ -26,7 +26,7 @@ import * as chromeTypes from '../types/chrome.js'; import * as fs from 'fs'; import mri from 'mri'; import { RenderContext } from './lib/render-context.js'; -import { FeatureQuery } from './lib/feature-query.js'; +import { FeatureQuery, FeatureQueryModern } from './lib/feature-query.js'; import log from 'fancy-log'; import { RenderOverride } from './override.js'; @@ -148,27 +148,4 @@ function loadHistory(path) { } -class FeatureQueryModern extends FeatureQuery { - - /** - * @param {chromeTypes.FeatureSpec} f - * @return {boolean} - */ - filter(f) { - // Don't show anything that maxes out before MV3. - if (f.max_manifest_version && f.max_manifest_version < 3) { - return false; - } - - // Remove non-extension APIs. - if (f.extension_types && !f.extension_types.includes('extension')) { - return false; - } - - return super.filter(f); - } - -} - - await run(); diff --git a/types/chrome.d.ts b/types/chrome.d.ts index 6d1c3ba..59616cb 100644 --- a/types/chrome.d.ts +++ b/types/chrome.d.ts @@ -40,7 +40,7 @@ export type MinArray = T[]; export type Channel = 'stable' | 'beta' | 'dev' | 'canary' | 'trunk'; export type Platform = 'chromeos' | 'lacros' | 'linux' | 'mac' | 'win' | 'desktop_android'; export type Context = 'blessed_extension' | 'blessed_web_page' | 'content_script' | 'lock_screen_extension' | 'web_page' | 'webui' | 'webui_untrusted' | 'unblessed_extension'; -export type ExtensionType = 'extension' | 'hosted_app' | 'legacy_packaged_app' | 'platform_app' | 'shared_module' | 'theme' | 'login_screen_extension'; +export type ExtensionType = 'extension' | 'hosted_app' | 'legacy_packaged_app' | 'platform_app' | 'shared_module' | 'theme' | 'login_screen_extension' | 'chromeos_system_extension'; export type All = 'all'; export type SessionType = 'regular' | 'kiosk' | 'kiosk.autolaunched'; export type Location = 'component' | 'external_component' | 'policy' | 'unpacked';