Skip to content

Commit

Permalink
feat: add devcontainer functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
Luc-cpl committed Jun 2, 2024
1 parent 12473b7 commit 3b11c13
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 15 deletions.
2 changes: 2 additions & 0 deletions CHANGEOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Console log colors for better readability
- Add a guest user home directory in CLI container to avoid permission issues
- Add `wp-setup code` command to open the project with Visual Studio Code with devcontainer support

### Changed

Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,29 @@ npx wp-setup run -w . wp-cli composer require pestphp/pest yoast/phpunit-polyfil

Then you can change the `global-pest` calls to `./vendor/bin/pest` in the commands above.


## Running in devcontainer

If you are using [VSCode](https://code.visualstudio.com/), you can easily run your project in a devcontainer with the following command:

```bash
npx wp-setup code
```

This will open the WP CLI container in your editor at WordPress root directory.

Optionally, you can pass the `--workdir` (`-w`) flag to change the working directory with support to your mapped volumes in your setup configuration.

```bash
npx wp-setup code --workdir .
```

If you want to open using the test CLI container, you can run with the `--test` flag:

```bash
npx wp-setup code --test
```

## Todo

- [x] - Start Command
Expand Down
4 changes: 4 additions & 0 deletions docker/wordpress-cli/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,8 @@ RUN chmod -R 777 /wordpress-tests-lib
RUN mv /usr/local/bin/install-wp-tests.sh /usr/local/bin/install-wp-tests
RUN chmod +x /usr/local/bin/install-wp-tests

# Create a single home directory for all users
RUN mkdir -p /home/guest && chmod 777 /home/guest
ENV HOME=/home/guest

USER ${USER}
70 changes: 55 additions & 15 deletions src/commands/dockerCommands.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import AbstractCommand from './abstractCommand';
import { ExecSyncOptions } from 'node:child_process';
import { join } from 'node:path';
import { exec as coreExec } from 'node:child_process';
import { VolumeInterface } from '@/interfaces/docker';
import { SetupInterface } from '@/interfaces/wordpress';
import { parseVolume } from '@/helpers/docker';
Expand All @@ -9,6 +10,7 @@ import { confirm } from "@/helpers/cli";
import { deleteVolume, exec, getServices } from "@/services/docker";
import { render } from "@/services/template";
import { runSetup } from '@/services/wordpress';
import { ConfigInterface } from '@/interfaces/setup';

export default class DockerCommands extends AbstractCommand {
public async start({ xdebug } : { xdebug?: boolean }) {
Expand Down Expand Up @@ -81,21 +83,7 @@ export default class DockerCommands extends AbstractCommand {
this.error(`The service "${service}" does not exist.`);
}

if (workdir && setupFile && !workdir.startsWith('/')) {
const setContainerPath = (volume: VolumeInterface, basePath = '') => ({
host: volume.host.includes('${PWD}') ? volume.host.replace('${PWD}', process.cwd()) : volume.host,
container: basePath + volume.container,
});

const plugins = (setupFile.plugins ?? []).map(parseVolume)
.map(volume => setContainerPath(volume, '/var/www/html/wp-content/plugins/'));
const themes = (setupFile.themes ?? []).map(parseVolume)
.map(volume => setContainerPath(volume, '/var/www/html/wp-content/themes/'));
const volumes = (setupFile.themes ?? []).map(parseVolume)
.map(volume => setContainerPath(volume));
const directory = join(process.cwd(), workdir);
workdir = [...plugins, ...themes, ...volumes].find(volume => volume.host === directory)?.container ?? workdir;
}
workdir = this.getWorkdir(setupFile, workdir);

const config = await this.getConfig();
const running = (await getServices(config))
Expand Down Expand Up @@ -126,6 +114,58 @@ export default class DockerCommands extends AbstractCommand {
return this.run('wp-test-cli', command);
}

async code({ editor, workdir = false, test = false } : { workdir: string|false, editor?: string, test: boolean }) {
const setupFile = getJsonFile(`${process.cwd()}/wp-setup.json`);
workdir = this.getWorkdir(setupFile, workdir);
editor = editor ?? setupFile?.editor ?? 'vscode';
const serviceName = test ? 'wp-test-cli' : 'wp-cli';
const service = (await getServices(await this.getConfig()))
.filter(service => service.State === 'running')
.find(service => service.Service === serviceName);

if (!service) {
this.error(`The service "${serviceName}" is not running.`);
}

const containerHex = this.containerIdToHex(service?.ID ?? '');

if (editor === 'vscode') {
const command = `code --folder-uri=vscode-remote://attached-container+${containerHex}${workdir ? workdir : '/var/www/html'}`;
coreExec(command);
this.success();
}

this.error(`The editor "${editor}" is not supported.`);
}

private containerIdToHex(containerId: string) {
let hexString = '';
for (let i = 0; i < containerId.length; i++) {
hexString += containerId.charCodeAt(i).toString(16).padStart(2, '0');
}
return hexString;
}

private getWorkdir(setupFile: ConfigInterface|null, workdir: string|false) {
if (workdir && setupFile && !workdir.startsWith('/')) {
const setContainerPath = (volume: VolumeInterface, basePath = '') => ({
host: volume.host.includes('${PWD}') ? volume.host.replace('${PWD}', process.cwd()) : volume.host,
container: basePath + volume.container,
});

const plugins = (setupFile.plugins ?? []).map(parseVolume)
.map(volume => setContainerPath(volume, '/var/www/html/wp-content/plugins/'));
const themes = (setupFile.themes ?? []).map(parseVolume)
.map(volume => setContainerPath(volume, '/var/www/html/wp-content/themes/'));
const volumes = (setupFile.themes ?? []).map(parseVolume)
.map(volume => setContainerPath(volume));
const directory = join(process.cwd(), workdir);
workdir = [...plugins, ...themes, ...volumes].find(volume => volume.host === directory)?.container ?? workdir;
}

return workdir;
}

private exec = async (command: string, options = {} as ExecSyncOptions) => {
const config = await this.getConfig();
if (options.stdio === undefined) {
Expand Down
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,11 @@ program.command('wp-test')
.allowUnknownOption()
.action(command => docker.wpCliTest(command));

program.command('code')
.description('Open the code editor in the development environment.')
.option('-w, --workdir <directory>', 'Can be a binded relative path from host or an absolute path in the container (default to service workdir).')
.option('-e, --editor <editor>', 'The code editor to use (default to the one in the setup file).')
.option('--test', 'Open the code editor in the test environment.', false)
.action(options => docker.code(options));

program.parse();
2 changes: 2 additions & 0 deletions src/interfaces/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export interface ComposeExecInterface {
}

export interface DockerPsItem {
ID: string;
Name: string;
State: string;
Service: string;
}
1 change: 1 addition & 0 deletions src/interfaces/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { VolumeInterface } from "./docker";

export interface ConfigInterface {
[key: string]: unknown;
editor: 'vscode' | null;
include: string;
host: string;
multisite: boolean|'subdomain'|'subdirectory';
Expand Down

0 comments on commit 3b11c13

Please sign in to comment.