Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blueprints: Add ifAlreadyInstalled to installPlugin and installTheme steps #1244

Merged
merged 2 commits into from
Apr 16, 2024
Merged
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
37 changes: 10 additions & 27 deletions packages/playground/blueprints/public/blueprint-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -546,33 +546,6 @@
},
"required": ["file", "step"]
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"progress": {
"type": "object",
"properties": {
"weight": {
"type": "number"
},
"caption": {
"type": "string"
}
},
"additionalProperties": false
},
"step": {
"type": "string",
"const": "importFile"
},
"file": {
"$ref": "#/definitions/FileReference",
"description": "The file to import"
}
},
"required": ["file", "step"]
},
{
"type": "object",
"additionalProperties": false,
Expand Down Expand Up @@ -620,6 +593,11 @@
},
"additionalProperties": false
},
"ifAlreadyInstalled": {
"type": "string",
"enum": ["overwrite", "skip", "error"],
"description": "What to do if the asset already exists."
},
"step": {
"type": "string",
"const": "installPlugin",
Expand Down Expand Up @@ -652,6 +630,11 @@
},
"additionalProperties": false
},
"ifAlreadyInstalled": {
"type": "string",
"enum": ["overwrite", "skip", "error"],
"description": "What to do if the asset already exists."
},
"step": {
"type": "string",
"const": "installTheme",
Expand Down
34 changes: 33 additions & 1 deletion packages/playground/blueprints/src/lib/steps/install-asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,22 @@ export interface InstallAssetOptions {
* </code>
*/
targetPath: string;
/**
* What to do if the asset already exists.
*/
ifAlreadyInstalled?: 'overwrite' | 'skip' | 'error';
}

/**
* Install asset: Extract folder from zip file and move it to target
*/
export async function installAsset(
playground: UniversalPHP,
{ targetPath, zipFile }: InstallAssetOptions
{
targetPath,
zipFile,
ifAlreadyInstalled = 'overwrite',
}: InstallAssetOptions
): Promise<{
assetFolderPath: string;
assetFolderName: string;
Expand Down Expand Up @@ -75,6 +83,30 @@ export async function installAsset(

// Move asset folder to target path
const assetFolderPath = `${targetPath}/${assetFolderName}`;

// Handle the scenario when the asset is already installed.
if (await playground.fileExists(assetFolderPath)) {
if (!(await playground.isDir(assetFolderPath))) {
throw new Error(
`Cannot install asset ${assetFolderName} to ${assetFolderPath} because a file with the same name already exists. Note it's a file, not a directory! Is this by mistake?`
);
Comment on lines +90 to +92
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plugins can be a file, for example, Hello Dolly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good spot!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, that's not a big deal in this case because we always assume a folder name when unzipping a plugin. If the zip contains just a hello-dolly.php file, we'll make up a folder name. It's not perfect, but that's how it works today and changing it is outside of scope of this PR. I'm happy to find a better solution in Blueprints v2.

}
if (ifAlreadyInstalled === 'overwrite') {
await playground.rmdir(assetFolderPath, {
recursive: true,
});
} else if (ifAlreadyInstalled === 'skip') {
return {
assetFolderPath,
assetFolderName,
};
} else {
throw new Error(
`Cannot install asset ${assetFolderName} to ${targetPath} because it already exists and ` +
`the ifAlreadyInstalled option was set to ${ifAlreadyInstalled}`
);
}
}
await playground.mv(tmpAssetPath, assetFolderPath);

return {
Expand Down
220 changes: 133 additions & 87 deletions packages/playground/blueprints/src/lib/steps/install-plugin.spec.ts
Original file line number Diff line number Diff line change
@@ -1,113 +1,159 @@
import { NodePHP } from '@php-wasm/node';
import { compileBlueprint, runBlueprintSteps } from '../compile';
import { RecommendedPHPVersion } from '@wp-playground/wordpress';
import { installPlugin } from './install-plugin';
import { phpVar } from '@php-wasm/util';

async function zipFiles(
php: NodePHP,
fileName: string,
files: Record<string, string>
) {
const zipFileName = 'test.zip';
const zipFilePath = `/${zipFileName}`;

await php.run({
code: `<?php $zip = new ZipArchive();
$zip->open("${zipFileName}", ZIPARCHIVE::CREATE);
$files = ${phpVar(files)};
foreach($files as $path => $content) {
$zip->addFromString($path, $content);
}
$zip->close();`,
});

describe('Blueprint step installPlugin', () => {
let php: NodePHP;
beforeEach(async () => {
php = await NodePHP.load(RecommendedPHPVersion, {
const zip = await php.readFileAsBuffer(zipFilePath);
php.unlink(zipFilePath);
return new File([zip], fileName);
}

describe('Blueprint step installPlugin – without a root-level folder', () => {
it('should install a plugin even when it is zipped directly without a root-level folder', async () => {
const php = await NodePHP.load(RecommendedPHPVersion, {
requestHandler: {
documentRoot: '/wordpress',
},
});
});

it('should install a plugin', async () => {
// Create test plugin
const pluginName = 'test-plugin';

php.mkdir(`/${pluginName}`);
php.writeFile(
`/${pluginName}/index.php`,
`/**\n * Plugin Name: Test Plugin`
);

// Note the package name is different from plugin folder name
const zipFileName = `${pluginName}-0.0.1.zip`;

await php.run({
code: `<?php $zip = new ZipArchive();
$zip->open("${zipFileName}", ZIPARCHIVE::CREATE);
$zip->addFile("/${pluginName}/index.php");
$zip->close();`,
});

php.rmdir(`/${pluginName}`);

expect(php.fileExists(zipFileName)).toBe(true);

// Create plugins folder
const rootPath = await php.documentRoot;
const rootPath = php.documentRoot;
const pluginsPath = `${rootPath}/wp-content/plugins`;

php.mkdir(pluginsPath);

await runBlueprintSteps(
compileBlueprint({
steps: [
{
step: 'installPlugin',
pluginZipFile: {
resource: 'vfs',
path: zipFileName,
},
options: {
activate: false,
},
},
],
}),
php
);
// Create test plugin
const pluginName = 'test-plugin';

php.unlink(zipFileName);
await installPlugin(php, {
pluginZipFile: await zipFiles(
php,
// Note the ZIP filename is different from plugin folder name
`${pluginName}-0.0.1.zip`,
{
'index.php': `/**\n * Plugin Name: Test Plugin`,
}
),
ifAlreadyInstalled: 'overwrite',
options: {
activate: false,
},
});

expect(php.fileExists(`${pluginsPath}/${pluginName}`)).toBe(true);
expect(php.fileExists(`${pluginsPath}/${pluginName}-0.0.1`)).toBe(true);
});
});

it('should install a plugin even when it is zipped directly without a root-level folder', async () => {
// Create test plugin
const pluginName = 'test-plugin';

php.writeFile('/index.php', `/**\n * Plugin Name: Test Plugin`);
describe('Blueprint step installPlugin', () => {
let php: NodePHP;
// Create plugins folder
let rootPath = '';
let installedPluginPath = '';
const pluginName = 'test-plugin';
const zipFileName = `${pluginName}-0.0.1.zip`;
beforeEach(async () => {
php = await NodePHP.load(RecommendedPHPVersion, {
requestHandler: {
documentRoot: '/wordpress',
},
});
rootPath = php.documentRoot;
php.mkdir(`${rootPath}/wp-content/plugins`);
installedPluginPath = `${rootPath}/wp-content/plugins/${pluginName}`;
});

// Note the package name is different from plugin folder name
const zipFileName = `${pluginName}-0.0.1.zip`;
afterEach(() => {
php.exit();
});

await php.run({
code: `<?php $zip = new ZipArchive();
$zip->open("${zipFileName}", ZIPARCHIVE::CREATE);
$zip->addFile("/index.php");
$zip->close();`,
it('should install a plugin', async () => {
await installPlugin(php, {
pluginZipFile: await zipFiles(php, zipFileName, {
[`${pluginName}/index.php`]: `/**\n * Plugin Name: Test Plugin`,
}),
ifAlreadyInstalled: 'overwrite',
options: {
activate: false,
},
});
expect(php.fileExists(installedPluginPath)).toBe(true);
});

expect(php.fileExists(zipFileName)).toBe(true);
describe('ifAlreadyInstalled option', () => {
beforeEach(async () => {
await installPlugin(php, {
pluginZipFile: await zipFiles(php, zipFileName, {
[`${pluginName}/index.php`]: `/**\n * Plugin Name: Test Plugin`,
}),
ifAlreadyInstalled: 'overwrite',
options: {
activate: false,
},
});
});

// Create plugins folder
const rootPath = await php.documentRoot;
const pluginsPath = `${rootPath}/wp-content/plugins`;
it('ifAlreadyInstalled=overwrite should overwrite the plugin if it already exists', async () => {
// Install the plugin
await installPlugin(php, {
pluginZipFile: await zipFiles(php, zipFileName, {
[`${pluginName}/index.php`]: `/**\n * Plugin Name: A different Plugin`,
}),
ifAlreadyInstalled: 'overwrite',
options: {
activate: false,
},
});
expect(
php.readFileAsText(`${installedPluginPath}/index.php`)
).toContain('Plugin Name: A different Plugin');
});

php.mkdir(pluginsPath);
it('ifAlreadyInstalled=skip should skip the plugin if it already exists', async () => {
// Install the plugin
await installPlugin(php, {
pluginZipFile: await zipFiles(php, zipFileName, {
[`${pluginName}/index.php`]: `/**\n * Plugin Name: A different Plugin`,
}),
ifAlreadyInstalled: 'skip',
options: {
activate: false,
},
});
expect(
php.readFileAsText(`${installedPluginPath}/index.php`)
).toContain('Plugin Name: Test Plugin');
});

await runBlueprintSteps(
compileBlueprint({
steps: [
{
step: 'installPlugin',
pluginZipFile: {
resource: 'vfs',
path: zipFileName,
},
options: {
activate: false,
},
it('ifAlreadyInstalled=error should throw an error if the plugin already exists', async () => {
// Install the plugin
await expect(
installPlugin(php, {
pluginZipFile: await zipFiles(php, zipFileName, {
[`${pluginName}/index.php`]: `/**\n * Plugin Name: A different Plugin`,
}),
ifAlreadyInstalled: 'error',
options: {
activate: false,
},
],
}),
php
);

php.unlink(zipFileName);
expect(php.fileExists(`${pluginsPath}/${pluginName}-0.0.1`)).toBe(true);
})
).rejects.toThrowError();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { StepHandler } from '.';
import { installAsset } from './install-asset';
import { InstallAssetOptions, installAsset } from './install-asset';
import { activatePlugin } from './activate-plugin';
import { zipNameToHumanName } from '../utils/zip-name-to-human-name';

Expand All @@ -23,7 +23,8 @@ import { zipNameToHumanName } from '../utils/zip-name-to-human-name';
* }
* </code>
*/
export interface InstallPluginStep<ResourceType> {
export interface InstallPluginStep<ResourceType>
extends Pick<InstallAssetOptions, 'ifAlreadyInstalled'> {
/**
* The step identifier.
*/
Expand Down Expand Up @@ -54,14 +55,15 @@ export interface InstallPluginOptions {
*/
export const installPlugin: StepHandler<InstallPluginStep<File>> = async (
playground,
{ pluginZipFile, options = {} },
{ pluginZipFile, ifAlreadyInstalled, options = {} },
progress?
) => {
const zipFileName = pluginZipFile.name.split('/').pop() || 'plugin.zip';
const zipNiceName = zipNameToHumanName(zipFileName);

progress?.tracker.setCaption(`Installing the ${zipNiceName} plugin`);
const { assetFolderPath } = await installAsset(playground, {
ifAlreadyInstalled,
zipFile: pluginZipFile,
targetPath: `${await playground.documentRoot}/wp-content/plugins`,
});
Expand Down
Loading
Loading