@42wp/dev-env 1.0.0 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -2
- package/package.json +1 -1
- package/src/cli.js +9 -1
- package/src/commands/init.js +66 -0
- package/src/commands/start.js +97 -5
- package/src/lib/config.js +20 -0
- package/src/lib/docker.js +21 -2
- package/src/lib/projectConfig.js +91 -0
- package/src/lib/projectState.js +66 -0
- package/src/locales/en.js +19 -1
- package/src/locales/pt.js +19 -1
- package/src/templates/demo-seed.php +159 -1
- package/src/templates/project.Dockerfile +10 -0
- package/src/templates/wp-config.php +3 -0
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ This installs the `42wp` command globally.
|
|
|
25
25
|
## Usage
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
+
42wp init [project] # scaffold a .42wp-dev-env with this project's options
|
|
28
29
|
42wp global start # start Traefik + MySQL + phpMyAdmin + Mailpit
|
|
29
30
|
42wp start <project> # build & start a project (run from your theme/plugin repo)
|
|
30
31
|
42wp update <project> # update an existing project to a newer WordPress image
|
|
@@ -37,6 +38,44 @@ This installs the `42wp` command globally.
|
|
|
37
38
|
|
|
38
39
|
Run `42wp` with no arguments for the full command list.
|
|
39
40
|
|
|
41
|
+
### Project config (`.42wp-dev-env`)
|
|
42
|
+
|
|
43
|
+
Drop a `.42wp-dev-env` file in your project repo to save its `42wp start` options,
|
|
44
|
+
so you (and your team) can just run `42wp start` with no flags. Scaffold one with
|
|
45
|
+
`42wp init`:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
42wp init jovempan --subdomains --vip --locale pt_BR --theme 42wp-jovem-pan --demo-content
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
That writes:
|
|
52
|
+
|
|
53
|
+
```ini
|
|
54
|
+
project_slug=jovempan
|
|
55
|
+
multisite=subdomain # false | subdir | subdomain
|
|
56
|
+
username=admin
|
|
57
|
+
password=password
|
|
58
|
+
locale=pt_BR
|
|
59
|
+
default_theme=42wp-jovem-pan
|
|
60
|
+
wpvip=true
|
|
61
|
+
initial_content=200
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`42wp start`, run in that directory, reads the file and applies it — and **CLI
|
|
65
|
+
flags override** any value for a one-off run. With `project_slug` set you can run
|
|
66
|
+
`42wp start` with no name. `42wp init` won't overwrite an existing file unless you
|
|
67
|
+
pass `--force`.
|
|
68
|
+
|
|
69
|
+
| Key | Effect |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| `project_slug` | project name (so `42wp start` needs no argument) |
|
|
72
|
+
| `multisite` | `false`, `subdir` or `subdomain` |
|
|
73
|
+
| `username` / `password` | admin credentials (plaintext — local dev only) |
|
|
74
|
+
| `locale` | WordPress site language, e.g. `pt_BR` (`pt-br` is normalized) |
|
|
75
|
+
| `default_theme` | theme to `wp theme activate` after install |
|
|
76
|
+
| `wpvip` | clone + mount the VIP mu-plugins |
|
|
77
|
+
| `initial_content` | `true` (200 posts) or a number |
|
|
78
|
+
|
|
40
79
|
### WordPress version
|
|
41
80
|
|
|
42
81
|
By default projects are built on **`wordpress:php8.4-apache`** (newest WordPress,
|
|
@@ -93,6 +132,21 @@ posts (it doesn't skip when posts already exist), and a failure never aborts
|
|
|
93
132
|
registers itself (e.g. the `42-framework` mu-plugin) — the seeder does not register
|
|
94
133
|
any taxonomy. If `42wp_author` isn't available, the author links are simply skipped.
|
|
95
134
|
|
|
135
|
+
When the project ships the **42-framework** (detected by `client-mu-plugins/42-framework`
|
|
136
|
+
or a `composer.json` that requires `42wp/wp-mu-plugins`), the seeder additionally
|
|
137
|
+
configures — **once, idempotently** — a fuller site:
|
|
138
|
+
|
|
139
|
+
- the `/%category%/%postname%/` permalink structure
|
|
140
|
+
- a `42wp_homepage` post set as the static front page (falls back to a regular page
|
|
141
|
+
if the type isn't registered)
|
|
142
|
+
- 4 category-driven menus — Header Primário / Secundário and Footer Primário / Social
|
|
143
|
+
(with a nested footer and YouTube/X/Instagram links) — assigned to the theme's
|
|
144
|
+
registered menu locations
|
|
145
|
+
|
|
146
|
+
Re-running (or `42wp seed`) reuses the existing homepage and menus instead of
|
|
147
|
+
duplicating them; only posts keep appending. On a plain WordPress site these extra
|
|
148
|
+
steps are skipped and only the content above is seeded.
|
|
149
|
+
|
|
96
150
|
To add demo content to an **already-running** project without recreating it, use
|
|
97
151
|
`seed`:
|
|
98
152
|
|
|
@@ -111,6 +165,21 @@ project on `start`:
|
|
|
111
165
|
42wp start my-theme --user joao --pass s3cr3t
|
|
112
166
|
```
|
|
113
167
|
|
|
168
|
+
### Site language
|
|
169
|
+
|
|
170
|
+
New sites install in English (`en_US`) by default. Pick a WordPress locale with
|
|
171
|
+
`--locale`; the language pack is downloaded and activated automatically:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
42wp start my-site --locale pt_BR # Brazilian Portuguese
|
|
175
|
+
42wp start my-site --locale es_ES # Spanish (Spain)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Set a default for every project with the `FORTYTWO_WP_LOCALE` env var. This is the
|
|
179
|
+
**site** language — distinct from `--lang`, which controls the `42wp` CLI's own
|
|
180
|
+
messages (`en`/`pt`). The pack is downloaded into `wp-content/languages`; if the
|
|
181
|
+
download fails (e.g. offline), the site stays in English and `start` continues.
|
|
182
|
+
|
|
114
183
|
### Multisite
|
|
115
184
|
|
|
116
185
|
Start a site as a WordPress network with `--multisite` (subdirectory) or add
|
|
@@ -177,7 +246,7 @@ State lives under `~/.42wp` (override with `FORTYTWO_HOME`):
|
|
|
177
246
|
~/.42wp/
|
|
178
247
|
docker-compose.global.yml # the global layer (created on first run)
|
|
179
248
|
mysql-data/ # persisted MySQL data
|
|
180
|
-
projects/<project>/ # generated wp-config.php, Dockerfile, docker-compose.yml
|
|
249
|
+
projects/<project>/ # generated wp-config.php, Dockerfile, docker-compose.yml, meta.json
|
|
181
250
|
```
|
|
182
251
|
|
|
183
252
|
`42wp start` generates an ephemeral `wp-config.php` (fetching real salts from
|
|
@@ -186,13 +255,30 @@ api.wordpress.org, with a local fallback when offline), a `Dockerfile`, and a
|
|
|
186
255
|
`wp core install`. The DB name is the project name with hyphens turned into
|
|
187
256
|
underscores; a trailing `.suffix` (e.g. `.localhost`) is stripped.
|
|
188
257
|
|
|
258
|
+
**Where a project is bound.** `42wp start` mounts the directory you run it from as
|
|
259
|
+
`wp-content`, so it first checks that directory looks like one — it must contain
|
|
260
|
+
`plugins/` and `themes/`, otherwise `start` refuses (rather than building a broken
|
|
261
|
+
site). Pass `--force` to override (e.g. bootstrapping a brand-new, empty project).
|
|
262
|
+
The chosen directory is recorded in the project's `meta.json`, and later `start`s
|
|
263
|
+
reuse it — so re-running from a different folder can't silently re-point the
|
|
264
|
+
container. Run `42wp start <name> --rebind` from a new directory to move the binding
|
|
265
|
+
there on purpose.
|
|
266
|
+
|
|
267
|
+
The generated `wp-config.php` always defines `WP_ENVIRONMENT_TYPE` as `local`, so
|
|
268
|
+
`wp_get_environment_type()` reports `local` (not the WordPress default of
|
|
269
|
+
`production`) — code and plugins that branch on the environment behave as they
|
|
270
|
+
should in dev. The constant is set before mu-plugins load, so it holds even under
|
|
271
|
+
`--vip`. The config is regenerated every `start`, so to apply this to an
|
|
272
|
+
**existing** project, just re-run `42wp start <project>`.
|
|
273
|
+
|
|
189
274
|
## Configuration
|
|
190
275
|
|
|
191
276
|
| Env var | Default | Description |
|
|
192
277
|
| --------------- | -------------- | --------------------------------------------- |
|
|
193
278
|
| `FORTYTWO_HOME` | `~/.42wp` | Where the global compose, DB data and projects live |
|
|
194
279
|
| `FORTYTWO_WP_TAG` | `php8.4-apache` | Default WordPress image tag for new projects (overridden by `--wp`) |
|
|
195
|
-
| `
|
|
280
|
+
| `FORTYTWO_WP_LOCALE` | unset (`en_US`) | Default WordPress **site** language for new projects (overridden by `--locale`) |
|
|
281
|
+
| `FORTYTWO_LANG` | auto (`LANG`) | CLI UI language: `en` or `pt` |
|
|
196
282
|
| `NO_COLOR` | unset | Disable colored output |
|
|
197
283
|
|
|
198
284
|
You can also pass `--lang en` / `--lang pt` before the command.
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import { wp } from './commands/wp.js';
|
|
|
10
10
|
import { update } from './commands/update.js';
|
|
11
11
|
import { rm } from './commands/rm.js';
|
|
12
12
|
import { seed } from './commands/seed.js';
|
|
13
|
+
import { init } from './commands/init.js';
|
|
13
14
|
import { startGlobal, stopGlobal } from './commands/global.js';
|
|
14
15
|
|
|
15
16
|
function extractLang(argv) {
|
|
@@ -24,7 +25,7 @@ function extractLang(argv) {
|
|
|
24
25
|
|
|
25
26
|
// Flags that take a value (--wp tag, --user bob, --pass secret). Anything else
|
|
26
27
|
// (e.g. --vip) is a boolean. The `--key=value` form works for all of them.
|
|
27
|
-
const VALUE_FLAGS = new Set(['wp', 'user', 'pass']);
|
|
28
|
+
const VALUE_FLAGS = new Set(['wp', 'user', 'pass', 'locale', 'theme']);
|
|
28
29
|
|
|
29
30
|
// Pull options out of an arg list, leaving positionals behind. Used by `start`
|
|
30
31
|
// and `update`; NOT by `wp`, whose args pass through verbatim to WP-CLI.
|
|
@@ -53,6 +54,7 @@ function printUsage() {
|
|
|
53
54
|
plain(t('usage.line'));
|
|
54
55
|
plain('');
|
|
55
56
|
plain(t('usage.commands'));
|
|
57
|
+
plain(t('usage.init'));
|
|
56
58
|
plain(t('usage.start'));
|
|
57
59
|
plain(t('usage.update'));
|
|
58
60
|
plain(t('usage.stop'));
|
|
@@ -99,6 +101,12 @@ export async function run(rawArgs) {
|
|
|
99
101
|
break;
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
case 'init': {
|
|
105
|
+
const { opts, positionals } = parseOpts(argv.slice(1));
|
|
106
|
+
await init(positionals[0], opts);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
|
|
102
110
|
case 'wp':
|
|
103
111
|
await wp(argv[1], argv.slice(2));
|
|
104
112
|
break;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// `42wp init [project] [flags]` — scaffold a .42wp-dev-env file in the current
|
|
2
|
+
// directory from the given flags (or sensible defaults), so `42wp start` can read
|
|
3
|
+
// the project's options next time. Refuses to overwrite unless --force / --yes.
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
import { normalizeName } from '../lib/naming.js';
|
|
9
|
+
import { PROJECT_CONFIG_FILE } from '../lib/projectConfig.js';
|
|
10
|
+
import {
|
|
11
|
+
resolveLocale,
|
|
12
|
+
resolveDemoCount,
|
|
13
|
+
DEFAULT_ADMIN_USER,
|
|
14
|
+
DEFAULT_ADMIN_PASS,
|
|
15
|
+
} from '../lib/config.js';
|
|
16
|
+
import { success, error, plain } from '../lib/log.js';
|
|
17
|
+
import { t } from '../lib/i18n.js';
|
|
18
|
+
|
|
19
|
+
export async function init(rawName, opts = {}) {
|
|
20
|
+
const dir = process.cwd();
|
|
21
|
+
const file = path.join(dir, PROJECT_CONFIG_FILE);
|
|
22
|
+
|
|
23
|
+
// Don't clobber an existing file unless explicitly forced.
|
|
24
|
+
try {
|
|
25
|
+
await fs.access(file);
|
|
26
|
+
if (!opts.force && !opts.yes) {
|
|
27
|
+
error(t('init.exists', { file: PROJECT_CONFIG_FILE }));
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// no existing file — good to write
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const slug = normalizeName(rawName || opts.project_slug || path.basename(dir));
|
|
36
|
+
const subdomains = !!opts.subdomains;
|
|
37
|
+
const multisiteMode = subdomains ? 'subdomain' : opts.multisite ? 'subdir' : 'false';
|
|
38
|
+
const user = opts.user || DEFAULT_ADMIN_USER;
|
|
39
|
+
const pass = opts.pass || DEFAULT_ADMIN_PASS;
|
|
40
|
+
const locale = resolveLocale(opts.locale) || 'en_US';
|
|
41
|
+
const theme = opts.theme || '';
|
|
42
|
+
const vip = !!opts.vip;
|
|
43
|
+
const demo = resolveDemoCount(opts['demo-content']); // null | number
|
|
44
|
+
const initialContent = demo ? String(demo) : 'false';
|
|
45
|
+
|
|
46
|
+
const content =
|
|
47
|
+
[
|
|
48
|
+
'# 42wp dev environment — project config (.42wp-dev-env)',
|
|
49
|
+
'# Read by `42wp start` in this directory. CLI flags override these values.',
|
|
50
|
+
'# multisite: false | subdir | subdomain · booleans: true | false',
|
|
51
|
+
'# locale uses the WordPress format, e.g. pt_BR, es_ES',
|
|
52
|
+
'',
|
|
53
|
+
`project_slug=${slug}`,
|
|
54
|
+
`multisite=${multisiteMode}`,
|
|
55
|
+
`username=${user}`,
|
|
56
|
+
`password=${pass}`,
|
|
57
|
+
`locale=${locale}`,
|
|
58
|
+
`default_theme=${theme}`,
|
|
59
|
+
`wpvip=${vip}`,
|
|
60
|
+
`initial_content=${initialContent}`,
|
|
61
|
+
].join('\n') + '\n';
|
|
62
|
+
|
|
63
|
+
await fs.writeFile(file, content, 'utf8');
|
|
64
|
+
success(t('init.created', { file: PROJECT_CONFIG_FILE }));
|
|
65
|
+
plain(t('init.next'));
|
|
66
|
+
}
|
package/src/commands/start.js
CHANGED
|
@@ -14,6 +14,15 @@ import {
|
|
|
14
14
|
routerRule,
|
|
15
15
|
} from '../lib/naming.js';
|
|
16
16
|
import { projectDir } from '../lib/paths.js';
|
|
17
|
+
import { readProjectConfig } from '../lib/projectConfig.js';
|
|
18
|
+
import {
|
|
19
|
+
missingWpDirs,
|
|
20
|
+
pathExists,
|
|
21
|
+
readProjectMeta,
|
|
22
|
+
writeProjectMeta,
|
|
23
|
+
readComposeRepoDir,
|
|
24
|
+
REQUIRED_WP_DIRS,
|
|
25
|
+
} from '../lib/projectState.js';
|
|
17
26
|
import { renderTemplate, render, readTemplate } from '../lib/render.js';
|
|
18
27
|
import { fetchSalts } from '../lib/salts.js';
|
|
19
28
|
import { run, compose, dockerExec, dockerExecCapture } from '../lib/docker.js';
|
|
@@ -26,6 +35,7 @@ import {
|
|
|
26
35
|
wpImage,
|
|
27
36
|
multisiteConfig,
|
|
28
37
|
resolveDemoCount,
|
|
38
|
+
resolveLocale,
|
|
29
39
|
DEFAULT_ADMIN_USER,
|
|
30
40
|
DEFAULT_ADMIN_PASS,
|
|
31
41
|
VIP_MU_PLUGINS_REPO,
|
|
@@ -50,34 +60,87 @@ async function ensureVipMuPlugins(envDir) {
|
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
export async function start(rawName, opts = {}) {
|
|
53
|
-
|
|
63
|
+
// The directory the user is sitting in may carry a .42wp-dev-env with saved
|
|
64
|
+
// options. CLI flags still take precedence over it.
|
|
65
|
+
const cwd = process.cwd();
|
|
66
|
+
const fileOpts = readProjectConfig(cwd);
|
|
67
|
+
if (Object.keys(fileOpts).length > 0) {
|
|
68
|
+
opts = { ...fileOpts, ...opts };
|
|
69
|
+
step(t('start.usingConfig'));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const projectName = rawName || opts.project_slug;
|
|
73
|
+
if (!projectName) {
|
|
54
74
|
error(t('start.needName'));
|
|
55
75
|
process.exitCode = 1;
|
|
56
76
|
return;
|
|
57
77
|
}
|
|
58
78
|
|
|
59
|
-
|
|
60
|
-
const repoDir = process.cwd();
|
|
61
|
-
|
|
62
|
-
const name = normalizeName(rawName);
|
|
79
|
+
const name = normalizeName(projectName);
|
|
63
80
|
validateName(name);
|
|
64
81
|
|
|
65
82
|
const dom = domain(name);
|
|
66
83
|
const db = dbName(name);
|
|
67
84
|
const container = containerName(db);
|
|
68
85
|
const envDir = projectDir(name);
|
|
86
|
+
|
|
87
|
+
// Resolve which host directory is mounted as wp-content. The FIRST start binds
|
|
88
|
+
// to the current directory and records it in meta.json; later starts reuse that
|
|
89
|
+
// path, so re-running from another folder can't silently re-point the mount.
|
|
90
|
+
// --rebind moves the binding to the current directory.
|
|
91
|
+
const meta = await readProjectMeta(envDir);
|
|
92
|
+
// Recorded binding, or (for projects created before meta.json) recover it from
|
|
93
|
+
// the existing compose so an upgrade doesn't lose the original directory.
|
|
94
|
+
const boundDir = meta.repoDir || (await readComposeRepoDir(envDir));
|
|
95
|
+
let repoDir = cwd;
|
|
96
|
+
if (boundDir && !opts.rebind) {
|
|
97
|
+
if (await pathExists(boundDir)) {
|
|
98
|
+
repoDir = boundDir;
|
|
99
|
+
if (path.resolve(repoDir) !== path.resolve(cwd)) {
|
|
100
|
+
step(t('start.boundTo', { dir: repoDir }));
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
// The recorded directory is gone — fall back to the current one.
|
|
104
|
+
step(t('start.boundMissing', { dir: boundDir }));
|
|
105
|
+
repoDir = cwd;
|
|
106
|
+
}
|
|
107
|
+
} else if (opts.rebind && boundDir && path.resolve(boundDir) !== path.resolve(cwd)) {
|
|
108
|
+
step(t('start.rebound', { dir: cwd }));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Guard: refuse to mount a directory that isn't a wp-content root (missing the
|
|
112
|
+
// minimal plugins/ + themes/), which would produce a broken WordPress install.
|
|
113
|
+
// --force overrides (e.g. bootstrapping a brand-new, empty project).
|
|
114
|
+
if (!opts.force) {
|
|
115
|
+
const missing = await missingWpDirs(repoDir);
|
|
116
|
+
if (missing.length) {
|
|
117
|
+
error(
|
|
118
|
+
t('start.notWpContent', {
|
|
119
|
+
dir: repoDir,
|
|
120
|
+
missing: missing.map((d) => `${d}/`).join(', '),
|
|
121
|
+
required: REQUIRED_WP_DIRS.map((d) => `${d}/`).join(', '),
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
process.exitCode = 1;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
69
128
|
const adminUser = opts.user || DEFAULT_ADMIN_USER;
|
|
70
129
|
const adminPass = opts.pass || DEFAULT_ADMIN_PASS;
|
|
71
130
|
// --subdomains implies multisite; without it, --multisite is subdirectory mode.
|
|
72
131
|
const subdomains = !!opts.subdomains;
|
|
73
132
|
const multisite = !!opts.multisite || subdomains;
|
|
74
133
|
const demoCount = resolveDemoCount(opts['demo-content']); // null when not requested
|
|
134
|
+
const locale = resolveLocale(opts.locale); // '' = WordPress default (en_US)
|
|
135
|
+
const theme = opts.theme || ''; // activate this theme after install (if present)
|
|
75
136
|
|
|
76
137
|
if (!(await ensureDockerRunning())) return;
|
|
77
138
|
await checkGlobalLayer();
|
|
78
139
|
|
|
79
140
|
step(t('start.preparing', { domain: dom }));
|
|
80
141
|
await fs.mkdir(envDir, { recursive: true });
|
|
142
|
+
// Record (or update, on --rebind) the directory bound as wp-content.
|
|
143
|
+
await writeProjectMeta(envDir, { repoDir });
|
|
81
144
|
|
|
82
145
|
// 1. Database on the global MySQL.
|
|
83
146
|
step(t('start.creatingDb', { db }));
|
|
@@ -175,12 +238,30 @@ export async function start(rawName, opts = {}) {
|
|
|
175
238
|
`--admin_user=${adminUser}`,
|
|
176
239
|
`--admin_password=${adminPass}`,
|
|
177
240
|
`--admin_email=${ADMIN_EMAIL}`,
|
|
241
|
+
...(locale ? [`--locale=${locale}`] : []),
|
|
178
242
|
'--skip-email',
|
|
179
243
|
'--skip-plugins',
|
|
180
244
|
'--skip-themes',
|
|
181
245
|
'--allow-root',
|
|
182
246
|
]);
|
|
183
247
|
|
|
248
|
+
// 7a. Install and activate the requested site language (downloads the .mo pack
|
|
249
|
+
// into wp-content/languages). Non-fatal — a failure (e.g. offline) leaves
|
|
250
|
+
// the site in English rather than aborting the whole start.
|
|
251
|
+
if (locale && locale !== 'en_US') {
|
|
252
|
+
step(t('start.installingLang', { locale }));
|
|
253
|
+
const lang = await dockerExecCapture(container, [
|
|
254
|
+
'wp',
|
|
255
|
+
'language',
|
|
256
|
+
'core',
|
|
257
|
+
'install',
|
|
258
|
+
locale,
|
|
259
|
+
'--activate',
|
|
260
|
+
'--allow-root',
|
|
261
|
+
]);
|
|
262
|
+
if (lang.code !== 0) step(t('start.langFailed', { locale }));
|
|
263
|
+
}
|
|
264
|
+
|
|
184
265
|
// 7b. Multisite: create the network tables while WP is still single-site
|
|
185
266
|
// (--skip-config: we manage wp-config ourselves), then enable the MULTISITE
|
|
186
267
|
// constants. Order matters — the constants must come AFTER the tables exist.
|
|
@@ -202,6 +283,15 @@ export async function start(rawName, opts = {}) {
|
|
|
202
283
|
await writeWpConfig(multisiteConfig(dom, { subdomains }));
|
|
203
284
|
}
|
|
204
285
|
|
|
286
|
+
// 7c. Activate the project theme (from .42wp-dev-env `default_theme=` or --theme)
|
|
287
|
+
// BEFORE seeding demo content, so its menu locations/features are registered
|
|
288
|
+
// when the seeder runs. Non-fatal — a missing theme just logs a warning.
|
|
289
|
+
if (theme) {
|
|
290
|
+
step(t('start.activatingTheme', { theme }));
|
|
291
|
+
const act = await dockerExecCapture(container, ['wp', 'theme', 'activate', theme, '--allow-root']);
|
|
292
|
+
if (act.code !== 0) step(t('start.themeFailed', { theme }));
|
|
293
|
+
}
|
|
294
|
+
|
|
205
295
|
// 8. Pretty permalinks.
|
|
206
296
|
step(t('start.permalinks'));
|
|
207
297
|
await dockerExec(container, [
|
|
@@ -229,6 +319,8 @@ export async function start(rawName, opts = {}) {
|
|
|
229
319
|
plain(colors.green(t('start.admin', { url })));
|
|
230
320
|
plain(colors.green(t('start.user', { user: adminUser })));
|
|
231
321
|
plain(colors.green(t('start.pass', { pass: adminPass })));
|
|
322
|
+
if (locale) plain(colors.green(t('start.locale', { locale })));
|
|
323
|
+
if (theme) plain(colors.green(t('start.theme', { theme })));
|
|
232
324
|
if (multisite) {
|
|
233
325
|
const mode = subdomains ? 'subdomain' : 'subdirectory';
|
|
234
326
|
plain(colors.green(t('start.multisite', { mode, url })));
|
package/src/lib/config.js
CHANGED
|
@@ -34,6 +34,26 @@ export function resolveWpTag(override) {
|
|
|
34
34
|
return override || process.env.FORTYTWO_WP_TAG || DEFAULT_WP_TAG;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// WordPress site language (locale). Empty means the WordPress default (en_US) — no
|
|
38
|
+
// language pack is downloaded. Set per project with --locale (e.g. pt_BR, es_ES),
|
|
39
|
+
// or globally with FORTYTWO_WP_LOCALE. Distinct from --lang, which is the CLI's own
|
|
40
|
+
// UI language.
|
|
41
|
+
export const DEFAULT_WP_LOCALE = '';
|
|
42
|
+
|
|
43
|
+
// Normalize a locale to the WordPress format: `pt-br` / `pt_br` -> `pt_BR`.
|
|
44
|
+
// Values that don't fit the simple ll[_-]RR shape (e.g. `en_US`, `es_419`,
|
|
45
|
+
// `de_DE_formal`) pass through unchanged.
|
|
46
|
+
export function normalizeLocale(loc) {
|
|
47
|
+
if (!loc) return '';
|
|
48
|
+
const s = String(loc).trim();
|
|
49
|
+
const m = /^([a-z]{2,3})[-_]([a-z]{2})$/i.exec(s);
|
|
50
|
+
return m ? `${m[1].toLowerCase()}_${m[2].toUpperCase()}` : s;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function resolveLocale(override) {
|
|
54
|
+
return normalizeLocale(override || process.env.FORTYTWO_WP_LOCALE || DEFAULT_WP_LOCALE);
|
|
55
|
+
}
|
|
56
|
+
|
|
37
57
|
// Full image reference for display/logging.
|
|
38
58
|
export function wpImage(tag) {
|
|
39
59
|
return `wordpress:${tag}`;
|
package/src/lib/docker.js
CHANGED
|
@@ -4,6 +4,25 @@
|
|
|
4
4
|
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
|
|
7
|
+
// Format a command + args for human-readable error/log lines. Args that contain
|
|
8
|
+
// spaces or shell metacharacters are POSIX-single-quoted so the line is
|
|
9
|
+
// unambiguous and copy-pasteable. This is DISPLAY ONLY — real execution always
|
|
10
|
+
// passes args as an array to spawn() (no shell), so nothing here affects how
|
|
11
|
+
// commands actually run.
|
|
12
|
+
export function formatCommand(cmd, args = []) {
|
|
13
|
+
return [cmd, ...args].map(quoteForDisplay).join(' ');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function quoteForDisplay(arg) {
|
|
17
|
+
const s = String(arg);
|
|
18
|
+
if (s === '') return "''";
|
|
19
|
+
// Bare word when it's only safe characters (letters, digits and common URL/flag
|
|
20
|
+
// punctuation) — e.g. --url=http://base.localhost stays unquoted.
|
|
21
|
+
if (/^[A-Za-z0-9_@%+=:,.\/-]+$/.test(s)) return s;
|
|
22
|
+
// Otherwise single-quote, escaping any embedded single quotes.
|
|
23
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
24
|
+
}
|
|
25
|
+
|
|
7
26
|
// Run a command, streaming its stdio to the user. Resolves on exit 0, rejects otherwise.
|
|
8
27
|
export function run(cmd, args, opts = {}) {
|
|
9
28
|
return new Promise((resolve, reject) => {
|
|
@@ -11,7 +30,7 @@ export function run(cmd, args, opts = {}) {
|
|
|
11
30
|
child.on('error', reject);
|
|
12
31
|
child.on('close', (code) => {
|
|
13
32
|
if (code === 0) resolve();
|
|
14
|
-
else reject(new Error(`${cmd
|
|
33
|
+
else reject(new Error(`${formatCommand(cmd, args)} exited with code ${code}`));
|
|
15
34
|
});
|
|
16
35
|
});
|
|
17
36
|
}
|
|
@@ -24,7 +43,7 @@ export function runWithInput(cmd, args, input, opts = {}) {
|
|
|
24
43
|
child.on('error', reject);
|
|
25
44
|
child.on('close', (code) => {
|
|
26
45
|
if (code === 0) resolve();
|
|
27
|
-
else reject(new Error(`${cmd
|
|
46
|
+
else reject(new Error(`${formatCommand(cmd, args)} exited with code ${code}`));
|
|
28
47
|
});
|
|
29
48
|
child.stdin.write(input);
|
|
30
49
|
child.stdin.end();
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Reads an optional per-project config file (.42wp-dev-env) from the directory a
|
|
2
|
+
// command is run in, so a repo can carry its own `42wp start` defaults. Values are
|
|
3
|
+
// returned shaped like the parsed CLI opts, so a command can merge them as
|
|
4
|
+
// defaults UNDER the real flags (a CLI flag always wins over the file).
|
|
5
|
+
//
|
|
6
|
+
// Format: `key=value`, one per line. `#` comments and blank lines are ignored;
|
|
7
|
+
// surrounding quotes on a value are stripped.
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
export const PROJECT_CONFIG_FILE = '.42wp-dev-env';
|
|
13
|
+
|
|
14
|
+
// File key -> opts key. Both the documented names and the CLI-flag names are
|
|
15
|
+
// accepted so the file stays forgiving. `project_slug` becomes the project name.
|
|
16
|
+
// `multisite` is handled separately (its value picks subdir/subdomain).
|
|
17
|
+
const KEY_MAP = {
|
|
18
|
+
project_slug: 'project_slug',
|
|
19
|
+
slug: 'project_slug',
|
|
20
|
+
username: 'user',
|
|
21
|
+
user: 'user',
|
|
22
|
+
password: 'pass',
|
|
23
|
+
pass: 'pass',
|
|
24
|
+
locale: 'locale',
|
|
25
|
+
default_theme: 'theme',
|
|
26
|
+
theme: 'theme',
|
|
27
|
+
wpvip: 'vip',
|
|
28
|
+
vip: 'vip',
|
|
29
|
+
wp: 'wp',
|
|
30
|
+
initial_content: 'demo-content',
|
|
31
|
+
'demo-content': 'demo-content',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const BOOL_KEYS = new Set(['vip']);
|
|
35
|
+
|
|
36
|
+
const isTrue = (v) => /^(1|true|yes|on)$/i.test(v);
|
|
37
|
+
|
|
38
|
+
// Parse .42wp-dev-env from `dir`. Returns {} when the file is absent or unreadable.
|
|
39
|
+
export function readProjectConfig(dir) {
|
|
40
|
+
let text;
|
|
41
|
+
try {
|
|
42
|
+
text = fs.readFileSync(path.join(dir, PROJECT_CONFIG_FILE), 'utf8');
|
|
43
|
+
} catch {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const out = {};
|
|
48
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
49
|
+
const line = rawLine.trim();
|
|
50
|
+
if (!line || line.startsWith('#')) continue;
|
|
51
|
+
const eq = line.indexOf('=');
|
|
52
|
+
if (eq === -1) continue;
|
|
53
|
+
|
|
54
|
+
const rawKey = line.slice(0, eq).trim().toLowerCase();
|
|
55
|
+
let value = line.slice(eq + 1).trim();
|
|
56
|
+
// Strip an inline comment (" # ..."), unless the value is quoted. A '#' with no
|
|
57
|
+
// preceding whitespace is kept, so values like a password can contain it.
|
|
58
|
+
if (value[0] !== '"' && value[0] !== "'") {
|
|
59
|
+
value = value.replace(/\s+#.*$/, '').trim();
|
|
60
|
+
}
|
|
61
|
+
// Strip surrounding matching quotes.
|
|
62
|
+
value = value.replace(/^(['"])(.*)\1$/, '$2');
|
|
63
|
+
|
|
64
|
+
// multisite=subdir|subdomain (true == subdir; false/absent == single site).
|
|
65
|
+
if (rawKey === 'multisite') {
|
|
66
|
+
const v = value.toLowerCase();
|
|
67
|
+
if (/^(subdomain|subdomains)$/.test(v)) {
|
|
68
|
+
out.multisite = true;
|
|
69
|
+
out.subdomains = true;
|
|
70
|
+
} else if (/^(subdir|subdirectory|true|yes|1|on)$/.test(v)) {
|
|
71
|
+
out.multisite = true;
|
|
72
|
+
out.subdomains = false;
|
|
73
|
+
}
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const key = KEY_MAP[rawKey];
|
|
78
|
+
if (!key) continue; // ignore unknown keys
|
|
79
|
+
|
|
80
|
+
if (BOOL_KEYS.has(key)) {
|
|
81
|
+
out[key] = isTrue(value);
|
|
82
|
+
} else if (key === 'demo-content') {
|
|
83
|
+
// true -> bare flag (default count); a number -> that count; false -> omit.
|
|
84
|
+
if (isTrue(value)) out[key] = true;
|
|
85
|
+
else if (/^\d+$/.test(value)) out[key] = value;
|
|
86
|
+
} else if (value !== '') {
|
|
87
|
+
out[key] = value;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Per-project runtime state kept in the generated project dir (~/.42wp/projects/<name>/):
|
|
2
|
+
// - meta.json remembers which host directory is bound as wp-content, so
|
|
3
|
+
// re-running `start` from another folder can't silently re-point the mount.
|
|
4
|
+
// - a guard that the directory about to be mounted really is a wp-content
|
|
5
|
+
// (has the minimal plugins/ and themes/ subdirectories).
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs/promises';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
const META_FILE = 'meta.json';
|
|
11
|
+
|
|
12
|
+
// Sub-directories that must exist for a folder to count as a wp-content root.
|
|
13
|
+
export const REQUIRED_WP_DIRS = ['plugins', 'themes'];
|
|
14
|
+
|
|
15
|
+
// Returns the required wp-content subdirs that are missing from `dir` (empty means
|
|
16
|
+
// the directory looks like a valid wp-content).
|
|
17
|
+
export async function missingWpDirs(dir) {
|
|
18
|
+
const missing = [];
|
|
19
|
+
for (const sub of REQUIRED_WP_DIRS) {
|
|
20
|
+
try {
|
|
21
|
+
const st = await fs.stat(path.join(dir, sub));
|
|
22
|
+
if (!st.isDirectory()) missing.push(sub);
|
|
23
|
+
} catch {
|
|
24
|
+
missing.push(sub);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return missing;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function pathExists(p) {
|
|
31
|
+
try {
|
|
32
|
+
await fs.stat(p);
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Read the project's meta.json (returns {} when absent/unreadable).
|
|
40
|
+
export async function readProjectMeta(envDir) {
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(await fs.readFile(path.join(envDir, META_FILE), 'utf8'));
|
|
43
|
+
} catch {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Recover the wp-content bind-mount source from an existing docker-compose.yml,
|
|
49
|
+
// for projects created before meta.json existed. Returns '' when not found.
|
|
50
|
+
export async function readComposeRepoDir(envDir) {
|
|
51
|
+
try {
|
|
52
|
+
const yml = await fs.readFile(path.join(envDir, 'docker-compose.yml'), 'utf8');
|
|
53
|
+
const m = yml.match(/^\s*-\s*(.+):\/var\/www\/html\/wp-content\s*$/m);
|
|
54
|
+
return m ? m[1].trim() : '';
|
|
55
|
+
} catch {
|
|
56
|
+
return '';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Persist the project's meta.json (merging over any existing fields).
|
|
61
|
+
export async function writeProjectMeta(envDir, data) {
|
|
62
|
+
const current = await readProjectMeta(envDir);
|
|
63
|
+
const next = { ...current, ...data };
|
|
64
|
+
await fs.writeFile(path.join(envDir, META_FILE), `${JSON.stringify(next, null, 2)}\n`, 'utf8');
|
|
65
|
+
return next;
|
|
66
|
+
}
|
package/src/locales/en.js
CHANGED
|
@@ -13,7 +13,13 @@ export default {
|
|
|
13
13
|
'global.invalid': "Invalid global command. Use 'start' or 'stop'.",
|
|
14
14
|
|
|
15
15
|
// start
|
|
16
|
-
'start.needName': 'You must provide a project name. e.g.: 42wp start jovempan',
|
|
16
|
+
'start.needName': 'You must provide a project name (or set project_slug in .42wp-dev-env). e.g.: 42wp start jovempan',
|
|
17
|
+
'start.usingConfig': 'Using options from .42wp-dev-env (CLI flags override).',
|
|
18
|
+
'start.notWpContent':
|
|
19
|
+
'${dir} is not a wp-content directory (missing ${missing}). Run 42wp start from your project root (it needs ${required}), or pass --force to override.',
|
|
20
|
+
'start.boundTo': 'Project is bound to ${dir}; keeping it. Use --rebind to point it at the current directory.',
|
|
21
|
+
'start.boundMissing': 'Bound directory ${dir} no longer exists — binding to the current directory.',
|
|
22
|
+
'start.rebound': 'Rebinding project to ${dir}.',
|
|
17
23
|
'start.preparing': 'Preparing environment for ${domain}...',
|
|
18
24
|
'start.creatingDb': 'Creating database: ${db}',
|
|
19
25
|
'start.genConfig': 'Generating ephemeral wp-config.php...',
|
|
@@ -25,6 +31,10 @@ export default {
|
|
|
25
31
|
'start.upping': 'Starting project containers...',
|
|
26
32
|
'start.waitingWp': 'Waiting for the WordPress container to respond...',
|
|
27
33
|
'start.installing': 'Running silent WordPress install...',
|
|
34
|
+
'start.installingLang': 'Installing WordPress language pack (${locale})...',
|
|
35
|
+
'start.langFailed': 'Could not install the ${locale} language pack (continuing in English).',
|
|
36
|
+
'start.activatingTheme': 'Activating theme (${theme})...',
|
|
37
|
+
'start.themeFailed': 'Could not activate the "${theme}" theme (is it in the repo?).',
|
|
28
38
|
'start.enablingMultisite': 'Enabling multisite (network tables + wp-config constants)...',
|
|
29
39
|
'start.permalinks': 'Configuring permalinks...',
|
|
30
40
|
'start.demoGenerating': 'Seeding ${count} demo posts (authors, tags, categories, images)...',
|
|
@@ -34,8 +44,15 @@ export default {
|
|
|
34
44
|
'start.admin': 'Admin: ${url}/wp-admin',
|
|
35
45
|
'start.user': 'User: ${user}',
|
|
36
46
|
'start.pass': 'Password: ${pass}',
|
|
47
|
+
'start.locale': 'Language: ${locale}',
|
|
48
|
+
'start.theme': 'Theme: ${theme}',
|
|
37
49
|
'start.multisite': 'Multisite: ${mode}. Network admin: ${url}/wp-admin/network/',
|
|
38
50
|
|
|
51
|
+
// init
|
|
52
|
+
'init.exists': '${file} already exists here. Re-run with --force to overwrite.',
|
|
53
|
+
'init.created': 'Created ${file}. Edit it, then run 42wp start.',
|
|
54
|
+
'init.next': 'Tip: with project_slug set, you can run 42wp start with no name.',
|
|
55
|
+
|
|
39
56
|
// update
|
|
40
57
|
'update.needName': 'Tell me which project to update. e.g.: 42wp update jovempan',
|
|
41
58
|
'update.notFound': "Project '${name}' not found at ${dir}. Run 42wp start ${name} first.",
|
|
@@ -77,6 +94,7 @@ export default {
|
|
|
77
94
|
// usage
|
|
78
95
|
'usage.line': 'Usage: 42wp <command> [project] [arguments]',
|
|
79
96
|
'usage.commands': 'Commands:',
|
|
97
|
+
'usage.init': ' init [project] Scaffold a .42wp-dev-env in the current directory.',
|
|
80
98
|
'usage.start': ' start <project> Start the environment using \'.localhost\'.',
|
|
81
99
|
'usage.update': ' update <project> Update an existing project to a newer WordPress image.',
|
|
82
100
|
'usage.stop': ' stop <project> Stop the project containers.',
|
package/src/locales/pt.js
CHANGED
|
@@ -13,7 +13,13 @@ export default {
|
|
|
13
13
|
'global.invalid': "Comando global inválido. Use 'start' ou 'stop'.",
|
|
14
14
|
|
|
15
15
|
// start
|
|
16
|
-
'start.needName': 'Você precisa informar o nome do projeto. Ex: 42wp start jovempan',
|
|
16
|
+
'start.needName': 'Você precisa informar o nome do projeto (ou definir project_slug no .42wp-dev-env). Ex: 42wp start jovempan',
|
|
17
|
+
'start.usingConfig': 'Usando opções do .42wp-dev-env (flags da CLI têm prioridade).',
|
|
18
|
+
'start.notWpContent':
|
|
19
|
+
'${dir} não é um diretório wp-content (falta ${missing}). Rode 42wp start na raiz do projeto (precisa de ${required}), ou use --force para ignorar.',
|
|
20
|
+
'start.boundTo': 'O projeto está vinculado a ${dir}; mantendo. Use --rebind para apontar para o diretório atual.',
|
|
21
|
+
'start.boundMissing': 'O diretório vinculado ${dir} não existe mais — vinculando ao diretório atual.',
|
|
22
|
+
'start.rebound': 'Revinculando o projeto para ${dir}.',
|
|
17
23
|
'start.preparing': 'Preparando ambiente para ${domain}...',
|
|
18
24
|
'start.creatingDb': 'Criando banco de dados: ${db}',
|
|
19
25
|
'start.genConfig': 'Gerando wp-config.php efêmero...',
|
|
@@ -25,6 +31,10 @@ export default {
|
|
|
25
31
|
'start.upping': 'Subindo containers do projeto...',
|
|
26
32
|
'start.waitingWp': 'Aguardando o container WP responder...',
|
|
27
33
|
'start.installing': 'Executando instalação silenciosa do WordPress...',
|
|
34
|
+
'start.installingLang': 'Instalando o pacote de idioma do WordPress (${locale})...',
|
|
35
|
+
'start.langFailed': 'Não foi possível instalar o pacote de idioma ${locale} (continuando em inglês).',
|
|
36
|
+
'start.activatingTheme': 'Ativando o tema (${theme})...',
|
|
37
|
+
'start.themeFailed': 'Não foi possível ativar o tema "${theme}" (ele está no repositório?).',
|
|
28
38
|
'start.enablingMultisite': 'Habilitando multisite (tabelas de rede + constantes no wp-config)...',
|
|
29
39
|
'start.permalinks': 'Configurando Permalinks...',
|
|
30
40
|
'start.demoGenerating': 'Gerando ${count} posts de demonstração (autores, tags, categorias, imagens)...',
|
|
@@ -34,8 +44,15 @@ export default {
|
|
|
34
44
|
'start.admin': 'Admin: ${url}/wp-admin',
|
|
35
45
|
'start.user': 'Usuário: ${user}',
|
|
36
46
|
'start.pass': 'Senha: ${pass}',
|
|
47
|
+
'start.locale': 'Idioma: ${locale}',
|
|
48
|
+
'start.theme': 'Tema: ${theme}',
|
|
37
49
|
'start.multisite': 'Multisite: ${mode}. Admin da rede: ${url}/wp-admin/network/',
|
|
38
50
|
|
|
51
|
+
// init
|
|
52
|
+
'init.exists': '${file} já existe aqui. Rode novamente com --force para sobrescrever.',
|
|
53
|
+
'init.created': 'Criado ${file}. Edite-o e rode 42wp start.',
|
|
54
|
+
'init.next': 'Dica: com project_slug definido, você pode rodar 42wp start sem o nome.',
|
|
55
|
+
|
|
39
56
|
// update
|
|
40
57
|
'update.needName': 'Informe o projeto para atualizar. Ex: 42wp update jovempan',
|
|
41
58
|
'update.notFound': "Projeto '${name}' não encontrado em ${dir}. Rode 42wp start ${name} primeiro.",
|
|
@@ -77,6 +94,7 @@ export default {
|
|
|
77
94
|
// uso
|
|
78
95
|
'usage.line': 'Uso: 42wp <comando> [projeto] [argumentos]',
|
|
79
96
|
'usage.commands': 'Comandos:',
|
|
97
|
+
'usage.init': ' init [projeto] Cria um .42wp-dev-env no diretório atual.',
|
|
80
98
|
'usage.start': " start <projeto> Inicia o ambiente usando '.localhost'.",
|
|
81
99
|
'usage.update': ' update <projeto> Atualiza um projeto existente para uma imagem mais recente do WordPress.',
|
|
82
100
|
'usage.stop': ' stop <projeto> Para os containers do projeto.',
|
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
* - <count> posts: rich HTML content + plain-text excerpt + featured image,
|
|
10
10
|
* each linked to 1 category, 2-5 tags and 1-3 authors, with varied past dates.
|
|
11
11
|
*
|
|
12
|
+
* When the 42-framework is part of the project (its files live under
|
|
13
|
+
* client-mu-plugins, or composer.json requires it) it also configures, once
|
|
14
|
+
* (idempotent):
|
|
15
|
+
* - the `/%category%/%postname%/` permalink structure
|
|
16
|
+
* - a `42wp_homepage` post set as the static front page
|
|
17
|
+
* - 4 category-driven menus (header primary/secondary, footer primary/social)
|
|
18
|
+
* assigned to the theme's registered locations
|
|
19
|
+
* On a plain WordPress site those steps are skipped and only content is seeded.
|
|
20
|
+
*
|
|
12
21
|
* This file is managed by the dev environment; edits will be overwritten.
|
|
13
22
|
*/
|
|
14
23
|
|
|
@@ -199,4 +208,153 @@ for ( $i = 0; $i < $count; $i++ ) {
|
|
|
199
208
|
}
|
|
200
209
|
}
|
|
201
210
|
|
|
202
|
-
WP_CLI::
|
|
211
|
+
WP_CLI::log( sprintf( 'Created %d demo posts.', $created ) );
|
|
212
|
+
|
|
213
|
+
/* -------------------------------------------------------------------------- *
|
|
214
|
+
* 42-framework extras — permalinks, front page, menus
|
|
215
|
+
*
|
|
216
|
+
* Only applied when the 42-framework is part of the project: its files under
|
|
217
|
+
* client-mu-plugins/42-framework, or a composer.json that requires it
|
|
218
|
+
* (42wp/wp-mu-plugins). On a plain WordPress site these steps are skipped and
|
|
219
|
+
* only the content above is seeded.
|
|
220
|
+
* -------------------------------------------------------------------------- */
|
|
221
|
+
|
|
222
|
+
$framework_active = is_dir( WP_CONTENT_DIR . '/client-mu-plugins/42-framework' );
|
|
223
|
+
if ( ! $framework_active && is_readable( WP_CONTENT_DIR . '/composer.json' ) ) {
|
|
224
|
+
$composer = (string) file_get_contents( WP_CONTENT_DIR . '/composer.json' );
|
|
225
|
+
$framework_active = ( false !== strpos( $composer, '42-framework' ) || false !== strpos( $composer, '42wp/wp-mu-plugins' ) );
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if ( ! $framework_active ) {
|
|
229
|
+
WP_CLI::log( '42-framework not detected — skipped permalinks, front page and menus.' );
|
|
230
|
+
WP_CLI::success( sprintf( 'Demo content ready (%d posts).', $created ) );
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* Permalinks: /%category%/%postname%/. Soft flush so we never rewrite the
|
|
235
|
+
* mounted .htaccess. Idempotent — re-setting the same structure is a no-op. */
|
|
236
|
+
if ( '/%category%/%postname%/' !== get_option( 'permalink_structure' ) ) {
|
|
237
|
+
global $wp_rewrite;
|
|
238
|
+
$wp_rewrite->set_permalink_structure( '/%category%/%postname%/' );
|
|
239
|
+
$wp_rewrite->flush_rules( false );
|
|
240
|
+
WP_CLI::log( 'Permalinks set to /%category%/%postname%/.' );
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* Front page: a `42wp_homepage` post (falls back to a page if the type isn't
|
|
244
|
+
* registered), reused when it already exists. */
|
|
245
|
+
$home_type = post_type_exists( '42wp_homepage' ) ? '42wp_homepage' : 'page';
|
|
246
|
+
$home_ids = get_posts( [ 'post_type' => $home_type, 'name' => 'homepage', 'post_status' => 'any', 'numberposts' => 1, 'fields' => 'ids' ] );
|
|
247
|
+
$home_id = $home_ids ? (int) $home_ids[0] : 0;
|
|
248
|
+
if ( ! $home_id ) {
|
|
249
|
+
$home_id = wp_insert_post( [
|
|
250
|
+
'post_title' => 'Homepage',
|
|
251
|
+
'post_name' => 'homepage',
|
|
252
|
+
'post_status' => 'publish',
|
|
253
|
+
'post_type' => $home_type,
|
|
254
|
+
'post_author' => 1,
|
|
255
|
+
], true );
|
|
256
|
+
}
|
|
257
|
+
if ( ! is_wp_error( $home_id ) && $home_id ) {
|
|
258
|
+
update_post_meta( $home_id, '42wp_seo_metadata_noindex', '1' );
|
|
259
|
+
update_option( 'show_on_front', 'page' );
|
|
260
|
+
update_option( 'page_on_front', (int) $home_id );
|
|
261
|
+
WP_CLI::log( sprintf( 'Front page set to %s #%d.', $home_type, $home_id ) );
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* Menus: 4 category-driven menus, populated once and assigned to the theme's
|
|
265
|
+
* registered locations. The category items reuse the terms created above. */
|
|
266
|
+
$cat_by_name = [];
|
|
267
|
+
foreach ( $cat_names as $cn ) {
|
|
268
|
+
$term = get_term_by( 'name', $cn, 'category' );
|
|
269
|
+
if ( $term ) {
|
|
270
|
+
$cat_by_name[ $cn ] = (int) $term->term_id;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
$menu_specs = [
|
|
275
|
+
'42wp_menu_header_primary' => [
|
|
276
|
+
'name' => 'Header Primário',
|
|
277
|
+
'items' => [ [ 'cat', 'Política' ], [ 'cat', 'Mundo' ], [ 'cat', 'Cultura' ], [ 'cat', 'Entretenimento' ] ],
|
|
278
|
+
],
|
|
279
|
+
'42wp_menu_header_secondary' => [
|
|
280
|
+
'name' => 'Header Secundário',
|
|
281
|
+
'items' => [ [ 'cat', 'Saúde' ], [ 'cat', 'Brasil' ], [ 'cat', 'Tecnologia' ] ],
|
|
282
|
+
],
|
|
283
|
+
'42wp_menu_footer_primary' => [
|
|
284
|
+
'name' => 'Footer Primário',
|
|
285
|
+
'items' => [
|
|
286
|
+
[ 'cat', 'Esportes', [ [ 'cat', 'Entretenimento' ], [ 'cat', 'Brasil' ], [ 'cat', 'Tecnologia' ] ] ],
|
|
287
|
+
[ 'cat', 'Economia', [ [ 'cat', 'Política' ], [ 'cat', 'Saúde' ], [ 'cat', 'Esportes' ] ] ],
|
|
288
|
+
[ 'cat', 'Ciência', [ [ 'cat', 'Mundo' ], [ 'cat', 'Cultura' ] ] ],
|
|
289
|
+
[ 'cat', 'Tecnologia', [ [ 'cat', 'Saúde' ], [ 'cat', 'Brasil' ], [ 'cat', 'Economia' ] ] ],
|
|
290
|
+
[ 'cat', 'Política', [ [ 'cat', 'Mundo' ], [ 'cat', 'Cultura' ], [ 'cat', 'Entretenimento' ], [ 'cat', 'Ciência' ] ] ],
|
|
291
|
+
],
|
|
292
|
+
],
|
|
293
|
+
'42wp_menu_footer_social' => [
|
|
294
|
+
'name' => 'Footer Social',
|
|
295
|
+
'items' => [ [ 'link', 'YouTube', 'https://youtube.com' ], [ 'link', 'X', 'https://x.com' ], [ 'link', 'Instagram', 'https://instagram.com' ] ],
|
|
296
|
+
],
|
|
297
|
+
];
|
|
298
|
+
|
|
299
|
+
// Recursively add menu items (category archives or custom links), honoring nesting.
|
|
300
|
+
$add_items = static function ( $menu_id, array $items, $parent = 0 ) use ( &$add_items, $cat_by_name ) {
|
|
301
|
+
foreach ( $items as $item ) {
|
|
302
|
+
if ( 'cat' === $item[0] ) {
|
|
303
|
+
if ( empty( $cat_by_name[ $item[1] ] ) ) {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
$new_item = wp_update_nav_menu_item( $menu_id, 0, [
|
|
307
|
+
'menu-item-title' => $item[1],
|
|
308
|
+
'menu-item-type' => 'taxonomy',
|
|
309
|
+
'menu-item-object' => 'category',
|
|
310
|
+
'menu-item-object-id' => $cat_by_name[ $item[1] ],
|
|
311
|
+
'menu-item-parent-id' => $parent,
|
|
312
|
+
'menu-item-status' => 'publish',
|
|
313
|
+
] );
|
|
314
|
+
if ( ! is_wp_error( $new_item ) && ! empty( $item[2] ) ) {
|
|
315
|
+
$add_items( $menu_id, $item[2], (int) $new_item );
|
|
316
|
+
}
|
|
317
|
+
} elseif ( 'link' === $item[0] ) {
|
|
318
|
+
wp_update_nav_menu_item( $menu_id, 0, [
|
|
319
|
+
'menu-item-title' => $item[1],
|
|
320
|
+
'menu-item-url' => $item[2],
|
|
321
|
+
'menu-item-type' => 'custom',
|
|
322
|
+
'menu-item-parent-id' => $parent,
|
|
323
|
+
'menu-item-status' => 'publish',
|
|
324
|
+
] );
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
$registered = get_registered_nav_menus();
|
|
330
|
+
$locations = get_theme_mod( 'nav_menu_locations', [] );
|
|
331
|
+
if ( ! is_array( $locations ) ) {
|
|
332
|
+
$locations = [];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
$menus_done = 0;
|
|
336
|
+
foreach ( $menu_specs as $location => $spec ) {
|
|
337
|
+
$menu = wp_get_nav_menu_object( $spec['name'] );
|
|
338
|
+
if ( $menu ) {
|
|
339
|
+
$menu_id = (int) $menu->term_id;
|
|
340
|
+
} else {
|
|
341
|
+
$menu_id = wp_create_nav_menu( $spec['name'] );
|
|
342
|
+
if ( is_wp_error( $menu_id ) ) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Populate only when empty, so re-runs don't duplicate items.
|
|
348
|
+
if ( empty( wp_get_nav_menu_items( $menu_id ) ) ) {
|
|
349
|
+
$add_items( $menu_id, $spec['items'] );
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if ( isset( $registered[ $location ] ) ) {
|
|
353
|
+
$locations[ $location ] = (int) $menu_id;
|
|
354
|
+
}
|
|
355
|
+
$menus_done++;
|
|
356
|
+
}
|
|
357
|
+
set_theme_mod( 'nav_menu_locations', $locations );
|
|
358
|
+
WP_CLI::log( sprintf( 'Menus ready: %d created/assigned.', $menus_done ) );
|
|
359
|
+
|
|
360
|
+
WP_CLI::success( sprintf( 'Demo content ready: %d posts, front page and %d menus.', $created, $menus_done ) );
|
|
@@ -11,6 +11,16 @@ RUN echo "xdebug.mode=debug" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.
|
|
|
11
11
|
&& echo "xdebug.client_host=host.docker.internal" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
|
|
12
12
|
&& echo "xdebug.start_with_request=yes" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
|
|
13
13
|
|
|
14
|
+
# Raise the upload limit to 2G (WP caps at min(upload_max_filesize, post_max_size);
|
|
15
|
+
# the PHP default post_max_size is only 8M, so both must be raised together).
|
|
16
|
+
RUN { \
|
|
17
|
+
echo "upload_max_filesize = 2048M"; \
|
|
18
|
+
echo "post_max_size = 2048M"; \
|
|
19
|
+
echo "memory_limit = 2048M"; \
|
|
20
|
+
echo "max_execution_time = 300"; \
|
|
21
|
+
echo "max_input_time = 300"; \
|
|
22
|
+
} > /usr/local/etc/php/conf.d/uploads.ini
|
|
23
|
+
|
|
14
24
|
RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \
|
|
15
25
|
&& chmod +x wp-cli.phar \
|
|
16
26
|
&& mv wp-cli.phar /usr/local/bin/wp
|
|
@@ -15,6 +15,9 @@ define( 'WP_DEBUG_LOG', true );
|
|
|
15
15
|
define( 'WP_DEBUG_DISPLAY', false );
|
|
16
16
|
define( 'DISABLE_WP_CRON', true );
|
|
17
17
|
|
|
18
|
+
/* Dev environment: make wp_get_environment_type() report 'local'. */
|
|
19
|
+
define( 'WP_ENVIRONMENT_TYPE', 'local' );
|
|
20
|
+
|
|
18
21
|
{{MULTISITE_CONFIG}}
|
|
19
22
|
/* That's all, stop editing! Happy publishing. */
|
|
20
23
|
|