diff --git a/packages/docs/site/docs/main/resources.md b/packages/docs/site/docs/main/resources.md index 3c1d09b58e..63f07a8e81 100644 --- a/packages/docs/site/docs/main/resources.md +++ b/packages/docs/site/docs/main/resources.md @@ -22,70 +22,70 @@ There's a set of redirections in place to make it easier the access to some of t ## Frequently sought links -- [Demo](https://playground.wordpress.net/) -- [GitHub Repository](https://github.com/WordPress/wordpress-playground) -- [Documentation](https://wordpress.github.io/wordpress-playground/) -- [Playground tools Repository](https://github.com/WordPress/playground-tools) +- [Demo](https://playground.wordpress.net/) +- [GitHub Repository](https://github.com/WordPress/wordpress-playground) +- [Documentation](https://wordpress.github.io/wordpress-playground/) +- [Playground tools Repository](https://github.com/WordPress/playground-tools) ## Apps built with WordPress Playground -- [Official demo](https://playground.wordpress.net/) and the [showcase](https://developer.wordpress.org/playground) app – install a theme, try out a plugin, create a few pages, export what you've built -- [@wp-playground/cli](https://www.npmjs.com/package/@wp-playground/cli) – a CLI tool for instant WordPress dev environment -- [WordPress Playground for VS Code](https://marketplace.visualstudio.com/items?itemName=WordPressPlayground.wordpress-playground) -- Live Translations: [App](https://translate.wordpress.org/projects/wp-plugins/friends/dev/pl/default/playground/), [announcement](https://make.wordpress.org/polyglots/2023/04/19/wp-translation-playground/), [more details](https://make.wordpress.org/polyglots/2023/05/08/translate-live-updates-to-the-translation-playground/) -- [Interactive code block](https://wordpress.org/plugins/interactive-code-block/) which powers the [HTML Tag Processor tutorial](https://adamadam.blog/2023/02/16/how-to-modify-html-in-a-php-wordpress-plugin-using-the-new-tag-processor-api/) and the [Playground JS API tutorial](https://wordpress.github.io/wordpress-playground/developers/apis/javascript-api/) -- [Gutenberg Pull Request previewer](https://playground.wordpress.net/gutenberg.html) -- [Notifications plugin live demo](https://johnhooks.io/playground-experiment/) -- [GraphQL REPL](https://www.wpgraphql.com/2023/06/15/announcing-the-wpgraphql-repl) -- [Blocknotes](https://twitter.com/adamzielin/status/1669478239771799552) – the first ever iOS app running WordPress on your phone -- [Playground embedder](https://joost.blog/embedded-playground/) to embed Playground examples in WordPress.org documentation using shortcodes -- [Plugin demos on wp.org](https://gist.github.com/adamziel/0fe3ffc1fb5202a907a87d055ee37135) – a user script that adds a "demo" tab to plugin pages on WordPress.org -- [WordPress Pull Request previewer](https://playground.wordpress.net/wordpress.html) -- [Synchronization between two Playgrounds](https://playground.wordpress.net/demos/sync.html) -- [Time Travel](https://playground.wordpress.net/demos/time-traveling.html) -- [WP-CLI](https://playground.wordpress.net/demos/wp-cli.html) -- [PHP implementation of Blueprints](https://playground.wordpress.net/demos/php-blueprints.html) +- [Official demo](https://playground.wordpress.net/) and the [showcase](https://developer.wordpress.org/playground) app – install a theme, try out a plugin, create a few pages, export what you've built +- [@wp-playground/cli](https://www.npmjs.com/package/@wp-playground/cli) – a CLI tool for instant WordPress dev environment +- [WordPress Playground for VS Code](https://marketplace.visualstudio.com/items?itemName=WordPressPlayground.wordpress-playground) +- Live Translations: [App](https://translate.wordpress.org/projects/wp-plugins/friends/dev/pl/default/playground/), [announcement](https://make.wordpress.org/polyglots/2023/04/19/wp-translation-playground/), [more details](https://make.wordpress.org/polyglots/2023/05/08/translate-live-updates-to-the-translation-playground/) +- [Interactive code block](https://wordpress.org/plugins/interactive-code-block/) which powers the [HTML Tag Processor tutorial](https://adamadam.blog/2023/02/16/how-to-modify-html-in-a-php-wordpress-plugin-using-the-new-tag-processor-api/) and the [Playground JS API tutorial](https://wordpress.github.io/wordpress-playground/developers/apis/javascript-api/) +- [Gutenberg Pull Request previewer](https://playground.wordpress.net/gutenberg.html) +- [Notifications plugin live demo](https://johnhooks.io/playground-experiment/) +- [GraphQL REPL](https://www.wpgraphql.com/2023/06/15/announcing-the-wpgraphql-repl) +- [Blocknotes](https://twitter.com/adamzielin/status/1669478239771799552) – the first ever iOS app running WordPress on your phone +- [Playground embedder](https://joost.blog/embedded-playground/) to embed Playground examples in WordPress.org documentation using shortcodes +- [Plugin demos on wp.org](https://gist.github.com/adamziel/0fe3ffc1fb5202a907a87d055ee37135) – a user script that adds a "demo" tab to plugin pages on WordPress.org +- [WordPress Pull Request previewer](https://playground.wordpress.net/wordpress.html) +- [Synchronization between two Playgrounds](https://playground.wordpress.net/demos/sync.html) +- [Time Travel](https://playground.wordpress.net/demos/time-traveling.html) +- [WP-CLI](https://playground.wordpress.net/demos/wp-cli.html) +- [PHP implementation of Blueprints](https://playground.wordpress.net/demos/php-blueprints.html) ## Reading materials -- [Build in-browser WordPress experiences with WordPress Playground and WebAssembly](https://web.dev/wordpress-playground/) -- [WordPress Playground on developer.wordpress.org](https://developer.wordpress.org/playground) -- [In-Browser WordPress Tech Demos: WordPress Development with WordPress Playground](https://make.wordpress.org/core/2023/04/13/in-browser-wordpress-tech-demos-wordpress-development-with-wordpress-playground/) -- [Initial announcement on make.wordpress.org](https://make.wordpress.org/core/2022/09/23/client-side-webassembly-wordpress-with-no-server/) -- [Hackernews discussion](https://news.ycombinator.com/item?id=32960560) +- [Build in-browser WordPress experiences with WordPress Playground and WebAssembly](https://web.dev/wordpress-playground/) +- [WordPress Playground on developer.wordpress.org](https://developer.wordpress.org/playground) +- [In-Browser WordPress Tech Demos: WordPress Development with WordPress Playground](https://make.wordpress.org/core/2023/04/13/in-browser-wordpress-tech-demos-wordpress-development-with-wordpress-playground/) +- [Initial announcement on make.wordpress.org](https://make.wordpress.org/core/2022/09/23/client-side-webassembly-wordpress-with-no-server/) +- [Hackernews discussion](https://news.ycombinator.com/item?id=32960560) ## Videos -- Developer Hours Videos: - - [Americas Region (May 23,2023)](https://wordpress.tv/2023/05/23/developer-hours-wordpress-playground-americas/) - - [APAC/EMEA Region (May 24,2023)](https://wordpress.tv/2023/05/24/developer-hours-wordpress-playground-apac-emea/) - - [Creating WordPress Playground Blueprints for Testing and Demos (May 28, 2024)](https://wordpress.tv/2024/05/28/developer-hours-creating-wordpress-playground-blueprints-for-testing-and-demos/) by Birgit Pauli-Haack & Nick Diego - - [Developer Hours: Everything you need to know about WordPress Playground (Dec 17, 2024)](https://wordpress.tv/2024/12/17/developer-hours-everything-you-need-to-know-about-wordpress-playground/) by Nick Diego & Ryan Welcher -- [Playground at State of the Word](https://youtu.be/VeigCZuxnfY?t=2912) -- [Playground at WCEU 2023](https://www.youtube.com/watch?v=e-CwouzTGp4&t=26946s) -- [Watch "WordPress Playground: the ultimate learning, testing, & teaching tool for WordPress"](https://www.youtube.com/watch?v=dN_LaenY8bI) by Anne McCarthy -- [WordPress Playground for developers](https://wordpress.tv/2024/12/16/wordpress-playground-for-developers/) by Berislav Grgicak and Jonathan Bossenger -- [WordPress Playground Block code editor theme support](https://wordpress.tv/2024/10/05/wordpress-playground-block-code-editor-theme-support/) by Jonathan Bossenger -- [WordPress Playground – use WordPress without a server at WCEU 2024](https://wordpress.tv/2024/07/03/wordpress-playground-use-wordpress-without-a-server/) by Adam Zielinski -- [Code, Test, Repeat: Accelerating Development with WordPress Playground at WordCamp Larissa 2024](https://wordpress.tv/2024/12/13/code-test-repeat-accelerating-development-with-wordpress-playground/) by Uros Tasic -- [Liberating data with WordPress Playground in a Browser Extension at WordCamp Netherlands 2024](https://wordpress.tv/2024/12/24/liberating-data-with-wordpress-playground-in-a-browser-extension/) by Alex Kirk -- [Beyond the Playground: WordPress as a Tool and Product Builder at WCUS 2024](https://wordpress.tv/2024/10/10/beyond-the-playground-wordpress-as-a-tool-and-product-builder/) by Dennis Snell -- [Create a demo with Playground at WC Asia 2025](https://wordpress.tv/2025/04/30/create-a-demo-with-playground/) by Birgit Pauli-Haack -- [Dissecting WordPress Playground at WordCamp Nepal 2025](https://wordpress.tv/2025/04/30/dissecting-wordpress-playground/) by Sakar Upadhyaya Khatiwada -- [Building Automated Test with WordPress Playground at WCEU 2025](https://wordpress.tv/2025/06/07/building-automated-tests-with-wordpress-playground/)by Berislav Grgicak -- [From Zero to Demo: Mastering WordPress Playground Blueprints at WCEU 2025](https://wordpress.tv/2025/06/07/from-zero-to-demo-mastering-wordpress-playground-blueprints/) by Birgit Pauli-Haack -- [Playground at WordCamp Gliwice (in Polish)](https://www.youtube.com/watch?v=AUHklF9GdL8&list=PLiCne9CeL82_hGuJOAJlsc84WxVDSH-c9&index=4) by Adam Zielinski -- [WordPress Playground at WordCamp Wrocław 2024 (in Polish)](https://wordpress.tv/2024/12/02/wordpress-playground-przelom-w-wordpressie-2/) by Adam Zielinski -- [WordPress Playground at WordCamp Gdynia 2025 (in Polish)](https://wordpress.tv/2025/04/21/wordpress-playground/) by Magdalena Paciorek -- [Discovering Playground, the demo tool(in Spanish)](https://wordpress.tv/2024/08/09/descubriendo-playground-la-herramienta-para-hacer-demos/) by Alex Cuadra -- [WordPress Playground: Complete and functional WordPress installation(in Spanish)](https://wordpress.tv/2024/02/07/wordpress-playground-instalacion-completa-y-funcional-de-wordpress/) by Fernando García Rebolledo -- [Playground: A throwaway WordPress within your browser at WordCamp Madrid 2025(in Spanish)](https://wordpress.tv/2025/03/09/playground-un-wordpress-de-usar-y-tirar-dentro-de-tu-navegador/) by Álvaro Gómez Velasco -- [Use WordPress with just a browser! WordPress Playground Tutorial: Basic usage of Playground (in Japanese)](https://www.youtube.com/watch?v=6s_B0WvJauU) by Shimomura Tomoki -- [WordPress Playground: How to use Blueprints](https://www.youtube.com/watch?v=Vcao6uXguWg) by Shimomura Tomoki -- [Streamlined Block Theme Development: Using WordPress Playground and GitHub for No-Code Version Control of Site Editor Changes](https://wordpress.tv/2025/09/30/streamlined-block-theme-development-using-wordpress-playground-and-github-for-no-code-version-contr/) by Birgit Pauli-Haack -- [Playground, la mejor herramienta jamás inventada para enseñar WordPress (in Spanish)](https://wordpress.tv/2025/10/05/playground-la-mejor-herramienta-jamas-inventada-para-ensenar-wordpress/) by Nilo Vélez -- [Testing Faster Than a Red Bull Pit Stop: WordPress Playground and WooCommerce Blueprints](https://wordpress.tv/2025/09/30/testing-faster-than-a-red-bull-pit-stop-wordpress-playground-and-woocommerce-blueprints/) by Daniel Dudzic -- [Is WordPress playground only for developers?](https://wordpress.tv/2025/10/25/is-wordpress-playground-only-for-developers/) by Fellyph Cintra -- [How to test the next WordPress release with WordPress Playground](https://wordpress.tv/2025/11/13/how-to-test-the-next-wordpress-release-with-wordpress-playground/) by Fellyph cintra -- [Running WordPress directly from the JavaScript code with runCLI](https://wordpress.tv/2025/10/22/running-wordpress-directly-from-the-javascript-code-with-runcli/) by Fellyph Cintra -- [WordPress Playground: The Path to Test Automation](https://wordpress.tv/2025/11/24/wordpress-playground-the-path-to-test-automation/) by Fellyph Cintra +- Developer Hours Videos: + - [Americas Region (May 23,2023)](https://wordpress.tv/2023/05/23/developer-hours-wordpress-playground-americas/) + - [APAC/EMEA Region (May 24,2023)](https://wordpress.tv/2023/05/24/developer-hours-wordpress-playground-apac-emea/) + - [Creating WordPress Playground Blueprints for Testing and Demos (May 28, 2024)](https://wordpress.tv/2024/05/28/developer-hours-creating-wordpress-playground-blueprints-for-testing-and-demos/) by Birgit Pauli-Haack & Nick Diego + - [Developer Hours: Everything you need to know about WordPress Playground (Dec 17, 2024)](https://wordpress.tv/2024/12/17/developer-hours-everything-you-need-to-know-about-wordpress-playground/) by Nick Diego & Ryan Welcher +- [Playground at State of the Word](https://youtu.be/VeigCZuxnfY?t=2912) +- [Playground at WCEU 2023](https://www.youtube.com/watch?v=e-CwouzTGp4&t=26946s) +- [Watch "WordPress Playground: the ultimate learning, testing, & teaching tool for WordPress"](https://www.youtube.com/watch?v=dN_LaenY8bI) by Anne McCarthy +- [WordPress Playground for developers](https://wordpress.tv/2024/12/16/wordpress-playground-for-developers/) by Berislav Grgicak and Jonathan Bossenger +- [WordPress Playground Block code editor theme support](https://wordpress.tv/2024/10/05/wordpress-playground-block-code-editor-theme-support/) by Jonathan Bossenger +- [WordPress Playground – use WordPress without a server at WCEU 2024](https://wordpress.tv/2024/07/03/wordpress-playground-use-wordpress-without-a-server/) by Adam Zielinski +- [Code, Test, Repeat: Accelerating Development with WordPress Playground at WordCamp Larissa 2024](https://wordpress.tv/2024/12/13/code-test-repeat-accelerating-development-with-wordpress-playground/) by Uros Tasic +- [Liberating data with WordPress Playground in a Browser Extension at WordCamp Netherlands 2024](https://wordpress.tv/2024/12/24/liberating-data-with-wordpress-playground-in-a-browser-extension/) by Alex Kirk +- [Beyond the Playground: WordPress as a Tool and Product Builder at WCUS 2024](https://wordpress.tv/2024/10/10/beyond-the-playground-wordpress-as-a-tool-and-product-builder/) by Dennis Snell +- [Create a demo with Playground at WC Asia 2025](https://wordpress.tv/2025/04/30/create-a-demo-with-playground/) by Birgit Pauli-Haack +- [Dissecting WordPress Playground at WordCamp Nepal 2025](https://wordpress.tv/2025/04/30/dissecting-wordpress-playground/) by Sakar Upadhyaya Khatiwada +- [Building Automated Test with WordPress Playground at WCEU 2025](https://wordpress.tv/2025/06/07/building-automated-tests-with-wordpress-playground/)by Berislav Grgicak +- [From Zero to Demo: Mastering WordPress Playground Blueprints at WCEU 2025](https://wordpress.tv/2025/06/07/from-zero-to-demo-mastering-wordpress-playground-blueprints/) by Birgit Pauli-Haack +- [Playground at WordCamp Gliwice (in Polish)](https://www.youtube.com/watch?v=AUHklF9GdL8&list=PLiCne9CeL82_hGuJOAJlsc84WxVDSH-c9&index=4) by Adam Zielinski +- [WordPress Playground at WordCamp Wrocław 2024 (in Polish)](https://wordpress.tv/2024/12/02/wordpress-playground-przelom-w-wordpressie-2/) by Adam Zielinski +- [WordPress Playground at WordCamp Gdynia 2025 (in Polish)](https://wordpress.tv/2025/04/21/wordpress-playground/) by Magdalena Paciorek +- [Discovering Playground, the demo tool(in Spanish)](https://wordpress.tv/2024/08/09/descubriendo-playground-la-herramienta-para-hacer-demos/) by Alex Cuadra +- [WordPress Playground: Complete and functional WordPress installation(in Spanish)](https://wordpress.tv/2024/02/07/wordpress-playground-instalacion-completa-y-funcional-de-wordpress/) by Fernando García Rebolledo +- [Playground: A throwaway WordPress within your browser at WordCamp Madrid 2025(in Spanish)](https://wordpress.tv/2025/03/09/playground-un-wordpress-de-usar-y-tirar-dentro-de-tu-navegador/) by Álvaro Gómez Velasco +- [Use WordPress with just a browser! WordPress Playground Tutorial: Basic usage of Playground (in Japanese)](https://www.youtube.com/watch?v=6s_B0WvJauU) by Shimomura Tomoki +- [WordPress Playground: How to use Blueprints](https://www.youtube.com/watch?v=Vcao6uXguWg) by Shimomura Tomoki +- [Streamlined Block Theme Development: Using WordPress Playground and GitHub for No-Code Version Control of Site Editor Changes](https://wordpress.tv/2025/09/30/streamlined-block-theme-development-using-wordpress-playground-and-github-for-no-code-version-contr/) by Birgit Pauli-Haack +- [Playground, la mejor herramienta jamás inventada para enseñar WordPress (in Spanish)](https://wordpress.tv/2025/10/05/playground-la-mejor-herramienta-jamas-inventada-para-ensenar-wordpress/) by Nilo Vélez +- [Testing Faster Than a Red Bull Pit Stop: WordPress Playground and WooCommerce Blueprints](https://wordpress.tv/2025/09/30/testing-faster-than-a-red-bull-pit-stop-wordpress-playground-and-woocommerce-blueprints/) by Daniel Dudzic +- [Is WordPress playground only for developers?](https://wordpress.tv/2025/10/25/is-wordpress-playground-only-for-developers/) by Fellyph Cintra +- [How to test the next WordPress release with WordPress Playground](https://wordpress.tv/2025/11/13/how-to-test-the-next-wordpress-release-with-wordpress-playground/) by Fellyph cintra +- [Running WordPress directly from the JavaScript code with runCLI](https://wordpress.tv/2025/10/22/running-wordpress-directly-from-the-javascript-code-with-runcli/) by Fellyph Cintra +- [WordPress Playground: The Path to Test Automation](https://wordpress.tv/2025/11/24/wordpress-playground-the-path-to-test-automation/) by Fellyph Cintra diff --git a/packages/php-wasm/node/src/test/php-instance-manager.spec.ts b/packages/php-wasm/node/src/test/php-instance-manager.spec.ts index 0b6ce8be97..3cdf4ba314 100644 --- a/packages/php-wasm/node/src/test/php-instance-manager.spec.ts +++ b/packages/php-wasm/node/src/test/php-instance-manager.spec.ts @@ -1,6 +1,10 @@ import { RecommendedPHPVersion } from '@wp-playground/common'; import { loadNodeRuntime } from '..'; -import { PHP, PHPProcessManager, SinglePHPInstanceManager } from '@php-wasm/universal'; +import { + PHP, + PHPProcessManager, + SinglePHPInstanceManager, +} from '@php-wasm/universal'; describe('SinglePHPInstanceManager', () => { it('should return the PHP instance passed in the constructor', async () => { @@ -60,9 +64,9 @@ describe('SinglePHPInstanceManager', () => { }); it('should throw an error when neither php nor phpFactory is provided', () => { - expect( - () => new SinglePHPInstanceManager({}) - ).toThrowError(/requires either php or phpFactory/); + expect(() => new SinglePHPInstanceManager({})).toThrowError( + /requires either php or phpFactory/ + ); }); it('should only call the factory once even with concurrent getPrimaryPhp calls', async () => { diff --git a/packages/php-wasm/stream-compression/src/zip/encode-zip.ts b/packages/php-wasm/stream-compression/src/zip/encode-zip.ts index 9ce3ad39d5..ae6bffc3ba 100644 --- a/packages/php-wasm/stream-compression/src/zip/encode-zip.ts +++ b/packages/php-wasm/stream-compression/src/zip/encode-zip.ts @@ -107,7 +107,7 @@ function encodeZipTransform() { ...header, signature: SIGNATURE_CENTRAL_DIRECTORY, fileComment: new Uint8Array(0), - diskNumber: 1, + diskNumber: 0, internalAttributes: 0, externalAttributes: 0, firstByteAt: fileOffset, @@ -121,10 +121,10 @@ function encodeZipTransform() { } const centralDirectoryEnd: CentralDirectoryEndEntry = { signature: SIGNATURE_CENTRAL_DIRECTORY_END, - numberOfDisks: 1, + numberOfDisks: 0, centralDirectoryOffset, centralDirectorySize, - centralDirectoryStartDisk: 1, + centralDirectoryStartDisk: 0, numberCentralDirectoryRecordsOnThisDisk: offsetToFileHeaderMap.size, numberCentralDirectoryRecords: offsetToFileHeaderMap.size, diff --git a/packages/playground/blueprints/src/lib/v1/resources.spec.ts b/packages/playground/blueprints/src/lib/v1/resources.spec.ts index 33255c6613..5c352f2498 100644 --- a/packages/playground/blueprints/src/lib/v1/resources.spec.ts +++ b/packages/playground/blueprints/src/lib/v1/resources.spec.ts @@ -2,6 +2,10 @@ import { UrlResource, GitDirectoryResource, BundledResource, + isGithubProxyUrl, + rewriteGithubProxyUrl, + ZipResource, + Resource, } from './resources'; import { expect, describe, it, vi, beforeEach, afterEach } from 'vitest'; import { StreamedFile } from '@php-wasm/stream-compression'; @@ -399,3 +403,205 @@ describe('BlueprintResource', () => { expect(streamFile).toHaveBeenCalledWith('missing.txt'); }); }); + +describe('isGithubProxyUrl', () => { + it('should return true for github-proxy.com URLs', () => { + expect( + isGithubProxyUrl('https://github-proxy.com/proxy/?repo=owner/name') + ).toBe(true); + expect( + isGithubProxyUrl( + 'https://github-proxy.com/https://github.com/owner/repo' + ) + ).toBe(true); + }); + + it('should return false for non-github-proxy.com URLs', () => { + expect(isGithubProxyUrl('https://example.com/file.zip')).toBe(false); + expect(isGithubProxyUrl('https://github.com/owner/repo')).toBe(false); + }); + + it('should return false for invalid URLs', () => { + expect(isGithubProxyUrl('not a url')).toBe(false); + }); +}); + +describe('rewriteGithubProxyUrl', () => { + it('should return null for non-github-proxy.com URLs', () => { + expect( + rewriteGithubProxyUrl('https://example.com/file.zip') + ).toBeNull(); + expect( + rewriteGithubProxyUrl('https://github.com/owner/repo') + ).toBeNull(); + }); + + it('should return null for invalid URLs', () => { + expect(rewriteGithubProxyUrl('not a url')).toBeNull(); + }); + + it('should return null for github-proxy.com URLs without repo parameter', () => { + expect( + rewriteGithubProxyUrl( + 'https://github-proxy.com/proxy/?branch=trunk' + ) + ).toBeNull(); + }); + + it('should rewrite basic repo URL (default branch)', () => { + const result = rewriteGithubProxyUrl( + 'https://github-proxy.com/proxy/?repo=owner/name' + ); + expect(result).toEqual({ + resource: 'zip', + inner: { + resource: 'git:directory', + url: 'https://github.com/owner/name', + ref: 'HEAD', + }, + }); + }); + + it('should rewrite repo URL with branch', () => { + const result = rewriteGithubProxyUrl( + 'https://github-proxy.com/proxy/?repo=owner/name&branch=trunk' + ); + expect(result).toEqual({ + resource: 'zip', + inner: { + resource: 'git:directory', + url: 'https://github.com/owner/name', + ref: 'trunk', + }, + }); + }); +}); + +describe('ZipResource', () => { + it('should wrap a literal directory resource in a ZIP', async () => { + const innerResource = Resource.create( + { + resource: 'literal:directory', + name: 'my-plugin', + files: { + 'readme.txt': 'Hello World', + 'plugin.php': ' { + const innerResource = Resource.create( + { + resource: 'literal:directory', + name: 'my-plugin', + files: { 'test.txt': 'test' }, + }, + {} + ); + + const zipResource = new ZipResource( + { + resource: 'zip', + inner: { + resource: 'literal:directory', + name: 'my-plugin', + files: {}, + }, + name: 'custom.zip', + }, + innerResource + ); + + const zipFile = await zipResource.resolve(); + expect(zipFile.name).toBe('custom.zip'); + }); + + it('should not double .zip extension', async () => { + const innerResource = Resource.create( + { + resource: 'literal:directory', + name: 'already.zip', + files: { 'test.txt': 'test' }, + }, + {} + ); + + const zipResource = new ZipResource( + { + resource: 'zip', + inner: { + resource: 'literal:directory', + name: 'already.zip', + files: {}, + }, + }, + innerResource + ); + + const zipFile = await zipResource.resolve(); + expect(zipFile.name).toBe('already.zip'); + }); +}); + +describe('Resource.create with github-proxy.com URLs', () => { + let consoleWarnSpy: ReturnType; + + beforeEach(() => { + consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + }); + + afterEach(() => { + consoleWarnSpy.mockRestore(); + }); + + it('should rewrite github-proxy.com URL to zip resource and emit warning', () => { + const resource = Resource.create( + { + resource: 'url', + url: 'https://github-proxy.com/proxy/?repo=owner/name&branch=trunk', + }, + {} + ); + + // Check that a warning was emitted + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining('github-proxy.com is deprecated') + ); + + // The resource should be a ZipResource (wrapped in decorators) + expect(resource).toBeDefined(); + }); + + it('should not emit warning for non-github-proxy.com URLs', () => { + Resource.create( + { + resource: 'url', + url: 'https://example.com/file.zip', + }, + {} + ); + + expect(consoleWarnSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/playground/blueprints/src/lib/v1/resources.ts b/packages/playground/blueprints/src/lib/v1/resources.ts index 13552a8ac5..c1b2f78bee 100644 --- a/packages/playground/blueprints/src/lib/v1/resources.ts +++ b/packages/playground/blueprints/src/lib/v1/resources.ts @@ -15,7 +15,11 @@ import { } from '@wp-playground/storage'; import { zipNameToHumanName } from '../utils/zip-name-to-human-name'; import { fetchWithCorsProxy } from '@php-wasm/web'; -import { StreamedFile } from '@php-wasm/stream-compression'; +import { + StreamedFile, + encodeZip, + collectFile, +} from '@php-wasm/stream-compression'; import type { StreamBundledFile } from './types'; import { createDotGitDirectory } from '@wp-playground/storage'; @@ -47,6 +51,7 @@ export const ResourceTypes = [ 'url', 'git:directory', 'bundled', + 'zip', ] as const; export type VFSReference = { @@ -114,13 +119,23 @@ export type BundledReference = { path: string; }; +export type ZipWrapperReference = { + /** Identifies the resource as a ZIP wrapper */ + resource: 'zip'; + /** The inner resource to wrap in a ZIP file */ + inner: FileReference | DirectoryReference; + /** Optional filename for the ZIP (defaults to inner resource name + .zip) */ + name?: string; +}; + export type FileReference = | VFSReference | LiteralReference | CoreThemeReference | CorePluginReference | UrlReference - | BundledReference; + | BundledReference + | ZipWrapperReference; export type DirectoryReference = | GitDirectoryReference @@ -135,6 +150,123 @@ export function isResourceReference(ref: any): ref is FileReference { ); } +/** + * Checks if a URL is a github-proxy.com URL that can be rewritten. + * + * @param url The URL to check + * @returns true if the URL is a github-proxy.com URL + */ +export function isGithubProxyUrl(url: string): boolean { + try { + const parsed = new URL(url); + return parsed.hostname === 'github-proxy.com'; + } catch { + return false; + } +} + +/** + * Rewrites a github-proxy.com URL to an equivalent Blueprint resource reference. + * + * github-proxy.com is being deprecated. This function enables automatic migration + * of existing Blueprints that use github-proxy.com URLs to native Blueprint resources. + * + * Supported URL patterns: + * - `?repo=owner/name` - Full repository at default branch + * - `?repo=owner/name&branch=trunk` - Full repository at specific branch + * - `?repo=owner/name&pr=123` - Full repository at PR head + * - `?repo=owner/name&commit=abc` - Full repository at specific commit + * - `?repo=owner/name&release=v1.0` - Full repository at release tag + * - `?repo=owner/name&directory=subdir` - Subdirectory of repository + * - `?repo=owner/name&release=v1.0&asset=file.zip` - Release asset download + * - `https://github-proxy.com/https://github.com/...` - Direct GitHub URL proxy + * + * @param url The github-proxy.com URL to rewrite + * @returns A ZipWrapperReference (wrapping git:directory) or UrlReference, or null if URL cannot be rewritten + */ +export function rewriteGithubProxyUrl( + url: string +): ZipWrapperReference | UrlReference | null { + let parsed: URL; + try { + parsed = new URL(url); + } catch { + return null; + } + + if (parsed.hostname !== 'github-proxy.com') { + return null; + } + + // Handle direct GitHub URL proxy: https://github-proxy.com/https://github.com/... + // The pathname starts with a slash, so we slice it off + const pathAsUrl = parsed.pathname.slice(1); + if ( + pathAsUrl.startsWith('https://github.com/') || + pathAsUrl.startsWith('http://github.com/') + ) { + return { resource: 'url', url: pathAsUrl }; + } + + // For /proxy/ endpoints, extract query parameters + const params = parsed.searchParams; + const repo = params.get('repo'); + if (!repo) { + return null; + } + + // Handle release asset downloads (returns a file, not a directory) + const release = params.get('release'); + const asset = params.get('asset'); + if (release && asset) { + // GitHub supports /releases/latest/download/ URLs natively + const releasePath = + release === 'latest' + ? 'releases/latest/download' + : `releases/download/${release}`; + return { + resource: 'url', + url: `https://github.com/${repo}/${releasePath}/${asset}`, + }; + } + + // Determine the git ref based on the URL parameters + let ref: string; + let refType: 'branch' | 'tag' | 'commit' | undefined; + + const pr = params.get('pr'); + const commit = params.get('commit'); + const branch = params.get('branch'); + + if (pr) { + ref = `refs/pull/${pr}/head`; + } else if (commit) { + ref = commit; + refType = 'commit'; + } else if (release) { + ref = release; + refType = 'tag'; + } else { + ref = branch || 'HEAD'; + } + + const directory = params.get('directory'); + + // Always wrap in zip to match github-proxy.com semantics (which always returns ZIP files) + const gitDirectoryRef: GitDirectoryReference = { + resource: 'git:directory', + url: `https://github.com/${repo}`, + ref, + ...(refType && { refType }), + ...(directory && { path: directory }), + }; + + return { + resource: 'zip', + inner: gitDirectoryRef, + }; +} + export abstract class Resource { /** Optional progress tracker to monitor progress */ protected _progress?: ProgressTracker; @@ -189,6 +321,22 @@ export abstract class Resource { ) => Record; } ): Resource { + // Automatically rewrite github-proxy.com URLs to native Blueprint resources. + // github-proxy.com is being deprecated - this provides graceful migration. + if (ref.resource === 'url' && isGithubProxyUrl(ref.url)) { + const rewritten = rewriteGithubProxyUrl(ref.url); + if (rewritten) { + // eslint-disable-next-line no-console + console.warn( + `[Blueprints] github-proxy.com is deprecated and will stop working soon. ` + + `The URL "${ref.url}" has been automatically converted to a ${rewritten.resource} resource. ` + + `Please update your Blueprint to use native resource types. ` + + `See: https://wordpress.github.io/wordpress-playground/blueprints/steps/resources` + ); + ref = rewritten; + } + } + let resource: Resource; switch (ref.resource) { case 'vfs': @@ -225,6 +373,18 @@ export abstract class Resource { progress ); break; + case 'zip': { + // Recursively create the inner resource + const innerResource = Resource.create(ref.inner, { + semaphore, + progress, + corsProxy, + streamBundledFile, + gitAdditionalHeadersCallback, + }); + resource = new ZipResource(ref, innerResource, progress); + break; + } default: throw new Error( `Unknown resource type: ${(ref as any).resource}` @@ -240,7 +400,7 @@ export abstract class Resource { } export abstract class ResourceDecorator< - T extends File | Directory + T extends File | Directory, > extends Resource { protected resource: Resource; constructor(resource: Resource) { @@ -770,7 +930,7 @@ export function toDirectoryZipName(rawInput: string) { * A decorator for a resource that adds caching functionality. */ export class CachedResource< - T extends File | Directory + T extends File | Directory, > extends ResourceDecorator { protected override promise?: Promise; @@ -788,7 +948,7 @@ export class CachedResource< * through a semaphore. */ export class SemaphoreResource< - T extends File | Directory + T extends File | Directory, > extends ResourceDecorator { private readonly semaphore: Semaphore; constructor(resource: Resource, semaphore: Semaphore) { @@ -886,3 +1046,95 @@ export class BundledResource extends Resource { return true; } } + +/** + * A `Resource` that wraps another resource and outputs it as a ZIP file. + * This is useful for converting directory resources to ZIP files, enabling + * compatibility with steps that expect ZIP input (like `unzip`). + */ +export class ZipResource extends Resource { + private reference: ZipWrapperReference; + private innerResource: Resource; + + constructor( + reference: ZipWrapperReference, + innerResource: Resource, + _progress?: ProgressTracker + ) { + super(); + this.reference = reference; + this.innerResource = innerResource; + this._progress = _progress; + } + + /** @inheritDoc */ + async resolve(): Promise { + this.progress?.setCaption(`Creating ZIP: ${this.name}`); + + // Resolve the inner resource first + const innerResult = await this.innerResource.resolve(); + + // Convert to an iterable of File objects for encodeZip + let files: File[]; + + if (innerResult instanceof File) { + // Inner resource is already a File - wrap it in a ZIP + files = [innerResult]; + } else { + // Inner resource is a Directory - convert FileTree to Files + files = fileTreeToFiles(innerResult.files, innerResult.name); + } + + // Create the ZIP using encodeZip + const zipStream = encodeZip(files); + const zipFile = await collectFile(this.name, zipStream); + + this.progress?.set(100); + return zipFile; + } + + /** @inheritDoc */ + get name(): string { + if (this.reference.name) { + return this.reference.name; + } + const innerName = this.innerResource.name; + return innerName.endsWith('.zip') ? innerName : `${innerName}.zip`; + } + + /** @inheritDoc */ + override get isAsync(): boolean { + return true; + } +} + +/** + * Converts a FileTree to an array of File objects suitable for ZIP encoding. + * Each file's name includes its relative path within the tree. + */ +function fileTreeToFiles(tree: FileTree, baseName: string): File[] { + const files: File[] = []; + + function traverse(node: FileTree, currentPath: string) { + for (const [name, value] of Object.entries(node)) { + const fullPath = currentPath ? `${currentPath}/${name}` : name; + + if (value instanceof Uint8Array) { + files.push(new File([value], `${baseName}/${fullPath}`)); + } else if (typeof value === 'string') { + files.push( + new File( + [new TextEncoder().encode(value)], + `${baseName}/${fullPath}` + ) + ); + } else { + // It's a nested FileTree + traverse(value, fullPath); + } + } + } + + traverse(tree, ''); + return files; +} diff --git a/packages/playground/remote/vite.config.ts b/packages/playground/remote/vite.config.ts index 2865748ed2..4403ec5df5 100644 --- a/packages/playground/remote/vite.config.ts +++ b/packages/playground/remote/vite.config.ts @@ -50,7 +50,7 @@ export default defineConfig(({ mode }) => { ? process.env['CORS_PROXY_URL'] : mode === 'production' ? 'https://wordpress-playground-cors-proxy.net/?' - : 'http://127.0.0.1:5263/cors-proxy.php?'; + : '/cors-proxy/?'; plugins.push( virtualModule({ @@ -92,6 +92,17 @@ export default defineConfig(({ mode }) => { port: remoteDevServerPort, host: remoteDevServerHost, allowedHosts: ['playground.test', 'playground-preview.test'], + proxy: { + // Proxy CORS requests to the local PHP CORS proxy server. + // This avoids Private Network Access (PNA) restrictions in Chrome + // when making cross-origin requests between different local ports. + '/cors-proxy': { + target: 'http://127.0.0.1:5263', + changeOrigin: true, + rewrite: (path) => + path.replace(/^\/cors-proxy\/\?/, '/cors-proxy.php?'), + }, + }, fs: { // Allow serving files from the 'packages' directory allow: ['../../'], diff --git a/packages/playground/website/vite.config.ts b/packages/playground/website/vite.config.ts index 0e17659df8..cc10eb0657 100644 --- a/packages/playground/website/vite.config.ts +++ b/packages/playground/website/vite.config.ts @@ -44,7 +44,7 @@ export default defineConfig(({ command, mode }) => { ? process.env.CORS_PROXY_URL : mode === 'production' ? 'https://wordpress-playground-cors-proxy.net/?' - : 'http://127.0.0.1:5263/cors-proxy.php?'; + : '/cors-proxy/?'; return { // Split traffic from this server on dev so that the iframe content and @@ -75,6 +75,15 @@ export default defineConfig(({ command, mode }) => { allowedHosts: ['playground.test', 'playground-preview.test'], proxy: { ...proxy, + // Proxy CORS requests to the local PHP CORS proxy server. + // This avoids Private Network Access (PNA) restrictions in Chrome + // when making cross-origin requests between different local ports. + '/cors-proxy': { + target: 'http://127.0.0.1:5263', + changeOrigin: true, + rewrite: (path) => + path.replace(/^\/cors-proxy\/\?/, '/cors-proxy.php?'), + }, // Proxy requests to the website-extras '^/website-extras/': { target: `http://${websiteExtrasDevServerHost}:${websiteExtrasDevServerPort}`,