Skip to content

Commit 845bcc4

Browse files
committed
Implement GitDirectoryResource
Related to #1787, Follows up on #1793 Implements GitDirectoryResource to enable loading files directly from git repositories as follows: ```ts { "landingPage": "/guides/for-plugin-developers.md", "steps": [ { "step": "writeFiles", "writeToPath": "/wordpress/guides", "filesTree": { "resource": "git:directory", "url": "https://github.com/WordPress/wordpress-playground.git", "ref": "trunk", "path": "packages/docs/site/docs/main/guides" } } ] } ``` ## Implementation details Uses git client functions merged in #1764 to sparse checkout the requested files. It also leans on the PHP CORS proxy which is now started as a part of the `npm run dev` command. The CORS proxy URL is configurable per `compileBlueprint()` call so that each Playground runtime may choose to either use it or not. For example, it wouldn't be very useful in the CLI version of Playground. ## Testing plan Go to `http://localhost:5400/website-server/#{%20%22landingPage%22:%20%22/guides/for-plugin-developers.md%22,%20%22steps%22:%20[%20{%20%22step%22:%20%22writeFiles%22,%20%22writeToPath%22:%20%22/wordpress/guides%22,%20%22filesTree%22:%20{%20%22resource%22:%20%22git:directory%22,%20%22url%22:%20%22https://github.com/WordPress/wordpress-playground.git%22,%20%22ref%22:%20%22trunk%22,%20%22path%22:%20%22packages/docs/site/docs/main/guides%22%20}%20}%20]%20}` and confirm Playground loads a markdown file.
1 parent 8639616 commit 845bcc4

File tree

8 files changed

+84
-21
lines changed

8 files changed

+84
-21
lines changed

packages/playground/blueprints/src/lib/compile.ts

+18
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ export interface CompileBlueprintOptions {
6767
semaphore?: Semaphore;
6868
/** Optional callback with step output */
6969
onStepCompleted?: OnStepCompleted;
70+
/**
71+
* Proxy URL to use for cross-origin requests.
72+
*
73+
* For example, if corsProxy is set to "https://cors.wordpress.net/proxy.php",
74+
* then the CORS requests to https://github.com/WordPress/gutenberg.git would actually
75+
* be made to https://cors.wordpress.net/proxy.php/https://github.com/WordPress/gutenberg.git.
76+
*/
77+
corsProxy?: string;
7078
}
7179

7280
/**
@@ -83,6 +91,7 @@ export function compileBlueprint(
8391
progress = new ProgressTracker(),
8492
semaphore = new Semaphore({ concurrency: 3 }),
8593
onStepCompleted = () => {},
94+
corsProxy,
8695
}: CompileBlueprintOptions = {}
8796
): CompiledBlueprint {
8897
// Deep clone the blueprint to avoid mutating the input
@@ -293,6 +302,7 @@ export function compileBlueprint(
293302
semaphore,
294303
rootProgressTracker: progress,
295304
totalProgressWeight,
305+
corsProxy,
296306
})
297307
);
298308

@@ -480,6 +490,12 @@ interface CompileStepArgsOptions {
480490
rootProgressTracker: ProgressTracker;
481491
/** The total progress weight of all the steps in the blueprint */
482492
totalProgressWeight: number;
493+
/**
494+
* Proxy URL to use for cross-origin requests.
495+
*
496+
* @see CompileBlueprintOptions.corsProxy
497+
*/
498+
corsProxy?: string;
483499
}
484500

