diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d22383b48e89..ae9d3b412ea3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3554,7 +3554,42 @@ importers: specifier: 6.0.1 version: 6.0.1(webpack@5.105.2) - projects/packages/podcast: {} + projects/packages/podcast: + devDependencies: + '@automattic/jetpack-wp-build-polyfills': + specifier: workspace:* + version: link:../wp-build-polyfills + '@babel/core': + specifier: 7.29.0 + version: 7.29.0 + '@babel/runtime': + specifier: 7.29.2 + version: 7.29.2 + '@types/react': + specifier: 18.3.28 + version: 18.3.28 + '@wordpress/browserslist-config': + specifier: 6.44.0 + version: 6.44.0 + '@wordpress/build': + specifier: 0.13.0 + version: 0.13.0(@babel/core@7.29.0)(browserslist@4.28.2) + '@wordpress/element': + specifier: 6.44.0 + version: 6.44.0 + '@wordpress/i18n': + specifier: 6.17.0 + version: 6.17.0 + browserslist: + specifier: ^4.24.0 + version: 4.28.2 + optionalDependencies: + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) projects/packages/post-list: dependencies: @@ -24019,6 +24054,28 @@ snapshots: - browserslist - supports-color + '@wordpress/build@0.13.0(@babel/core@7.29.0)(browserslist@4.28.2)': + dependencies: + '@emotion/babel-plugin': 11.13.5 + autoprefixer: 10.4.27(postcss@8.5.10) + browserslist-to-esbuild: 2.1.1(browserslist@4.28.2) + change-case: 4.1.2 + chokidar: 4.0.3 + cssnano: 7.1.4(postcss@8.5.10) + esbuild: 0.27.4 + esbuild-plugin-babel: 0.2.3(@babel/core@7.29.0) + esbuild-sass-plugin: 3.3.1(esbuild@0.27.4)(sass-embedded@1.97.3) + fast-glob: 3.3.3 + moment-timezone: 0.5.48 + postcss: 8.5.10 + postcss-modules: 6.0.1(postcss@8.5.10) + rtlcss: 4.3.0 + sass-embedded: 1.97.3 + transitivePeerDependencies: + - '@babel/core' + - browserslist + - supports-color + '@wordpress/commands@1.44.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@wordpress/base-styles': 6.20.0 diff --git a/projects/packages/jetpack-mu-wpcom/changelog/add-podcast-wp-build-scaffold b/projects/packages/jetpack-mu-wpcom/changelog/add-podcast-wp-build-scaffold new file mode 100644 index 000000000000..34a43e15a4c8 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/changelog/add-podcast-wp-build-scaffold @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Podcast: initialize the jetpack-podcast package from jetpack-mu-wpcom (so Simple sites pick it up where load-jetpack.php doesn't run), and when the `jetpack_podcast_untangle` filter is on, register the new in-admin "Jetpack > Podcast" page in place of the legacy Calypso "Podcasting" link. Default behavior (filter off) is unchanged. diff --git a/projects/packages/jetpack-mu-wpcom/composer.json b/projects/packages/jetpack-mu-wpcom/composer.json index 832f38ddb5ca..6da6a23e1ef0 100644 --- a/projects/packages/jetpack-mu-wpcom/composer.json +++ b/projects/packages/jetpack-mu-wpcom/composer.json @@ -17,6 +17,7 @@ "automattic/jetpack-google-analytics": "@dev", "automattic/jetpack-masterbar": "@dev", "automattic/jetpack-newsletter": "@dev", + "automattic/jetpack-podcast": "@dev", "automattic/jetpack-redirect": "@dev", "automattic/jetpack-rtc": "@dev", "automattic/jetpack-stats-admin": "@dev", diff --git a/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php b/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php index e362aa5efbd2..ecfb0f00d10c 100644 --- a/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php +++ b/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php @@ -385,6 +385,12 @@ public static function load_wpcom_user_features() { // are registered on Simple sites (where load-jetpack.php doesn't run). \Automattic\Jetpack\Newsletter\Settings::init(); + // Initialize the Podcast package on Simple sites (where late_initialization + // in class.jetpack.php doesn't run). Gated by `jetpack_podcast_untangle` + // inside Podcast::init() so the legacy podcasting code keeps running + // until the flag flips. + \Automattic\Jetpack\Podcast\Podcast::init(); + // Only load the Masterbar features on WoA sites. if ( class_exists( '\Automattic\Jetpack\Status\Host' ) && ( new \Automattic\Jetpack\Status\Host() )->is_woa_site() ) { // This is temporary. After we cleanup Masterbar on WPCOM we should load Masterbar for Simple sites too. diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-admin-menu/wpcom-admin-menu.php b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-admin-menu/wpcom-admin-menu.php index f299e5fdf496..38aad8eab7da 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-admin-menu/wpcom-admin-menu.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-admin-menu/wpcom-admin-menu.php @@ -9,6 +9,7 @@ use Automattic\Jetpack\Connection\Manager as Connection_Manager; use Automattic\Jetpack\Newsletter\Settings as Newsletter_Settings; +use Automattic\Jetpack\Podcast\Admin_Page as Podcast_Admin_Page; use Automattic\Jetpack\Redirect; use Automattic\Jetpack\Subscribers_Dashboard\Dashboard as Subscribers_Dashboard; @@ -401,15 +402,21 @@ function () { $subscribers_dashboard->add_wp_admin_submenu(); } - // Jetpack > Podcasting - add_submenu_page( - 'jetpack', - __( 'Podcasting', 'jetpack-mu-wpcom' ), - __( 'Podcasting', 'jetpack-mu-wpcom' ), - 'manage_options', - 'https://wordpress.com/settings/podcasting/' . $domain, - null // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal -- Core should ideally document null for no-callback arg. https://core.trac.wordpress.org/ticket/52539. - ); + // When the `jetpack_podcast_untangle` filter is on, register the new + // "Jetpack > Podcast" in-admin page from the jetpack-podcast package. + // Otherwise keep the legacy "Jetpack > Podcasting" Calypso link unchanged. + if ( apply_filters( 'jetpack_podcast_untangle', false ) ) { + Podcast_Admin_Page::add_wp_admin_submenu(); + } else { + add_submenu_page( + 'jetpack', + __( 'Podcasting', 'jetpack-mu-wpcom' ), + __( 'Podcasting', 'jetpack-mu-wpcom' ), + 'manage_options', + 'https://wordpress.com/settings/podcasting/' . $domain, + null // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal -- Core should ideally document null for no-callback arg. https://core.trac.wordpress.org/ticket/52539. + ); + } if ( $is_simple_site ) { // Jetpack > Newsletter. @@ -459,6 +466,7 @@ function () { 'search', 'subscribers', 'newsletter', + 'jetpack-podcast', 'podcasting', 'traffic', 'jetpack#/settings', diff --git a/projects/packages/podcast/.gitattributes b/projects/packages/podcast/.gitattributes index b3028ba45f00..1b9fa596c937 100644 --- a/projects/packages/podcast/.gitattributes +++ b/projects/packages/podcast/.gitattributes @@ -3,6 +3,10 @@ .github/ export-ignore package.json export-ignore +# Files to include in the mirror repo, but excluded via gitignore +# Remember to end all directories with `/**` to properly tag every file. +/build/** production-include + # Files to exclude from the mirror repo, but included in the monorepo. # Remember to end all directories with `/**` to properly tag every file. .gitignore production-exclude @@ -10,3 +14,4 @@ changelog/** production-exclude .phpcs.dir.xml production-exclude tests/** production-exclude .phpcsignore production-exclude +routes/**/*.tsx production-exclude diff --git a/projects/packages/podcast/.gitignore b/projects/packages/podcast/.gitignore index 8424444d7508..cf368200ac9d 100644 --- a/projects/packages/podcast/.gitignore +++ b/projects/packages/podcast/.gitignore @@ -1,4 +1,5 @@ vendor/ node_modules/ .cache/ +build/ composer.lock diff --git a/projects/packages/podcast/.phpcsignore b/projects/packages/podcast/.phpcsignore new file mode 100644 index 000000000000..567609b1234a --- /dev/null +++ b/projects/packages/podcast/.phpcsignore @@ -0,0 +1 @@ +build/ diff --git a/projects/packages/podcast/changelog/add-wp-build-scaffold b/projects/packages/podcast/changelog/add-wp-build-scaffold new file mode 100644 index 000000000000..f46a7cb45596 --- /dev/null +++ b/projects/packages/podcast/changelog/add-wp-build-scaffold @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Add an empty wp-build dashboard scaffold and the "Jetpack > Podcast" wp-admin entry, gated behind the `jetpack_podcast_untangle` filter. With the filter off (default), nothing changes; with it on, a placeholder Podcast page renders inside the standard wp-admin chrome. diff --git a/projects/packages/podcast/composer.json b/projects/packages/podcast/composer.json index 9b758835a39a..fdeb581799fc 100644 --- a/projects/packages/podcast/composer.json +++ b/projects/packages/podcast/composer.json @@ -5,9 +5,8 @@ "license": "GPL-2.0-or-later", "require": { "php": ">=7.2", - "automattic/jetpack-autoloader": "@dev", - "automattic/jetpack-composer-plugin": "@dev", - "automattic/jetpack-status": "@dev" + "automattic/jetpack-status": "@dev", + "automattic/jetpack-wp-build-polyfills": "@dev" }, "require-dev": { "yoast/phpunit-polyfills": "^4.0.0", @@ -23,6 +22,12 @@ ] }, "scripts": { + "build-development": "pnpm run build", + "build-production": "pnpm run build-production", + "watch": [ + "Composer\\Config::disableProcessTimeout", + "pnpm run watch" + ], "phpunit": [ "phpunit-select-config phpunit.#.xml.dist --colors=always" ], @@ -58,8 +63,6 @@ }, "config": { "allow-plugins": { - "automattic/jetpack-autoloader": true, - "automattic/jetpack-composer-plugin": true, "roots/wordpress-core-installer": true } } diff --git a/projects/packages/podcast/package.json b/projects/packages/podcast/package.json index 51bdce16ea03..fd66f8c84edc 100644 --- a/projects/packages/podcast/package.json +++ b/projects/packages/podcast/package.json @@ -13,5 +13,41 @@ "directory": "projects/packages/podcast" }, "license": "GPL-2.0-or-later", - "author": "Automattic" + "author": "Automattic", + "type": "module", + "scripts": { + "build": "pnpm run clean && pnpm run build:wp-build && pnpm run build:boot-asset", + "build:boot-asset": "provide-boot-asset-file", + "build:wp-build": "wp-build", + "build-production": "pnpm run clean && pnpm run build:wp-build && pnpm run build:boot-asset", + "clean": "rm -rf build/", + "watch": "wp-build --watch" + }, + "browserslist": [ + "extends @wordpress/browserslist-config" + ], + "devDependencies": { + "@automattic/jetpack-wp-build-polyfills": "workspace:*", + "@babel/core": "7.29.0", + "@babel/runtime": "7.29.2", + "@types/react": "18.3.28", + "@wordpress/browserslist-config": "6.44.0", + "@wordpress/build": "0.13.0", + "@wordpress/element": "6.44.0", + "@wordpress/i18n": "6.17.0", + "browserslist": "^4.24.0" + }, + "optionalDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "wpPlugin": { + "name": "jetpack_podcast", + "scriptGlobal": "jetpackPodcast", + "packageNamespace": "jetpack-podcast", + "handlePrefix": "jetpack-podcast", + "pages": [ + "jetpack-podcast-dashboard" + ] + } } diff --git a/projects/packages/podcast/routes/dashboard/package.json b/projects/packages/podcast/routes/dashboard/package.json new file mode 100644 index 000000000000..7dab7404e236 --- /dev/null +++ b/projects/packages/podcast/routes/dashboard/package.json @@ -0,0 +1,14 @@ +{ + "name": "_@jetpack-podcast/dashboard-route", + "version": "1.0.0", + "private": true, + "dependencies": { + "@types/react": "18.3.28", + "@wordpress/element": "6.44.0", + "@wordpress/i18n": "6.17.0" + }, + "route": { + "path": "/", + "page": "jetpack-podcast-dashboard" + } +} diff --git a/projects/packages/podcast/routes/dashboard/route.tsx b/projects/packages/podcast/routes/dashboard/route.tsx new file mode 100644 index 000000000000..91499ada4b32 --- /dev/null +++ b/projects/packages/podcast/routes/dashboard/route.tsx @@ -0,0 +1 @@ +export const route = {}; diff --git a/projects/packages/podcast/routes/dashboard/stage.tsx b/projects/packages/podcast/routes/dashboard/stage.tsx new file mode 100644 index 000000000000..4cee550f9434 --- /dev/null +++ b/projects/packages/podcast/routes/dashboard/stage.tsx @@ -0,0 +1,6 @@ +const Stage = () => { + // "Podcast" is a product name, do not translate. + return

