kizlo 0.1.0 → 0.2.0
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/dist/adapters/auth.d.ts +11 -2
- package/dist/adapters/auth.js +5 -1
- package/dist/adapters/captcha.d.ts +3 -1
- package/dist/adapters/captcha.js +5 -1
- package/dist/adapters/cookies.d.ts +7 -0
- package/dist/adapters/cookies.js +8 -0
- package/dist/adapters/geo.d.ts +13 -2
- package/dist/adapters/geo.js +5 -1
- package/dist/adapters/logger.d.ts +8 -1
- package/dist/adapters/logger.js +5 -1
- package/dist/cli/banner.js +12 -1
- package/dist/cli/commands/dev.js +262 -0
- package/dist/cli/commands/init.js +235 -43
- package/dist/cli/commands/test.js +133 -34
- package/dist/cli/commands/watch.js +2 -2
- package/dist/cli/daemon/config.js +73 -7
- package/dist/cli/daemon/generate.js +1 -1
- package/dist/cli/daemon/lock.js +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/presets/base.js +8 -1
- package/dist/cli/presets/nextjs.js +1 -1
- package/dist/cli/utils.js +90 -6
- package/dist/cli/wp/bootstrap.js +13 -10
- package/dist/cli/wp/byo.js +113 -0
- package/dist/cli/wp/compose/docker-compose.yml +13 -2
- package/dist/cli/wp/compose/opcache.ini +16 -0
- package/dist/cli/wp/compose/override.js +104 -0
- package/dist/cli/wp/compose/override.test.ts +115 -0
- package/dist/cli/wp/compose/override.ts +134 -0
- package/dist/cli/wp/compose/remap-entrypoint.sh +17 -0
- package/dist/cli/wp/constants.js +7 -8
- package/dist/cli/wp/dev.js +146 -0
- package/dist/cli/wp/docker.d.ts +55 -15
- package/dist/cli/wp/docker.js +102 -41
- package/dist/cli/wp/ports.js +72 -0
- package/dist/cli/wp/session.js +150 -0
- package/dist/cli/wp/settings.js +73 -0
- package/dist/cli/wp/setup.js +1 -1
- package/dist/cli/wp/stack.js +58 -0
- package/dist/cli/wp/types.d.ts +26 -5
- package/dist/cli/wp/types.js +5 -1
- package/dist/cli/wp/utils.d.ts +29 -2
- package/dist/cli/wp/utils.js +61 -3
- package/dist/cli/wp/watchdog.d.ts +1 -0
- package/dist/cli/wp/watchdog.js +45 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js +2 -2
- package/dist/config.d.ts +49 -3
- package/dist/context.d.ts +12 -1
- package/dist/context.js +1 -1
- package/dist/index.d.ts +16 -11
- package/dist/index.js +10 -6
- package/dist/integrations/nextjs/kizlo.server.d.ts +1 -1
- package/dist/integrations/nextjs/kizlo.server.js +2 -2
- package/dist/integrations/nextjs/utils.js +2 -2
- package/dist/kizlo.d.ts +27 -6
- package/dist/kizlo.js +24 -11
- package/dist/menu/fixture.d.ts +1 -1
- package/dist/package.js +1 -1
- package/dist/post/fixture.d.ts +1 -1
- package/dist/service.d.ts +2 -0
- package/dist/service.js +3 -0
- package/dist/settings/service.d.ts +22 -0
- package/dist/settings/service.interface.d.ts +134 -0
- package/dist/settings/service.js +43 -0
- package/dist/shared/error.d.ts +16 -5
- package/dist/shared/error.js +4 -0
- package/dist/shared/extension.d.ts +2 -0
- package/dist/shared/middleware.d.ts +13 -10
- package/dist/shared/middleware.js +7 -0
- package/dist/shared/procedure.d.ts +34 -28
- package/dist/shared/procedure.js +3 -6
- package/dist/shared/types.d.ts +19 -13
- package/dist/test/client.d.ts +5 -0
- package/dist/test/client.js +5 -0
- package/dist/test/server.d.ts +6 -0
- package/dist/test/server.js +6 -0
- package/dist/test.d.ts +4 -4
- package/dist/test.js +4 -4
- package/dist/webhook/index.d.ts +14 -13
- package/dist/webhook/index.js +5 -5
- package/dist/webhook/schema.d.ts +3 -3
- package/dist/webhook/schema.js +37 -37
- package/dist/webhook/utils.js +4 -4
- package/dist/wordpress/service.js +6 -0
- package/package.json +2 -2
- package/dist/cli/commands/wp.js +0 -66
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { credentialsPath, findConfigDir } from "../wp/utils.js";
|
|
2
|
-
import { detectPackageManager } from "../utils.js";
|
|
3
2
|
import { log } from "./logger.js";
|
|
3
|
+
import { detectPackageManager } from "../utils.js";
|
|
4
4
|
import z from "zod/v4";
|
|
5
|
-
import path from "node:path";
|
|
6
5
|
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
7
|
import { createJiti } from "jiti";
|
|
8
8
|
|
|
9
9
|
//#region src/cli/daemon/config.ts
|
|
@@ -12,6 +12,14 @@ const fixtureSchema = z.custom((value) => typeof value === "object" && value !==
|
|
|
12
12
|
const configSchema = z.object({
|
|
13
13
|
dir: z.string().optional(),
|
|
14
14
|
alias: z.string().optional(),
|
|
15
|
+
name: z.string().optional(),
|
|
16
|
+
dev: z.object({
|
|
17
|
+
path: z.string().optional(),
|
|
18
|
+
port: z.number().int().positive().optional(),
|
|
19
|
+
dbPort: z.number().int().positive().optional(),
|
|
20
|
+
byo: z.string().optional(),
|
|
21
|
+
fixtures: z.array(fixtureSchema).optional()
|
|
22
|
+
}).optional(),
|
|
15
23
|
test: z.object({
|
|
16
24
|
port: z.number().int().positive().optional(),
|
|
17
25
|
fixtures: z.array(fixtureSchema).optional(),
|
|
@@ -29,6 +37,9 @@ const CONFIG_FILES = [
|
|
|
29
37
|
"kizlo.config.js",
|
|
30
38
|
"kizlo.config.mjs"
|
|
31
39
|
];
|
|
40
|
+
const DEFAULT_DEV_PORT = 8080;
|
|
41
|
+
const DEFAULT_DEV_DB_PORT = 3307;
|
|
42
|
+
const DEFAULT_TEST_PORT = 8889;
|
|
32
43
|
function defaultDir(cwd) {
|
|
33
44
|
return fs.existsSync(path.join(cwd, "src")) ? "src/lib/kizlo" : "lib/kizlo";
|
|
34
45
|
}
|
|
@@ -66,22 +77,77 @@ async function resolveConfig(cwd, flags) {
|
|
|
66
77
|
};
|
|
67
78
|
}
|
|
68
79
|
/**
|
|
80
|
+
* Sanitize an arbitrary package/dir name into a valid Docker compose project id:
|
|
81
|
+
* lowercase, `@scope/pkg` → `scope-pkg`, dropping anything outside `[a-z0-9_-]`.
|
|
82
|
+
*/
|
|
83
|
+
function sanitizeProjectName(raw) {
|
|
84
|
+
return raw.toLowerCase().replace(/^@/, "").replace(/\//g, "-").replace(/[^a-z0-9_-]/g, "").replace(/^[-_]+|[-_]+$/g, "") || "kizlo";
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Base name for the local stacks: the config `name` if set, else the `package.json`
|
|
88
|
+
* `name` at `configDir`, else the config dir basename — sanitized to a Docker id.
|
|
89
|
+
*/
|
|
90
|
+
function resolveStackName(configDir, configName) {
|
|
91
|
+
if (configName) return sanitizeProjectName(configName);
|
|
92
|
+
let pkgName;
|
|
93
|
+
try {
|
|
94
|
+
pkgName = JSON.parse(fs.readFileSync(path.join(configDir, "package.json"), "utf8")).name;
|
|
95
|
+
} catch {
|
|
96
|
+
pkgName = void 0;
|
|
97
|
+
}
|
|
98
|
+
return sanitizeProjectName(pkgName ?? path.basename(configDir));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
69
101
|
* Resolve the `test` block from `kizlo.config.*` into concrete values for the
|
|
70
|
-
* `
|
|
71
|
-
*
|
|
102
|
+
* `test` command, applying defaults (port 8889, auto-detected package manager)
|
|
103
|
+
* and anchoring the credentials artifact to the config directory.
|
|
72
104
|
*/
|
|
73
105
|
async function resolveTestConfig(cwd) {
|
|
74
106
|
const configDir = findConfigDir(cwd);
|
|
75
|
-
const
|
|
107
|
+
const fileConfig = await loadConfigFile(configDir);
|
|
108
|
+
const test = fileConfig?.test ?? {};
|
|
76
109
|
return {
|
|
77
110
|
configDir,
|
|
111
|
+
project: `${resolveStackName(configDir, fileConfig?.name)}-test`,
|
|
78
112
|
command: test.command,
|
|
79
|
-
port: test.port ??
|
|
113
|
+
port: test.port ?? DEFAULT_TEST_PORT,
|
|
114
|
+
portExplicit: test.port !== void 0,
|
|
80
115
|
fixtures: test.fixtures ?? [],
|
|
81
116
|
credentialsPath: credentialsPath(cwd),
|
|
82
117
|
packageManager: test.packageManager ?? detectPackageManager(configDir)
|
|
83
118
|
};
|
|
84
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Resolve the `dev` block from `kizlo.config.*` into concrete values for the `dev`
|
|
122
|
+
* command, applying defaults (port 8080). `dev.path` is required — the whole
|
|
123
|
+
* WordPress install lives there, so we make the user choose a real folder rather
|
|
124
|
+
* than hide it under a default.
|
|
125
|
+
*/
|
|
126
|
+
async function resolveDevConfig(cwd) {
|
|
127
|
+
const configDir = findConfigDir(cwd);
|
|
128
|
+
const fileConfig = await loadConfigFile(configDir);
|
|
129
|
+
const dev = fileConfig?.dev ?? {};
|
|
130
|
+
if (!dev.path) {
|
|
131
|
+
log.error("`dev.path` is required in kizlo.config — it's the folder your local WordPress lives in.\nPick a real, visible folder (it persists your site between runs), e.g. dev: { path: \"wordpress\" }.");
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
if (dev.byo && dev.fixtures?.length) {
|
|
135
|
+
log.error("`dev.byo` and `dev.fixtures` are mutually exclusive — byo imports an existing site, so it brings its own data.");
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
configDir,
|
|
140
|
+
project: `${resolveStackName(configDir, fileConfig?.name)}-dev`,
|
|
141
|
+
port: dev.port ?? DEFAULT_DEV_PORT,
|
|
142
|
+
portExplicit: dev.port !== void 0,
|
|
143
|
+
dbPort: dev.dbPort ?? DEFAULT_DEV_DB_PORT,
|
|
144
|
+
dbPortExplicit: dev.dbPort !== void 0,
|
|
145
|
+
byo: dev.byo ? path.resolve(configDir, dev.byo) : void 0,
|
|
146
|
+
fixtures: dev.fixtures ?? [],
|
|
147
|
+
wordpressPath: dev.path,
|
|
148
|
+
wordpressDir: path.resolve(configDir, dev.path)
|
|
149
|
+
};
|
|
150
|
+
}
|
|
85
151
|
|
|
86
152
|
//#endregion
|
|
87
|
-
export { resolveConfig, resolveTestConfig };
|
|
153
|
+
export { DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT, resolveConfig, resolveDevConfig, resolveStackName, resolveTestConfig };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { generateContract } from "../../shared/contract.js";
|
|
2
2
|
import { loadEnvFiles } from "../utils.js";
|
|
3
|
-
import path from "node:path";
|
|
4
3
|
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
5
|
import { createJiti } from "jiti";
|
|
6
6
|
|
|
7
7
|
//#region src/cli/daemon/generate.ts
|
package/dist/cli/daemon/lock.js
CHANGED
package/dist/cli/index.js
CHANGED
|
@@ -13,7 +13,7 @@ runMain(defineCommand({
|
|
|
13
13
|
init: () => import("./commands/init.js").then((m) => m.init),
|
|
14
14
|
watch: () => import("./commands/watch.js").then((m) => m.watch),
|
|
15
15
|
generate: () => import("./commands/generate.js").then((m) => m.generate),
|
|
16
|
-
|
|
16
|
+
dev: () => import("./commands/dev.js").then((m) => m.dev),
|
|
17
17
|
test: () => import("./commands/test.js").then((m) => m.test)
|
|
18
18
|
}
|
|
19
19
|
}));
|
package/dist/cli/presets/base.js
CHANGED
|
@@ -3,7 +3,7 @@ const base = {
|
|
|
3
3
|
id: "base",
|
|
4
4
|
label: "Generic (no framework detected)",
|
|
5
5
|
detect: () => 1,
|
|
6
|
-
baseUrlEnvKey: "
|
|
6
|
+
baseUrlEnvKey: "KIZLO_BACKEND_URL",
|
|
7
7
|
serverEntry() {
|
|
8
8
|
return `import { createKizlo } from "kizlo"
|
|
9
9
|
|
|
@@ -11,6 +11,13 @@ export const { router, client, context, handler } = createKizlo()
|
|
|
11
11
|
`;
|
|
12
12
|
},
|
|
13
13
|
clientEntry(ctx) {
|
|
14
|
+
if (ctx.clientUrl) return `import { createKizloClient } from "kizlo"
|
|
15
|
+
import { contract } from "./${ctx.serverDirName}/generated"
|
|
16
|
+
|
|
17
|
+
// Your Kizlo backend runs on a different origin than this app. Swap in your bundler's
|
|
18
|
+
// public env var (e.g. import.meta.env.VITE_KIZLO_BACKEND_URL) to set it per environment.
|
|
19
|
+
export const client = createKizloClient(contract, { url: "${ctx.clientUrl}" })
|
|
20
|
+
`;
|
|
14
21
|
return `import { createKizloClient } from "kizlo"
|
|
15
22
|
import { contract } from "./${ctx.serverDirName}/generated"
|
|
16
23
|
|
|
@@ -8,7 +8,7 @@ const nextjs = {
|
|
|
8
8
|
...ctx.pkg.devDependencies
|
|
9
9
|
}.next ? 100 : 0;
|
|
10
10
|
},
|
|
11
|
-
baseUrlEnvKey: "
|
|
11
|
+
baseUrlEnvKey: "NEXT_PUBLIC_KIZLO_BACKEND_URL",
|
|
12
12
|
apiPath: "/api/kizlo",
|
|
13
13
|
serverEntry() {
|
|
14
14
|
return `import { createKizlo } from "kizlo/nextjs/server"
|
package/dist/cli/utils.js
CHANGED
|
@@ -1,10 +1,31 @@
|
|
|
1
|
+
import { log } from "./daemon/logger.js";
|
|
2
|
+
import { PortInUseError, resolveHostPort } from "./wp/ports.js";
|
|
1
3
|
import { spawnSync } from "node:child_process";
|
|
4
|
+
import fs from "node:fs";
|
|
2
5
|
import path from "node:path";
|
|
3
6
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import fs from "node:fs";
|
|
5
7
|
import * as p from "@clack/prompts";
|
|
6
8
|
|
|
7
9
|
//#region src/cli/utils.ts
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a host port for a stack, exiting with a clear message when an *explicitly*
|
|
12
|
+
* configured port (`fixed`) is already taken. A default port auto-steps to the next free
|
|
13
|
+
* one (another project's stack or a stray server just shifts us over); an explicit
|
|
14
|
+
* `configKey` collision is the user's own to resolve, so we stop rather than silently
|
|
15
|
+
* serving on a port they didn't choose.
|
|
16
|
+
*/
|
|
17
|
+
async function pickStackPort(preferred, { fixed, host, configKey }) {
|
|
18
|
+
try {
|
|
19
|
+
return await resolveHostPort(preferred, {
|
|
20
|
+
fixed,
|
|
21
|
+
host
|
|
22
|
+
});
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (!(error instanceof PortInUseError)) throw error;
|
|
25
|
+
log.error(`${configKey} ${preferred} is set in kizlo.config but is already in use.\nFree that port or change ${configKey} — an explicitly set port is never auto-reassigned.`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
8
29
|
function stripJsonComments(source) {
|
|
9
30
|
return source.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (match, comment) => comment ? "" : match);
|
|
10
31
|
}
|
|
@@ -117,11 +138,49 @@ function envKeysPresent(contents, keys) {
|
|
|
117
138
|
return keys.filter((key) => new RegExp(`^\\s*${key}\\s*=`, "m").test(contents));
|
|
118
139
|
}
|
|
119
140
|
/**
|
|
141
|
+
* The grouped `.env` layout shared by `init` and `dev`, so both write the same sectioned file:
|
|
142
|
+
* a target switch, the backend URL, then the production and dev connection blocks. `baseUrlEnvKey`
|
|
143
|
+
* varies per preset (e.g. `NEXT_PUBLIC_KIZLO_BACKEND_URL`), so it's threaded in. Only sections whose
|
|
144
|
+
* keys are actually being appended get a header, so a remote/prod run skips the dev block and vice versa.
|
|
145
|
+
*/
|
|
146
|
+
function envGroups(baseUrlEnvKey) {
|
|
147
|
+
return [
|
|
148
|
+
{
|
|
149
|
+
comment: "Kizlo Target (dev | prod)",
|
|
150
|
+
keys: ["KIZLO_TARGET"]
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
comment: "Kizlo Backend URL",
|
|
154
|
+
keys: [baseUrlEnvKey]
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
comment: "Kizlo Production Env",
|
|
158
|
+
keys: [
|
|
159
|
+
"KIZLO_SITE_SECRET",
|
|
160
|
+
"KIZLO_WORDPRESS_URL",
|
|
161
|
+
"KIZLO_WORDPRESS_USERNAME",
|
|
162
|
+
"KIZLO_WORDPRESS_APPLICATION_PASSWORD"
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
comment: "Kizlo Development Env",
|
|
167
|
+
keys: [
|
|
168
|
+
"KIZLO_DEV_SITE_SECRET",
|
|
169
|
+
"KIZLO_DEV_WORDPRESS_URL",
|
|
170
|
+
"KIZLO_DEV_WORDPRESS_USERNAME",
|
|
171
|
+
"KIZLO_DEV_WORDPRESS_APPLICATION_PASSWORD"
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
120
177
|
* Merges `values` into an existing .env body: other variables, comments and
|
|
121
178
|
* blank lines are preserved. A managed key is rewritten only when listed in
|
|
122
|
-
* `overwriteKeys`; otherwise its existing line is kept. Missing keys are appended
|
|
179
|
+
* `overwriteKeys`; otherwise its existing line is kept. Missing keys are appended —
|
|
180
|
+
* grouped under their `groups` comment header (with a blank-line separator) when one
|
|
181
|
+
* is given, or as bare lines otherwise.
|
|
123
182
|
*/
|
|
124
|
-
function mergeEnv(existing, values, overwriteKeys) {
|
|
183
|
+
function mergeEnv(existing, values, overwriteKeys, groups) {
|
|
125
184
|
const body = existing.replace(/\s*$/, "");
|
|
126
185
|
const lines = body.length ? body.split(/\r?\n/) : [];
|
|
127
186
|
const added = [];
|
|
@@ -140,10 +199,20 @@ function mergeEnv(existing, values, overwriteKeys) {
|
|
|
140
199
|
}
|
|
141
200
|
return line;
|
|
142
201
|
});
|
|
143
|
-
|
|
202
|
+
const remaining = new Map(Object.entries(values).filter(([key]) => !present.has(key)));
|
|
203
|
+
const append = (key) => {
|
|
144
204
|
added.push(key);
|
|
145
|
-
next.push(`${key}=${
|
|
205
|
+
next.push(`${key}=${remaining.get(key)}`);
|
|
206
|
+
remaining.delete(key);
|
|
207
|
+
};
|
|
208
|
+
for (const group of groups ?? []) {
|
|
209
|
+
const groupKeys = group.keys.filter((key) => remaining.has(key));
|
|
210
|
+
if (!groupKeys.length) continue;
|
|
211
|
+
if (next.length) next.push("");
|
|
212
|
+
next.push(`# ${group.comment}`);
|
|
213
|
+
for (const key of groupKeys) append(key);
|
|
146
214
|
}
|
|
215
|
+
for (const key of remaining.keys()) append(key);
|
|
147
216
|
return {
|
|
148
217
|
content: `${next.join("\n")}\n`,
|
|
149
218
|
added,
|
|
@@ -177,6 +246,21 @@ async function withSpinner(message, fn, done) {
|
|
|
177
246
|
throw error;
|
|
178
247
|
}
|
|
179
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Wrap a command group's default `run` so it fires only for the bare invocation
|
|
251
|
+
* (`kizlo test`), not when a subcommand was given (`kizlo test up`). citty runs a
|
|
252
|
+
* group's own `run` *in addition to* the matched subcommand, so without this guard
|
|
253
|
+
* the default would double-fire on every subcommand. Pass the group's subcommand
|
|
254
|
+
* names; the first non-flag arg is matched the same way citty picks the subcommand.
|
|
255
|
+
*/
|
|
256
|
+
function groupDefault(subCommandNames, run) {
|
|
257
|
+
const names = new Set(subCommandNames);
|
|
258
|
+
return async (ctx) => {
|
|
259
|
+
const sub = ctx.rawArgs.find((arg) => !arg.startsWith("-"));
|
|
260
|
+
if (sub !== void 0 && names.has(sub)) return;
|
|
261
|
+
await run(ctx);
|
|
262
|
+
};
|
|
263
|
+
}
|
|
180
264
|
|
|
181
265
|
//#endregion
|
|
182
|
-
export { addDependencyArgs, detectImportAlias, detectPackageManager, ensureGitignored, envKeysPresent, getVersion, loadEnvFiles, mergeEnv, resolveModuleImport, runCommand, withSpinner, writeFileIfAbsent };
|
|
266
|
+
export { addDependencyArgs, detectImportAlias, detectPackageManager, ensureGitignored, envGroups, envKeysPresent, getVersion, groupDefault, loadEnvFiles, mergeEnv, pickStackPort, resolveModuleImport, runCommand, withSpinner, writeFileIfAbsent };
|
package/dist/cli/wp/bootstrap.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ensurePlugin, resolvePluginSource } from "./utils.js";
|
|
2
|
-
import { DEFAULT_PLUGINS, DEFAULT_PORT, SEED_MARKER_OPTION, SEED_VERSION, TEST_ADMIN, TEST_USER } from "./constants.js";
|
|
3
1
|
import { compose, composeUp, wpCli } from "./docker.js";
|
|
2
|
+
import { DEFAULT_PLUGINS, DEFAULT_PORT, SEED_MARKER_OPTION, SEED_VERSION, TEST_ADMIN, TEST_USER } from "./constants.js";
|
|
3
|
+
import { ensurePlugins } from "./utils.js";
|
|
4
4
|
|
|
5
5
|
//#region src/cli/wp/bootstrap.ts
|
|
6
6
|
async function seedUsers() {
|
|
@@ -22,7 +22,7 @@ async function seedUsers() {
|
|
|
22
22
|
if (!id) throw new Error("seed/users.json must include a user with username \"user\"");
|
|
23
23
|
return Number(id);
|
|
24
24
|
}
|
|
25
|
-
async function createAdminAppPassword() {
|
|
25
|
+
async function createAdminAppPassword(label = "harness") {
|
|
26
26
|
await compose([
|
|
27
27
|
"exec",
|
|
28
28
|
"-T",
|
|
@@ -39,7 +39,7 @@ async function createAdminAppPassword() {
|
|
|
39
39
|
"application-password",
|
|
40
40
|
"create",
|
|
41
41
|
TEST_ADMIN.username,
|
|
42
|
-
|
|
42
|
+
label,
|
|
43
43
|
"--porcelain"
|
|
44
44
|
]);
|
|
45
45
|
}
|
|
@@ -50,9 +50,7 @@ async function createAdminAppPassword() {
|
|
|
50
50
|
* construct the REST client in Phase 2.
|
|
51
51
|
*/
|
|
52
52
|
async function bootstrapWp(config) {
|
|
53
|
-
const
|
|
54
|
-
const url = `http://localhost:${port}`;
|
|
55
|
-
process.env.WP_PORT = String(port);
|
|
53
|
+
const url = `http://localhost:${config.port ?? DEFAULT_PORT}`;
|
|
56
54
|
await composeUp();
|
|
57
55
|
if ((await compose([
|
|
58
56
|
"exec",
|
|
@@ -77,7 +75,7 @@ async function bootstrapWp(config) {
|
|
|
77
75
|
"/%postname%/",
|
|
78
76
|
"--hard"
|
|
79
77
|
]);
|
|
80
|
-
|
|
78
|
+
await ensurePlugins([...DEFAULT_PLUGINS, ...config.fixtures?.flatMap((f) => f.plugins ?? []) ?? []]);
|
|
81
79
|
const adminId = Number(await wpCli([
|
|
82
80
|
"user",
|
|
83
81
|
"get",
|
|
@@ -88,7 +86,12 @@ async function bootstrapWp(config) {
|
|
|
88
86
|
const appPassword = await createAdminAppPassword();
|
|
89
87
|
await wpCli([
|
|
90
88
|
"option",
|
|
91
|
-
"
|
|
89
|
+
"delete",
|
|
90
|
+
SEED_MARKER_OPTION
|
|
91
|
+
]).catch(() => {});
|
|
92
|
+
await wpCli([
|
|
93
|
+
"option",
|
|
94
|
+
"add",
|
|
92
95
|
SEED_MARKER_OPTION,
|
|
93
96
|
SEED_VERSION,
|
|
94
97
|
"--autoload=no"
|
|
@@ -111,4 +114,4 @@ async function bootstrapWp(config) {
|
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
//#endregion
|
|
114
|
-
export { bootstrapWp };
|
|
117
|
+
export { bootstrapWp, createAdminAppPassword, seedUsers };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { cpSync, createReadStream, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "node:fs";
|
|
3
|
+
import { join, resolve, sep } from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
|
|
6
|
+
//#region src/cli/wp/byo.ts
|
|
7
|
+
/** Path under the config's `.kizlo/` dir where the dump is staged for import, then deleted. */
|
|
8
|
+
const SQL_STAGE_REL = ".kizlo/byo-import.sql";
|
|
9
|
+
function tar(args) {
|
|
10
|
+
try {
|
|
11
|
+
return execFileSync("tar", args, {
|
|
12
|
+
encoding: "utf8",
|
|
13
|
+
maxBuffer: 64 * 1024 * 1024
|
|
14
|
+
});
|
|
15
|
+
} catch (error) {
|
|
16
|
+
const e = error;
|
|
17
|
+
throw new Error(`tar ${args.join(" ")} failed:\n${e.stderr || e.message}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Drop a leading `./` that some tar archives prefix entries with. */
|
|
21
|
+
function norm(entry) {
|
|
22
|
+
return entry.replace(/^\.\//, "");
|
|
23
|
+
}
|
|
24
|
+
/** Move `src` to `dest`, falling back to copy+remove across filesystems (rename's EXDEV). */
|
|
25
|
+
function moveInto(src, dest) {
|
|
26
|
+
try {
|
|
27
|
+
renameSync(src, dest);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error.code !== "EXDEV") throw error;
|
|
30
|
+
cpSync(src, dest, { recursive: true });
|
|
31
|
+
rmSync(src, {
|
|
32
|
+
recursive: true,
|
|
33
|
+
force: true
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Validate a BYO archive and return the name of its single root-level `.sql` dump. The
|
|
39
|
+
* archive must be a `.tar.gz` holding a top-level `wordpress/` directory and exactly one
|
|
40
|
+
* root-level `*.sql`, and must live outside `wordpressDir` (else `reset`, which wipes
|
|
41
|
+
* that folder, would delete the archive before it could be re-read).
|
|
42
|
+
*/
|
|
43
|
+
function validateByoArchive(archivePath, wordpressDir) {
|
|
44
|
+
if (!existsSync(archivePath)) throw new Error(`dev.byo archive not found: ${archivePath}`);
|
|
45
|
+
if (!/\.tar(\.(gz|xz|bz2|zst))?$|\.(tgz|txz|tbz2?|tzst)$/i.test(archivePath)) throw new Error(`dev.byo must be a tar archive (.tar.gz, .tar.xz, .tar.bz2, .tar.zst, .tar): ${archivePath}`);
|
|
46
|
+
if (resolve(archivePath).startsWith(resolve(wordpressDir) + sep)) throw new Error(`dev.byo archive must live outside dev.path — reset would delete it: ${archivePath}`);
|
|
47
|
+
const entries = tar(["-tf", archivePath]).split("\n").map(norm).filter(Boolean);
|
|
48
|
+
if (!entries.some((e) => e === "wordpress/" || e.startsWith("wordpress/"))) throw new Error("dev.byo archive must contain a top-level `wordpress/` directory");
|
|
49
|
+
const sqls = entries.filter((e) => /^[^/]+\.sql$/.test(e));
|
|
50
|
+
if (sqls.length !== 1) throw new Error(`dev.byo archive must contain exactly one root-level .sql file (found ${sqls.length})`);
|
|
51
|
+
return sqls[0];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Detect the dump's table prefix from its `{prefix}options` table — the one table every
|
|
55
|
+
* WordPress install has exactly one of. Streams the file and stops at the first match;
|
|
56
|
+
* falls back to `wp_` when none is found (so a standard dump still imports).
|
|
57
|
+
*/
|
|
58
|
+
async function detectTablePrefix(sqlPath) {
|
|
59
|
+
const rl = createInterface({
|
|
60
|
+
input: createReadStream(sqlPath),
|
|
61
|
+
crlfDelay: Number.POSITIVE_INFINITY
|
|
62
|
+
});
|
|
63
|
+
try {
|
|
64
|
+
for await (const line of rl) {
|
|
65
|
+
const match = line.match(/(?:CREATE TABLE(?:\s+IF NOT EXISTS)?|INSERT INTO)\s+`?([0-9a-zA-Z$_]+?)options`?\b/i);
|
|
66
|
+
if (match) return match[1];
|
|
67
|
+
}
|
|
68
|
+
} finally {
|
|
69
|
+
rl.close();
|
|
70
|
+
}
|
|
71
|
+
return "wp_";
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Lay a BYO install onto disk before the containers boot: extract the archive, move its
|
|
75
|
+
* `wordpress/` files into `wordpressDir` (dropping the bundled `wp-config.php` — the image
|
|
76
|
+
* regenerates one with the compose DB credentials), stage the dump under `.kizlo/`, and
|
|
77
|
+
* read its table prefix. Returns the prefix (to match the regenerated config) and the host
|
|
78
|
+
* path of the staged dump (the caller pipes it into the `mysql` container, then deletes it).
|
|
79
|
+
*/
|
|
80
|
+
async function prepareByo(archivePath, wordpressDir, configDir) {
|
|
81
|
+
const sqlName = validateByoArchive(archivePath, wordpressDir);
|
|
82
|
+
const tmp = join(configDir, ".kizlo", "byo-extract");
|
|
83
|
+
rmSync(tmp, {
|
|
84
|
+
recursive: true,
|
|
85
|
+
force: true
|
|
86
|
+
});
|
|
87
|
+
mkdirSync(tmp, { recursive: true });
|
|
88
|
+
try {
|
|
89
|
+
tar([
|
|
90
|
+
"-xf",
|
|
91
|
+
archivePath,
|
|
92
|
+
"-C",
|
|
93
|
+
tmp
|
|
94
|
+
]);
|
|
95
|
+
mkdirSync(wordpressDir, { recursive: true });
|
|
96
|
+
for (const entry of readdirSync(join(tmp, "wordpress"))) moveInto(join(tmp, "wordpress", entry), join(wordpressDir, entry));
|
|
97
|
+
rmSync(join(wordpressDir, "wp-config.php"), { force: true });
|
|
98
|
+
const sqlPath = join(configDir, SQL_STAGE_REL);
|
|
99
|
+
moveInto(join(tmp, sqlName), sqlPath);
|
|
100
|
+
return {
|
|
101
|
+
prefix: await detectTablePrefix(sqlPath),
|
|
102
|
+
sqlPath
|
|
103
|
+
};
|
|
104
|
+
} finally {
|
|
105
|
+
rmSync(tmp, {
|
|
106
|
+
recursive: true,
|
|
107
|
+
force: true
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
//#endregion
|
|
113
|
+
export { prepareByo };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
name
|
|
2
|
-
|
|
1
|
+
# Project name is supplied per-stack via `docker compose -p <project>`.
|
|
3
2
|
services:
|
|
4
3
|
mysql:
|
|
5
4
|
image: mysql:8
|
|
@@ -36,8 +35,20 @@ services:
|
|
|
36
35
|
WORDPRESS_DB_NAME: ${MYSQL_DATABASE:-wordpress}
|
|
37
36
|
WORDPRESS_DB_USER: ${MYSQL_USER:-wordpress}
|
|
38
37
|
WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD:-wppass}
|
|
38
|
+
# The wordpress image eval()s WORDPRESS_CONFIG_EXTRA at runtime, and Compose
|
|
39
|
+
# interpolates `$` in this file first — so every PHP variable below is written
|
|
40
|
+
# `$$` to survive Compose and reach PHP as a literal `$`.
|
|
39
41
|
WORDPRESS_CONFIG_EXTRA: |
|
|
40
42
|
define('WP_ENVIRONMENT_TYPE', 'local');
|
|
43
|
+
// Dev only: serve under whatever host the request came in on (localhost or a LAN
|
|
44
|
+
// IP) instead of the stored siteurl, so the stack is reachable from other devices
|
|
45
|
+
// on the network, like `next dev`. CLI requests have no HTTP_HOST and fall back to
|
|
46
|
+
// the database value, so wp-cli/bootstrap stay on the canonical URL.
|
|
47
|
+
if (!empty($$_SERVER['HTTP_HOST'])) {
|
|
48
|
+
$$kizlo_scheme = (!empty($$_SERVER['HTTPS']) && $$_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://';
|
|
49
|
+
define('WP_HOME', $$kizlo_scheme . $$_SERVER['HTTP_HOST']);
|
|
50
|
+
define('WP_SITEURL', $$kizlo_scheme . $$_SERVER['HTTP_HOST']);
|
|
51
|
+
}
|
|
41
52
|
volumes:
|
|
42
53
|
- wp_data:/var/www/html
|
|
43
54
|
healthcheck:
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
; Kizlo dev — PHP OPcache tuned for local development.
|
|
2
|
+
;
|
|
3
|
+
; OPcache keeps compiled PHP bytecode in memory, so WordPress skips re-reading and
|
|
4
|
+
; recompiling its hundreds of files on every request — the single biggest speedup
|
|
5
|
+
; when the install is bind-mounted from the host.
|
|
6
|
+
;
|
|
7
|
+
; validate_timestamps=1 (with revalidate_freq=0) makes OPcache re-check each file's
|
|
8
|
+
; mtime every request, so live edits from a bind mount show up immediately. Never
|
|
9
|
+
; set this to 0 in dev or your changes will appear "stuck" behind a stale cache.
|
|
10
|
+
opcache.enable=1
|
|
11
|
+
opcache.enable_cli=0
|
|
12
|
+
opcache.memory_consumption=128
|
|
13
|
+
opcache.interned_strings_buffer=16
|
|
14
|
+
opcache.max_accelerated_files=10000
|
|
15
|
+
opcache.validate_timestamps=1
|
|
16
|
+
opcache.revalidate_freq=0
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { OPCACHE_INI, REMAP_ENTRYPOINT } from "../constants.js";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
//#region src/cli/wp/compose/override.ts
|
|
6
|
+
/** Generated dev override location, relative to the config root. */
|
|
7
|
+
const OVERRIDE_REL = ".kizlo/docker-compose.dev.override.yml";
|
|
8
|
+
/** Generated test override location (plugin mounts only), relative to the config root. */
|
|
9
|
+
const TEST_OVERRIDE_REL = ".kizlo/docker-compose.test.override.yml";
|
|
10
|
+
const WP_ROOT = "/var/www/html";
|
|
11
|
+
const PLUGINS_DIR = `${WP_ROOT}/wp-content/plugins`;
|
|
12
|
+
/** Where the dev OPcache config is dropped so the container's PHP picks it up. */
|
|
13
|
+
const PHP_CONF_TARGET = "/usr/local/etc/php/conf.d/zz-kizlo-opcache.ini";
|
|
14
|
+
/** Where the Linux uid-remap entrypoint is mounted (run via `sh`, so no +x needed). */
|
|
15
|
+
const REMAP_TARGET = "/usr/local/bin/kizlo-remap-entrypoint.sh";
|
|
16
|
+
function renderService(name, block) {
|
|
17
|
+
const lines = [` ${name}:`];
|
|
18
|
+
if (block.user) lines.push(` user: "${block.user}"`);
|
|
19
|
+
if (block.entrypoint) lines.push(` entrypoint: ${JSON.stringify(block.entrypoint)}`);
|
|
20
|
+
if (block.env) {
|
|
21
|
+
lines.push(" environment:");
|
|
22
|
+
for (const [key, value] of Object.entries(block.env)) lines.push(` ${key}: "${value}"`);
|
|
23
|
+
}
|
|
24
|
+
if (block.ports?.length) lines.push(" ports:", ...block.ports.map((p) => ` - "${p}"`));
|
|
25
|
+
if (block.volumes?.length) lines.push(" volumes:", ...block.volumes);
|
|
26
|
+
return lines.join("\n");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A compose volume line (`- "host:container"`). `:ro` marks it read-only — the
|
|
30
|
+
* container can read it but never write back to the host file.
|
|
31
|
+
*/
|
|
32
|
+
function bind(host, container, readOnly = false) {
|
|
33
|
+
return ` - "${host}:${container}${readOnly ? ":ro" : ""}"`;
|
|
34
|
+
}
|
|
35
|
+
/** Resolve each local-plugin dir to a volume bind under `wp-content/plugins`, layered by basename. */
|
|
36
|
+
function pluginBinds(configDir, mounts) {
|
|
37
|
+
return mounts.map((entry) => bind(isAbsolute(entry) ? entry : resolve(configDir, entry), `${PLUGINS_DIR}/${basename(entry)}`));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Generate the dev compose override. The whole install is bind-mounted from the
|
|
41
|
+
* host `wordpressDir`, so every file is live and editable; local plugin dirs layer
|
|
42
|
+
* on top by basename; and the dev OPcache config is dropped into PHP (a speed win
|
|
43
|
+
* with live-edit-safe settings). Binds apply to both `wordpress` (serves the site)
|
|
44
|
+
* and `wp-cli` (activates plugins). On Linux (`hostUser` set) the container also
|
|
45
|
+
* runs as the host user so generated files are owned by you, not uid 33. Returns
|
|
46
|
+
* the override path to append to the dev stack's compose files.
|
|
47
|
+
*/
|
|
48
|
+
function writeDevOverride(configDir, opts) {
|
|
49
|
+
const path$1 = resolve(configDir, OVERRIDE_REL);
|
|
50
|
+
mkdirSync(dirname(path$1), { recursive: true });
|
|
51
|
+
mkdirSync(opts.wordpressDir, { recursive: true });
|
|
52
|
+
const mounts = pluginBinds(configDir, opts.mounts);
|
|
53
|
+
const rootBind = bind(opts.wordpressDir, WP_ROOT);
|
|
54
|
+
const remap = opts.hostUser ? {
|
|
55
|
+
wordpress: {
|
|
56
|
+
user: "0:0",
|
|
57
|
+
entrypoint: ["sh", REMAP_TARGET],
|
|
58
|
+
env: {
|
|
59
|
+
KIZLO_PUID: String(opts.hostUser.uid),
|
|
60
|
+
KIZLO_PGID: String(opts.hostUser.gid)
|
|
61
|
+
},
|
|
62
|
+
scriptBind: bind(REMAP_ENTRYPOINT, REMAP_TARGET, true)
|
|
63
|
+
},
|
|
64
|
+
wpCliUser: `${opts.hostUser.uid}:${opts.hostUser.gid}`
|
|
65
|
+
} : void 0;
|
|
66
|
+
const wordpress = {
|
|
67
|
+
user: remap?.wordpress.user,
|
|
68
|
+
entrypoint: remap?.wordpress.entrypoint,
|
|
69
|
+
env: remap?.wordpress.env,
|
|
70
|
+
volumes: [
|
|
71
|
+
...remap ? [remap.wordpress.scriptBind] : [],
|
|
72
|
+
rootBind,
|
|
73
|
+
...mounts,
|
|
74
|
+
bind(OPCACHE_INI, PHP_CONF_TARGET, true)
|
|
75
|
+
]
|
|
76
|
+
};
|
|
77
|
+
const wpCli = {
|
|
78
|
+
user: remap?.wpCliUser,
|
|
79
|
+
volumes: [rootBind, ...mounts]
|
|
80
|
+
};
|
|
81
|
+
const services = [renderService("wordpress", wordpress), renderService("wp-cli", wpCli)];
|
|
82
|
+
if (opts.dbPort) services.push(renderService("mysql", { ports: [`127.0.0.1:${opts.dbPort}:3306`] }));
|
|
83
|
+
writeFileSync(path$1, `services:\n${services.join("\n")}\n`);
|
|
84
|
+
return path$1;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Generate the test compose override: bind-mount the fixtures' local plugin dirs over
|
|
88
|
+
* `wp-content/plugins` (by basename) on both `wordpress` and `wp-cli`, layered on top of
|
|
89
|
+
* the test stack's `wp_data` volume. Unlike the dev override it touches nothing else —
|
|
90
|
+
* the test stack keeps its volume-backed install, OPcache, and default entrypoints — so a
|
|
91
|
+
* fixture carrying a `{ path }` plugin runs against your live source with no build/zip
|
|
92
|
+
* step, exactly as in `kizlo dev`. Returns the override path to append to the test stack's
|
|
93
|
+
* compose files.
|
|
94
|
+
*/
|
|
95
|
+
function writeTestOverride(configDir, mounts) {
|
|
96
|
+
const path$1 = resolve(configDir, TEST_OVERRIDE_REL);
|
|
97
|
+
mkdirSync(dirname(path$1), { recursive: true });
|
|
98
|
+
const binds = pluginBinds(configDir, mounts);
|
|
99
|
+
writeFileSync(path$1, `services:\n${[renderService("wordpress", { volumes: binds }), renderService("wp-cli", { volumes: binds })].join("\n")}\n`);
|
|
100
|
+
return path$1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
104
|
+
export { writeDevOverride, writeTestOverride };
|