Skip to content
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ How the app works from a user's point of view — the toolchain it bundles, keep

- Integrate Playground's XDebug.
- Explore bundling MySQL server with the app.
- Potentially integrate with Studio to benefit from PHP version selector, wp-cli integration and other Studio features.
- Potentially integrate with Studio to benefit from wp-cli integration and other Studio features.
- Resolve conflicts in the app. Branches, commits and pushes are already handled for each work item, but when a work item's branch cannot move onto current trunk, the app still hands it to a mentor.
- Internationalization: the app's interface available in several languages, not only English.

Expand Down
6 changes: 3 additions & 3 deletions docs/guide/logs-and-debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ All three panes read in the terminal's own monospace font, so the columns of a P

## The debug.log tab

Anything WordPress or your code writes to the PHP error log — `error_log()` calls, notices, warnings, deprecations, `_doing_it_wrong()`, fatals — appears here while the dev server runs. This works because every site is booted with WordPress's debug constants already set. They are not configurable:
Anything WordPress or your code writes to the PHP error log — `error_log()` calls, notices, warnings, deprecations, `_doing_it_wrong()`, fatals — appears here while the dev server runs. This works because every site is booted with WordPress's debug constants already set. Two of them, `WP_DEBUG` and `SCRIPT_DEBUG`, can be turned off under **Sites** in [Settings](./settings); the rest are not configurable:

| Constant | Value | Effect |
| --- | --- | --- |
| `WP_DEBUG` | `true` | Notices, warnings, and deprecations are reported. |
| `WP_DEBUG` | `true` | Notices, warnings, and deprecations are reported. Off in Settings, notices and deprecations are not; warnings and errors still reach the log and the browser, and `error_log()` calls still reach the log. |
| `WP_DEBUG_LOG` | `true` | They are written to `wp-content/debug.log`, which this panel tails. |
| `WP_DEBUG_DISPLAY` | `true` | Errors are also printed in the browser. |
| `SCRIPT_DEBUG` | `true` | Core serves unminified JS and CSS. |
| `SCRIPT_DEBUG` | `true` | Core serves unminified JS and CSS. Can be turned off in Settings. |
| `WP_DISABLE_FATAL_ERROR_HANDLER` | `true` | A fatal shows the actual error instead of WordPress's "critical error" recovery screen. |
| `AUTOMATIC_UPDATER_DISABLED` | `true` | Core's automatic updater does not run (and does not fill the log with its own messages). |

Expand Down
8 changes: 8 additions & 0 deletions docs/guide/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ The app's settings are in one dialog, opened from the cog at the bottom right of
- **Language** — which language the app is shown in: your system's language, which is the default, or one of the languages the app has a translation for. A change applies after a relaunch, which the dialog offers; relaunching stops running servers and builds, as quitting does. Translations come from [translate.wordpress.org](https://translate.wordpress.org/projects/meta/contributor-toolkit/) and ship with the app once they are mostly complete, so the list grows from release to release.
- **New sites go here** — the folder new sites are created in, each in a subfolder of its own. With one set, the [create-site dialog](./creating-a-site) starts on it; you can still pick another folder for one site without changing the setting. **Forget this folder** clears it, and the dialog goes back to asking each time. A folder that no longer exists is refused when you choose it.

## Sites

What every site's development server runs with. A change applies the next time a server starts; one that is running keeps what it started with until you stop and start it. The open site's details, in the right-hand column, show the PHP version beside the checkout and which debug constants are on.

- **PHP version** — the PHP the site runs on, from the versions the bundled WordPress Playground has. 8.3 unless you choose another.
- **Report notices and deprecations (WP_DEBUG)** — on, notices and deprecations are reported along with warnings and errors, written to `debug.log` and shown in the browser; see [Logs and debugging](./logs-and-debugging). Off, notices and deprecations are not reported; warnings and errors still reach `debug.log` and the browser, and `error_log()` calls still reach `debug.log`: the app's PHP keeps logging and display on whatever WP_DEBUG says.
- **Use unminified scripts (SCRIPT_DEBUG)** — on, Core serves its JavaScript and CSS unminified.

## Account