Podcast

; +}; + +export { Stage as stage }; diff --git a/projects/packages/podcast/src/class-admin-page.php b/projects/packages/podcast/src/class-admin-page.php new file mode 100644 index 000000000000..4aa92d268597 --- /dev/null +++ b/projects/packages/podcast/src/class-admin-page.php @@ -0,0 +1,272 @@ + Podcast" wp-admin screen on Simple and Atomic when the + * `jetpack_podcast_untangle` filter is enabled. Until that filter flips, every + * entry point here is a no-op so the legacy podcasting experience keeps + * running unchanged. + * + * Menu registration is owned by `wpcom-admin-menu.php` (in the + * `jetpack-mu-wpcom` package), which calls `add_wp_admin_submenu()` at + * `admin_menu` priority 999999 — late enough that the Jetpack parent menu + * already exists. wpcom-admin-menu runs on both Simple and Atomic, so a single + * registration path covers both. Standalone Jetpack is excluded by the host + * gate in `Podcast::init()`. + * + * The wp-build chassis is loaded inline from `init()` and routed onto our + * user-facing slug via `bridge_wp_build_enqueue()` — mirroring + * `Automattic\Jetpack\Scan_Page\Jetpack_Scan`. + */ +class Admin_Page { + + /** + * URL-facing menu slug. + * + * @var string + */ + const ADMIN_PAGE_SLUG = 'jetpack-podcast'; + + /** + * Internal slug emitted by `@wordpress/build` (`wpPlugin.pages[0]` + * plus the `-wp-admin` suffix the build template appends). Used to + * find the auto-generated render / enqueue functions. + * + * @var string + */ + const WP_BUILD_SLUG = 'jetpack-podcast-dashboard-wp-admin'; + + /** + * Whether the class has already wired its admin hooks. + * + * @var bool + */ + private static $initialized = false; + + /** + * Wire the admin hooks. Called from `Podcast::init()` once the + * `jetpack_podcast_untangle` filter and host gates have been satisfied. + * + * Menu registration itself is handled by `wpcom-admin-menu.php` calling + * `add_wp_admin_submenu()` at `admin_menu` priority 999999. Here we set + * up the wp-build chassis at plugins_loaded time so: + * - `WP_Build_Polyfills::register()` registers BEFORE `wp_default_scripts` + * fires (otherwise `@wordpress/boot` never lands in the import map). + * - The wp-build render function is defined before the menu callback runs. + */ + public static function init() { + if ( self::$initialized ) { + return; + } + self::$initialized = true; + + self::load_wp_build(); + self::bridge_wp_build_enqueue(); + self::fix_boot_import_map_ordering(); + } + + /** + * Register the Podcast submenu under Jetpack on Simple and Atomic. + * + * Called from `wpcom-admin-menu.php` at priority 999999 once the Jetpack + * parent menu exists. Bails when the untangle filter is off so the legacy + * "Podcasting" Calypso link in `wpcom-admin-menu.php` keeps rendering. + */ + public static function add_wp_admin_submenu() { + if ( ! self::is_enabled() ) { + return; + } + + $page_suffix = add_submenu_page( + 'jetpack', + /** "Podcast" is a product name, do not translate. */ + 'Podcast', + 'Podcast', + 'manage_options', + self::ADMIN_PAGE_SLUG, + self::get_render_callback() + ); + + if ( $page_suffix ) { + add_action( 'load-' . $page_suffix, array( __CLASS__, 'admin_init' ) ); + } + } + + /** + * Wire admin-init actions once we know the Podcast page is loading. + * + * Subsequent PRs in the untangle train layer script-data + Tracks here. + * The wp-build dashboard manages its own enqueue pipeline (bridged via + * `bridge_wp_build_enqueue()`). + */ + public static function admin_init() { + // Intentionally empty for now. + } + + /** + * Bridge wp-build's auto-generated enqueue function — which checks for + * `?page=jetpack-podcast-dashboard-wp-admin` — to our user-facing slug + * `?page=jetpack-podcast`. Hooked at priority 9 so the wp-build copy + * (registered at priority 10) sees the original `$_GET['page']` and skips + * its own enqueue. + * + * Mirrors `Automattic\Jetpack\Scan_Page\Jetpack_Scan::bridge_wp_build_enqueue`. + */ + private static function bridge_wp_build_enqueue() { + add_action( + 'admin_enqueue_scripts', + static function ( $hook_suffix ) { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( ! isset( $_GET['page'] ) || self::ADMIN_PAGE_SLUG !== $_GET['page'] ) { + return; + } + + $enqueue_fn = 'jetpack_podcast_jetpack_podcast_dashboard_wp_admin_enqueue_scripts'; + if ( ! function_exists( $enqueue_fn ) ) { + return; + } + + // phpcs:disable WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $original = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : null; + $_GET['page'] = self::WP_BUILD_SLUG; + // @phan-suppress-next-line PhanUndeclaredFunctionInCallable -- Function is generated by @wordpress/build into build/pages/jetpack-podcast-dashboard/page-wp-admin.php, which is outside Phan's analysis scope. The function_exists() guard above protects the call at runtime. + call_user_func( $enqueue_fn, $hook_suffix ); + if ( null === $original ) { + unset( $_GET['page'] ); + } else { + $_GET['page'] = $original; + } + // phpcs:enable WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + }, + 9 + ); + } + + /** + * Fix import map ordering for the wp-build boot script. + * + * In wp-admin, `_wp_footer_scripts` (classic scripts) and + * `print_import_map` both hook into `admin_print_footer_scripts` at + * priority 10, but `_wp_footer_scripts` is registered first. This causes + * the inline `import("@wordpress/boot")` to execute before the import + * map exists. + * + * This fix moves the `import()` call from the classic inline script to a + * `