diff --git a/package.json b/package.json index 6ddd8b9fe2..16c21066d6 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "build:website": "nx build playground-website", "deploy": "gh-pages -d dist/docs -t true", "dev": "nx dev playground-website", + "dev:interactivity": "nx dev interactive-block-playground", "lint": "nx run-many --all --target=lint", "predeploy": "nx build docs-site", "prepublishOnly": "npm run build", diff --git a/packages/playground/interactive-block-playground/README.md b/packages/playground/interactive-block-playground/README.md new file mode 100644 index 0000000000..8518ff528a --- /dev/null +++ b/packages/playground/interactive-block-playground/README.md @@ -0,0 +1,5 @@ +### Interactive Block Playground + +Built with [WordPress Playground](https://github.com/WordPress/wordpress-playground). + +https://user-images.githubusercontent.com/5417266/233141638-b8143576-fb56-462d-9abb-fce117ba84ba.mov diff --git a/packages/playground/interactive-block-playground/build-zips.cjs b/packages/playground/interactive-block-playground/build-zips.cjs new file mode 100644 index 0000000000..6357131d7b --- /dev/null +++ b/packages/playground/interactive-block-playground/build-zips.cjs @@ -0,0 +1,48 @@ +const chokidar = require('chokidar'); +const archiver = require('archiver'); +const fs = require('fs'); + +const helloFolderPath = './hello'; +const outputZipPath = './zips/hello.zip'; + +// Function to zip the 'hello' folder and save it as 'hello.zip' +function zipHelloFolder() { + const output = fs.createWriteStream(outputZipPath); + const archive = archiver('zip', { + zlib: { level: 9 }, // Sets the compression level. + }); + + output.on('close', () => { + console.log(`hello.zip has been created.`); + }); + + archive.on('error', (err) => { + throw err; + }); + + archive.pipe(output); + + archive.directory(helloFolderPath, false); + + archive.finalize(); +} + +// Watch the 'hello' folder for changes +const watcher = chokidar.watch(helloFolderPath, { + persistent: true, + ignoreInitial: true, +}); + +watcher + .on('add', (path) => { + console.log(`File ${path} has been added.`); + zipHelloFolder(); + }) + .on('change', (path) => { + console.log(`File ${path} has been changed.`); + zipHelloFolder(); + }) + .on('unlink', (path) => { + console.log(`File ${path} has been removed.`); + zipHelloFolder(); + }); diff --git a/packages/playground/interactive-block-playground/hello/blocks/hello/block.json b/packages/playground/interactive-block-playground/hello/blocks/hello/block.json new file mode 100644 index 0000000000..01b9c78016 --- /dev/null +++ b/packages/playground/interactive-block-playground/hello/blocks/hello/block.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "hello/log-block", + "version": "0.1.0", + "title": "Hello - Log block", + "category": "text", + "icon": "heart", + "description": "", + "textdomain": "hello", + "supports": { + "interactivity": true + }, + "editorScript": "file:./index.js", + "render": "file:./render.php", + "viewScript": "file:./view.js", + "style": "file:./style.css" +} diff --git a/packages/playground/interactive-block-playground/hello/blocks/hello/index.asset.php b/packages/playground/interactive-block-playground/hello/blocks/hello/index.asset.php new file mode 100644 index 0000000000..c4a34c8173 --- /dev/null +++ b/packages/playground/interactive-block-playground/hello/blocks/hello/index.asset.php @@ -0,0 +1 @@ + array('wp-block-editor', 'wp-blocks', 'wp-element'), 'version' => '3cf3fb8a26c0120e24db'); diff --git a/packages/playground/interactive-block-playground/hello/blocks/hello/index.js b/packages/playground/interactive-block-playground/hello/blocks/hello/index.js new file mode 100644 index 0000000000..4ed8e11869 --- /dev/null +++ b/packages/playground/interactive-block-playground/hello/blocks/hello/index.js @@ -0,0 +1,16 @@ +const { registerBlockType } = wp.blocks; +const { useBlockProps } = wp.blockEditor; +const { createElement } = wp.element; + +const Edit = () => { + return createElement( + 'p', + useBlockProps(), + 'Hello World! (from the editor).' + ); +}; + +registerBlockType('hello/log-block', { + edit: Edit, + save: () => null, +}); diff --git a/packages/playground/interactive-block-playground/hello/blocks/hello/render.php b/packages/playground/interactive-block-playground/hello/blocks/hello/render.php new file mode 100644 index 0000000000..516089a565 --- /dev/null +++ b/packages/playground/interactive-block-playground/hello/blocks/hello/render.php @@ -0,0 +1,14 @@ + + +
+> + +
diff --git a/packages/playground/interactive-block-playground/hello/blocks/hello/style.css b/packages/playground/interactive-block-playground/hello/blocks/hello/style.css new file mode 100644 index 0000000000..b5b90fff0f --- /dev/null +++ b/packages/playground/interactive-block-playground/hello/blocks/hello/style.css @@ -0,0 +1,17 @@ +body > :not(.wp-site-blocks) { + display: none; +} + +.wp-site-blocks > :not(main) { + display: none; +} + +main > :not(.entry-content) { + display: none; +} + +.entry-content { + position: absolute; + top: 0; + left: 0; +} diff --git a/packages/playground/interactive-block-playground/hello/blocks/hello/view.asset.php b/packages/playground/interactive-block-playground/hello/blocks/hello/view.asset.php new file mode 100644 index 0000000000..9cb4f18936 --- /dev/null +++ b/packages/playground/interactive-block-playground/hello/blocks/hello/view.asset.php @@ -0,0 +1 @@ + array(), 'version' => '8ff3477eebdb6c7f40ea'); diff --git a/packages/playground/interactive-block-playground/hello/blocks/hello/view.js b/packages/playground/interactive-block-playground/hello/blocks/hello/view.js new file mode 100644 index 0000000000..659d5f09d2 --- /dev/null +++ b/packages/playground/interactive-block-playground/hello/blocks/hello/view.js @@ -0,0 +1,12 @@ +// Disclaimer: Importing the `store` using a global is just a temporary solution. +const { store } = window.__experimentalInteractivity; + +store({ + actions: { + hello: { + log: () => { + console.log('hello!'); + }, + }, + }, +}); diff --git a/packages/playground/interactive-block-playground/hello/hello.php b/packages/playground/interactive-block-playground/hello/hello.php new file mode 100644 index 0000000000..1046e0afc7 --- /dev/null +++ b/packages/playground/interactive-block-playground/hello/hello.php @@ -0,0 +1,61 @@ +query( $handle, 'registered' ); + if ( ! $script ) { + return false; + } + + if ( ! in_array( $dep, $script->deps, true ) ) { + $script->deps[] = $dep; + + if ( $in_footer ) { + // move script to the footer + $wp_scripts->add_data( $handle, 'group', 1 ); + } + } + + return true; +} + +function hello_auto_inject_interactivity_dependency() { + $registered_blocks = \WP_Block_Type_Registry::get_instance()->get_all_registered(); + + foreach ( $registered_blocks as $name => $block ) { + $has_interactivity_support = $block->supports['interactivity'] ?? false; + + if ( ! $has_interactivity_support ) { + continue; + } + foreach ( $block->view_script_handles as $handle ) { + add_hello_script_dependency( $handle, 'wp-directive-runtime', true ); + } + } +} \ No newline at end of file diff --git a/packages/playground/interactive-block-playground/index.css b/packages/playground/interactive-block-playground/index.css new file mode 100644 index 0000000000..3bb3ece520 --- /dev/null +++ b/packages/playground/interactive-block-playground/index.css @@ -0,0 +1,17 @@ +.editors { + display: grid; + grid-template-rows: 35px 2fr; + grid-auto-flow: column; + grid-gap: 20px; + justify-content: start; +} + +textarea { + width: 387px; + height: 240px; + font-family: monospace; +} + +.iframe-container { + position: relative; +} diff --git a/packages/playground/interactive-block-playground/index.html b/packages/playground/interactive-block-playground/index.html new file mode 100644 index 0000000000..40fd242b0a --- /dev/null +++ b/packages/playground/interactive-block-playground/index.html @@ -0,0 +1,28 @@ + + + + + + + Interactivity API - Interactive docs + + + +
+ +
+
+
+

