and markers with real data. * So there is one rendering implementation and none of Gutenberg's JS/PHP drift. * --------------------------------------------------------------------------- */ if (!defined('ABSPATH')) { exit; } /* * Read from the plugin header rather than typed twice. * * It was a literal, and by 2026-09-02 it had drifted to `1.5.4-M6` while the header said `1.7.17-M6` — * nobody had touched it in twelve releases. That is not cosmetic: this constant is what the admin screen * shows as the version badge, so the plugin was telling buyers it was a version it was not, and it is * the cache-busting fallback for assets when a file mtime is unavailable, so on those hosts a buyer could * keep the previous release's JavaScript after updating. * * Both failures get much worse now that updates arrive on their own: "did the update actually apply?" is * answered by that badge. */ define('AUMFRAME_VERSION', (string) (get_file_data(__FILE__, ['Version' => 'Version'])['Version'] ?: '0')); define('AUMFRAME_FILE', __FILE__); define('AUMFRAME_DIR', plugin_dir_path(__FILE__)); define('AUMFRAME_URL', plugin_dir_url(__FILE__)); /** * The `?ver=` cache key for assets — the file's modification time, not the plugin version. * * The problem with a version number: it only changes at release. Rebuild assets/editor.js during development and * the URL does not change by a character, so the browser confidently keeps the old one — producing "I definitely * changed the code and the page does not react", with the real cause sitting in the HTTP cache where nobody * thinks to look. This has bitten this project more than once (most recently: motion.js gained an export while * the browser kept running the old build, reporting "stagger is not a function"). * * A timestamp only changes when the file actually does, so caching in production is as effective as a version * number while invalidating automatically during development. It falls back to the version if the stat fails — * one failed stat must not stop an asset being emitted. */ function aumframe_asset_ver(string $relative): string { $path = AUMFRAME_DIR . ltrim($relative, '/'); $mtime = @filemtime($path); return $mtime ? (string) $mtime : AUMFRAME_VERSION; } /** * The version for a whole ES module graph = the newest mtime in that graph. * * Why the entry point alone will not do. * * `wp_enqueue_script` only adds `?ver=` to the entry, which then passes that ver down to the modules it imports * (see `assets/runtime-boot.js`). That step has existed for a long time — but the version itself was **the * entry's own mtime**, while what actually changes daily are the submodules (`runtime.js`, `webgl.js`…). * * So: change runtime.js → the entry does not move → ver does not change → **the browser keeps serving the cached * old submodule**. The symptom is "I definitely fixed it, the file on the server is new, and the page still * behaves the old way" — and curl returns the new file (curl has no cache), which only deepens the confusion. * * Taking the newest mtime across the graph means any submodule change moves the version. * The directory scan costs a few milliseconds per request without an object cache, which is far cheaper than one * wrong diagnosis. */ function aumframe_module_ver(array $relatives): string { $newest = 0; foreach ($relatives as $rel) { $path = AUMFRAME_DIR . ltrim($rel, '/'); if (is_dir($path)) { $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS)); foreach ($it as $f) { if ($f->isFile() && substr($f->getFilename(), -3) === '.js') { $newest = max($newest, (int) $f->getMTime()); } } } else { $newest = max($newest, (int) @filemtime($path)); } } return $newest > 0 ? (string) $newest : AUMFRAME_VERSION; } /** * Is this request being taken over by Aumframe? * * For the theme's use: on a taken-over page the theme's header.php and template files never run, while * `wp_head` and `wp_footer` fire as usual — so all of the theme's front-end scripts still load while the DOM * they operate on does not exist. A taken-over page was measured carrying roughly 130KB of dead code (jQuery and * migrate alone are 99KB, and Aumframe does not use a line of jQuery). * * The test lives in the plugin and is called by the theme, rather than the plugin guessing which of the theme's * scripts to keep — only the theme knows what each of its scripts does. A plugin dequeuing somebody else's * assets will eventually take out something required, and the error will surface far from its cause. * * Note the timing: `Router::match()` depends on the main query, so the earliest it can be asked is after `wp`. * `wp_enqueue_scripts` comes after that, so it is safe. */ function aumframe_is_taking_over(): bool { if (!did_action('wp') || is_admin()) { return false; } return (bool) \Aumframe\Router::match(); } /* * There is deliberately no `load_plugin_textdomain()` call. * * There used to be one. A wordpress.org reviewer asked AumNexCart to remove exactly this, and having the * same objection raised twice is the worst kind of rejection — so it is gone here before submission too. * * Since WP 4.6 core loads a plugin's own `languages/` directory by itself, on first use of `__()`, using the * `Domain Path` header above. This plugin requires 6.2, so the call was pure duplication. * * Measured rather than inferred from the docs: on a site in another locale, with this plugin's own * bundled `.mo` present and the call commented out, `__()` still returned the translated string. The * bundled translation does not depend on it. That was worth checking, because the failure mode if it * were wrong is a translated site silently showing an English interface throughout, with nothing * reported to explain why. */ spl_autoload_register(function ($class) { if (strpos($class, 'Aumframe\\') !== 0) { return; } $relative = str_replace('\\', '/', substr($class, strlen('Aumframe\\'))); $file = AUMFRAME_DIR . 'src/' . $relative . '.php'; if (is_readable($file)) { require_once $file; } }); register_activation_hook(__FILE__, ['Aumframe\\Storage', 'install']); add_action('plugins_loaded', ['Aumframe\\Plugin', 'boot']); /* * The WPML / Polylang bridge is procedural, so the autoloader above does not reach it and it is * required explicitly. * * It must be required after `after_setup_theme`, not here. Plugins load before themes, so a check * made at plugin boot for an older copy of the same bridge in the active theme always answers "not * present"; the theme's functions.php then registers it as well, leaving two callbacks doing the * same work at the same priority. * * Priority 20 misses no call: all six filters fire during front-end rendering or on an admin * screen, long after `after_setup_theme`. * * The stand-aside check itself is at the top of that file, as a file-level `return`, next to the * reason for it. */ add_action( 'after_setup_theme', function () { require_once AUMFRAME_DIR . 'src/Integrations/wpml-bridge.php'; }, 20 ); /* * WP-CLI commands, so importing a kit can be scripted rather than only clicked through the admin. * Building many sites by hand gives no exit code to tell which one failed. * * Registered here rather than in `Plugin::boot()`: WP-CLI has to know the command exists before * `after_wp_load`, and `boot()` runs on `plugins_loaded`. */ if (defined('WP_CLI') && WP_CLI) { WP_CLI::add_command('aumframe import', ['Aumframe\\Cli', 'import']); WP_CLI::add_command('aumframe kit-requires', ['Aumframe\\Cli', 'kit_requires']); WP_CLI::add_command('aumframe doctor', ['Aumframe\\Cli', 'doctor']); }