ES module src (production), read by enqueue(). * * @var array */ private static $module_src = []; /** * True when the Vite dev server is running. * * @return bool */ public static function is_dev(): bool { return file_exists( EPICFORMS_PATH . 'dist/.hot' ) && '' !== self::origin(); } /** * The dev server origin, e.g. https://localhost:3000. * * @return string */ public static function origin(): string { $origin = trim( (string) file_get_contents( EPICFORMS_PATH . 'dist/.hot' ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local file. // Only a bare scheme://host[:port] is meaningful here. return preg_match( '#^https?://[a-z0-9.-]+(:\d+)?$#i', $origin ) ? $origin : ''; } /** * Register a frontend entry under its classic handles. * * Prod: registers the dist JS/CSS files exactly as before. Dev: registers * the same handles as stubs (no src) so existing dependency references * (e.g. Elementor's get_script_depends()) keep resolving while the actual * code loads from the dev server as ES modules. * * @param string $handle Script handle, e.g. 'epicforms-admin-client'. * @param string $css_handle Style handle, e.g. 'epicforms-admin-css'. * @param string $dist_js Plugin-relative dist JS, e.g. 'dist/admin.js'. * @param string $dist_css Plugin-relative dist CSS, e.g. 'dist/admin.css'. * @param array $deps Classic script dependencies. */ public static function register( string $handle, string $css_handle, string $dist_js, string $dist_css, array $deps = [] ): void { wp_deregister_script( $handle ); wp_deregister_style( $css_handle ); // The classic handle is a *stub* (no src): it exists so its $deps (wp-element, // wp-i18n, …) are enqueued — providing the window.wp.* globals the ESM bundle's // build-time shims read — and so dependents like Elementor's get_script_depends() // keep resolving. The actual code is an ES module, enqueued in enqueue(). wp_register_script( $handle, false, $deps, self::is_dev() ? null : EPICFORMS_VERSION, true ); if ( self::is_dev() ) { wp_register_style( $css_handle, false, [], null ); return; } wp_register_style( $css_handle, EPICFORMS_URL . $dist_css, [], EPICFORMS_VERSION ); self::$module_src[ $handle ] = EPICFORMS_URL . $dist_js; } /** * Enqueue a registered entry, adding the dev-server modules in dev mode. * * @param string $handle Script handle registered via register(). * @param string $css_handle Style handle registered via register(). * @param string $entry Source entry, e.g. 'src/admin/index.jsx'. */ public static function enqueue( string $handle, string $css_handle, string $entry ): void { wp_enqueue_script( $handle ); // classic stub -> enqueues $deps (wp-element, …) first. wp_enqueue_style( $css_handle ); if ( self::is_dev() ) { $origin = self::origin(); self::print_react_refresh_preamble( $origin ); wp_enqueue_script_module( 'epicforms-vite-client', $origin . '/@vite/client', [], null ); wp_enqueue_script_module( 'epicforms-dev-' . $handle, $origin . '/' . $entry, [ 'epicforms-vite-client' ], null ); return; } // Production: the built code is an ES module. It has no bare imports (shared deps // are shim-aliased to window.wp.* at build time), so no import map is needed. The // classic stub above (non-deferred) runs first and provides window.wp.*; this // deferred module runs after and its shims find those globals. if ( isset( self::$module_src[ $handle ] ) ) { wp_enqueue_script_module( 'epicforms-mod-' . $handle, self::$module_src[ $handle ], [], EPICFORMS_VERSION ); } } /** * Print the @vitejs/plugin-react refresh preamble once per request. * * Must execute before the entry module. Prefers the head; if the head has * already been printed (e.g. Elementor widgets enqueue during content * rendering), falls back to the footer at priority 1 — WordPress prints * enqueued script modules on the footer hook at priority 10, so the * preamble still comes first. * * @param string $origin Dev server origin. */ private static function print_react_refresh_preamble( string $origin ): void { if ( self::$preamble_printed ) { return; } self::$preamble_printed = true; $print = function () use ( $origin ) { // JSON-encoded, not URL-escaped: the origin lands in a JS string. $runtime = wp_json_encode( $origin . '/@react-refresh', JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ); wp_print_inline_script_tag( 'import RefreshRuntime from ' . $runtime . ';' . 'RefreshRuntime.injectIntoGlobalHook(window);' . 'window.$RefreshReg$ = () => {};' . 'window.$RefreshSig$ = () => (type) => type;' . 'window.__vite_plugin_react_preamble_installed__ = true;', [ 'type' => 'module' ] ); }; if ( is_admin() ) { $hook = did_action( 'admin_head' ) ? 'admin_print_footer_scripts' : 'admin_head'; } else { $hook = did_action( 'wp_head' ) ? 'wp_footer' : 'wp_head'; } add_action( $hook, $print, 1 ); } }