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

wp now: activate plugin and theme the first time #351

Merged
merged 47 commits into from
May 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
db5ef96
blueprints: install JSDOM. DOMParser is not available in Node
sejas May 16, 2023
c7275d3
wp-now: activate plugin and theme the first time
sejas May 16, 2023
f57d901
playground-client: add jsdom as external
sejas May 16, 2023
932dd3c
blueprints: improve activateTheme docblock
sejas May 16, 2023
e9010af
chore: move jsdom from devDependencies to dependencies in package.json
sejas May 16, 2023
4bfe33a
Merge branch 'trunk' of github.com:Automattic/wordpress-playground in…
sejas May 16, 2023
cb7c70d
wp-now: await unitl the installation finishes
sejas May 16, 2023
d9505de
Merge branch 'trunk' into add/wp-now-plugin-theme-activation
sejas May 16, 2023
37458fb
Merge branch 'trunk' of github.com:Automattic/wordpress-playground in…
sejas May 16, 2023
f2756fb
Merge branch 'trunk' of github.com:Automattic/wordpress-playground in…
sejas May 16, 2023
24821ee
blueprints: refactor activateTheme to use switch_theme
sejas May 16, 2023
43c9023
Merge branch 'add/wp-now-plugin-theme-activation' of github.com:Autom…
sejas May 16, 2023
c02ed81
blueprints: remove unused import for activateTheme
sejas May 16, 2023
ed28a2b
blueprints: make asDOM isomorphic conditionally loading jsdom for NodeJS
sejas May 16, 2023
571ce93
wp-now: remove unused extractThemeName
sejas May 16, 2023
5a44dca
fix code style
sejas May 16, 2023
4a5b2be
blueprints: refactor activatePlugin to accept path and run php commands
sejas May 16, 2023
d814103
wp-now: refactor plugin activation to use the file path
sejas May 16, 2023
fe116cb
blueprints: undo asDOM function and restore jsdom to devDependencies
sejas May 16, 2023
d630396
wp-now: add tests for getPluginFile
sejas May 16, 2023
7fba1f4
Merge branch 'trunk' of github.com:Automattic/wordpress-playground in…
sejas May 16, 2023
11422b8
wp-now: refactor isPluginDirectory to use getPluginFile
sejas May 16, 2023
87fdf1f
playground: undo unintended changes
sejas May 16, 2023
83a794f
wp-now: remove unused slugify
sejas May 16, 2023
12e6eaf
Merge branch 'trunk' of github.com:Automattic/wordpress-playground in…
sejas May 17, 2023
28b2bac
wp-now: remove pluginFile console log
sejas May 17, 2023
1871991
wp-now: display plugin file not found and avoid activate the plugin
sejas May 17, 2023
91e4888
wp-now: getPluginFile use regular expression to match lower cases
sejas May 17, 2023
e07ca89
wp-now: limit plugin search to first 4KB of the file
sejas May 17, 2023
b9fe2c0
wp-now: search to be inside a comment block on getPluginFile
sejas May 17, 2023
ca5e41c
wp-now: add heuristicSort to getPluginFile
sejas May 17, 2023
4d01b54
Merge branch 'trunk' of github.com:Automattic/wordpress-playground in…
sejas May 17, 2023
659d75e
wp-now: remove condition before activatePlugin.
sejas May 17, 2023
d161ef4
blueprints: throw error if required files to activate plugin or theme…
sejas May 17, 2023
5c1f71d
wp-now: add auto activateTheme activatePlugin integration tests
sejas May 17, 2023
4438ed0
wp-now: include tests for second time the plugin should not be installed
sejas May 17, 2023
bda1d0d
wp-now: include tests for second time the theme should not be installed
sejas May 17, 2023
c6f4d3a
Merge branch 'trunk' of github.com:Automattic/wordpress-playground in…
sejas May 17, 2023
2db98e9
wp-now: update getPluginFile regex
sejas May 17, 2023
460fcfb
Merge branch 'trunk' of github.com:Automattic/wordpress-playground in…
sejas May 17, 2023
f719eba
wp-now: refactor activate plugin/theme tests to use getWpNowConfig
sejas May 17, 2023
7eb9767
blueprints: add guard statements for activatePlugin and activateTheme
sejas May 17, 2023
380d9e5
wp-now: increase readFileHead to 8KB
sejas May 17, 2023
49818b0
wp-now: use WordPress core RegEx to match Plugin name
sejas May 17, 2023
ff48d9d
wp-now: refactor readFileHead to independent file
sejas May 17, 2023
a829a6a
wp-now: improve theme inference by using same core regex
sejas May 17, 2023
f736d00
blueprints: fix condition on activatePlugin
sejas May 17, 2023
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
36 changes: 20 additions & 16 deletions packages/playground/blueprints/src/lib/steps/activate-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { StepHandler } from '.';
import { asDOM } from './common';

