diff --git a/packages/playground/blueprints/src/lib/compile.ts b/packages/playground/blueprints/src/lib/compile.ts index 19a087466d..07e0801e18 100644 --- a/packages/playground/blueprints/src/lib/compile.ts +++ b/packages/playground/blueprints/src/lib/compile.ts @@ -67,6 +67,14 @@ export interface CompileBlueprintOptions { semaphore?: Semaphore; /** Optional callback with step output */ onStepCompleted?: OnStepCompleted; + /** + * Proxy URL to use for cross-origin requests. + * + * For example, if corsProxy is set to "https://cors.wordpress.net/proxy.php", + * then the CORS requests to https://github.com/WordPress/gutenberg.git would actually + * be made to https://cors.wordpress.net/proxy.php/https://github.com/WordPress/gutenberg.git. + */ + corsProxy?: string; } /** @@ -83,6 +91,7 @@ export function compileBlueprint( progress = new ProgressTracker(), semaphore = new Semaphore({ concurrency: 3 }), onStepCompleted = () => {}, + corsProxy, }: CompileBlueprintOptions = {} ): CompiledBlueprint { // Deep clone the blueprint to avoid mutating the input @@ -293,6 +302,7 @@ export function compileBlueprint( semaphore, rootProgressTracker: progress, totalProgressWeight, + corsProxy, }) ); @@ -480,6 +490,12 @@ interface CompileStepArgsOptions { rootProgressTracker: ProgressTracker; /** The total progress weight of all the steps in the blueprint */ totalProgressWeight: number; + /** + * Proxy URL to use for cross-origin requests. + * + * @see CompileBlueprintOptions.corsProxy + */ + corsProxy?: string; } /** @@ -496,6 +512,7 @@ function compileStep( semaphore, rootProgressTracker, totalProgressWeight, + corsProxy, }: CompileStepArgsOptions ): { run: CompiledStep; step: S; resources: Array> } { const stepProgress = rootProgressTracker.stage( @@ -508,6 +525,7 @@ function compileStep( if (isResourceReference(value)) { value = Resource.create(value, { semaphore, + corsProxy, }); } args[key] = value; diff --git a/packages/playground/blueprints/src/lib/resources.ts b/packages/playground/blueprints/src/lib/resources.ts index ebef0dec1b..40811d7f9f 100644 --- a/packages/playground/blueprints/src/lib/resources.ts +++ b/packages/playground/blueprints/src/lib/resources.ts @@ -4,6 +4,11 @@ import { } from '@php-wasm/progress'; import { FileTree, UniversalPHP } from '@php-wasm/universal'; import { Semaphore } from '@php-wasm/util'; +import { + listDescendantFiles, + listGitFiles, + sparseCheckout, +} from '@wp-playground/storage'; import { zipNameToHumanName } from './utils/zip-name-to-human-name'; export type { FileTree }; @@ -129,10 +134,12 @@ export abstract class Resource { { semaphore, progress, + corsProxy, }: { /** Optional semaphore to limit concurrent downloads */ semaphore?: Semaphore; progress?: ProgressTracker; + corsProxy?: string; } ): Resource { let resource: Resource; @@ -153,7 +160,9 @@ export abstract class Resource { resource = new UrlResource(ref, progress); break; case 'git:directory': - resource = new GitDirectoryResource(ref, progress); + resource = new GitDirectoryResource(ref, progress, { + corsProxy, + }); break; case 'literal:directory': resource = new LiteralDirectoryResource(ref, progress); @@ -442,26 +451,34 @@ export class UrlResource extends FetchResource { export class GitDirectoryResource extends Resource { constructor( private reference: GitDirectoryReference, - public override _progress?: ProgressTracker + public override _progress?: ProgressTracker, + private options?: { corsProxy?: string } ) { super(); } async resolve() { - // @TODO: Use the actual sparse checkout logic here once - // https://github.com/WordPress/wordpress-playground/pull/1764 lands. - throw new Error('Not implemented yet'); + const repoUrl = this.options?.corsProxy + ? `${this.options.corsProxy}/${this.reference.url}` + : this.reference.url; + const ref = `refs/heads/${this.reference.ref}`; + const allFiles = await listGitFiles(repoUrl, ref); + const filesToClone = listDescendantFiles(allFiles, this.reference.path); + let files = await sparseCheckout(repoUrl, ref, filesToClone); + // Remove the path prefix from the cloned file names. + files = Object.fromEntries( + Object.entries(files).map(([name, contents]) => { + name = name.substring(this.reference.path.length); + name = name.replace(/^\/+/, ''); + return [name, contents]; + }) + ); return { - name: 'hello-world', - files: { - 'README.md': 'Hello, World!', - 'index.php': ` { assertValidRemote(remoteUrl); @@ -116,6 +129,7 @@ export async function startPlaygroundWeb({ const compiled = compileBlueprint(blueprint, { progress: progressTracker.stage(0.5), onStepCompleted: onBlueprintStepCompleted, + corsProxy, }); await new Promise((resolve) => { diff --git a/packages/playground/php-cors-proxy/proxy.php b/packages/playground/php-cors-proxy/cors-proxy.php similarity index 100% rename from packages/playground/php-cors-proxy/proxy.php rename to packages/playground/php-cors-proxy/cors-proxy.php diff --git a/packages/playground/storage/src/lib/git-sparse-checkout.ts b/packages/playground/storage/src/lib/git-sparse-checkout.ts index c6d3cbfb10..729870ec18 100644 --- a/packages/playground/storage/src/lib/git-sparse-checkout.ts +++ b/packages/playground/storage/src/lib/git-sparse-checkout.ts @@ -42,7 +42,7 @@ export async function sparseCheckout( fullyQualifiedBranchName: string, filesPaths: string[] ) { - const refs = await listRefs(repoUrl, fullyQualifiedBranchName); + const refs = await listGitRefs(repoUrl, fullyQualifiedBranchName); const commitHash = refs[fullyQualifiedBranchName]; const treesIdx = await fetchWithoutBlobs(repoUrl, commitHash); const objects = await resolveObjects(treesIdx, commitHash, filesPaths); @@ -84,11 +84,11 @@ export type FileTree = FileTreeFile | FileTreeFolder; * @param fullyQualifiedBranchName The full name of the branch to fetch from (e.g., 'refs/heads/main'). * @returns A list of all files in the repository. */ -export async function listFiles( +export async function listGitFiles( repoUrl: string, fullyQualifiedBranchName: string ): Promise { - const refs = await listRefs(repoUrl, fullyQualifiedBranchName); + const refs = await listGitRefs(repoUrl, fullyQualifiedBranchName); if (!(fullyQualifiedBranchName in refs)) { throw new Error(`Branch ${fullyQualifiedBranchName} not found`); } @@ -131,7 +131,7 @@ function gitTreeToFileTree(tree: GitTree): FileTree[] { * @param fullyQualifiedBranchPrefix The prefix of the refs to fetch. For example: refs/heads/my-feature-branch * @returns A map of refs to their corresponding commit hashes. */ -export async function listRefs( +export async function listGitRefs( repoUrl: string, fullyQualifiedBranchPrefix: string ) { diff --git a/packages/playground/website/project.json b/packages/playground/website/project.json index 7f69d1e0e9..62109178c4 100644 --- a/packages/playground/website/project.json +++ b/packages/playground/website/project.json @@ -50,6 +50,7 @@ "executor": "nx:run-commands", "options": { "commands": [ + "nx run playground-php-cors-proxy:start", "nx dev playground-remote --configuration=development-for-website", "sleep 1; nx dev:standalone playground-website --hmr --output-style=stream-without-prefixes" ], diff --git a/packages/playground/website/src/lib/state/redux/boot-site-client.ts b/packages/playground/website/src/lib/state/redux/boot-site-client.ts index 7df006b8bf..6543c086dd 100644 --- a/packages/playground/website/src/lib/state/redux/boot-site-client.ts +++ b/packages/playground/website/src/lib/state/redux/boot-site-client.ts @@ -20,6 +20,8 @@ import { getRemoteUrl } from '../../config'; import { setActiveModal, setActiveSiteError } from './slice-ui'; import { PlaygroundDispatch, PlaygroundReduxState } from './store'; import { selectSiteBySlug } from './slice-sites'; +// @ts-ignore +import { corsProxyUrl } from 'virtual:cors-proxy-url'; export function bootSiteClient( siteSlug: string, @@ -131,6 +133,7 @@ export function bootSiteClient( ] : [], shouldInstallWordPress: !isWordPressInstalled, + corsProxy: corsProxyUrl, }); // @TODO: Remove backcompat code after 2024-12-01. diff --git a/packages/playground/website/vite.config.ts b/packages/playground/website/vite.config.ts index 5f41e2bde4..70c7861ac7 100644 --- a/packages/playground/website/vite.config.ts +++ b/packages/playground/website/vite.config.ts @@ -1,6 +1,6 @@ /// import { defineConfig } from 'vite'; -import type { Plugin, ViteDevServer } from 'vite'; +import type { CommonServerOptions, Plugin, ViteDevServer } from 'vite'; import react from '@vitejs/plugin-react'; // eslint-disable-next-line @nx/enforce-module-boundaries import { viteTsConfigPaths } from '../../vite-extensions/vite-ts-config-paths'; @@ -21,8 +21,9 @@ import { join } from 'node:path'; import { buildVersionPlugin } from '../../vite-extensions/vite-build-version'; import { listAssetsRequiredForOfflineMode } from '../../vite-extensions/vite-list-assets-required-for-offline-mode'; import { addManifestJson } from '../../vite-extensions/vite-manifest'; +import virtualModule from '../../vite-extensions/vite-virtual-module'; -const proxy = { +const proxy: CommonServerOptions['proxy'] = { '^/plugin-proxy': { target: 'https://playground.wordpress.net', changeOrigin: true, @@ -77,6 +78,15 @@ export default defineConfig(({ command, mode }) => { }), ignoreWasmImports(), buildVersionPlugin('website-config'), + virtualModule({ + name: 'cors-proxy-url', + content: ` + export const corsProxyUrl = '${ + mode === 'production' + ? '/cors-proxy.php' + : 'http://127.0.0.1:5263/cors-proxy.php' + }';`, + }), // GitHub OAuth flow { name: 'configure-server',