Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 139 additions & 28 deletions packages/playground/blueprints/src/lib/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,16 @@ export type CoreThemeReference = {
resource: 'wordpress.org/themes';
/** The slug of the WordPress Core theme */
slug: string;
/** The name of the WordPress Core theme */
name?: string;
};
export type CorePluginReference = {
/** Identifies the file resource as a WordPress Core plugin */
resource: 'wordpress.org/plugins';
/** The slug of the WordPress Core plugin */
slug: string;
/** The name of the WordPress Core plugin */
name?: string;
};
export type UrlReference = {
/** Identifies the file resource as a URL */
Expand Down Expand Up @@ -100,6 +104,8 @@ export type DirectoryReference =
| GitDirectoryReference
| DirectoryLiteralReference;

export type APIFetchableReference = CoreThemeReference | CorePluginReference;

export function isResourceReference(ref: any): ref is FileReference {
return (
ref &&
Expand Down Expand Up @@ -417,6 +423,90 @@ export abstract class FetchResource extends Resource<File> {
}
}

/**
* A base class for `FetchResource`s that require fetching data from an API prior.
*/
export abstract class APIBasedFetchResource extends FetchResource {
protected apiResult?: any;
protected resource: APIFetchableReference;

/**
* Creates a new instance of `APIBasedFetchResource`.
* @param resource The API fetchable reference.
* @param progress The progress tracker.
*/
constructor(resource: APIFetchableReference, _progress?: ProgressTracker) {
super(_progress);

this.resource = resource;
}

/** @inheritDoc */
override async resolve() {
const url = this.getAPIURL();
try {
let response = await fetchWithCorsProxy(
url,
undefined,
undefined,
await this.playground?.absoluteUrl
);
if (response.ok) {
response = await cloneResponseMonitorProgress(
response,
this.progress?.loadingListener ?? noop
);
}

this.apiResult = await response.json();

this.resource.name = this.name;
} catch {
// swallow the error, we'll gracefully degrade to using the slug.
}

return await super.resolve();
}

/**
* Gets the URL to fetch the data from.
* @returns The URL.
*/
protected abstract getAPIURL(): string;

/**
* Gets the caption for the progress tracker.
* @returns The caption.
*/
protected override get caption() {
return `Fetching ${this.name}`;
}

override get name() {
return (
decodeAssetNameFromAPI(this.apiResult?.name) ||
this.resource.name ||
zipNameToHumanName(this.resource.slug)
);
}

getURL() {
return this.apiResult?.download_link;
}
}

/**
* The WordPress.org API returns asset names with HTML entities encoded. Decode them.
*
* @param str The string to decode.
* @returns The decoded string.
*/
function decodeAssetNameFromAPI(str?: string) {
return str?.replace(/&#([0-9]+);/g, (entity, entityNum) =>
String.fromCharCode(parseInt(entityNum, 10))
);
}

/**
* Parses the Content-Disposition header to extract the filename.
*
Expand Down Expand Up @@ -632,42 +722,52 @@ export class LiteralDirectoryResource extends Resource<Directory> {
/**
* A `Resource` that represents a WordPress core theme.
*/
export class CoreThemeResource extends FetchResource {
private resource: CoreThemeReference;
export class CoreThemeResource extends APIBasedFetchResource {
override getAPIURL() {
return `https://api.wordpress.org/themes/info/1.2/?action=theme_information&slug=${encodeURIComponent(
zipNameToSlug(this.resource.slug)
)}`;
}

override getURL() {
if (this.resource.slug.endsWith('.zip')) {
return `https://downloads.wordpress.org/themes/${encodeURIComponent(
this.resource.slug
)}`;
}

constructor(resource: CoreThemeReference, progress?: ProgressTracker) {
super(progress);
this.resource = resource;
}
override get name() {
return zipNameToHumanName(this.resource.slug);
}
getURL() {
const zipName = toDirectoryZipName(this.resource.slug);
return `https://downloads.wordpress.org/theme/${zipName}`;
return (
this.apiResult?.download_link ||
`https://downloads.wordpress.org/themes/${encodeURIComponent(
toDirectoryZipName(this.resource.slug)
)}`
);
}
}

/**
* A resource that fetches a WordPress plugin from wordpress.org.
*/
export class CorePluginResource extends FetchResource {
private resource: CorePluginReference;

constructor(resource: CorePluginReference, progress?: ProgressTracker) {
super(progress);
this.resource = resource;
}

/** @inheritDoc */
override get name() {
return zipNameToHumanName(this.resource.slug);
}
export class CorePluginResource extends APIBasedFetchResource {
override getAPIURL() {
return `https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&slug=${encodeURIComponent(
zipNameToSlug(this.resource.slug)
)}`;
}

override getURL() {
if (this.resource.slug.endsWith('.zip')) {
return `https://downloads.wordpress.org/plugins/${encodeURIComponent(
this.resource.slug
)}`;
}

/** @inheritDoc */
getURL() {
const zipName = toDirectoryZipName(this.resource.slug);
return `https://downloads.wordpress.org/plugin/${zipName}`;
return (
this.apiResult?.download_link ||
`https://downloads.wordpress.org/plugins/${encodeURIComponent(
toDirectoryZipName(this.resource.slug)
)}`
);
}
}

Expand All @@ -686,6 +786,17 @@ export function toDirectoryZipName(rawInput: string) {
return rawInput + '.latest-stable.zip';
}

/**
* Transforms a plugin/theme ZIP name into a slug.
* WordPress.org Slugs never contain `.`, we can safely strip the extension.
*
* @param zipName The ZIP name to transform.
* @returns The slug derived from the ZIP name.
*/
export function zipNameToSlug(zipName: string) {
return zipName?.replace(/\..+$/g, '');
}

/**
* A decorator for a resource that adds caching functionality.
*/
Expand Down
10 changes: 8 additions & 2 deletions packages/playground/blueprints/src/lib/steps/activate-theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,26 @@ export interface ActivateThemeStep {
* The name of the theme folder inside wp-content/themes/
*/
themeFolderName: string;
/**
* Optional nice name for the theme, used in progress captions.
*/
themeNiceName?: string;
}

/**
* Activates a WordPress theme (if it's installed).
*
* @param playground The playground client.
* @param themeFolderName The theme folder name.
* @param themeNiceName Optional nice name for the theme, used in progress captions.
*/
export const activateTheme: StepHandler<ActivateThemeStep> = async (
playground,
{ themeFolderName },
{ themeFolderName, themeNiceName },
progress
) => {
progress?.tracker.setCaption(`Activating ${themeFolderName}`);
themeNiceName = themeNiceName || themeFolderName;
progress?.tracker.setCaption(`Activating ${themeNiceName}`);
const docroot = await playground.documentRoot;

const themeFolderPath = `${docroot}/wp-content/themes/${themeFolderName}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export const installPlugin: StepHandler<
// @TODO: Consider validating whether this is a zip file?
const zipFileName =
pluginData.name.split('/').pop() || 'plugin.zip';
assetNiceName = zipNameToHumanName(zipFileName);
assetNiceName = pluginData.name || zipNameToHumanName(zipFileName);

progress?.tracker.setCaption(
`Installing the ${assetNiceName} plugin`
Expand All @@ -144,7 +144,7 @@ export const installPlugin: StepHandler<
targetFolderName: targetFolderName,
});
assetFolderPath = assetResult.assetFolderPath;
assetNiceName = assetResult.assetFolderName;
assetNiceName = pluginData.name || assetResult.assetFolderName;
} else if (pluginData.name.endsWith('.php')) {
const destinationFilePath = joinPaths(
pluginsDirectoryPath,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export const installTheme: StepHandler<
if (themeData instanceof File) {
// @TODO: Consider validating whether this is a zip file?
const zipFileName = themeData.name.split('/').pop() || 'theme.zip';
assetNiceName = zipNameToHumanName(zipFileName);
assetNiceName = themeData.name || zipNameToHumanName(zipFileName);

progress?.tracker.setCaption(`Installing the ${assetNiceName} theme`);
const assetResult = await installAsset(playground, {
Expand Down Expand Up @@ -125,6 +125,7 @@ export const installTheme: StepHandler<
playground,
{
themeFolderName: assetFolderName,
themeNiceName: assetNiceName,
},
progress
);
Expand Down