export interface ActivatePluginStep {
step: 'activatePlugin';
plugin: string;
/* Path to the plugin file relative to the plugins directory. */
pluginPath: string;
}

/**
Expand All @@ -14,22 +14,26 @@ export interface ActivatePluginStep {
*/
export const activatePlugin: StepHandler<ActivatePluginStep> = async (
playground,
{ plugin },
{ pluginPath },
progress
) => {
progress?.tracker.setCaption(`Activating ${plugin}`);
const pluginsPage = asDOM(
await playground.request({
url: '/wp-admin/plugins.php',
})
progress?.tracker.setCaption(`Activating ${pluginPath}`);
const requiredFiles = [
`${playground.documentRoot}/wp-load.php`,
`${playground.documentRoot}/wp-admin/includes/plugin.php`,
];
const requiredFilesExist = requiredFiles.every((file) =>
playground.fileExists(file)
);

const link = pluginsPage.querySelector(
`tr[data-slug="${plugin}"] a`
)! as HTMLAnchorElement;
const href = link.attributes.getNamedItem('href')!.value;

await playground.request({
url: '/wp-admin/' + href,
if (!requiredFilesExist) {
throw new Error(
`Required WordPress files do not exist: ${requiredFiles.join(', ')}`
);
}
await playground.run({
code: `<?php
${requiredFiles.map((file) => `require_once( '${file}' );`).join('\n')}
activate_plugin('${pluginPath}');
`,
});
};
32 changes: 32 additions & 0 deletions packages/playground/blueprints/src/lib/steps/activate-theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { StepHandler } from '.';

export interface ActivateThemeStep {
step: 'activateTheme';
themeFolderName: string;
}

/**
* Activates a WordPress theme in the Playground.
*
* @param playground The playground client.
* @param themeFolderName The theme folder name.
*/
export const activateTheme: StepHandler<ActivateThemeStep> = async (
playground,
{ themeFolderName },
progress
) => {
progress?.tracker.setCaption(`Activating ${themeFolderName}`);
const wpLoadPath = `${playground.documentRoot}/wp-load.php`;
if (!playground.fileExists(wpLoadPath)) {
throw new Error(
`Required WordPress file does not exist: ${wpLoadPath}`
);
}
await playground.run({
code: `<?php
require_once( '${wpLoadPath}' );
switch_theme( '${themeFolderName}' );
`,
});
};
1 change: 1 addition & 0 deletions packages/playground/blueprints/src/lib/steps/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { activatePlugin } from './activate-plugin';
export { activateTheme } from './activate-theme';
export { applyWordPressPatches } from './apply-wordpress-patches';
export {
rm,
Expand Down
2 changes: 2 additions & 0 deletions packages/playground/blueprints/src/lib/steps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
WriteFileStep,
} from './client-methods';
import { DefineWpConfigConstsStep } from './define-wp-config-consts';
import { ActivateThemeStep } from './activate-theme';
import { DefineVirtualWpConfigConstsStep } from './define-virtual-wp-config-consts';

export type Step = GenericStep<FileReference>;
Expand All @@ -38,6 +39,7 @@ export type StepDefinition = Step & {

export type GenericStep<Resource> =
| ActivatePluginStep
| ActivateThemeStep
| ApplyWordPressPatchesStep
| CpStep
| DefineWpConfigConstsStep
Expand Down
2 changes: 1 addition & 1 deletion packages/wp-now/src/tests/mode-examples/theme/style.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Theme Name: Twenty Twenty
Theme Name: Yolo Theme
Text Domain: twentytwenty
Version: 2.2
Tested up to: 6.2
Expand Down
99 changes: 99 additions & 0 deletions packages/wp-now/src/tests/wp-now.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,4 +384,103 @@ describe('Test starting different modes', () => {

expectRequiredRootFiles(requiredFiles, wpNowOptions.documentRoot, php);
});

/**
* Test that startWPNow in "plugin" mode auto installs the plugin.
*/
test('startWPNow auto installs the plugin', async () => {
const projectPath = path.join(tmpExampleDirectory, 'plugin');
const options = await getWpNowConfig({ path: projectPath });
const { php } = await startWPNow(options);
const codeIsPluginActivePhp = `<?php
require_once('${php.documentRoot}/wp-load.php');
require_once('${php.documentRoot}/wp-admin/includes/plugin.php');

if (is_plugin_active('plugin/sample-plugin.php')) {
echo 'plugin/sample-plugin.php is active';
}
`;
const isPluginActive = await php.run({
code: codeIsPluginActivePhp,
});

expect(isPluginActive.text).toContain(
'plugin/sample-plugin.php is active'
);
});

/**
* Test that startWPNow in "plugin" mode does not auto install the plugin the second time.
*/
test('startWPNow auto installs the plugin', async () => {
const projectPath = path.join(tmpExampleDirectory, 'plugin');
const options = await getWpNowConfig({ path: projectPath });
const { php } = await startWPNow(options);
const deactivatePluginPhp = `<?php
require_once('${php.documentRoot}/wp-load.php');
require_once('${php.documentRoot}/wp-admin/includes/plugin.php');
deactivate_plugins('plugin/sample-plugin.php');
`;
await php.run({ code: deactivatePluginPhp });
// Run startWPNow a second time.
const { php: phpSecondTime } = await startWPNow(options);
const codeIsPluginActivePhp = `<?php
require_once('${php.documentRoot}/wp-load.php');
require_once('${php.documentRoot}/wp-admin/includes/plugin.php');

if (is_plugin_active('plugin/sample-plugin.php')) {
echo 'plugin/sample-plugin.php is active';
} else {
echo 'plugin not active';
}
`;
const isPluginActive = await phpSecondTime.run({
code: codeIsPluginActivePhp,
});

expect(isPluginActive.text).toContain('plugin not active');
});

/**
* Test that startWPNow in "theme" mode auto activates the theme.
*/
test('startWPNow auto installs the theme', async () => {
const projectPath = path.join(tmpExampleDirectory, 'theme');
const options = await getWpNowConfig({ path: projectPath });
const { php } = await startWPNow(options);
const codeActiveThemeNamePhp = `<?php
require_once('${php.documentRoot}/wp-load.php');
echo wp_get_theme()->get('Name');
`;
const themeName = await php.run({
code: codeActiveThemeNamePhp,
});

expect(themeName.text).toContain('Yolo Theme');
});

/**
* Test that startWPNow in "theme" mode does not auto activate the theme the second time.
*/
test('startWPNow auto installs the theme', async () => {
const projectPath = path.join(tmpExampleDirectory, 'theme');
const options = await getWpNowConfig({ path: projectPath });
const { php } = await startWPNow(options);
const switchThemePhp = `<?php
require_once('${php.documentRoot}/wp-load.php');
switch_theme('twentytwentythree');
`;
await php.run({ code: switchThemePhp });
// Run startWPNow a second time.
const { php: phpSecondTime } = await startWPNow(options);
const codeActiveThemeNamePhp = `<?php
require_once('${php.documentRoot}/wp-load.php');
echo wp_get_theme()->get('Name');
`;
const themeName = await phpSecondTime.run({
code: codeActiveThemeNamePhp,
});

expect(themeName.text).toContain('Twenty Twenty-Three');
});
});
30 changes: 29 additions & 1 deletion packages/wp-now/src/wp-now.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,20 @@ import {
downloadSqliteIntegrationPlugin,
downloadWordPress,
} from './download';
import {
activatePlugin,
activateTheme,
defineVirtualWpConfigConsts,
login,
} from '@wp-playground/blueprints';
import { WPNowOptions, WPNowMode } from './config';
import { defineVirtualWpConfigConsts, login } from '@wp-playground/blueprints';
import {
isPluginDirectory,
isThemeDirectory,
isWpContentDirectory,
isWordPressDirectory,
isWordPressDevelopDirectory,
getPluginFile,
} from './wp-playground-wordpress';
import { output } from './output';

Expand Down Expand Up @@ -65,6 +71,7 @@ export default async function startWPNow(
await downloadWordPress(options.wordPressVersion);
await downloadSqliteIntegrationPlugin();
await downloadMuPlugins();
const isFirstTimeProject = !fs.existsSync(options.wpContentPath);
switch (options.mode) {
case WPNowMode.WP_CONTENT:
await runWpContentMode(php, options);
Expand All @@ -87,6 +94,14 @@ export default async function startWPNow(
username: 'admin',
password: 'password',
});

if (
isFirstTimeProject &&
[WPNowMode.PLUGIN, WPNowMode.THEME].includes(options.mode)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we also activate everything in wp-content mode?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

💥, I think it is a different use case. But it makes sense to me.
@wojtekn, Would you like to code that functionality?

Copy link
Member

Choose a reason for hiding this comment

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

@sejas @wojtekn Let's hold off activating everything in wp-content mode until we receive some explicit feedback indicating such.

) {
await activatePluginOrTheme(php, options);
}

return {
php,
options,
Expand Down Expand Up @@ -232,6 +247,19 @@ async function initWordPress(
return { initializeDefaultDatabase };
}

async function activatePluginOrTheme(
php: NodePHP,
{ projectPath, mode }: WPNowOptions
) {
if (mode === WPNowMode.PLUGIN) {
const pluginFile = getPluginFile(projectPath);
await activatePlugin(php, { pluginPath: pluginFile });
} else if (mode === WPNowMode.THEME) {
const themeFolderName = path.basename(projectPath);
await activateTheme(php, { themeFolderName });
}
}

function mountMuPlugins(php: NodePHP, vfsDocumentRoot: string) {
php.mount(
path.join(WP_NOW_PATH, 'mu-plugins'),
Expand Down
39 changes: 39 additions & 0 deletions packages/wp-now/src/wp-playground-wordpress/get-plugin-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import fs from 'fs-extra';
import path, { basename } from 'path';
import { readFileHead } from './read-file-head';

/**
* Sorts the files in the given array using a heuristic.
* The plugin file usually has the same name as the project folder.
* @param files The files to sort.
* @returns The sorted files.
*/
function heuristicSort(files: string[], projectPath: string) {
const heuristicsBestGuess = `${basename(projectPath)}.php`;
const heuristicsBestGuessIndex = files.indexOf(heuristicsBestGuess);
if (heuristicsBestGuessIndex !== -1) {
files.splice(heuristicsBestGuessIndex, 1);
files.unshift(heuristicsBestGuess);
}
return files;
}

/**
*
* @param projectPath The path to the plugin.
* @returns Path to the plugin file relative to the plugins directory.
*/
export function getPluginFile(projectPath: string) {
const files = heuristicSort(fs.readdirSync(projectPath), projectPath);
for (const file of files) {
if (file.endsWith('.php')) {
const fileContent = readFileHead(path.join(projectPath, file));
const pluginNameRegex =
/^(?:[ \t]*<\?php)?[ \t/*#@]*Plugin Name:(.*)$/im;
if (pluginNameRegex.test(fileContent)) {
return path.join(path.basename(projectPath), file);
}
}
}
return null;
}
2 changes: 2 additions & 0 deletions packages/wp-now/src/wp-playground-wordpress/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ export * from './is-theme-directory';
export * from './is-wp-content-directory';
export * from './is-wordpress-directory';
export * from './is-wordpress-develop-directory';
export * from './get-plugin-file';
export * from './read-file-head';
18 changes: 3 additions & 15 deletions packages/wp-now/src/wp-playground-wordpress/is-plugin-directory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import fs from 'fs-extra';
import path from 'path';
import { getPluginFile } from './get-plugin-file';

/**
* Checks if the given path is a WordPress plugin.
Expand All @@ -8,17 +7,6 @@ import path from 'path';
* @returns A boolean value indicating whether the project is a WordPress plugin.
*/
export function isPluginDirectory(projectPath: string): Boolean {
const files = fs.readdirSync(projectPath);
for (const file of files) {
if (file.endsWith('.php')) {
const fileContent = fs.readFileSync(
path.join(projectPath, file),
'utf8'
);
if (fileContent.toLowerCase().includes('plugin name:')) {
return true;
}
}
}
return false;
const pluginFile = getPluginFile(projectPath);
return pluginFile !== null;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs from 'fs-extra';
import path from 'path';
import { readFileHead } from './read-file-head';

/**
* Checks if the given path is a WordPress theme directory.
Expand All @@ -12,9 +13,7 @@ export function isThemeDirectory(projectPath: string): Boolean {
if (!styleCSSExists) {
return false;
}
const styleCSS = fs.readFileSync(
path.join(projectPath, 'style.css'),
'utf-8'
);
return styleCSS.includes('Theme Name:');
const styleCSS = readFileHead(path.join(projectPath, 'style.css'));
const themeNameRegex = /^(?:[ \t]*<\?php)?[ \t/*#@]*Theme Name:(.*)$/im;
return themeNameRegex.test(styleCSS);
}
17 changes: 17 additions & 0 deletions packages/wp-now/src/wp-playground-wordpress/read-file-head.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import fs from 'fs-extra';

/**
*
* @param filePath The path to the file to read.
* @param length The number of bytes to read from the file. By default 8KB.
* @returns The first `length` bytes of the file as string.
* @see https://developer.wordpress.org/reference/functions/get_file_data/
*/
export function readFileHead(filePath: string, length = 8192) {
const buffer = Buffer.alloc(length);
const fd = fs.openSync(filePath, 'r');
fs.readSync(fd, buffer, 0, buffer.length, 0);
const fileContentBuffer = buffer.toString('utf8');
fs.closeSync(fd);
return fileContentBuffer.toString();
}
Loading