render.php

+ +

view.js

+ +
+ + + diff --git a/packages/playground/interactive-block-playground/package.json b/packages/playground/interactive-block-playground/package.json new file mode 100644 index 0000000000..a2c9ca1a88 --- /dev/null +++ b/packages/playground/interactive-block-playground/package.json @@ -0,0 +1,12 @@ +{ + "name": "@wp-playground/interactive-block-playground", + "version": "1.0.0", + "description": "", + "scripts": { + "build-zips": "node build-zips.js" + }, + "author": "The WordPress Contributors", + "license": "GPL-2.0-or-later", + "type": "module", + "private": "true" +} diff --git a/packages/playground/interactive-block-playground/project.json b/packages/playground/interactive-block-playground/project.json new file mode 100644 index 0000000000..a39a6b2218 --- /dev/null +++ b/packages/playground/interactive-block-playground/project.json @@ -0,0 +1,62 @@ +{ + "name": "interactive-block-playground", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/playground/interactive-block-playground/src", + "projectType": "application", + "implicitDependencies": ["playground-remote"], + "targets": { + "build:standalone": { + "executor": "@nrwl/vite:build", + "outputs": ["{options.outputPath}"], + "defaultConfiguration": "production", + "options": { + "outputPath": "dist/packages/playground/interactive-block-playground" + }, + "configurations": { + "development": { + "mode": "development" + }, + "production": { + "mode": "production" + } + } + }, + "dev": { + "executor": "nx:run-commands", + "options": { + "commands": [ + "nx dev playground-remote --configuration=development-for-website", + "nx dev:standalone interactive-block-playground --hmr --output-style=stream-without-prefixes" + ], + "parallel": true, + "color": true + } + }, + "dev:standalone": { + "executor": "@nrwl/vite:dev-server", + "defaultConfiguration": "development", + "options": { + "buildTarget": "interactive-block-playground:build" + }, + "configurations": { + "development": { + "buildTarget": "interactive-block-playground:build:standalone:development", + "hmr": true + }, + "production": { + "buildTarget": "interactive-block-playground:build:standalone:production", + "hmr": false + } + } + }, + "typecheck": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + "yarn tsc -p packages/playground/interactive-block-playground/tsconfig.json --noEmit" + ] + } + } + }, + "tags": ["scope:web-client"] +} diff --git a/packages/playground/interactive-block-playground/src/config.ts b/packages/playground/interactive-block-playground/src/config.ts new file mode 100644 index 0000000000..c2abc7fde0 --- /dev/null +++ b/packages/playground/interactive-block-playground/src/config.ts @@ -0,0 +1 @@ +export { remotePlaygroundOrigin } from 'virtual:interactive-block-playground-config'; diff --git a/packages/playground/interactive-block-playground/src/index.ts b/packages/playground/interactive-block-playground/src/index.ts new file mode 100644 index 0000000000..37ed20dcf2 --- /dev/null +++ b/packages/playground/interactive-block-playground/src/index.ts @@ -0,0 +1,119 @@ +import { phpVars, startPlaygroundWeb } from '@wp-playground/client'; + +import { remotePlaygroundOrigin } from './config'; + +// Set the text content of the render.php element +document.getElementById('render.php')!.textContent = ` + +
> + +
+`; + +// Set the text content of the view.js +document.getElementById( + 'view.js' +)!.textContent = `// Disclaimer: Importing the "store" using a global is just a temporary solution. +const { store } = window.__experimentalInteractivity; + +store({ + actions: { + hello: { + log: () => { + console.log("hello!"); + }, + }, + }, +});`; + +(async () => { + const playground = document.querySelector( + '#playground' + ) as HTMLIFrameElement; + + const js = phpVars({ + title: 'Test post', + content: '', + }); + + const client = await startPlaygroundWeb({ + iframe: playground, + remoteUrl: `${remotePlaygroundOrigin}/remote.html`, + blueprint: { + landingPage: '/wp-admin', + preferredVersions: { + php: '8.0', + wp: 'latest', + }, + steps: [ + { + step: 'login', + username: 'admin', + password: 'password', + }, + { + step: 'installPlugin', + pluginZipFile: { + resource: 'wordpress.org/plugins', + slug: 'gutenberg', + }, + }, + { + step: 'installPlugin', + pluginZipFile: { + resource: 'url', + url: 'zips/hello.zip', + }, + }, + { + step: 'runPHP', + code: ` ${js.title}, + "post_content" => ${js.content}, + "post_status" => "publish", + ]); + file_put_contents('/post-id.txt', $post_id); + `, + }, + { + step: 'installPlugin', + pluginZipFile: { + resource: 'url', + url: '/plugin-proxy?repo=WordPress/block-interactivity-experiments&name=block-interactivity-experiments.zip', + }, + }, + ], + }, + }); + + const postId = await client.readFileAsText('/post-id.txt'); + await client.goTo(`/?p=${postId}`); + + document + .getElementById('render.php')! + .addEventListener('keyup', async (e) => { + client.writeFile( + '/wordpress/wp-content/plugins/hello/blocks/hello/render.php', + (e.target as HTMLTextAreaElement).value + ); + + await client.goTo(`/?p=${postId}`); + }); + + document.getElementById('view.js')!.addEventListener('keyup', async (e) => { + client.writeFile( + '/wordpress/wp-content/plugins/hello/blocks/hello/view.js', + (e.target as HTMLTextAreaElement).value + ); + + await client.goTo(`/?p=${postId}`); + }); +})(); diff --git a/packages/playground/interactive-block-playground/src/types.d.ts b/packages/playground/interactive-block-playground/src/types.d.ts new file mode 100644 index 0000000000..602b204f26 --- /dev/null +++ b/packages/playground/interactive-block-playground/src/types.d.ts @@ -0,0 +1,3 @@ +declare module 'virtual:interactive-block-playground-config' { + export const remotePlaygroundOrigin: string; +} diff --git a/packages/playground/interactive-block-playground/tsconfig.app.json b/packages/playground/interactive-block-playground/tsconfig.app.json new file mode 100644 index 0000000000..06d48f3ea3 --- /dev/null +++ b/packages/playground/interactive-block-playground/tsconfig.app.json @@ -0,0 +1,22 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": ["node", "vite/client"] + }, + "files": [ + "../../../node_modules/@nrwl/react/typings/cssmodule.d.ts", + "../../../node_modules/@nrwl/react/typings/image.d.ts" + ], + "exclude": [ + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.jsx", + "src/**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/packages/playground/interactive-block-playground/tsconfig.json b/packages/playground/interactive-block-playground/tsconfig.json new file mode 100644 index 0000000000..ae44604ae9 --- /dev/null +++ b/packages/playground/interactive-block-playground/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "types": ["vite/client", "vitest", "react"] + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json" +} diff --git a/packages/playground/interactive-block-playground/tsconfig.spec.json b/packages/playground/interactive-block-playground/tsconfig.spec.json new file mode 100644 index 0000000000..2b7ee41aec --- /dev/null +++ b/packages/playground/interactive-block-playground/tsconfig.spec.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node"] + }, + "include": [ + "vite.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ], + "files": [ + "../../../node_modules/@nrwl/react/typings/cssmodule.d.ts", + "../../../node_modules/@nrwl/react/typings/image.d.ts" + ] +} diff --git a/packages/playground/interactive-block-playground/vite.config.ts b/packages/playground/interactive-block-playground/vite.config.ts new file mode 100644 index 0000000000..a2c3cdac2a --- /dev/null +++ b/packages/playground/interactive-block-playground/vite.config.ts @@ -0,0 +1,79 @@ +/// +import { defineConfig } from 'vite'; +import viteTsConfigPaths from 'vite-tsconfig-paths'; +// eslint-disable-next-line @nrwl/nx/enforce-module-boundaries +import { + websiteDevServerHost, + websiteDevServerPort, + remoteDevServerHost, + remoteDevServerPort, +} from '../build-config'; +// eslint-disable-next-line @nrwl/nx/enforce-module-boundaries +import virtualModule from '../vite-virtual-module'; + +const proxy = { + '^/plugin-proxy.*repo=.*': { + target: 'https://playground.wordpress.net', + changeOrigin: true, + secure: true, + }, + '/plugin-proxy': { + target: 'https://downloads.wordpress.org', + changeOrigin: true, + secure: true, + rewrite: (path: string) => { + const url = new URL(path, 'http://example.com'); + if (url.searchParams.has('plugin')) { + return `/plugin/${url.searchParams.get('plugin')}`; + } else if (url.searchParams.has('theme')) { + return `/theme/${url.searchParams.get('theme')}`; + } + throw new Error('Invalid request'); + }, + }, +}; + +export default defineConfig(({ command }) => { + const playgroundOrigin = + command === 'build' + ? // In production, both the website and the playground are served from the same domain. + '' + : // In dev, the website and the playground are served from different domains. + `http://${remoteDevServerHost}:${remoteDevServerPort}`; + return { + cacheDir: + '../../../node_modules/.vite/packages-interactive-block-playground', + + preview: { + port: websiteDevServerPort, + host: websiteDevServerHost, + headers: { + 'Cross-Origin-Resource-Policy': 'cross-origin', + 'Cross-Origin-Embedder-Policy': 'credentialless', + }, + proxy, + }, + + server: { + port: websiteDevServerPort, + host: websiteDevServerHost, + headers: { + 'Cross-Origin-Resource-Policy': 'cross-origin', + 'Cross-Origin-Embedder-Policy': 'credentialless', + }, + proxy, + }, + + plugins: [ + viteTsConfigPaths({ + root: '../../../', + }), + virtualModule({ + name: 'interactive-block-playground-config', + content: `export const remotePlaygroundOrigin = ${JSON.stringify( + playgroundOrigin + )};`, + }), + ], + }; +}); diff --git a/packages/playground/interactive-block-playground/zips/block-interactivity-experiments.zip b/packages/playground/interactive-block-playground/zips/block-interactivity-experiments.zip new file mode 100644 index 0000000000..4853916d9e Binary files /dev/null and b/packages/playground/interactive-block-playground/zips/block-interactivity-experiments.zip differ diff --git a/packages/playground/interactive-block-playground/zips/hello.zip b/packages/playground/interactive-block-playground/zips/hello.zip new file mode 100644 index 0000000000..7c8dc6701d Binary files /dev/null and b/packages/playground/interactive-block-playground/zips/hello.zip differ diff --git a/packages/playground/website/public/plugin-proxy.php b/packages/playground/website/public/plugin-proxy.php index e61c43d807..2dacf0b4c8 100644 --- a/packages/playground/website/public/plugin-proxy.php +++ b/packages/playground/website/public/plugin-proxy.php @@ -112,6 +112,25 @@ public function streamFromGithubPR($organization, $repo, $pr, $workflow_name, $a } } + public function streamFromGithubReleases($repo, $name) + { + $zipUrl = "https://github.com/$repo/releases/latest/download/$name"; + try { + $this->streamHttpResponse($zipUrl, [ + 'content-length', + 'x-frame-options', + 'last-modified', + 'etag', + 'date', + 'age', + 'vary', + 'cache-Control' + ]); + } catch (ApiException $e) { + throw new ApiException("Plugin or theme '$name' not found"); + } + } + protected function gitHubRequest($url, $decode = true) { $headers[] = 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'; @@ -247,6 +266,15 @@ function ($curl, $body) use (&$extra_headers_sent, $default_headers) { $_GET['workflow'], $_GET['artifact'] ); + } else if (isset($_GET['repo']) && isset($_GET['name'])) { + + // Only allow downloads from the block-interactivity-experiments repo for now. + if ($_GET['repo'] !== 'WordPress/block-interactivity-experiments') { + throw new ApiException('Invalid repo. Only "WordPress/block-interactivity-experiments" is allowed.'); + } + + $downloader->streamFromGithubReleases($_GET['repo'], $_GET['name']); + } else { throw new ApiException('Invalid query parameters'); } @@ -256,4 +284,4 @@ function ($curl, $body) use (&$extra_headers_sent, $default_headers) { header('Content-Type: application/json'); } die(json_encode(['error' => $e->getMessage()])); -} \ No newline at end of file +}