- **WordPress.org username** and **Event** — who a patch you [hand to a mentor](./submit-mentor) says it is from, and where it was written. The same two answers the handoff asks for, so changing them here changes them there. Leave the event empty when you are not at one. A profile link pasted as the username is kept as the username it names.
Expand Down
Binary file modified docs/public/screenshots/create-site-modal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/public/screenshots/debug-log.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/public/screenshots/dev-server-running.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/public/screenshots/empty-state.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/public/screenshots/gutenberg-site-view.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/public/screenshots/linked-pull-requests.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/public/screenshots/mail-panel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/public/screenshots/site-menu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/public/screenshots/site-view.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/public/screenshots/site-with-tickets.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/public/screenshots/stale-site-notice.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/public/screenshots/trac-ticket-panel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions scripts/screenshots/shots.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,9 @@ const shots = [
{
slug: 'gutenberg-site-view',
tier: 'fixture',
// The details have grown a row (#559): tall enough for their Build
// watch section to be whole.
viewport: { width: 1200, height: 900 },
variant: 'gutenberg',
prepare: async (page) => {
await selectSite(page, 'my-gutenberg-fix');
Expand All @@ -231,6 +234,9 @@ const shots = [
{
slug: 'site-menu',
tier: 'fixture',
// The details have grown a row (#559): tall enough for their Build
// watch section to be whole.
viewport: { width: 1200, height: 900 },
variant: 'seeded',
prepare: async (page) => {
await selectSite(page, 'my-first-patch');
Expand Down Expand Up @@ -364,6 +370,9 @@ const shots = [
{
slug: 'site-view',
tier: 'fixture',
// The details have grown a row (#559): tall enough for their Build
// watch section to be whole.
viewport: { width: 1200, height: 900 },
variant: 'seeded',
prepare: async (page) => {
await openReadySite(page);
Expand Down
41 changes: 35 additions & 6 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,22 @@ function languages() {

ipcMain.handle('i18n:languages', async () => ({ ok: true, languages: await languages() }));

// The PHP versions the bundled Playground can run a site on (#559): what
// its own module says, read once it is asked for. The module is the server
// runner's and not otherwise main's, so it is loaded then and not at start.
let phpVersionsList = null;
function phpVersions() {
if (!phpVersionsList) {
// Not a declared dependency: it is @wp-playground/cli's, at whatever
// version that package pins, which is the one the runner serves with.
const { SupportedPHPVersions } = require('@php-wasm/universal');
phpVersionsList = [...SupportedPHPVersions];
}
return phpVersionsList;
}

ipcMain.handle('playground:php-versions', () => ({ ok: true, versions: phpVersions(), fallback: SETTINGS.phpVersion.fallback }));

// What the settings dialog offers after the language is changed. `quit`, not
// `exit`: the quit sweep ends every child the app started, as it does on any
// quit, and the relaunch is a quit. The new instance gets this one's
Expand Down Expand Up @@ -3501,7 +3517,8 @@ ipcMain.handle('settings:set', async (_e, key, value) => {
const accepted = acceptSetting(key, value, {
isAbsolute: path.isAbsolute,
isDirectory: () => directory,
isLanguage: (tag) => known.some((language) => language.tag === tag)
isLanguage: (tag) => known.some((language) => language.tag === tag),
isPhpVersion: (version) => phpVersions().includes(version)
});
if (!accepted.ok) return { ok: false, error: accepted.error };
await setPreference(key, accepted.value);
Expand Down Expand Up @@ -3834,13 +3851,25 @@ ipcMain.handle('playground:start', async (event, sitePath) => {
const serve = projectTypeForSite(await readSiteMeta(sitePath)).serve;
const isPluginMount = serve.strategy === 'plugin-mount';
const buildDir = path.join(sitePath, 'build');
const serveConfig = isPluginMount
? { strategy: 'plugin-mount', pluginDir: sitePath, pluginSlug: serve.pluginSlug }
: { strategy: 'docroot', docroot: buildDir };
// The PHP version and the debug flags the settings hold (#559), read at
// each start so a change applies to the next. A version the bundled
// Playground no longer has, after a bump, is passed over for the fallback.
const logScope = playgroundLogScope(sitePath);
const settings = readSettings((await getStore()).get('preferences'));
const phpVersion = phpVersions().includes(settings.phpVersion) ? settings.phpVersion : SETTINGS.phpVersion.fallback;
if (phpVersion !== settings.phpVersion) {
logEvent(logScope, `PHP ${settings.phpVersion} is set but this build does not have it; starting on PHP ${phpVersion}`);
}
const serveConfig = {
...(isPluginMount
? { strategy: 'plugin-mount', pluginDir: sitePath, pluginSlug: serve.pluginSlug }
: { strategy: 'docroot', docroot: buildDir }),
phpVersion,
debug: { wpDebug: settings.wpDebug, scriptDebug: settings.scriptDebug }
};
const serveCwd = isPluginMount ? sitePath : buildDir;
const runnerPath = path.join(__dirname, 'server-runner.js');
const logScope = playgroundLogScope(sitePath);
logEvent(logScope, `starting ${serve.strategy} server for ${serveCwd} (smtp port ${(smtp && smtp.port) ? smtp.port : 25})`);
logEvent(logScope, `starting ${serve.strategy} server for ${serveCwd} on PHP ${phpVersion}, WP_DEBUG ${settings.wpDebug ? 'on' : 'off'}, SCRIPT_DEBUG ${settings.scriptDebug ? 'on' : 'off'} (smtp port ${(smtp && smtp.port) ? smtp.port : 25})`);
const child = spawnRunner(runnerPath, [JSON.stringify(serveConfig)], {
cwd: serveCwd,
extraEnv: {
Expand Down
3 changes: 3 additions & 0 deletions src/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ contextBridge.exposeInMainWorld('api', {
listLanguages: () => ipcRenderer.invoke('i18n:languages')
,
relaunch: () => ipcRenderer.invoke('app:relaunch')
,
// The PHP versions a site's server can run on, for the settings.
listPhpVersions: () => ipcRenderer.invoke('playground:php-versions')
,
// The menu's "Settings…" asked for the dialog.
subscribeSettingsOpen: (handler) => {
Expand Down
77 changes: 72 additions & 5 deletions src/renderer/components/settings-dialog.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { useEffect, useId, useMemo, useState } from 'react';
// The segmented control the design has for a choice of a few. The design
// system has no other, and documents this one under these names: it is
// stable in use and has not been given its final export yet.
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis -- see above.
import { __experimentalToggleGroupControl as ToggleGroupControl, __experimentalToggleGroupControlOption as ToggleGroupControlOption } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { Button, Dialog, InputControl, Notice, SelectControl, Stack, Tabs, Text } from '@wordpress/ui';
import { githubAccountLine, newSiteLocationNote, languageItems, languageValue, languageChanged, SYSTEM_LANGUAGE } from '../settings-view.cjs';
import { Button, Dialog, InputControl, Notice, SelectControl, Stack, SwitchControl, Tabs, Text } from '@wordpress/ui';
import { githubAccountLine, newSiteLocationNote, languageItems, languageValue, languageChanged, phpVersionChoice, SYSTEM_LANGUAGE } from '../settings-view.cjs';
import { FolderField } from './folder-field.jsx';

// A notice here is read by its role, and is not also spoken: the dialog it
Expand Down Expand Up @@ -128,6 +133,63 @@ function GeneralTab({ settings, loaded, onChange }) {
);
}

// What a site's development server runs with: the PHP it runs on, from the
// versions the bundled Playground has, and the two debug constants that can
// be turned off. Applied the next time a server starts; one that is running
// keeps what it started with until it is started again.
function SitesTab({ settings, php, onChange }) {
const [error, setError] = useState('');

const keep = async (key, value) => {
const result = await onChange(key, value);
setError(result?.ok ? '' : (result?.error || __('Could not keep that.')));
};
const choice = phpVersionChoice({ versions: php?.versions, fallback: php?.fallback, stored: settings ? settings.phpVersion : null });
// The versions could not be read: said in place of a control with nothing
// on it, and the version stays as it is.
const unread = php?.error ? __('The PHP versions could not be read. Quit the app and open it again.') : '';

return (
<Stack direction="column" gap="xl">
<Text variant="heading-lg" render={<h3 />}>{__('Development server')}</Text>
<Text variant="body-sm">{__('Applies the next time a site’s server starts. A server that is running keeps what it started with.')}</Text>
<ToggleGroupControl
__nextHasNoMarginBottom
__next40pxDefaultSize
isBlock
label={__('PHP version')}
help={unread || choice.note || undefined}
value={choice.value || undefined}
disabled={!settings || !php?.fallback}
onChange={(value) => { if (value) keep('phpVersion', value); }}
>
{(php?.versions || []).map((version) => (
<ToggleGroupControlOption key={version} value={version} label={version} />
))}
</ToggleGroupControl>
<SwitchControl
label={__('Report notices and deprecations (WP_DEBUG)')}
description={__('Off, notices and deprecations are not reported. Warnings and errors still reach debug.log and the browser, and error_log() calls still reach debug.log.')}
checked={settings ? settings.wpDebug : true}
disabled={!settings}
onCheckedChange={(checked) => keep('wpDebug', checked)}
/>
<SwitchControl
label={__('Use unminified scripts (SCRIPT_DEBUG)')}
description={__('Core serves its JavaScript and CSS unminified, so they can be read and stepped through in the browser.')}
checked={settings ? settings.scriptDebug : true}
disabled={!settings}
onCheckedChange={(checked) => keep('scriptDebug', checked)}
/>
{error ? (
<Notice.Root intent="error" role="alert" spokenMessage={SILENT}>
<Notice.Description>{error}</Notice.Description>
</Notice.Root>
) : null}
</Stack>
);
}

// Who the contributor is, as the mentor handoff asks it (#166) and through
// the same answers (useContributorProvenance), so that the two never
// disagree; and the GitHub account the app acts for (#167), which is signed
Expand Down Expand Up @@ -241,21 +303,25 @@ function AccountTab({ wporg }) {
// The tabs and what is on each. Inside the dialog's popup, which is there
// while the dialog is open and not otherwise, so every opening starts on
// General with nothing typed and not yet saved.
function SettingsPanels({ settings, loaded, onChange, wporg }) {
function SettingsPanels({ settings, loaded, php, onChange, wporg }) {
const [tab, setTab] = useState('general');
return (
<Dialog.Content>
<Tabs.Root value={tab} onValueChange={setTab} render={<Stack direction="column" gap="md" />}>
<div className="settings-tabs-bar">
<Tabs.List variant="minimal" className="settings-tabs">
<Tabs.Tab value="general">{__('General')}</Tabs.Tab>
<Tabs.Tab value="sites">{__('Sites')}</Tabs.Tab>
<Tabs.Tab value="account">{__('Account')}</Tabs.Tab>
</Tabs.List>
<hr className="card-divider" />
</div>
<Tabs.Panel value="general" tabIndex={-1} className="settings-panel">
<GeneralTab settings={settings} loaded={loaded} onChange={onChange} />
</Tabs.Panel>
<Tabs.Panel value="sites" tabIndex={-1} className="settings-panel">
<SitesTab settings={settings} php={php} onChange={onChange} />
</Tabs.Panel>
<Tabs.Panel value="account" tabIndex={-1} className="settings-panel">
<AccountTab wporg={wporg} />
</Tabs.Panel>
Expand All @@ -278,19 +344,20 @@ function SettingsPanels({ settings, loaded, onChange, wporg }) {
* @param {boolean} props.open Whether the dialog is open.
* @param {?Object} props.settings The settings, or null while they are read.
* @param {?Object} props.loaded The settings as the window first read them, for what takes a relaunch.
* @param {?Object} props.php The PHP versions the bundle has and the fallback (useSettings), null while unread, `{ error }` when it could not be.
* @param {Function} props.onChange Changes one setting; resolves to `{ ok, settings }` or `{ ok: false, error }`.
* @param {Object} props.wporg The contributor's details and how to change them.
* @param {Function} props.onClose Asked for by the close button, Escape, or a press outside.
*/
export function SettingsDialog({ open, settings, loaded, onChange, wporg, onClose }) {
export function SettingsDialog({ open, settings, loaded, php, onChange, wporg, onClose }) {
return (
<Dialog.Root open={open} onOpenChange={(next) => { if (!next) onClose(); }}>
<Dialog.Popup size="medium" className="settings-dialog">
<Dialog.Header>
<Dialog.Title>{__('Settings')}</Dialog.Title>
<Dialog.CloseIcon />
</Dialog.Header>
<SettingsPanels settings={settings} loaded={loaded} onChange={onChange} wporg={wporg} />
<SettingsPanels settings={settings} loaded={loaded} php={php} onChange={onChange} wporg={wporg} />
</Dialog.Popup>
</Dialog.Root>
);
Expand Down
18 changes: 17 additions & 1 deletion src/renderer/hooks/use-settings.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ import { useCallback, useEffect, useState } from 'react';
// dialog opened before then can say it is still reading them. `loaded` is
// what main answered first and does not change: a setting that takes a
// relaunch, the language, is one whose value now differs from it.
// `php` is what the bundled Playground can run a site on, `{ versions,
// fallback }`, read once with the settings: the dialog offers the versions
// and the details say which one a server starts on, which is the fallback
// where the one set is not among them. Null until read; `{ error }` when it
// could not be, so the dialog can say so rather than offer nothing.
export function useSettings() {
const [settings, setSettings] = useState(null);
const [loaded, setLoaded] = useState(null);
const [php, setPhp] = useState(null);

useEffect(() => {
let cancelled = false;
Expand All @@ -20,6 +26,16 @@ export function useSettings() {
})
// eslint-disable-next-line no-console -- reaches the log file, see the note in useDetectedEditors.
.catch((err) => console.error('Could not read the settings:', err));
window.api.listPhpVersions()
.then((res) => {
if (cancelled) return;
setPhp(res?.ok ? { versions: res.versions, fallback: res.fallback } : { error: true });
})
.catch((err) => {
// eslint-disable-next-line no-console -- see the note above.
console.error('Could not read the PHP versions:', err);
if (!cancelled) setPhp({ error: true });
});
return () => { cancelled = true; };
}, []);

Expand All @@ -39,5 +55,5 @@ export function useSettings() {
return result;
}, []);

return { settings, loaded, change };
return { settings, loaded, php, change };
}
Loading
Loading