485501
/**
@@ -496,6 +512,7 @@ function compileStep<S extends StepDefinition>(
496512
semaphore,
497513
rootProgressTracker,
498514
totalProgressWeight,
515+
corsProxy,
499516
}: CompileStepArgsOptions
500517
): { run: CompiledStep; step: S; resources: Array<Resource<any>> } {
501518
const stepProgress = rootProgressTracker.stage(
@@ -508,6 +525,7 @@ function compileStep<S extends StepDefinition>(
508525
if (isResourceReference(value)) {
509526
value = Resource.create(value, {
510527
semaphore,
528+
corsProxy,
511529
});
512530
}
513531
args[key] = value;

packages/playground/blueprints/src/lib/resources.ts

+32-15
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import {
44
} from '@php-wasm/progress';
55
import { FileTree, UniversalPHP } from '@php-wasm/universal';
66
import { Semaphore } from '@php-wasm/util';
7+
import {
8+
listDescendantFiles,
9+
listGitFiles,
10+
sparseCheckout,
11+
} from '@wp-playground/storage';
712
import { zipNameToHumanName } from './utils/zip-name-to-human-name';
813

914
export type { FileTree };
@@ -129,10 +134,12 @@ export abstract class Resource<T extends File | Directory> {
129134
{
130135
semaphore,
131136
progress,
137+
corsProxy,
132138
}: {
133139
/** Optional semaphore to limit concurrent downloads */
134140
semaphore?: Semaphore;
135141
progress?: ProgressTracker;
142+
corsProxy?: string;
136143
}
137144
): Resource<File | Directory> {
138145
let resource: Resource<File | Directory>;
@@ -153,7 +160,9 @@ export abstract class Resource<T extends File | Directory> {
153160
resource = new UrlResource(ref, progress);
154161
break;
155162
case 'git:directory':
156-
resource = new GitDirectoryResource(ref, progress);
163+
resource = new GitDirectoryResource(ref, progress, {
164+
corsProxy,
165+
});
157166
break;
158167
case 'literal:directory':
159168
resource = new LiteralDirectoryResource(ref, progress);
@@ -442,26 +451,34 @@ export class UrlResource extends FetchResource {
442451
export class GitDirectoryResource extends Resource<Directory> {
443452
constructor(
444453
private reference: GitDirectoryReference,
445-
public override _progress?: ProgressTracker
454+
public override _progress?: ProgressTracker,
455+
private options?: { corsProxy?: string }
446456
) {
447457
super();
448458
}
449459

450460
async resolve() {
451-
// @TODO: Use the actual sparse checkout logic here once
452-
// https://github.com/WordPress/wordpress-playground/pull/1764 lands.
453-
throw new Error('Not implemented yet');
461+
const repoUrl = this.options?.corsProxy
462+
? `${this.options.corsProxy}/${this.reference.url}`
463+
: this.reference.url;
464+
const ref = `refs/heads/${this.reference.ref}`;
465+
const allFiles = await listGitFiles(repoUrl, ref);
466+
const filesToClone = listDescendantFiles(allFiles, this.reference.path);
467+
let files = await sparseCheckout(repoUrl, ref, filesToClone);
468+
// Remove the path prefix from the cloned file names.
469+
files = Object.fromEntries(
470+
Object.entries(files).map(([name, contents]) => {
471+
name = name.substring(this.reference.path.length);
472+
name = name.replace(/^\/+/, '');
473+
return [name, contents];
474+
})
475+
);
454476
return {
455-
name: 'hello-world',
456-
files: {
457-
'README.md': 'Hello, World!',
458-
'index.php': `<?php
459-
/**
460-
* Plugin Name: Hello World
461-
* Description: A simple plugin that says hello world.
462-
*/
463-
`,
464-
},
477+
name: `${this.reference.ref} (${this.reference.path})`.replaceAll(
478+
'/',
479+
'-'
480+
),
481+
files,
465482
};
466483
}
467484

packages/playground/client/src/index.ts

+14
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,18 @@ export interface StartPlaygroundOptions {
7575
* for more details.
7676
*/
7777
scope?: string;
78+
/**
79+
* Proxy URL to use for cross-origin requests.
80+
*
81+
* For example, if corsProxy is set to "https://cors.wordpress.net/proxy.php",
82+
* then the CORS requests to https://github.com/WordPress/wordpress-playground.git would actually
83+
* be made to https://cors.wordpress.net/proxy.php/https://github.com/WordPress/wordpress-playground.git.
84+
*
85+
* The Blueprints library will arbitrarily choose which requests to proxy. If you need
86+
* to proxy every single request, do not use this option. Instead, you should preprocess
87+
* your Blueprint to replace all cross-origin URLs with the proxy URL.
88+
*/
89+
corsProxy?: string;
7890
}
7991

8092
/**
@@ -96,6 +108,7 @@ export async function startPlaygroundWeb({
96108
onBeforeBlueprint,
97109
mounts,
98110
scope,
111+
corsProxy,
99112
shouldInstallWordPress,
100113
}: StartPlaygroundOptions): Promise<PlaygroundClient> {
101114
assertValidRemote(remoteUrl);
@@ -116,6 +129,7 @@ export async function startPlaygroundWeb({
116129
const compiled = compileBlueprint(blueprint, {
117130
progress: progressTracker.stage(0.5),
118131
onStepCompleted: onBlueprintStepCompleted,
132+
corsProxy,
119133
});
120134

121135
await new Promise((resolve) => {

packages/playground/storage/src/lib/git-sparse-checkout.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export async function sparseCheckout(
4242
fullyQualifiedBranchName: string,
4343
filesPaths: string[]
4444
) {
45-
const refs = await listRefs(repoUrl, fullyQualifiedBranchName);
45+
const refs = await listGitRefs(repoUrl, fullyQualifiedBranchName);
4646
const commitHash = refs[fullyQualifiedBranchName];
4747
const treesIdx = await fetchWithoutBlobs(repoUrl, commitHash);
4848
const objects = await resolveObjects(treesIdx, commitHash, filesPaths);
@@ -84,11 +84,11 @@ export type FileTree = FileTreeFile | FileTreeFolder;
8484
* @param fullyQualifiedBranchName The full name of the branch to fetch from (e.g., 'refs/heads/main').
8585
* @returns A list of all files in the repository.
8686
*/
87-
export async function listFiles(
87+
export async function listGitFiles(
8888
repoUrl: string,
8989
fullyQualifiedBranchName: string
9090
): Promise<FileTree[]> {
91-
const refs = await listRefs(repoUrl, fullyQualifiedBranchName);
91+
const refs = await listGitRefs(repoUrl, fullyQualifiedBranchName);
9292
if (!(fullyQualifiedBranchName in refs)) {
9393
throw new Error(`Branch ${fullyQualifiedBranchName} not found`);
9494
}
@@ -131,7 +131,7 @@ function gitTreeToFileTree(tree: GitTree): FileTree[] {
131131
* @param fullyQualifiedBranchPrefix The prefix of the refs to fetch. For example: refs/heads/my-feature-branch
132132
* @returns A map of refs to their corresponding commit hashes.
133133
*/
134-
export async function listRefs(
134+
export async function listGitRefs(
135135
repoUrl: string,
136136
fullyQualifiedBranchPrefix: string
137137
) {

packages/playground/website/project.json

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"executor": "nx:run-commands",
5151
"options": {
5252
"commands": [
53+
"nx run playground-php-cors-proxy:start",
5354
"nx dev playground-remote --configuration=development-for-website",
5455
"sleep 1; nx dev:standalone playground-website --hmr --output-style=stream-without-prefixes"
5556
],

packages/playground/website/src/lib/state/redux/boot-site-client.ts

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import { getRemoteUrl } from '../../config';
2020
import { setActiveModal, setActiveSiteError } from './slice-ui';
2121
import { PlaygroundDispatch, PlaygroundReduxState } from './store';
2222
import { selectSiteBySlug } from './slice-sites';
23+
// @ts-ignore
24+
import { corsProxyUrl } from 'virtual:cors-proxy-url';
2325

2426
export function bootSiteClient(
2527
siteSlug: string,
@@ -131,6 +133,7 @@ export function bootSiteClient(
131133
]
132134
: [],
133135
shouldInstallWordPress: !isWordPressInstalled,
136+
corsProxy: corsProxyUrl,
134137
});
135138

136139
// @TODO: Remove backcompat code after 2024-12-01.

packages/playground/website/vite.config.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference types="vitest" />
22
import { defineConfig } from 'vite';
3-
import type { Plugin, ViteDevServer } from 'vite';
3+
import type { CommonServerOptions, Plugin, ViteDevServer } from 'vite';
44
import react from '@vitejs/plugin-react';
55
// eslint-disable-next-line @nx/enforce-module-boundaries
66
import { viteTsConfigPaths } from '../../vite-extensions/vite-ts-config-paths';
@@ -21,8 +21,9 @@ import { join } from 'node:path';
2121
import { buildVersionPlugin } from '../../vite-extensions/vite-build-version';
2222
import { listAssetsRequiredForOfflineMode } from '../../vite-extensions/vite-list-assets-required-for-offline-mode';
2323
import { addManifestJson } from '../../vite-extensions/vite-manifest';
24+
import virtualModule from '../../vite-extensions/vite-virtual-module';
2425

25-
const proxy = {
26+
const proxy: CommonServerOptions['proxy'] = {
2627
'^/plugin-proxy': {
2728
target: 'https://playground.wordpress.net',
2829
changeOrigin: true,
@@ -77,6 +78,15 @@ export default defineConfig(({ command, mode }) => {
7778
}),
7879
ignoreWasmImports(),
7980
buildVersionPlugin('website-config'),
81+
virtualModule({
82+
name: 'cors-proxy-url',
83+
content: `
84+
export const corsProxyUrl = '${
85+
mode === 'production'
86+
? '/cors-proxy.php'
87+
: 'http://127.0.0.1:5263/cors-proxy.php'
88+
}';`,
89+
}),
8090
// GitHub OAuth flow
8191
{
8292
name: 'configure-server',

0 commit comments

Comments
 (0)