Skip to content

v2.14.0

Choose a tag to compare

@github-actions github-actions released this 14 Oct 20:02
· 101 commits to main since this release
Immutable release. Only release title and notes can be modified.
d02aa62

Added

  • min_framework_version and min_app_version manifest keys (#372) — min_host_version read like "minimum host application version" but was always compared against the cms-framework package version, so a plugin declaring "min_host_version": "1.0.0" meaning "Keystone 1.0" always passed. min_framework_version is the new name for that check. min_app_version is compared against the host application's version, which the host supplies through the new cms.plugins.appVersion config (env CMS_APP_VERSION), because the framework cannot detect it. Set it from the environment or from a service provider's register(), not with config() inside config/cms/plugins.php, where another file's key may not have loaded yet. Both checks run on activation and before an update reactivates a plugin. IncompatiblePluginException gains a constraint property (framework or app) and a forAppVersion() factory, and the activate and update endpoints' 409 plugin_incompatible payload gains constraint. When a check cannot run, because the version is unknown or not a comparable semver, activation still goes ahead, and the framework now fires ap.cmsFramework.plugin.versionCheckSkipped with (string $slug, array $details) alongside the logged warning, so a host can surface the skip.
  • Plugins can serve their federated bundle without an asset controller (#371) — a federated_module.entry that is a path inside a subdirectory of the plugin (e.g. dist/remoteEntry.js) publishes that directory on a new public route, GET /plugins/{slug}/assets/{path} (plugins.assets). The descriptor carries the absolute URL. Only active plugins are served. Files must resolve inside the directory, carry an allowed extension (JS, CSS, source maps, JSON, WASM, images, fonts) and not be dot-files. Files whose name carries a bundler content hash are cached immutable for a year. The entry and every other file revalidate with an ETag and Last-Modified, so a plugin update is picked up. Responses send nosniff, and SVGs are sandboxed and sent as attachments. A plugin publishes one asset directory. The route loads after every provider boots, so a plugin's own /plugins/{slug}/assets/{path} route still matches first. Hosts with a GET catch-all route must exclude the plugins/ prefix, or the catch-all answers first. Hosts that serve plugin assets themselves can turn the route off with cms.plugins.assetRoute (env CMS_PLUGINS_ASSET_ROUTE).
  • federated_module.boot_module (#371) — names an exposed module the host preloads before the first plugin page mounts, so a plugin can register its JS hooks first. It is bridged into the descriptor as bootModule, and registerFederatedModule() takes it as a new optional fourth argument, $bootModule.
  • TypeScript types for the incompatibility response and federated descriptors — PluginIncompatibleResponse and FederatedModuleDescriptor in resources/types/plugins.d.ts. index.d.ts now re-exports every plugins type, not only PluginResponse and UpdateType.

Changed

  • federated_module manifest validation is stricter (#371) — Breaking for manifests that 2.13 accepted. The rules run on install, on update, and when plugins:sync / syncFromDisk() refreshes an installed plugin. A refreshed manifest that fails is reported as failed and leaves the plugin's row unchanged, while the plugin keeps running. An update with such a manifest rolls back. Republish affected plugins with a compliant plugin.json:
    • entry must be an absolute https URL, a plain http URL to a loopback dev server (localhost, *.localhost, 127.0.0.0/8, [::1]), or a path inside a subdirectory of the plugin. Move a plugin-root entry into a directory such as dist/. Use https for remote hosts. Register a root-relative host URL (/build/remoteEntry.js) from PHP with registerFederatedModule() instead of the manifest. URLs with embedded credentials are refused.
    • exposes must be a list of module ids. Turn a { "./Page": "./src/Page.tsx" } map into ["./Page"].
    • name and boot_module must be non-empty strings. Unknown keys are logged and ignored.
  • registerFederatedModule() resolves relative and filesystem-path entries — a path relative to the plugin directory is served through plugins.assets, as above. An absolute filesystem path inside the plugin, the $this->pluginPath( 'dist/remoteEntry.js' ) pattern the hello-world example used, is now treated as that relative path instead of being handed to hosts as a "URL" that leaked the server's install path and never loaded. A path under public_path() becomes its root-relative URL. Any other filesystem path is logged and not registered. Absolute and root-relative URLs pass through as before.

Deprecated

  • min_host_version (#372) — a deprecated alias of min_framework_version, still enforced. A manifest may declare both, so hosts older than 2.14.0, which read only min_host_version, keep enforcing the minimum. When both are declared, their values must match. Plugin::$min_host_version is deprecated in favour of Plugin::$min_framework_version.

Fixed

  • A legacy update_url feed no longer reports "up to date" for a login page or a malformed response (#373) — a feed behind auth answered 302 to its login page. The check followed the redirect, read the 200 text/html as having no version, and cached "no update" for the whole TTL with nothing logged. A body that is not a JSON object now fails with invalidJsonResponse(), and a missing or blank version fails with missingRequiredField( 'version' ). Neither is cached, and the scheduled check reports both as failures. The feed is now requested with Accept: application/json. It follows up to five redirects, restricted to https (and http only when cms.updates.allow_insecure_transport is on), so release-download URLs and CDNs keep working and a redirect can't downgrade the transport. The feed URL in error messages has credentials redacted, and the version written to the plugin row is trimmed.
  • The hello-world example registers a servable federated entry — it passed pluginPath( 'dist/remoteEntry.js' ), a filesystem path, which overrode the manifest's servable entry. It now passes dist/remoteEntry.js.