Skip to content

Commit 53c7650

Browse files
authored
[Playground CLI] Move mounting code to mount.ts (#2362)
## Motivation for the change, related issues Consolidates two mounting-related files to sets the stage for #2360. Right now it diffs two deleted files with one added file and it's difficult to reason about. There are no code changes other than relocating the code. ## Testing Instructions (or ideally a Blueprint) CI
1 parent b943763 commit 53c7650

File tree

4 files changed

+195
-196
lines changed

4 files changed

+195
-196
lines changed

packages/playground/cli/src/cli-auto-mount.ts

Lines changed: 0 additions & 190 deletions
This file was deleted.

packages/playground/cli/src/mount.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
import { basename, join } from 'path';
2+
import type {
3+
BlueprintDeclaration,
4+
StepDefinition,
5+
} from '@wp-playground/blueprints';
6+
import fs from 'fs';
7+
import type { RunCLIArgs } from './run-cli';
18
import { existsSync } from 'fs';
29
import path from 'path';
310
import { createNodeFsMountHandler } from '@php-wasm/node';
@@ -83,3 +90,185 @@ export function mountResources(php: PHP, mounts: Mount[]) {
8390
php.mount(mount.vfsPath, createNodeFsMountHandler(mount.hostPath));
8491
}
8592
}
93+
94+
export function expandAutoMounts(args: RunCLIArgs): RunCLIArgs {
95+
const path = process.cwd();
96+
97+
const mount = [...(args.mount || [])];
98+
const mountBeforeInstall = [...(args.mountBeforeInstall || [])];
99+
100+
if (isPluginDirectory(path)) {
101+
const pluginName = basename(path);
102+
mount.push({
103+
hostPath: path,
104+
vfsPath: `/wordpress/wp-content/plugins/${pluginName}`,
105+
});
106+
} else if (isThemeDirectory(path)) {
107+
const themeName = basename(path);
108+
mount.push({
109+
hostPath: path,
110+
vfsPath: `/wordpress/wp-content/themes/${themeName}`,
111+
});
112+
} else if (containsWpContentDirectories(path)) {
113+
mount.push(...wpContentMounts(path));
114+
} else if (containsFullWordPressInstallation(path)) {
115+
/**
116+
* We don't want Playground and WordPress to modify the OS filesystem on their own
117+
* by creating files like wp-config.php or wp-content/db.php.
118+
* To ensure WordPress can write to the /wordpress/ and /wordpress/wp-content/ directories,
119+
* we leave these directories as MEMFS nodes and mount individual files
120+
* and directories into them instead of mounting the entire directory as a NODEFS node.
121+
*/
122+
const files = fs.readdirSync(path);
123+
const mounts: Mount[] = [];
124+
for (const file of files) {
125+
if (file.startsWith('wp-content')) {
126+
continue;
127+
}
128+
mounts.push({
129+
hostPath: `${path}/${file}`,
130+
vfsPath: `/wordpress/${file}`,
131+
});
132+
}
133+
mountBeforeInstall.push(
134+
...mounts,
135+
...wpContentMounts(join(path, 'wp-content'))
136+
);
137+
} else {
138+
/**
139+
* By default, mount the current working directory as the Playground root.
140+
* This allows users to run and PHP or HTML files using the Playground CLI.
141+
*/
142+
mount.push({ hostPath: path, vfsPath: '/wordpress' });
143+
}
144+
145+
const blueprint = (args.blueprint as BlueprintDeclaration) || {};
146+
blueprint.steps = [...(blueprint.steps || []), ...getSteps(path)];
147+
148+
/**
149+
* If Playground is mounting a full WordPress directory,
150+
* it doesn't need to setup WordPress.
151+
*/
152+
const skipWordPressSetup =
153+
args.skipWordPressSetup || containsFullWordPressInstallation(path);
154+
155+
return {
156+
...args,
157+
blueprint,
158+
mount,
159+
mountBeforeInstall,
160+
skipWordPressSetup,
161+
} as RunCLIArgs;
162+
}
163+
164+
export function containsFullWordPressInstallation(path: string): boolean {
165+
const files = fs.readdirSync(path);
166+
return (
167+
files.includes('wp-admin') &&
168+
files.includes('wp-includes') &&
169+
files.includes('wp-content')
170+
);
171+
}
172+
173+
export function containsWpContentDirectories(path: string): boolean {
174+
const files = fs.readdirSync(path);
175+
return (
176+
files.includes('themes') ||
177+
files.includes('plugins') ||
178+
files.includes('mu-plugins') ||
179+
files.includes('uploads')
180+
);
181+
}
182+
183+
export function isThemeDirectory(path: string): boolean {
184+
const files = fs.readdirSync(path);
185+
if (!files.includes('style.css')) {
186+
return false;
187+
}
188+
const styleCssContent = fs.readFileSync(join(path, 'style.css'), 'utf8');
189+
const themeNameRegex = /^(?:[ \t]*<\?php)?[ \t/*#@]*Theme Name:(.*)$/im;
190+
return !!themeNameRegex.exec(styleCssContent);
191+
}
192+
193+
export function isPluginDirectory(path: string): boolean {
194+
const files = fs.readdirSync(path);
195+
const pluginNameRegex = /^(?:[ \t]*<\?php)?[ \t/*#@]*Plugin Name:(.*)$/im;
196+
const pluginNameMatch = files
197+
.filter((file) => file.endsWith('.php'))
198+
.find((file) => {
199+
const fileContent = fs.readFileSync(join(path, file), 'utf8');
200+
return !!pluginNameRegex.exec(fileContent);
201+
});
202+
return !!pluginNameMatch;
203+
}
204+
205+
/**
206+
* Returns a list of files and directories in the wp-content directory
207+
* to be mounted individually.
208+
*
209+
* This is needed because WordPress needs to be able to write to the
210+
* wp-content directory without Playground modifying the OS filesystem.
211+
*
212+
* See expandAutoMounts for more details.
213+
*/
214+
export function wpContentMounts(wpContentDir: string): Mount[] {
215+
const files = fs.readdirSync(wpContentDir);
216+
return (
217+
files
218+
/**
219+
* index.php is added by WordPress automatically and
220+
* can't be mounted from the current working directory
221+
* because it already exists.
222+
*
223+
* Because index.php should be empty, it's safe to not include it.
224+
*/
225+
.filter((file) => !file.startsWith('index.php'))
226+
.map((file) => ({
227+
hostPath: `${wpContentDir}/${file}`,
228+
vfsPath: `/wordpress/wp-content/${file}`,
229+
}))
230+
);
231+
}
232+
233+
export function getSteps(path: string): StepDefinition[] {
234+
if (isPluginDirectory(path)) {
235+
return [
236+
{
237+
step: 'activatePlugin',
238+
pluginPath: `/wordpress/wp-content/plugins/${basename(path)}`,
239+
},
240+
];
241+
} else if (isThemeDirectory(path)) {
242+
return [
243+
{
244+
step: 'activateTheme',
245+
themeFolderName: basename(path),
246+
},
247+
];
248+
} else if (
249+
containsWpContentDirectories(path) ||
250+
containsFullWordPressInstallation(path)
251+
) {
252+
/**
253+
* Playground needs to ensure there is an active theme.
254+
* Otherwise when WordPress loads it will show a white screen.
255+
*/
256+
return [
257+
{
258+
step: 'runPHP',
259+
code: `<?php
260+
require_once '/wordpress/wp-load.php';
261+
$theme = wp_get_theme();
262+
if (!$theme->exists()) {
263+
$themes = wp_get_themes();
264+
if (count($themes) > 0) {
265+
$themeName = array_keys($themes)[0];
266+
switch_theme($themeName);
267+
}
268+
}
269+
`,
270+
},
271+
];
272+
}
273+
return [];
274+
}

packages/playground/cli/src/resolve-blueprint.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export async function resolveBlueprint({
6363
switch (extension) {
6464
case '.zip':
6565
return ZipFilesystem.fromArrayBuffer(
66-
fs.readFileSync(blueprintPath)
66+
fs.readFileSync(blueprintPath).buffer as ArrayBuffer
6767
);
6868
case '.json': {
6969
const blueprintText = fs.readFileSync(blueprintPath, 'utf-8');

0 commit comments

Comments
 (0)