kizlo 0.9.0 → 0.10.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/LICENSE +201 -21
- package/dist/cli/commands/_setup.js +121 -77
- package/dist/cli/commands/_wiring.js +5 -4
- package/dist/cli/commands/create.js +32 -25
- package/dist/cli/commands/dev.js +63 -71
- package/dist/cli/commands/init.js +93 -103
- package/dist/cli/commands/test.js +34 -34
- package/dist/cli/daemon/config.js +18 -24
- package/dist/cli/presets/base.js +4 -2
- package/dist/cli/presets/nextjs.js +0 -1
- package/dist/cli/presets/template.js +76 -2
- package/dist/cli/utils.js +41 -19
- package/dist/cli/wp/compose/override.js +3 -3
- package/dist/cli/wp/compose/override.test.ts +2 -2
- package/dist/cli/wp/compose/override.ts +3 -3
- package/dist/cli/wp/constants.d.ts +10 -4
- package/dist/cli/wp/constants.js +10 -4
- package/dist/cli/wp/dev.js +13 -58
- package/dist/cli/wp/docker.d.ts +16 -4
- package/dist/cli/wp/docker.js +20 -7
- package/dist/cli/wp/settings.js +1 -1
- package/dist/cli/wp/setup.js +1 -1
- package/dist/cli/wp/stack.js +4 -4
- package/dist/config.d.ts +20 -23
- package/dist/index.d.ts +2 -2
- package/dist/integrations/nextjs/kizlo.server.js +1 -1
- package/dist/integrations/nextjs/utils.js +2 -2
- package/dist/kizlo.d.ts +16 -16
- package/dist/kizlo.js +19 -15
- package/dist/package.js +1 -1
- package/dist/router.d.ts +5 -5
- package/dist/test/server.js +1 -1
- package/dist/test.d.ts +3 -3
- package/dist/test.js +3 -3
- package/package.json +3 -3
- package/dist/cli/wp/byo.js +0 -113
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { dockerAvailable } from "../wp/docker.js";
|
|
2
1
|
import { addDependencyArgs, detectImportAlias, detectPackageManager, ensureGitignored, getVersion, loadEnvFiles, runCommandAsync } from "../utils.js";
|
|
3
2
|
import { PRESETS, detectPreset, getPreset } from "../presets/index.js";
|
|
4
3
|
import { fetchTemplate } from "../presets/source.js";
|
|
5
|
-
import { adaptFile, changesFor, fileEntries, patchEntries, readManifest } from "../presets/template.js";
|
|
6
|
-
import { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsNote, orCancel, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
|
|
4
|
+
import { adaptFile, changesFor, fileEntries, isOlderVersion, minCliError, patchEntries, readManifest, resolveDependencies } from "../presets/template.js";
|
|
5
|
+
import { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsNote, orCancel, readEnvExample, resolveEnvKeys, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
|
|
7
6
|
import { applyLayoutPatches, buildScaffoldContext, kizloConfigTemplate, reportScaffold, scaffoldFile, writeGeneratedContract } from "./_wiring.js";
|
|
8
7
|
import path from "node:path";
|
|
9
8
|
import fs from "node:fs";
|
|
@@ -32,7 +31,6 @@ function detectAppDir(cwd, hasSrcDir) {
|
|
|
32
31
|
return hasSrcDir ? "src/app" : "app";
|
|
33
32
|
}
|
|
34
33
|
async function collectInteractively(ctx) {
|
|
35
|
-
const conn = await collectConnectionInteractively(ctx.preset);
|
|
36
34
|
const dir = orCancel(await p.text({
|
|
37
35
|
message: "Kizlo directory",
|
|
38
36
|
initialValue: defaultDir(ctx.hasSrcDir),
|
|
@@ -49,7 +47,7 @@ async function collectInteractively(ctx) {
|
|
|
49
47
|
})).trim();
|
|
50
48
|
}
|
|
51
49
|
return {
|
|
52
|
-
...
|
|
50
|
+
...await collectConnectionInteractively(ctx.preset),
|
|
53
51
|
dir,
|
|
54
52
|
alias
|
|
55
53
|
};
|
|
@@ -60,7 +58,7 @@ async function collectInteractively(ctx) {
|
|
|
60
58
|
* later. Never fails — `--yes` always scaffolds a fillable project.
|
|
61
59
|
*/
|
|
62
60
|
function collectFromEnv(ctx) {
|
|
63
|
-
const conn = collectConnectionFromEnv(ctx.
|
|
61
|
+
const conn = collectConnectionFromEnv(ctx.envKeys);
|
|
64
62
|
const dir = defaultDir(ctx.hasSrcDir);
|
|
65
63
|
return {
|
|
66
64
|
...conn,
|
|
@@ -71,40 +69,23 @@ function collectFromEnv(ctx) {
|
|
|
71
69
|
function readPkg(pkgPath) {
|
|
72
70
|
return JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
73
71
|
}
|
|
74
|
-
function kizloDep(pkg) {
|
|
75
|
-
return pkg.dependencies?.kizlo ?? pkg.devDependencies?.kizlo;
|
|
76
|
-
}
|
|
77
|
-
/** A bare `x.y.z` from a version spec (range prefix and pre-release/build metadata dropped); undefined if none. */
|
|
78
|
-
function coerceVersion(spec) {
|
|
79
|
-
const match = /(\d+)\.(\d+)\.(\d+)/.exec(spec);
|
|
80
|
-
return match ? [
|
|
81
|
-
Number(match[1]),
|
|
82
|
-
Number(match[2]),
|
|
83
|
-
Number(match[3])
|
|
84
|
-
] : void 0;
|
|
85
|
-
}
|
|
86
|
-
/** Whether `have` is a strictly older release than `want`; false when either can't be compared. */
|
|
87
|
-
function isOlder(have, want) {
|
|
88
|
-
const a = coerceVersion(have);
|
|
89
|
-
const b = coerceVersion(want);
|
|
90
|
-
if (!a || !b) return false;
|
|
91
|
-
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] < b[i];
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
72
|
/**
|
|
95
|
-
* The template is authoritative for the
|
|
96
|
-
* (stamped at release), falling back to the running CLI's version in-repo
|
|
97
|
-
*
|
|
98
|
-
* never changed silently. Never downgrades
|
|
73
|
+
* The template is authoritative for the pinned versions: it declares `dependencies`/`devDependencies`
|
|
74
|
+
* in its manifest (stamped at release), falling back to the running CLI's version for `kizlo` in-repo
|
|
75
|
+
* before the first stamp. For each declared package the project already has at an older release, upgrade
|
|
76
|
+
* it up to the template's pin and say so — a deliberate pin is never changed silently. Never downgrades,
|
|
77
|
+
* and never adds a package the project doesn't already have (a missing `kizlo` is installed separately).
|
|
99
78
|
*/
|
|
100
|
-
async function
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
79
|
+
async function alignDependencies(cwd, pm, pkg, manifest) {
|
|
80
|
+
const { dependencies, devDependencies } = resolveDependencies(manifest);
|
|
81
|
+
for (const [name, want] of [...Object.entries(dependencies), ...Object.entries(devDependencies)]) {
|
|
82
|
+
const have = pkg.dependencies?.[name] ?? pkg.devDependencies?.[name];
|
|
83
|
+
if (!have || !isOlderVersion(have, want)) continue;
|
|
84
|
+
const s = p.spinner();
|
|
85
|
+
s.start(`Upgrading ${name} from ${have} to ${want}`);
|
|
86
|
+
const ok = await runCommandAsync(addDependencyArgs(pm, `${name}@${want}`), cwd, "ignore");
|
|
87
|
+
s.stop(ok ? `Upgraded ${name} to ${want}` : `Could not upgrade ${name} automatically — install ${name}@${want} yourself`);
|
|
88
|
+
}
|
|
108
89
|
}
|
|
109
90
|
const init = defineCommand({
|
|
110
91
|
meta: {
|
|
@@ -178,76 +159,85 @@ const init = defineCommand({
|
|
|
178
159
|
process.exit(1);
|
|
179
160
|
}
|
|
180
161
|
if (yes) loadEnvFiles(cwd);
|
|
181
|
-
const setup = yes ? collectFromEnv({
|
|
182
|
-
cwd,
|
|
183
|
-
hasSrcDir,
|
|
184
|
-
preset
|
|
185
|
-
}) : await collectInteractively({
|
|
186
|
-
cwd,
|
|
187
|
-
hasSrcDir,
|
|
188
|
-
preset
|
|
189
|
-
});
|
|
190
|
-
if (args.alias !== void 0) setup.alias = String(args.alias).trim();
|
|
191
|
-
setup.alias = aliasWithSlash(setup.alias);
|
|
192
|
-
if (setup.mode === "local" && !await dockerAvailable()) {
|
|
193
|
-
p.cancel("Docker isn't available — start Docker (or install it) and re-run, or choose “Use my own WordPress”.");
|
|
194
|
-
process.exit(1);
|
|
195
|
-
}
|
|
196
|
-
if (!hasKizlo) {
|
|
197
|
-
const spec = `kizlo@^${getVersion()}`;
|
|
198
|
-
const s = p.spinner();
|
|
199
|
-
s.start(`Installing kizlo with ${pm}`);
|
|
200
|
-
const ok = await runCommandAsync(addDependencyArgs(pm, spec), cwd, "ignore");
|
|
201
|
-
s.stop(ok ? "Installed kizlo" : "Could not install kizlo automatically");
|
|
202
|
-
if (!ok) p.log.warn(`Install it yourself: ${addDependencyArgs(pm, spec).join(" ")}`);
|
|
203
|
-
}
|
|
204
|
-
if (preset.apiPath && setup.baseUrl) setup.baseUrl = withApiPath(setup.baseUrl, preset.apiPath);
|
|
205
|
-
await setupLocalWordPress(cwd, setup);
|
|
206
|
-
await writeEnv(cwd, preset, setup, {
|
|
207
|
-
force,
|
|
208
|
-
yes
|
|
209
|
-
});
|
|
210
|
-
await syncRemote(setup);
|
|
211
|
-
const dirRel = setup.dir.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
212
|
-
const serverDirRel = path.join(dirRel, "server");
|
|
213
|
-
const clientUrl = setup.siteUrl && !sameOrigin(setup.siteUrl, setup.baseUrl) ? setup.baseUrl : void 0;
|
|
214
|
-
const scaffold = buildScaffoldContext(cwd, {
|
|
215
|
-
dirRel,
|
|
216
|
-
appDir: detectAppDir(cwd, hasSrcDir),
|
|
217
|
-
alias: setup.alias,
|
|
218
|
-
clientUrl
|
|
219
|
-
});
|
|
220
|
-
const files = [{
|
|
221
|
-
label: "Kizlo config",
|
|
222
|
-
relPath: "kizlo.config.ts",
|
|
223
|
-
contents: kizloConfigTemplate(dirRel, setup.alias, setup.devPath)
|
|
224
|
-
}];
|
|
225
162
|
let manifest;
|
|
163
|
+
let fetched;
|
|
226
164
|
if (preset.template) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
for (const entry of fileEntries(changesFor(manifest, "init"))) files.push(adaptFile(fetched.dir, entry, manifest.conventions, scaffold));
|
|
232
|
-
} finally {
|
|
165
|
+
fetched = await fetchTemplate(preset.template);
|
|
166
|
+
manifest = readManifest(fetched.dir);
|
|
167
|
+
const minErr = minCliError(manifest);
|
|
168
|
+
if (minErr) {
|
|
233
169
|
fetched.cleanup();
|
|
170
|
+
p.cancel(minErr);
|
|
171
|
+
process.exit(1);
|
|
234
172
|
}
|
|
235
|
-
}
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
173
|
+
}
|
|
174
|
+
const envKeys = resolveEnvKeys(preset, manifest);
|
|
175
|
+
try {
|
|
176
|
+
const setup = yes ? collectFromEnv({
|
|
177
|
+
cwd,
|
|
178
|
+
hasSrcDir,
|
|
179
|
+
preset,
|
|
180
|
+
envKeys
|
|
181
|
+
}) : await collectInteractively({
|
|
182
|
+
cwd,
|
|
183
|
+
hasSrcDir,
|
|
184
|
+
preset
|
|
185
|
+
});
|
|
186
|
+
if (args.alias !== void 0) setup.alias = String(args.alias).trim();
|
|
187
|
+
setup.alias = aliasWithSlash(setup.alias);
|
|
188
|
+
if (!hasKizlo) {
|
|
189
|
+
const spec = `kizlo@${manifest?.dependencies?.kizlo ?? `^${getVersion()}`}`;
|
|
190
|
+
const s = p.spinner();
|
|
191
|
+
s.start(`Installing kizlo with ${pm}`);
|
|
192
|
+
const ok = await runCommandAsync(addDependencyArgs(pm, spec), cwd, "ignore");
|
|
193
|
+
s.stop(ok ? "Installed kizlo" : "Could not install kizlo automatically");
|
|
194
|
+
if (!ok) p.log.warn(`Install it yourself: ${addDependencyArgs(pm, spec).join(" ")}`);
|
|
195
|
+
}
|
|
196
|
+
if (preset.apiPath && setup.baseUrl) setup.baseUrl = withApiPath(setup.baseUrl, preset.apiPath);
|
|
197
|
+
await setupLocalWordPress(cwd, setup);
|
|
198
|
+
await writeEnv(cwd, envKeys, setup, {
|
|
240
199
|
force,
|
|
241
|
-
yes
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
200
|
+
yes,
|
|
201
|
+
exampleTemplate: fetched ? readEnvExample(fetched.dir) : void 0
|
|
202
|
+
});
|
|
203
|
+
await syncRemote(setup);
|
|
204
|
+
const dirRel = setup.dir.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
205
|
+
const serverDirRel = path.join(dirRel, "server");
|
|
206
|
+
const clientUrl = setup.siteUrl && !sameOrigin(setup.siteUrl, setup.baseUrl) ? setup.baseUrl : void 0;
|
|
207
|
+
const scaffold = buildScaffoldContext(cwd, {
|
|
208
|
+
dirRel,
|
|
209
|
+
appDir: detectAppDir(cwd, hasSrcDir),
|
|
210
|
+
alias: setup.alias,
|
|
211
|
+
clientUrl
|
|
212
|
+
});
|
|
213
|
+
const files = [{
|
|
214
|
+
label: "Kizlo config",
|
|
215
|
+
relPath: "kizlo.config.ts",
|
|
216
|
+
contents: kizloConfigTemplate(dirRel, setup.alias, setup.mode === "local")
|
|
217
|
+
}];
|
|
218
|
+
if (manifest && fetched) {
|
|
219
|
+
await alignDependencies(cwd, pm, pkg, manifest);
|
|
220
|
+
for (const entry of fileEntries(changesFor(manifest, "init"))) files.push(adaptFile(fetched.dir, entry, manifest.conventions, scaffold));
|
|
221
|
+
} else if (preset.scaffolds) files.push(...preset.scaffolds(scaffold));
|
|
222
|
+
const scaffolded = [];
|
|
223
|
+
for (const file of files) scaffolded.push({
|
|
224
|
+
file,
|
|
225
|
+
result: await scaffoldFile(cwd, file, {
|
|
226
|
+
force,
|
|
227
|
+
yes
|
|
228
|
+
})
|
|
229
|
+
});
|
|
230
|
+
writeGeneratedContract(cwd, serverDirRel);
|
|
231
|
+
const gitignore = ensureGitignored(cwd, ".env");
|
|
232
|
+
ensureGitignored(cwd, ".kizlo/");
|
|
233
|
+
for (const { file, result } of scaffolded) reportScaffold(file, result, yes);
|
|
234
|
+
if (manifest) applyLayoutPatches(cwd, patchEntries(changesFor(manifest, "init")), manifest.conventions, scaffold);
|
|
235
|
+
if (gitignore !== "present") p.log.success(`${gitignore === "created" ? "Created" : "Updated"} .gitignore (ignoring .env)`);
|
|
236
|
+
nextStepsNote(setup);
|
|
237
|
+
p.outro("Kizlo is ready 🎉");
|
|
238
|
+
} finally {
|
|
239
|
+
fetched?.cleanup();
|
|
240
|
+
}
|
|
251
241
|
}
|
|
252
242
|
});
|
|
253
243
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createStack } from "../wp/docker.js";
|
|
1
|
+
import { createStack, dockerHint, dockerStatus } from "../wp/docker.js";
|
|
2
2
|
import { isSeeded, recordedPort } from "../wp/utils.js";
|
|
3
3
|
import { log } from "../daemon/logger.js";
|
|
4
4
|
import { isFree } from "../wp/ports.js";
|
|
@@ -19,8 +19,8 @@ async function resolve(cwd) {
|
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
|
-
* The host port to bring the test
|
|
23
|
-
* test
|
|
22
|
+
* The host port to bring the test WordPress up on. Unlike local dev WordPress (rebuilt every run), the
|
|
23
|
+
* test WordPress is reused and its tests connect via the URL recorded in the credentials artifact —
|
|
24
24
|
* so the port can't just be re-chosen each time:
|
|
25
25
|
*
|
|
26
26
|
* - A **running** stack keeps its live published port (fast reruns stay on the same URL).
|
|
@@ -40,7 +40,7 @@ async function resolveTestPort(cfg, stack, fresh) {
|
|
|
40
40
|
const recorded = recordedPort();
|
|
41
41
|
if (recorded !== void 0) {
|
|
42
42
|
if (await isFree(recorded)) return recorded;
|
|
43
|
-
log.error(`The test
|
|
43
|
+
log.error(`The test WordPress port ${recorded} (from a previous run) is now held by something else.\nFree that port, or run \`kizlo test reset\` to rebuild on a fresh one.`);
|
|
44
44
|
process.exit(1);
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -50,7 +50,7 @@ async function resolveTestPort(cfg, stack, fresh) {
|
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
|
-
* Boot the test
|
|
53
|
+
* Boot the test WordPress on a resolved host port and seed it once (idempotent — skips seeding when
|
|
54
54
|
* already seeded). Returns the port-bound stack and the port, so callers report the real URL.
|
|
55
55
|
*/
|
|
56
56
|
async function bringUp(cfg, stack, fresh = false) {
|
|
@@ -59,8 +59,8 @@ async function bringUp(cfg, stack, fresh = false) {
|
|
|
59
59
|
...cfg,
|
|
60
60
|
port
|
|
61
61
|
}));
|
|
62
|
-
await withSpinner("Starting
|
|
63
|
-
if (await isSeeded()) log.info("
|
|
62
|
+
await withSpinner("Starting test WordPress", () => bound.composeUp(), "Test WordPress ready");
|
|
63
|
+
if (await isSeeded()) log.info("Test WordPress already seeded — skipping seed.");
|
|
64
64
|
else await withSpinner("Seeding WordPress", () => runSeeds({
|
|
65
65
|
port,
|
|
66
66
|
fixtures: cfg.fixtures
|
|
@@ -90,30 +90,45 @@ function spawnTest(command, pm) {
|
|
|
90
90
|
child.on("close", (code) => resolvePromise(code ?? 0));
|
|
91
91
|
});
|
|
92
92
|
}
|
|
93
|
+
/** Log which test command is about to run, then spawn it and resolve with its exit code. */
|
|
94
|
+
function runProjectTests(cfg) {
|
|
95
|
+
log.info(cfg.command ? `Running: ${cfg.command}` : `Running: ${testCommand(cfg.packageManager).join(" ")}`);
|
|
96
|
+
return spawnTest(cfg.command, cfg.packageManager);
|
|
97
|
+
}
|
|
93
98
|
const runArgs = {
|
|
94
99
|
teardown: {
|
|
95
100
|
type: "boolean",
|
|
96
|
-
description: "Tear down the test
|
|
101
|
+
description: "Tear down the test WordPress after tests finish (default: leave it running for fast reruns)"
|
|
97
102
|
},
|
|
98
103
|
reset: {
|
|
99
104
|
type: "boolean",
|
|
100
105
|
description: "Wipe the database and reseed before running"
|
|
101
106
|
}
|
|
102
107
|
};
|
|
103
|
-
/**
|
|
108
|
+
/**
|
|
109
|
+
* Run the project's tests. When `test.local` is set, boot (+ seed) local WordPress first, run the
|
|
110
|
+
* suite (JS then the plugin's PHPUnit) against it, then leave it up (or tear down). Without
|
|
111
|
+
* `test.local`, there's no Docker WordPress to manage — just run the project's own test script.
|
|
112
|
+
*/
|
|
104
113
|
async function runSuite({ args }) {
|
|
105
|
-
const
|
|
114
|
+
const cfg = await resolveTestConfig(process.cwd());
|
|
115
|
+
if (!cfg.local) process.exit(await runProjectTests(cfg));
|
|
116
|
+
const status = await dockerStatus();
|
|
117
|
+
if (status !== "running") {
|
|
118
|
+
log.error(dockerHint(status));
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
const probe = createStack(testStack(cfg));
|
|
106
122
|
if (args.reset) await withSpinner("Wiping WordPress database", () => probe.composeDown({ volumes: true }), "Database wiped");
|
|
107
123
|
const { stack } = await bringUp(cfg, probe, Boolean(args.reset));
|
|
108
|
-
log.info(cfg.command ? `Running: ${cfg.command}` : `Running: ${testCommand(cfg.packageManager).join(" ")}`);
|
|
109
124
|
let code = 1;
|
|
110
125
|
try {
|
|
111
|
-
const jsCode = await
|
|
126
|
+
const jsCode = await runProjectTests(cfg);
|
|
112
127
|
const phpCode = await runPluginPhpunit(cfg);
|
|
113
128
|
code = jsCode !== 0 ? jsCode : phpCode;
|
|
114
129
|
} finally {
|
|
115
|
-
if (args.teardown) await withSpinner("Tearing down
|
|
116
|
-
else log.info("
|
|
130
|
+
if (args.teardown) await withSpinner("Tearing down test WordPress", () => stack.composeDown(), "Test WordPress stopped");
|
|
131
|
+
else log.info("Test WordPress left running for fast reruns — `kizlo test stop` to stop it, or rerun with --teardown.");
|
|
117
132
|
}
|
|
118
133
|
process.exit(code);
|
|
119
134
|
}
|
|
@@ -121,45 +136,30 @@ const subCommands = {
|
|
|
121
136
|
stop: defineCommand({
|
|
122
137
|
meta: {
|
|
123
138
|
name: "stop",
|
|
124
|
-
description: "Stop the
|
|
139
|
+
description: "Stop the test WordPress, keeping the database volume"
|
|
125
140
|
},
|
|
126
141
|
async run() {
|
|
127
142
|
const { stack } = await resolve(process.cwd());
|
|
128
|
-
await withSpinner("Stopping
|
|
129
|
-
}
|
|
130
|
-
}),
|
|
131
|
-
down: defineCommand({
|
|
132
|
-
meta: {
|
|
133
|
-
name: "down",
|
|
134
|
-
description: "Stop and remove the test stack containers"
|
|
135
|
-
},
|
|
136
|
-
args: { volumes: {
|
|
137
|
-
type: "boolean",
|
|
138
|
-
alias: "v",
|
|
139
|
-
description: "Also drop the database volume"
|
|
140
|
-
} },
|
|
141
|
-
async run({ args }) {
|
|
142
|
-
const { stack } = await resolve(process.cwd());
|
|
143
|
-
await withSpinner("Removing WordPress test stack", () => stack.composeDown({ volumes: args.volumes }), args.volumes ? "Stack removed (volumes dropped)" : "Stack removed (volumes kept)");
|
|
143
|
+
await withSpinner("Stopping test WordPress", () => stack.composeStop(), "Test WordPress stopped (volumes kept)");
|
|
144
144
|
}
|
|
145
145
|
}),
|
|
146
146
|
reset: defineCommand({
|
|
147
147
|
meta: {
|
|
148
148
|
name: "reset",
|
|
149
|
-
description: "Wipe the database and reseed
|
|
149
|
+
description: "Wipe the database and reseed fresh test WordPress"
|
|
150
150
|
},
|
|
151
151
|
async run() {
|
|
152
152
|
const { cfg, stack } = await resolve(process.cwd());
|
|
153
153
|
await withSpinner("Wiping WordPress database", () => stack.composeDown({ volumes: true }), "Database wiped");
|
|
154
154
|
const { port } = await bringUp(cfg, stack, true);
|
|
155
|
-
log.success(`
|
|
155
|
+
log.success(`Test WordPress ready on http://localhost:${port} — credentials at ${cfg.credentialsPath}`);
|
|
156
156
|
}
|
|
157
157
|
})
|
|
158
158
|
};
|
|
159
159
|
const test = defineCommand({
|
|
160
160
|
meta: {
|
|
161
161
|
name: "test",
|
|
162
|
-
description: "
|
|
162
|
+
description: "Run tests against local WordPress (stop | reset)"
|
|
163
163
|
},
|
|
164
164
|
args: runArgs,
|
|
165
165
|
subCommands,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { LOCAL_DIR_REL } from "../wp/constants.js";
|
|
1
2
|
import { credentialsPath, findConfigDir } from "../wp/utils.js";
|
|
2
3
|
import { log } from "./logger.js";
|
|
3
4
|
import { detectPackageManager } from "../utils.js";
|
|
@@ -14,13 +15,13 @@ const configSchema = z.object({
|
|
|
14
15
|
alias: z.string().optional(),
|
|
15
16
|
name: z.string().optional(),
|
|
16
17
|
dev: z.object({
|
|
17
|
-
|
|
18
|
+
local: z.boolean().optional(),
|
|
18
19
|
port: z.number().int().positive().optional(),
|
|
19
20
|
dbPort: z.number().int().positive().optional(),
|
|
20
|
-
byo: z.string().optional(),
|
|
21
21
|
fixtures: z.array(fixtureSchema).optional()
|
|
22
22
|
}).optional(),
|
|
23
23
|
test: z.object({
|
|
24
|
+
local: z.boolean().optional(),
|
|
24
25
|
port: z.number().int().positive().optional(),
|
|
25
26
|
fixtures: z.array(fixtureSchema).optional(),
|
|
26
27
|
packageManager: z.enum([
|
|
@@ -108,6 +109,7 @@ async function resolveTestConfig(cwd) {
|
|
|
108
109
|
const test = fileConfig?.test ?? {};
|
|
109
110
|
return {
|
|
110
111
|
configDir,
|
|
112
|
+
local: Boolean(test.local),
|
|
111
113
|
project: `${resolveStackName(configDir, fileConfig?.name)}-test`,
|
|
112
114
|
command: test.command,
|
|
113
115
|
port: test.port ?? DEFAULT_TEST_PORT,
|
|
@@ -118,33 +120,26 @@ async function resolveTestConfig(cwd) {
|
|
|
118
120
|
};
|
|
119
121
|
}
|
|
120
122
|
/**
|
|
121
|
-
* Whether
|
|
122
|
-
* has
|
|
123
|
-
* project
|
|
123
|
+
* Whether this project runs local WordPress under `kizlo dev` — `dev.local` is `true` in
|
|
124
|
+
* `kizlo.config.*`. When false, `kizlo dev` has nothing to boot and runs the contract watcher alone,
|
|
125
|
+
* the path a project pointing at its own WordPress takes. The flag is written by `create`/`init` when
|
|
126
|
+
* local WordPress is chosen, and lives next to the rest of the `dev` config, so it's committed and
|
|
127
|
+
* survives `kizlo dev reset`.
|
|
124
128
|
*/
|
|
125
|
-
async function
|
|
129
|
+
async function usesLocalWordPress(cwd) {
|
|
126
130
|
const fileConfig = await loadConfigFile(findConfigDir(cwd));
|
|
127
|
-
return Boolean(fileConfig?.dev?.
|
|
131
|
+
return Boolean(fileConfig?.dev?.local);
|
|
128
132
|
}
|
|
129
133
|
/**
|
|
130
|
-
* Resolve the `dev` block from `kizlo.config.*` into concrete values for the `dev`
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* project without a local stack runs the watcher alone rather than reaching here.
|
|
134
|
+
* Resolve the `dev` block from `kizlo.config.*` into concrete values for the `dev` command, applying
|
|
135
|
+
* defaults (port 8080). The install folder is fixed at `.kizlo/local` — no longer a config choice — so
|
|
136
|
+
* there's nothing required here. Callers gate on {@link usesLocalWordPress} first, since a project
|
|
137
|
+
* without local WordPress runs the watcher alone rather than reaching here.
|
|
135
138
|
*/
|
|
136
139
|
async function resolveDevConfig(cwd) {
|
|
137
140
|
const configDir = findConfigDir(cwd);
|
|
138
141
|
const fileConfig = await loadConfigFile(configDir);
|
|
139
142
|
const dev = fileConfig?.dev ?? {};
|
|
140
|
-
if (!dev.path) {
|
|
141
|
-
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\" }.");
|
|
142
|
-
process.exit(1);
|
|
143
|
-
}
|
|
144
|
-
if (dev.byo && dev.fixtures?.length) {
|
|
145
|
-
log.error("`dev.byo` and `dev.fixtures` are mutually exclusive — byo imports an existing site, so it brings its own data.");
|
|
146
|
-
process.exit(1);
|
|
147
|
-
}
|
|
148
143
|
return {
|
|
149
144
|
configDir,
|
|
150
145
|
project: `${resolveStackName(configDir, fileConfig?.name)}-dev`,
|
|
@@ -152,12 +147,11 @@ async function resolveDevConfig(cwd) {
|
|
|
152
147
|
portExplicit: dev.port !== void 0,
|
|
153
148
|
dbPort: dev.dbPort ?? DEFAULT_DEV_DB_PORT,
|
|
154
149
|
dbPortExplicit: dev.dbPort !== void 0,
|
|
155
|
-
byo: dev.byo ? path.resolve(configDir, dev.byo) : void 0,
|
|
156
150
|
fixtures: dev.fixtures ?? [],
|
|
157
|
-
wordpressPath:
|
|
158
|
-
wordpressDir: path.resolve(configDir,
|
|
151
|
+
wordpressPath: LOCAL_DIR_REL,
|
|
152
|
+
wordpressDir: path.resolve(configDir, LOCAL_DIR_REL)
|
|
159
153
|
};
|
|
160
154
|
}
|
|
161
155
|
|
|
162
156
|
//#endregion
|
|
163
|
-
export { DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT,
|
|
157
|
+
export { DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT, resolveConfig, resolveDevConfig, resolveStackName, resolveTestConfig, usesLocalWordPress };
|
package/dist/cli/presets/base.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { DEFAULT_ENV_KEYS } from "../utils.js";
|
|
2
|
+
|
|
1
3
|
//#region src/cli/presets/base.ts
|
|
2
4
|
const base = {
|
|
3
5
|
id: "base",
|
|
4
6
|
label: "Generic (no framework detected)",
|
|
5
7
|
detect: () => 1,
|
|
6
|
-
|
|
8
|
+
envKeys: DEFAULT_ENV_KEYS,
|
|
7
9
|
scaffolds(ctx) {
|
|
8
10
|
return [{
|
|
9
11
|
label: "Kizlo server instance",
|
|
@@ -24,7 +26,7 @@ function clientEntry(ctx) {
|
|
|
24
26
|
import { contract } from "./${ctx.serverDirName}/generated"
|
|
25
27
|
|
|
26
28
|
// Your Kizlo backend runs on a different origin than this app. Swap in your bundler's
|
|
27
|
-
// public env var (e.g. import.meta.env.
|
|
29
|
+
// public env var (e.g. import.meta.env.VITE_KIZLO_API_URL) to set it per environment.
|
|
28
30
|
export const client = createKizloClient(contract, { url: "${ctx.clientUrl}" })
|
|
29
31
|
`;
|
|
30
32
|
return `import { createKizloClient } from "kizlo"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getVersion } from "../utils.js";
|
|
1
2
|
import { resolvePatchTargetPath } from "./patch.js";
|
|
2
3
|
import z from "zod/v4";
|
|
3
4
|
import path from "node:path";
|
|
@@ -58,9 +59,36 @@ const bootstrapSchema = z.object({
|
|
|
58
59
|
flags: z.array(z.string()).default([])
|
|
59
60
|
});
|
|
60
61
|
const changeSchema = z.discriminatedUnion("kind", [fileSchema, patchSchema]);
|
|
62
|
+
/**
|
|
63
|
+
* The `.env` key names the scaffold writes and the pinned runtime later reads — the one part of the
|
|
64
|
+
* wiring contract that can drift when the scaffold-time CLI and the pinned runtime disagree. Declared
|
|
65
|
+
* here as data so the scaffold writes whatever names the pinned runtime expects. Semantic slots only;
|
|
66
|
+
* the CLI still fills each value (generate a secret, provision Docker, prompt for a remote URL).
|
|
67
|
+
*/
|
|
68
|
+
const envSchema = z.object({
|
|
69
|
+
baseUrl: z.string(),
|
|
70
|
+
remote: z.object({
|
|
71
|
+
siteSecret: z.string(),
|
|
72
|
+
wpUrl: z.string(),
|
|
73
|
+
wpUsername: z.string(),
|
|
74
|
+
wpPassword: z.string()
|
|
75
|
+
}),
|
|
76
|
+
local: z.object({
|
|
77
|
+
connect: z.string(),
|
|
78
|
+
siteSecret: z.string(),
|
|
79
|
+
wpUrl: z.string(),
|
|
80
|
+
wpUsername: z.string(),
|
|
81
|
+
wpPassword: z.string()
|
|
82
|
+
})
|
|
83
|
+
});
|
|
84
|
+
/** A package name → version-range map, package.json's own vocabulary. */
|
|
85
|
+
const depsSchema = z.record(z.string(), z.string());
|
|
61
86
|
const manifestSchema = z.object({
|
|
62
87
|
framework: z.string(),
|
|
63
|
-
|
|
88
|
+
env: envSchema.optional(),
|
|
89
|
+
dependencies: depsSchema.optional(),
|
|
90
|
+
devDependencies: depsSchema.optional(),
|
|
91
|
+
minCli: z.string().optional(),
|
|
64
92
|
bootstrap: bootstrapSchema.optional(),
|
|
65
93
|
conventions: conventionsSchema,
|
|
66
94
|
base: z.array(changeSchema).default([]),
|
|
@@ -88,6 +116,52 @@ function readManifest(templateDir) {
|
|
|
88
116
|
return manifestSchema.parse(JSON.parse(fs.readFileSync(manifestPath, "utf8")));
|
|
89
117
|
}
|
|
90
118
|
/**
|
|
119
|
+
* The dependencies a scaffolded project should pin, from the manifest. `kizlo` is always present —
|
|
120
|
+
* every project needs it — so it falls back to the running CLI's version in-repo before the first
|
|
121
|
+
* stamped release; any other declared package is taken verbatim (a template only lists packages it has
|
|
122
|
+
* a version for). Framework packages release in lockstep with `kizlo`, so a stamped manifest carries
|
|
123
|
+
* them all at the same released version.
|
|
124
|
+
*/
|
|
125
|
+
function resolveDependencies(manifest) {
|
|
126
|
+
return {
|
|
127
|
+
dependencies: {
|
|
128
|
+
kizlo: `^${getVersion()}`,
|
|
129
|
+
...manifest.dependencies
|
|
130
|
+
},
|
|
131
|
+
devDependencies: { ...manifest.devDependencies }
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** A bare `x.y.z` from a version spec (range prefix and pre-release/build metadata dropped); undefined if none. */
|
|
135
|
+
function coerceVersion(spec) {
|
|
136
|
+
const match = /(\d+)\.(\d+)\.(\d+)/.exec(spec);
|
|
137
|
+
return match ? [
|
|
138
|
+
Number(match[1]),
|
|
139
|
+
Number(match[2]),
|
|
140
|
+
Number(match[3])
|
|
141
|
+
] : void 0;
|
|
142
|
+
}
|
|
143
|
+
/** Whether `have` is a strictly older release than `want`; false when either can't be compared. */
|
|
144
|
+
function isOlderVersion(have, want) {
|
|
145
|
+
const a = coerceVersion(have);
|
|
146
|
+
const b = coerceVersion(want);
|
|
147
|
+
if (!a || !b) return false;
|
|
148
|
+
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] < b[i];
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The loud-failure backstop: an error message when the running CLI is too old to apply this manifest,
|
|
153
|
+
* or `undefined` when it's fine. Dormant by design — a manifest with no `minCli` never fails, so the
|
|
154
|
+
* check is inert until a structural change bumps the floor. The first CLI-incompatible manifest change
|
|
155
|
+
* sets `minCli`; from then on older CLIs (that already ship this check) stop with a clear message
|
|
156
|
+
* instead of silently under-wiring the project.
|
|
157
|
+
*/
|
|
158
|
+
function minCliError(manifest) {
|
|
159
|
+
if (!manifest.minCli) return void 0;
|
|
160
|
+
const running = getVersion();
|
|
161
|
+
if (!isOlderVersion(running, manifest.minCli)) return void 0;
|
|
162
|
+
return `This template needs the kizlo CLI >= ${manifest.minCli} (you have ${running}). Run \`npx kizlo@latest\` and try again.`;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
91
165
|
* The changes a command applies: the shared `base` set plus the command's own section. `create` gets
|
|
92
166
|
* `base + create` (plumbing plus the demo pages); `init` gets `base + init` (plumbing plus any
|
|
93
167
|
* init-only surgical patches). `base` is applied first so a later section can override it.
|
|
@@ -193,4 +267,4 @@ function rendersHtml(file) {
|
|
|
193
267
|
}
|
|
194
268
|
|
|
195
269
|
//#endregion
|
|
196
|
-
export { adaptFile, changesFor, fileEntries, findRootLayout, isExample, patchEntries, readManifest, resolvePatch };
|
|
270
|
+
export { adaptFile, changesFor, fileEntries, findRootLayout, isExample, isOlderVersion, minCliError, patchEntries, readManifest, resolveDependencies, resolvePatch };
|