kizlo 0.1.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 +21 -0
- package/README.md +74 -0
- package/dist/adapters/auth.d.ts +18 -0
- package/dist/adapters/auth.js +15 -0
- package/dist/adapters/captcha.d.ts +28 -0
- package/dist/adapters/captcha.js +86 -0
- package/dist/adapters/geo.d.ts +18 -0
- package/dist/adapters/geo.js +18 -0
- package/dist/adapters/geo.vercel.d.ts +6 -0
- package/dist/adapters/geo.vercel.js +41 -0
- package/dist/adapters/logger.d.ts +31 -0
- package/dist/adapters/logger.js +30 -0
- package/dist/cli/banner.js +26 -0
- package/dist/cli/commands/generate.js +34 -0
- package/dist/cli/commands/init.js +313 -0
- package/dist/cli/commands/test.js +68 -0
- package/dist/cli/commands/watch.js +53 -0
- package/dist/cli/commands/wp.js +66 -0
- package/dist/cli/daemon/config.js +87 -0
- package/dist/cli/daemon/generate.js +40 -0
- package/dist/cli/daemon/lock.js +31 -0
- package/dist/cli/daemon/logger.js +8 -0
- package/dist/cli/daemon/watch.js +37 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +22 -0
- package/dist/cli/presets/base.js +23 -0
- package/dist/cli/presets/index.js +16 -0
- package/dist/cli/presets/nextjs.js +39 -0
- package/dist/cli/utils.js +182 -0
- package/dist/cli/wp/bootstrap.js +114 -0
- package/dist/cli/wp/compose/docker-compose.yml +73 -0
- package/dist/cli/wp/constants.js +52 -0
- package/dist/cli/wp/docker.d.ts +24 -0
- package/dist/cli/wp/docker.js +69 -0
- package/dist/cli/wp/setup.js +39 -0
- package/dist/cli/wp/types.d.ts +72 -0
- package/dist/cli/wp/types.js +8 -0
- package/dist/cli/wp/utils.d.ts +39 -0
- package/dist/cli/wp/utils.js +105 -0
- package/dist/client.d.ts +24 -0
- package/dist/client.js +55 -0
- package/dist/comment/errors.js +47 -0
- package/dist/comment/index.js +138 -0
- package/dist/comment/schema.js +72 -0
- package/dist/comment/utils.js +35 -0
- package/dist/config.d.ts +39 -0
- package/dist/config.js +7 -0
- package/dist/context.d.ts +73 -0
- package/dist/context.js +145 -0
- package/dist/cookie.d.ts +19 -0
- package/dist/cookie.js +25 -0
- package/dist/email/service.d.ts +11 -0
- package/dist/email/service.interface.d.ts +8 -0
- package/dist/email/service.js +20 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +31 -0
- package/dist/integrations/nextjs/kizlo.client.d.ts +10 -0
- package/dist/integrations/nextjs/kizlo.client.js +13 -0
- package/dist/integrations/nextjs/kizlo.server.d.ts +12 -0
- package/dist/integrations/nextjs/kizlo.server.js +13 -0
- package/dist/integrations/nextjs/utils.js +11 -0
- package/dist/kizlo.d.ts +73 -0
- package/dist/kizlo.js +144 -0
- package/dist/menu/errors.js +16 -0
- package/dist/menu/fixture.d.ts +8 -0
- package/dist/menu/fixture.js +62 -0
- package/dist/menu/index.js +70 -0
- package/dist/menu/schema.js +70 -0
- package/dist/menu/utils.js +57 -0
- package/dist/nextjs/server.d.ts +2 -0
- package/dist/nextjs/server.js +3 -0
- package/dist/nextjs.d.ts +2 -0
- package/dist/nextjs.js +3 -0
- package/dist/package.js +5 -0
- package/dist/post/error.js +30 -0
- package/dist/post/fixture.d.ts +8 -0
- package/dist/post/fixture.js +71 -0
- package/dist/post/index.js +123 -0
- package/dist/post/schema.js +120 -0
- package/dist/post/utils.js +42 -0
- package/dist/post-type/service.js +48 -0
- package/dist/router.d.ts +624 -0
- package/dist/router.js +15 -0
- package/dist/seo/index.js +54 -0
- package/dist/seo/schema.d.ts +196 -0
- package/dist/seo/schema.js +96 -0
- package/dist/seo/types.d.ts +80 -0
- package/dist/seo/utils.js +44 -0
- package/dist/service.d.ts +17 -0
- package/dist/service.js +17 -0
- package/dist/shared/constants.js +10 -0
- package/dist/shared/contract.d.ts +21 -0
- package/dist/shared/contract.js +43 -0
- package/dist/shared/crypto.d.ts +28 -0
- package/dist/shared/crypto.js +34 -0
- package/dist/shared/error.d.ts +128 -0
- package/dist/shared/error.js +147 -0
- package/dist/shared/extension.d.ts +25 -0
- package/dist/shared/extension.js +7 -0
- package/dist/shared/identifier.d.ts +11 -0
- package/dist/shared/identifier.js +26 -0
- package/dist/shared/middleware.d.ts +29 -0
- package/dist/shared/procedure.d.ts +114 -0
- package/dist/shared/procedure.js +114 -0
- package/dist/shared/result.d.ts +24 -0
- package/dist/shared/result.js +49 -0
- package/dist/shared/router.d.ts +7 -0
- package/dist/shared/router.js +26 -0
- package/dist/shared/schema.d.ts +55 -0
- package/dist/shared/schema.js +43 -0
- package/dist/shared/serialize.d.ts +12 -0
- package/dist/shared/serialize.js +51 -0
- package/dist/shared/types.d.ts +35 -0
- package/dist/shared/utils.js +17 -0
- package/dist/test/auth.d.ts +7 -0
- package/dist/test/auth.js +15 -0
- package/dist/test/captcha.d.ts +7 -0
- package/dist/test/captcha.js +8 -0
- package/dist/test/client.d.ts +11 -0
- package/dist/test/client.js +14 -0
- package/dist/test/fixtures.d.ts +14 -0
- package/dist/test/fixtures.js +13 -0
- package/dist/test/server.d.ts +10 -0
- package/dist/test/server.js +32 -0
- package/dist/test/users.d.ts +14 -0
- package/dist/test/users.js +17 -0
- package/dist/test.d.ts +12 -0
- package/dist/test.js +13 -0
- package/dist/webhook/index.d.ts +38 -0
- package/dist/webhook/index.js +44 -0
- package/dist/webhook/schema.d.ts +45 -0
- package/dist/webhook/schema.js +72 -0
- package/dist/webhook/utils.js +21 -0
- package/dist/wordpress/comment/service.d.ts +28 -0
- package/dist/wordpress/comment/service.js +55 -0
- package/dist/wordpress/comment/types.d.ts +206 -0
- package/dist/wordpress/constants.d.ts +14 -0
- package/dist/wordpress/constants.js +13 -0
- package/dist/wordpress/error.d.ts +9 -0
- package/dist/wordpress/error.js +12 -0
- package/dist/wordpress/index.d.ts +15 -0
- package/dist/wordpress/menu/items/service.d.ts +28 -0
- package/dist/wordpress/menu/items/service.js +55 -0
- package/dist/wordpress/menu/items/types.d.ts +214 -0
- package/dist/wordpress/menu/service.d.ts +30 -0
- package/dist/wordpress/menu/service.js +58 -0
- package/dist/wordpress/menu/types.d.ts +115 -0
- package/dist/wordpress/post/service.d.ts +28 -0
- package/dist/wordpress/post/service.js +55 -0
- package/dist/wordpress/post/types.d.ts +290 -0
- package/dist/wordpress/service.d.ts +77 -0
- package/dist/wordpress/service.js +180 -0
- package/dist/wordpress/types.d.ts +67 -0
- package/dist/wordpress/user/service.d.ts +28 -0
- package/dist/wordpress/user/service.js +55 -0
- package/dist/wordpress/user/types.d.ts +221 -0
- package/dist/wordpress/utils.d.ts +7 -0
- package/dist/wordpress/utils.js +9 -0
- package/package.json +109 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { log } from "./logger.js";
|
|
2
|
+
import { generateOnce } from "./generate.js";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { FSWatcher } from "chokidar";
|
|
5
|
+
|
|
6
|
+
//#region src/cli/daemon/watch.ts
|
|
7
|
+
function debounce(fn, delay) {
|
|
8
|
+
let timer;
|
|
9
|
+
return ((...args) => {
|
|
10
|
+
clearTimeout(timer);
|
|
11
|
+
timer = setTimeout(() => void fn(...args), delay);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
async function regenerate(cfg) {
|
|
15
|
+
try {
|
|
16
|
+
if (await generateOnce(cfg)) log.success("Contract updated");
|
|
17
|
+
else log.warn(`No Kizlo server found in ${cfg.serverEntry}`);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
log.error("Failed to update the Kizlo contract:", error);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Watches the server directory and regenerates the contract on change. */
|
|
23
|
+
async function watch(cfg) {
|
|
24
|
+
const watcher = new FSWatcher({
|
|
25
|
+
persistent: true,
|
|
26
|
+
ignoreInitial: true,
|
|
27
|
+
ignored: path.resolve(cfg.cwd, cfg.generatedDir)
|
|
28
|
+
});
|
|
29
|
+
const onChange = debounce(() => regenerate(cfg), 300);
|
|
30
|
+
watcher.add(path.resolve(cfg.cwd, cfg.serverDir));
|
|
31
|
+
watcher.on("ready", () => log.start("Watching for changes..."));
|
|
32
|
+
watcher.on("all", () => void onChange());
|
|
33
|
+
return watcher;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
export { watch };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { getVersion } from "./utils.js";
|
|
3
|
+
import { defineCommand, runMain } from "citty";
|
|
4
|
+
|
|
5
|
+
//#region src/cli/index.ts
|
|
6
|
+
runMain(defineCommand({
|
|
7
|
+
meta: {
|
|
8
|
+
name: "kizlo",
|
|
9
|
+
version: getVersion(),
|
|
10
|
+
description: "Kizlo CLI — headless WordPress toolkit"
|
|
11
|
+
},
|
|
12
|
+
subCommands: {
|
|
13
|
+
init: () => import("./commands/init.js").then((m) => m.init),
|
|
14
|
+
watch: () => import("./commands/watch.js").then((m) => m.watch),
|
|
15
|
+
generate: () => import("./commands/generate.js").then((m) => m.generate),
|
|
16
|
+
wp: () => import("./commands/wp.js").then((m) => m.wp),
|
|
17
|
+
test: () => import("./commands/test.js").then((m) => m.test)
|
|
18
|
+
}
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
//#endregion
|
|
22
|
+
export { };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/cli/presets/base.ts
|
|
2
|
+
const base = {
|
|
3
|
+
id: "base",
|
|
4
|
+
label: "Generic (no framework detected)",
|
|
5
|
+
detect: () => 1,
|
|
6
|
+
baseUrlEnvKey: "SERVER_BASE_URL",
|
|
7
|
+
serverEntry() {
|
|
8
|
+
return `import { createKizlo } from "kizlo"
|
|
9
|
+
|
|
10
|
+
export const { router, client, context, handler } = createKizlo()
|
|
11
|
+
`;
|
|
12
|
+
},
|
|
13
|
+
clientEntry(ctx) {
|
|
14
|
+
return `import { createKizloClient } from "kizlo"
|
|
15
|
+
import { contract } from "./${ctx.serverDirName}/generated"
|
|
16
|
+
|
|
17
|
+
export const client = createKizloClient(contract)
|
|
18
|
+
`;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
23
|
+
export { base };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { base } from "./base.js";
|
|
2
|
+
import { nextjs } from "./nextjs.js";
|
|
3
|
+
|
|
4
|
+
//#region src/cli/presets/index.ts
|
|
5
|
+
/** Framework presets, in declaration order. `base` always matches last. */
|
|
6
|
+
const PRESETS = [nextjs, base];
|
|
7
|
+
function getPreset(id) {
|
|
8
|
+
return PRESETS.find((preset) => preset.id === id);
|
|
9
|
+
}
|
|
10
|
+
/** Highest-scoring preset for the project; falls back to `base`. */
|
|
11
|
+
function detectPreset(ctx) {
|
|
12
|
+
return PRESETS.reduce((best, preset) => preset.detect(ctx) > best.detect(ctx) ? preset : best);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
export { PRESETS, detectPreset, getPreset };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//#region src/cli/presets/nextjs.ts
|
|
2
|
+
const nextjs = {
|
|
3
|
+
id: "nextjs",
|
|
4
|
+
label: "Next.js",
|
|
5
|
+
detect(ctx) {
|
|
6
|
+
return {
|
|
7
|
+
...ctx.pkg.dependencies,
|
|
8
|
+
...ctx.pkg.devDependencies
|
|
9
|
+
}.next ? 100 : 0;
|
|
10
|
+
},
|
|
11
|
+
baseUrlEnvKey: "NEXT_PUBLIC_SERVER_BASE_URL",
|
|
12
|
+
apiPath: "/api/kizlo",
|
|
13
|
+
serverEntry() {
|
|
14
|
+
return `import { createKizlo } from "kizlo/nextjs/server"
|
|
15
|
+
|
|
16
|
+
export const { router, client, context, handler } = createKizlo()
|
|
17
|
+
`;
|
|
18
|
+
},
|
|
19
|
+
clientEntry(ctx) {
|
|
20
|
+
return `import { createKizloClient } from "kizlo/nextjs"
|
|
21
|
+
import { contract } from "./${ctx.serverDirName}/generated"
|
|
22
|
+
|
|
23
|
+
export const client = createKizloClient(contract)
|
|
24
|
+
`;
|
|
25
|
+
},
|
|
26
|
+
routeHandler(ctx) {
|
|
27
|
+
const dir = `${ctx.appDir}/api/kizlo/[[...rest]]`;
|
|
28
|
+
return {
|
|
29
|
+
path: `${dir}/route.ts`,
|
|
30
|
+
contents: `import { handler } from "${ctx.serverImport(dir)}"
|
|
31
|
+
|
|
32
|
+
export { handler as GET, handler as POST, handler as PUT, handler as PATCH, handler as DELETE, handler as OPTIONS }
|
|
33
|
+
`
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
export { nextjs };
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
|
|
7
|
+
//#region src/cli/utils.ts
|
|
8
|
+
function stripJsonComments(source) {
|
|
9
|
+
return source.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (match, comment) => comment ? "" : match);
|
|
10
|
+
}
|
|
11
|
+
function readTsconfigPaths(cwd) {
|
|
12
|
+
const file = path.join(cwd, "tsconfig.json");
|
|
13
|
+
if (!fs.existsSync(file)) return void 0;
|
|
14
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
15
|
+
let json;
|
|
16
|
+
try {
|
|
17
|
+
json = JSON.parse(raw);
|
|
18
|
+
} catch {
|
|
19
|
+
try {
|
|
20
|
+
json = JSON.parse(stripJsonComments(raw));
|
|
21
|
+
} catch {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return json.compilerOptions?.paths;
|
|
26
|
+
}
|
|
27
|
+
function relativeImport(targetRel, fromDirRel) {
|
|
28
|
+
const rel = path.relative(fromDirRel, targetRel).split(path.sep).join("/");
|
|
29
|
+
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Detects an import alias from tsconfig `paths` that covers `targetRel`, returning
|
|
33
|
+
* `{ prefix, importPath }`. The prefix is what `init` persists to kizlo.config;
|
|
34
|
+
* the import path is the full specifier. Returns undefined when no alias matches.
|
|
35
|
+
*/
|
|
36
|
+
function detectImportAlias(cwd, targetRel) {
|
|
37
|
+
const target = targetRel.split(path.sep).join("/").replace(/\/+$/, "");
|
|
38
|
+
for (const [alias, mappings] of Object.entries(readTsconfigPaths(cwd) ?? {})) {
|
|
39
|
+
if (!alias.endsWith("/*")) continue;
|
|
40
|
+
const prefix = alias.slice(0, -2);
|
|
41
|
+
for (const mapping of mappings ?? []) {
|
|
42
|
+
if (!mapping.endsWith("/*")) continue;
|
|
43
|
+
const base = mapping.slice(0, -2).replace(/^\.\//, "").replace(/^\.$/, "").replace(/\/+$/, "");
|
|
44
|
+
const baseSlash = base ? `${base}/` : "";
|
|
45
|
+
if (target === base || target.startsWith(baseSlash)) return {
|
|
46
|
+
prefix,
|
|
47
|
+
importPath: `${prefix}/${target.slice(baseSlash.length)}`
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Builds an import specifier for `targetRel` from `fromDirRel`. With an explicit
|
|
54
|
+
* `alias` prefix (from kizlo.config), builds `<alias>/<target>`; an empty alias
|
|
55
|
+
* forces a relative import. Without one, detects from tsconfig, else relative.
|
|
56
|
+
*/
|
|
57
|
+
function resolveModuleImport(cwd, targetRel, fromDirRel, alias) {
|
|
58
|
+
const target = targetRel.split(path.sep).join("/").replace(/\/+$/, "");
|
|
59
|
+
if (alias !== void 0) {
|
|
60
|
+
if (alias === "") return relativeImport(targetRel, fromDirRel);
|
|
61
|
+
const prefix = alias.replace(/\/+$/, "");
|
|
62
|
+
const base = detectImportAlias(cwd, targetRel);
|
|
63
|
+
return base && base.prefix === prefix ? base.importPath : `${prefix}/${target.replace(/^src\//, "")}`;
|
|
64
|
+
}
|
|
65
|
+
return detectImportAlias(cwd, targetRel)?.importPath ?? relativeImport(targetRel, fromDirRel);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Loads the project's `.env` files into `process.env`. The single place env
|
|
69
|
+
* values come from — shared by `init --yes` and the `kizlo watch`/`generate`
|
|
70
|
+
* daemon so they always read the same source.
|
|
71
|
+
*/
|
|
72
|
+
function loadEnvFiles(cwd) {
|
|
73
|
+
for (const name of [".env", ".env.local"]) {
|
|
74
|
+
const file = path.join(cwd, name);
|
|
75
|
+
if (fs.existsSync(file)) process.loadEnvFile(file);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function getVersion() {
|
|
79
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
80
|
+
const pkgPath = path.resolve(here, "../../package.json");
|
|
81
|
+
return JSON.parse(fs.readFileSync(pkgPath, "utf8")).version;
|
|
82
|
+
}
|
|
83
|
+
function detectPackageManager(cwd) {
|
|
84
|
+
if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
85
|
+
if (fs.existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
|
|
86
|
+
if (fs.existsSync(path.join(cwd, "bun.lockb")) || fs.existsSync(path.join(cwd, "bun.lock"))) return "bun";
|
|
87
|
+
return "npm";
|
|
88
|
+
}
|
|
89
|
+
function addDependencyArgs(pm, pkg) {
|
|
90
|
+
if (pm === "npm") return [
|
|
91
|
+
"npm",
|
|
92
|
+
"install",
|
|
93
|
+
pkg
|
|
94
|
+
];
|
|
95
|
+
return [
|
|
96
|
+
pm,
|
|
97
|
+
"add",
|
|
98
|
+
pkg
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
function runCommand(args, cwd, stdio = "inherit") {
|
|
102
|
+
const [command, ...rest] = args;
|
|
103
|
+
if (!command) return false;
|
|
104
|
+
return spawnSync(command, rest, {
|
|
105
|
+
cwd,
|
|
106
|
+
stdio,
|
|
107
|
+
shell: process.platform === "win32"
|
|
108
|
+
}).status === 0;
|
|
109
|
+
}
|
|
110
|
+
function writeFileIfAbsent(filePath, contents) {
|
|
111
|
+
if (fs.existsSync(filePath)) return false;
|
|
112
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
113
|
+
fs.writeFileSync(filePath, contents);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
function envKeysPresent(contents, keys) {
|
|
117
|
+
return keys.filter((key) => new RegExp(`^\\s*${key}\\s*=`, "m").test(contents));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Merges `values` into an existing .env body: other variables, comments and
|
|
121
|
+
* 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.
|
|
123
|
+
*/
|
|
124
|
+
function mergeEnv(existing, values, overwriteKeys) {
|
|
125
|
+
const body = existing.replace(/\s*$/, "");
|
|
126
|
+
const lines = body.length ? body.split(/\r?\n/) : [];
|
|
127
|
+
const added = [];
|
|
128
|
+
const updated = [];
|
|
129
|
+
const kept = [];
|
|
130
|
+
const present = /* @__PURE__ */ new Set();
|
|
131
|
+
const next = lines.map((line) => {
|
|
132
|
+
const key = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/)?.[1];
|
|
133
|
+
if (key && key in values) {
|
|
134
|
+
present.add(key);
|
|
135
|
+
if (overwriteKeys.has(key)) {
|
|
136
|
+
updated.push(key);
|
|
137
|
+
return `${key}=${values[key]}`;
|
|
138
|
+
}
|
|
139
|
+
kept.push(key);
|
|
140
|
+
}
|
|
141
|
+
return line;
|
|
142
|
+
});
|
|
143
|
+
for (const [key, value] of Object.entries(values)) if (!present.has(key)) {
|
|
144
|
+
added.push(key);
|
|
145
|
+
next.push(`${key}=${value}`);
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
content: `${next.join("\n")}\n`,
|
|
149
|
+
added,
|
|
150
|
+
updated,
|
|
151
|
+
kept
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function ensureGitignored(cwd, entry) {
|
|
155
|
+
const gitignorePath = path.join(cwd, ".gitignore");
|
|
156
|
+
const existed = fs.existsSync(gitignorePath);
|
|
157
|
+
const contents = existed ? fs.readFileSync(gitignorePath, "utf8") : "";
|
|
158
|
+
if (contents.split(/\r?\n/).some((line) => line.trim() === entry)) return "present";
|
|
159
|
+
const prefix = contents.length && !contents.endsWith("\n") ? `${contents}\n` : contents;
|
|
160
|
+
fs.writeFileSync(gitignorePath, `${prefix}${entry}\n`);
|
|
161
|
+
return existed ? "added" : "created";
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Run a slow async step behind a clack spinner. Shows `message` while `fn` runs,
|
|
165
|
+
* stops with `done` (or `message`) on success, and stops with an error mark then
|
|
166
|
+
* rethrows on failure so callers keep their existing try/finally control flow.
|
|
167
|
+
*/
|
|
168
|
+
async function withSpinner(message, fn, done) {
|
|
169
|
+
const s = p.spinner();
|
|
170
|
+
s.start(message);
|
|
171
|
+
try {
|
|
172
|
+
const result = await fn();
|
|
173
|
+
s.stop(done ?? message);
|
|
174
|
+
return result;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
s.error(`${message} failed`);
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
//#endregion
|
|
182
|
+
export { addDependencyArgs, detectImportAlias, detectPackageManager, ensureGitignored, envKeysPresent, getVersion, loadEnvFiles, mergeEnv, resolveModuleImport, runCommand, withSpinner, writeFileIfAbsent };
|
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
import { compose, composeUp, wpCli } from "./docker.js";
|
|
4
|
+
|
|
5
|
+
//#region src/cli/wp/bootstrap.ts
|
|
6
|
+
async function seedUsers() {
|
|
7
|
+
const existing = await wpCli([
|
|
8
|
+
"user",
|
|
9
|
+
"get",
|
|
10
|
+
TEST_USER.username,
|
|
11
|
+
"--field=ID"
|
|
12
|
+
]).catch(() => "");
|
|
13
|
+
const id = existing ? existing : await wpCli([
|
|
14
|
+
"user",
|
|
15
|
+
"create",
|
|
16
|
+
TEST_USER.username,
|
|
17
|
+
TEST_USER.email,
|
|
18
|
+
`--role=${TEST_USER.role}`,
|
|
19
|
+
`--user_pass=${TEST_USER.password}`,
|
|
20
|
+
"--porcelain"
|
|
21
|
+
]);
|
|
22
|
+
if (!id) throw new Error("seed/users.json must include a user with username \"user\"");
|
|
23
|
+
return Number(id);
|
|
24
|
+
}
|
|
25
|
+
async function createAdminAppPassword() {
|
|
26
|
+
await compose([
|
|
27
|
+
"exec",
|
|
28
|
+
"-T",
|
|
29
|
+
"wp-cli",
|
|
30
|
+
"wp",
|
|
31
|
+
"user",
|
|
32
|
+
"application-password",
|
|
33
|
+
"delete",
|
|
34
|
+
TEST_ADMIN.username,
|
|
35
|
+
"--all"
|
|
36
|
+
]);
|
|
37
|
+
return wpCli([
|
|
38
|
+
"user",
|
|
39
|
+
"application-password",
|
|
40
|
+
"create",
|
|
41
|
+
TEST_ADMIN.username,
|
|
42
|
+
"harness",
|
|
43
|
+
"--porcelain"
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Phase 1 — boot WordPress via docker + wp-cli: core install, plugins (kizlo core +
|
|
48
|
+
* each fixture's plugin), the base users, the admin app password, and the seed
|
|
49
|
+
* marker. Content (posts/menu) is left to fixtures. Returns everything needed to
|
|
50
|
+
* construct the REST client in Phase 2.
|
|
51
|
+
*/
|
|
52
|
+
async function bootstrapWp(config) {
|
|
53
|
+
const port = config.port ?? DEFAULT_PORT;
|
|
54
|
+
const url = `http://localhost:${port}`;
|
|
55
|
+
process.env.WP_PORT = String(port);
|
|
56
|
+
await composeUp();
|
|
57
|
+
if ((await compose([
|
|
58
|
+
"exec",
|
|
59
|
+
"-T",
|
|
60
|
+
"wp-cli",
|
|
61
|
+
"wp",
|
|
62
|
+
"core",
|
|
63
|
+
"is-installed"
|
|
64
|
+
])).code !== 0) await wpCli([
|
|
65
|
+
"core",
|
|
66
|
+
"install",
|
|
67
|
+
`--url=${url}`,
|
|
68
|
+
"--title=Kizlo Test",
|
|
69
|
+
`--admin_user=${TEST_ADMIN.username}`,
|
|
70
|
+
`--admin_password=${TEST_ADMIN.password}`,
|
|
71
|
+
`--admin_email=${TEST_ADMIN.email}`,
|
|
72
|
+
"--skip-email"
|
|
73
|
+
]);
|
|
74
|
+
await wpCli([
|
|
75
|
+
"rewrite",
|
|
76
|
+
"structure",
|
|
77
|
+
"/%postname%/",
|
|
78
|
+
"--hard"
|
|
79
|
+
]);
|
|
80
|
+
for (const plugin of [...DEFAULT_PLUGINS, ...config.fixtures?.flatMap((a) => a.plugins ?? []) ?? []]) await ensurePlugin(...resolvePluginSource(plugin));
|
|
81
|
+
const adminId = Number(await wpCli([
|
|
82
|
+
"user",
|
|
83
|
+
"get",
|
|
84
|
+
TEST_ADMIN.username,
|
|
85
|
+
"--field=ID"
|
|
86
|
+
]));
|
|
87
|
+
const userId = await seedUsers();
|
|
88
|
+
const appPassword = await createAdminAppPassword();
|
|
89
|
+
await wpCli([
|
|
90
|
+
"option",
|
|
91
|
+
"update",
|
|
92
|
+
SEED_MARKER_OPTION,
|
|
93
|
+
SEED_VERSION,
|
|
94
|
+
"--autoload=no"
|
|
95
|
+
]);
|
|
96
|
+
return {
|
|
97
|
+
url,
|
|
98
|
+
users: {
|
|
99
|
+
admin: {
|
|
100
|
+
id: adminId,
|
|
101
|
+
...TEST_ADMIN,
|
|
102
|
+
applicationPassword: appPassword
|
|
103
|
+
},
|
|
104
|
+
user: {
|
|
105
|
+
id: userId,
|
|
106
|
+
...TEST_USER
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
fixtures: {}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
//#endregion
|
|
114
|
+
export { bootstrapWp };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
name: kizlo-wp-test
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
mysql:
|
|
5
|
+
image: mysql:8
|
|
6
|
+
environment:
|
|
7
|
+
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpass}
|
|
8
|
+
MYSQL_DATABASE: ${MYSQL_DATABASE:-wordpress}
|
|
9
|
+
MYSQL_USER: ${MYSQL_USER:-wordpress}
|
|
10
|
+
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-wppass}
|
|
11
|
+
volumes:
|
|
12
|
+
- mysql_data:/var/lib/mysql
|
|
13
|
+
healthcheck:
|
|
14
|
+
test:
|
|
15
|
+
- CMD
|
|
16
|
+
- mysqladmin
|
|
17
|
+
- ping
|
|
18
|
+
- -h
|
|
19
|
+
- localhost
|
|
20
|
+
- -uroot
|
|
21
|
+
- -p${MYSQL_ROOT_PASSWORD:-rootpass}
|
|
22
|
+
interval: 5s
|
|
23
|
+
timeout: 5s
|
|
24
|
+
retries: 30
|
|
25
|
+
start_period: 20s
|
|
26
|
+
|
|
27
|
+
wordpress:
|
|
28
|
+
image: wordpress:latest
|
|
29
|
+
depends_on:
|
|
30
|
+
mysql:
|
|
31
|
+
condition: service_healthy
|
|
32
|
+
ports:
|
|
33
|
+
- "${WP_PORT:-8080}:80"
|
|
34
|
+
environment:
|
|
35
|
+
WORDPRESS_DB_HOST: mysql:3306
|
|
36
|
+
WORDPRESS_DB_NAME: ${MYSQL_DATABASE:-wordpress}
|
|
37
|
+
WORDPRESS_DB_USER: ${MYSQL_USER:-wordpress}
|
|
38
|
+
WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD:-wppass}
|
|
39
|
+
WORDPRESS_CONFIG_EXTRA: |
|
|
40
|
+
define('WP_ENVIRONMENT_TYPE', 'local');
|
|
41
|
+
volumes:
|
|
42
|
+
- wp_data:/var/www/html
|
|
43
|
+
healthcheck:
|
|
44
|
+
# wp-login.php always returns 200 unauthenticated; /wp-json can be gated to 401
|
|
45
|
+
# by plugins, so it's not a reliable liveness probe.
|
|
46
|
+
test: ["CMD-SHELL", "curl -sf http://localhost/wp-login.php > /dev/null || exit 1"]
|
|
47
|
+
interval: 5s
|
|
48
|
+
timeout: 5s
|
|
49
|
+
retries: 60
|
|
50
|
+
start_period: 30s
|
|
51
|
+
|
|
52
|
+
wp-cli:
|
|
53
|
+
image: wordpress:cli
|
|
54
|
+
depends_on:
|
|
55
|
+
wordpress:
|
|
56
|
+
condition: service_healthy
|
|
57
|
+
user: "33:33"
|
|
58
|
+
volumes:
|
|
59
|
+
- wp_data:/var/www/html
|
|
60
|
+
environment:
|
|
61
|
+
WORDPRESS_DB_HOST: mysql:3306
|
|
62
|
+
WORDPRESS_DB_NAME: ${MYSQL_DATABASE:-wordpress}
|
|
63
|
+
WORDPRESS_DB_USER: ${MYSQL_USER:-wordpress}
|
|
64
|
+
WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD:-wppass}
|
|
65
|
+
HOME: /tmp
|
|
66
|
+
WP_CLI_CACHE_DIR: /tmp/wp-cli-cache
|
|
67
|
+
# Kept warm (long-lived) so the harness can `docker compose exec wp-cli wp ...`
|
|
68
|
+
# instead of paying container-startup cost on every wp-cli call.
|
|
69
|
+
entrypoint: ["tail", "-f", "/dev/null"]
|
|
70
|
+
|
|
71
|
+
volumes:
|
|
72
|
+
mysql_data:
|
|
73
|
+
wp_data:
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { githubRelease } from "./utils.js";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
//#region src/cli/wp/constants.ts
|
|
6
|
+
const DEFAULT_PORT = 8080;
|
|
7
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
/** The docker-compose file shipped alongside this module (in both `src/` and `dist/`). */
|
|
9
|
+
const COMPOSE_FILE = resolve(HERE, "compose/docker-compose.yml");
|
|
10
|
+
const CONFIG_FILES = [
|
|
11
|
+
"kizlo.config.ts",
|
|
12
|
+
"kizlo.config.js",
|
|
13
|
+
"kizlo.config.mjs"
|
|
14
|
+
];
|
|
15
|
+
/** Credentials artifact location, relative to the config root. */
|
|
16
|
+
const CREDENTIALS_REL = ".kizlo/test-credentials.json";
|
|
17
|
+
const COMPOSE = [
|
|
18
|
+
"compose",
|
|
19
|
+
"-f",
|
|
20
|
+
COMPOSE_FILE
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* DB-side marker written as the final bootstrap step and checked by `isSeeded`.
|
|
24
|
+
* It records that this database holds a completed bootstrap — independent of any
|
|
25
|
+
* fixture content, which is optional and varies per consumer. Bump `SEED_VERSION`
|
|
26
|
+
* to force a reseed when the bootstrap contract changes.
|
|
27
|
+
*/
|
|
28
|
+
const SEED_MARKER_OPTION = "kizlo_test_seeded";
|
|
29
|
+
const SEED_VERSION = "1";
|
|
30
|
+
/** The kizlo core WordPress plugin, always installed during bootstrap. */
|
|
31
|
+
const DEFAULT_PLUGINS = [{
|
|
32
|
+
name: "kizlo",
|
|
33
|
+
source: githubRelease("kizlo-io/kizlo-wordpress", "kizlo-v1.0.0-beta.3")
|
|
34
|
+
}];
|
|
35
|
+
const TEST_ADMIN = {
|
|
36
|
+
firstName: "Admin",
|
|
37
|
+
lastName: "",
|
|
38
|
+
username: "admin",
|
|
39
|
+
password: "admin_pass",
|
|
40
|
+
email: "admin@example.com"
|
|
41
|
+
};
|
|
42
|
+
const TEST_USER = {
|
|
43
|
+
firstName: "User",
|
|
44
|
+
lastName: "",
|
|
45
|
+
username: "user",
|
|
46
|
+
email: "user@example.com",
|
|
47
|
+
password: "user_pass",
|
|
48
|
+
role: "subscriber"
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
export { COMPOSE, CONFIG_FILES, CREDENTIALS_REL, DEFAULT_PLUGINS, DEFAULT_PORT, SEED_MARKER_OPTION, SEED_VERSION, TEST_ADMIN, TEST_USER };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/cli/wp/docker.d.ts
|
|
2
|
+
interface RunResult {
|
|
3
|
+
code: number;
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
}
|
|
7
|
+
/** `docker compose <args>` against the harness compose file. */
|
|
8
|
+
declare function compose(args: string[], opts?: {
|
|
9
|
+
input?: string;
|
|
10
|
+
}): Promise<RunResult>;
|
|
11
|
+
/** Bring services up detached and wait for health checks. */
|
|
12
|
+
declare function composeUp(): Promise<void>;
|
|
13
|
+
/** Tear everything down, optionally removing volumes. */
|
|
14
|
+
declare function composeDown(opts?: {
|
|
15
|
+
volumes?: boolean;
|
|
16
|
+
}): Promise<void>;
|
|
17
|
+
/** `docker compose stop` — halt containers but keep volumes (DB intact). */
|
|
18
|
+
declare function composeStop(): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Run a wp-cli command against the warm `wp-cli` container and return trimmed stdout.
|
|
21
|
+
*/
|
|
22
|
+
declare function wpCli(args: string[]): Promise<string>;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { compose, composeDown, composeStop, composeUp, wpCli };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { COMPOSE } from "./constants.js";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
//#region src/cli/wp/docker.ts
|
|
5
|
+
function run(cmd, args, opts) {
|
|
6
|
+
return new Promise((resolvePromise, reject) => {
|
|
7
|
+
const child = spawn(cmd, args, { stdio: [
|
|
8
|
+
"pipe",
|
|
9
|
+
"pipe",
|
|
10
|
+
"pipe"
|
|
11
|
+
] });
|
|
12
|
+
let stdout = "";
|
|
13
|
+
let stderr = "";
|
|
14
|
+
child.stdout.on("data", (d) => {
|
|
15
|
+
stdout += d;
|
|
16
|
+
});
|
|
17
|
+
child.stderr.on("data", (d) => {
|
|
18
|
+
stderr += d;
|
|
19
|
+
});
|
|
20
|
+
child.on("error", reject);
|
|
21
|
+
child.on("close", (code) => resolvePromise({
|
|
22
|
+
code: code ?? 0,
|
|
23
|
+
stdout,
|
|
24
|
+
stderr
|
|
25
|
+
}));
|
|
26
|
+
if (opts?.input !== void 0) child.stdin.end(opts.input);
|
|
27
|
+
else child.stdin.end();
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** `docker compose <args>` against the harness compose file. */
|
|
31
|
+
async function compose(args, opts) {
|
|
32
|
+
return run("docker", [...COMPOSE, ...args], opts);
|
|
33
|
+
}
|
|
34
|
+
/** Bring services up detached and wait for health checks. */
|
|
35
|
+
async function composeUp() {
|
|
36
|
+
const res = await compose([
|
|
37
|
+
"up",
|
|
38
|
+
"-d",
|
|
39
|
+
"--wait"
|
|
40
|
+
]);
|
|
41
|
+
if (res.code !== 0) throw new Error(`docker compose up failed:\n${res.stderr}`);
|
|
42
|
+
}
|
|
43
|
+
/** Tear everything down, optionally removing volumes. */
|
|
44
|
+
async function composeDown(opts) {
|
|
45
|
+
const args = ["down"];
|
|
46
|
+
if (opts?.volumes) args.push("-v");
|
|
47
|
+
await compose(args);
|
|
48
|
+
}
|
|
49
|
+
/** `docker compose stop` — halt containers but keep volumes (DB intact). */
|
|
50
|
+
async function composeStop() {
|
|
51
|
+
await compose(["stop"]);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Run a wp-cli command against the warm `wp-cli` container and return trimmed stdout.
|
|
55
|
+
*/
|
|
56
|
+
async function wpCli(args) {
|
|
57
|
+
const res = await compose([
|
|
58
|
+
"exec",
|
|
59
|
+
"-T",
|
|
60
|
+
"wp-cli",
|
|
61
|
+
"wp",
|
|
62
|
+
...args
|
|
63
|
+
]);
|
|
64
|
+
if (res.code !== 0) throw new Error(`wp ${args.join(" ")} failed:\n${res.stderr || res.stdout}`);
|
|
65
|
+
return res.stdout.replace(/\r/g, "").trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
//#endregion
|
|
69
|
+
export { compose, composeDown, composeStop, composeUp, wpCli };
|