kizlo 0.18.0 → 0.19.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.
@@ -1,6 +1,5 @@
1
1
  import { generateContract } from "../../shared/contract.js";
2
2
  import { integrationEnv, resolveWordPressConnection } from "../../kizlo.js";
3
- import { WORDPRESS_CLIENT_META_REL } from "../wp/constants.js";
4
3
  import { node } from "../../integrations/node/integration.js";
5
4
  import { log } from "./logger.js";
6
5
  import { loadEnvFiles } from "../utils.js";
@@ -24,7 +23,7 @@ const CONTRACT_BARREL = [
24
23
  `import contractJson from "./contract.json"`,
25
24
  ``,
26
25
  `export const contract = contractJson as unknown as typeof procedures`,
27
- `export { endpoints, type WordPressClient } from "./wordpress"`,
26
+ `export { endpoints, type WordPressClient } from "./introspection"`,
28
27
  ``
29
28
  ].join("\n");
30
29
  var LegacyRouterExportError = class extends Error {
@@ -34,13 +33,13 @@ var LegacyRouterExportError = class extends Error {
34
33
  }
35
34
  };
36
35
  /**
37
- * `wordpress.ts` before the first generation. WordPress is reachable but untyped until then, so a
36
+ * `introspection.ts` before the first generation. WordPress is reachable but untyped until then, so a
38
37
  * freshly scaffolded project compiles, including Kizlo's own procedures, before it has ever
39
- * connected. `kizlo dev` / `kizlo generate` replaces this with the real client. `any` rather than an
40
- * empty tree on purpose: every endpoint a procedure names has to resolve to something until the
38
+ * connected. `kizlo dev` / `kizlo generate` replaces this with the real introspection. `any` rather than
39
+ * an empty tree on purpose: every endpoint a procedure names has to resolve to something until the
41
40
  * real shapes arrive, which `WP_Client` preserves by passing `any` through.
42
41
  */
43
- const WORDPRESS_STUB = [
42
+ const INTROSPECTION_STUB = [
44
43
  `import type { WP_Client } from "kizlo"`,
45
44
  ``,
46
45
  `export const endpoints = {} as any`,
@@ -123,8 +122,8 @@ function formatDiagnostic(diagnostic) {
123
122
  * partial contract is worth is the generator's call rather than the transport's. Taking it is the
124
123
  * default: a third-party plugin's broken declaration then costs its own routes and nothing else,
125
124
  * where refusing the document costs the project every route that still works, for a mistake in code
126
- * it does not own. Strict generation makes the opposite trade for CI, where a client silently short
127
- * of routes typechecks clean and ships the gap.
125
+ * it does not own. Strict generation makes the opposite trade for CI, where an introspection silently
126
+ * short of routes typechecks clean and ships the gap.
128
127
  */
129
128
  var PartialContractError = class extends Error {
130
129
  excluded;
@@ -155,7 +154,7 @@ function reportDiagnostics(document, strict) {
155
154
  const excluded = new Set(document.diagnostics.filter((diagnostic) => diagnostic.type === "error").map(diagnosticLocation));
156
155
  if (!excluded.size) return;
157
156
  if (strict) throw new PartialContractError(excluded.size);
158
- log.warn(`Generating without the ${excluded.size} excluded ${excluded.size === 1 ? "contribution" : "contributions"} above. Their routes and types are missing from the client; \`kizlo generate --strict\` refuses the contract instead.`);
157
+ log.warn(`Generating without the ${excluded.size} excluded ${excluded.size === 1 ? "contribution" : "contributions"} above. Their routes and types are missing from the introspection; \`kizlo generate --strict\` refuses the contract instead.`);
159
158
  }
160
159
  /**
161
160
  * Plugin versions already named as outdated. `kizlo dev` refetches every few seconds, and the answer
@@ -189,74 +188,69 @@ async function fetchDocument(cwd, options, etag) {
189
188
  return result;
190
189
  }
191
190
  /**
192
- * Produce the WordPress client without writing it or its ETag cache. Health checks use this to ask
191
+ * Produce the introspection source without writing it or its ETag cache. Health checks use this to ask
193
192
  * the same generator as `kizlo generate` what should be on disk, while leaving a contributor's
194
193
  * checkout untouched. Callers choose strictness just as generation does.
195
194
  */
196
- async function generateWordPressSource(cwd, options = {}) {
195
+ async function generateIntrospectionSource(cwd, options = {}) {
197
196
  const result = await fetchDocument(cwd, options);
198
197
  if (!result?.document) throw new Error("WordPress introspection returned no document.");
199
198
  const generated = generateWordPressModule(result.document);
200
199
  await assertGeneratedClientCompiles(generated);
201
200
  return generated.source;
202
201
  }
203
- async function generateWordPressOnce(cfg, options = {}) {
204
- const etag = readWordPressMeta(path.resolve(cfg.cwd, cfg.wordpressMetaPath), path.resolve(cfg.cwd, cfg.wordpressPath))?.etag;
205
- const result = await fetchDocument(cfg.cwd, options, etag);
206
- if (!result?.document) return "unchanged";
207
- const generated = generateWordPressModule(result.document);
208
- await assertGeneratedClientCompiles(generated);
209
- writeGeneratedWordPress(cfg, generated.source, wordPressMeta(result.etag, result.document.hash));
210
- return "generated";
211
- }
212
202
  /**
213
- * Write `<dir>/wordpress.ts` once for a whole workspace. Every package that ships procedures compiles
214
- * against the same file, so there is one description of WordPress in the repo rather than one per
215
- * package drifting apart. One shape too: a package that only wants the types today still gets the
216
- * class, so reaching for the runtime later needs no regeneration.
203
+ * Write `introspection.ts` at its resolved path, revalidating against the ETag cache. The one place
204
+ * the introspection reaches disk, whether or not the project has a server: a server-backed app writes
205
+ * it under `generated/`, and a package that only ships procedures writes it at its own `dir.introspection`.
217
206
  */
218
- async function generateWorkspaceClientOnce(cwd, dir, options = {}) {
219
- const file = path.resolve(cwd, dir, "wordpress.ts");
220
- const metaPath = path.resolve(cwd, WORDPRESS_CLIENT_META_REL);
221
- const result = await fetchDocument(cwd, options, readWordPressMeta(metaPath, file)?.etag);
207
+ async function generateIntrospectionOnce(cfg, options = {}) {
208
+ const file = path.resolve(cfg.cwd, cfg.introspectionPath);
209
+ const metaPath = path.resolve(cfg.cwd, cfg.introspectionMetaPath);
210
+ const result = await fetchDocument(cfg.cwd, options, readWordPressMeta(metaPath, file)?.etag);
222
211
  if (!result?.document) return "unchanged";
223
212
  const generated = generateWordPressModule(result.document);
224
213
  await assertGeneratedClientCompiles(generated);
225
- const contents = generated.source;
214
+ fs.mkdirSync(path.dirname(file), { recursive: true });
226
215
  fs.mkdirSync(path.dirname(metaPath), { recursive: true });
216
+ atomicWrite(file, generated.source);
227
217
  atomicWrite(metaPath, wordPressMeta(result.etag, result.document.hash));
228
- if (fs.existsSync(file) && fs.readFileSync(file, "utf8") === contents) return "unchanged";
229
- fs.mkdirSync(path.dirname(file), { recursive: true });
230
- atomicWrite(file, contents);
231
218
  return "generated";
232
219
  }
233
- function writeGeneratedWordPress(cfg, client, meta) {
234
- const metaPath = path.resolve(cfg.cwd, cfg.wordpressMetaPath);
235
- fs.mkdirSync(path.resolve(cfg.cwd, cfg.generatedDir), { recursive: true });
236
- fs.mkdirSync(path.dirname(metaPath), { recursive: true });
237
- atomicWrite(path.resolve(cfg.cwd, cfg.wordpressPath), client);
238
- atomicWrite(metaPath, meta);
239
- }
240
220
  /**
241
- * Loads the Kizlo server, builds its contract, and writes `contract.json` and
242
- * the generated barrel. Returns false when the entry has no Kizlo server.
221
+ * Generate everything `dir` resolves: the introspection always, plus the contract and barrel when a
222
+ * server is present. The server is loaded before the introspection is fetched, so a legacy `router`
223
+ * export is named as a migration error even with WordPress unreachable; a server whose entry exports no
224
+ * `procedures` still gets its introspection, and reports `contract: "empty"`.
243
225
  */
244
226
  async function generateOnce(cfg, options = {}) {
245
227
  loadEnvFiles(cfg.cwd);
246
- const entry = path.resolve(cfg.cwd, cfg.serverEntry);
247
- const server = await importIgnoringVirtualModules(cfg.cwd, entry);
248
- if (!server.procedures) {
249
- if (server.router) throw new LegacyRouterExportError(cfg.serverEntry);
250
- return false;
228
+ let procedures;
229
+ if (cfg.server) {
230
+ const entry = path.resolve(cfg.cwd, cfg.server.entry);
231
+ const server = await importIgnoringVirtualModules(cfg.cwd, entry);
232
+ if (!server.procedures && server.router) throw new LegacyRouterExportError(cfg.server.entry);
233
+ procedures = server.procedures;
251
234
  }
252
- const contract = JSON.stringify(await generateContract(server.procedures));
253
- const wordpress = await generateWordPressOnce(cfg, options);
254
- fs.mkdirSync(path.resolve(cfg.cwd, cfg.generatedDir), { recursive: true });
255
- atomicWrite(path.resolve(cfg.cwd, cfg.contractPath), contract);
256
- atomicWrite(path.resolve(cfg.cwd, cfg.barrelPath), CONTRACT_BARREL);
257
- if (wordpress === "generated") log.success("WordPress client generated");
258
- return true;
235
+ const introspection = await generateIntrospectionOnce(cfg, options);
236
+ if (!cfg.server) return {
237
+ introspection,
238
+ contract: "none"
239
+ };
240
+ if (!procedures) return {
241
+ introspection,
242
+ contract: "empty"
243
+ };
244
+ const contract = JSON.stringify(await generateContract(procedures));
245
+ fs.mkdirSync(path.resolve(cfg.cwd, cfg.server.contractDir), { recursive: true });
246
+ atomicWrite(path.resolve(cfg.cwd, cfg.server.contractPath), contract);
247
+ atomicWrite(path.resolve(cfg.cwd, cfg.server.barrelPath), CONTRACT_BARREL);
248
+ if (introspection === "generated") log.success("WordPress introspection generated");
249
+ return {
250
+ introspection,
251
+ contract: "built"
252
+ };
259
253
  }
260
254
 
261
255
  //#endregion
262
- export { CONTRACT_BARREL, PartialContractError, WORDPRESS_STUB, generateOnce, generateWordPressOnce, generateWordPressSource, generateWorkspaceClientOnce, reportGenerationError };
256
+ export { CONTRACT_BARREL, INTROSPECTION_STUB, PartialContractError, generateIntrospectionOnce, generateIntrospectionSource, generateOnce, reportGenerationError };
@@ -2,9 +2,9 @@ import { integrationEnv, resolveWordPressConnection } from "../../kizlo.js";
2
2
  import { node } from "../../integrations/node/integration.js";
3
3
  import { log } from "./logger.js";
4
4
  import { loadEnvFiles } from "../utils.js";
5
- import { resolveConfig, resolveWordPressClientDir } from "./config.js";
5
+ import { resolveConfig } from "./config.js";
6
6
  import { IntrospectionFetchError } from "../../wordpress/fetch-introspection.js";
7
- import { generateOnce, generateWordPressOnce, generateWorkspaceClientOnce, reportGenerationError } from "./generate.js";
7
+ import { generateIntrospectionOnce, generateOnce, reportGenerationError } from "./generate.js";
8
8
  import { acquire, lockPath, release } from "./lock.js";
9
9
  import path from "node:path";
10
10
  import { FSWatcher } from "chokidar";
@@ -19,51 +19,49 @@ function debounce(fn, delay) {
19
19
  }
20
20
  async function regenerate(cfg, credentials) {
21
21
  try {
22
- if (await generateOnce(cfg, { credentials })) log.success("Contract updated");
23
- else log.warn(`No Kizlo server found in ${cfg.serverEntry}`);
22
+ const result = await generateOnce(cfg, { credentials });
23
+ if (result.contract === "built") log.success("Contract updated");
24
+ else if (result.contract === "empty" && cfg.server) log.warn(`No Kizlo server found in ${cfg.server.entry}`);
24
25
  } catch (error) {
25
26
  reportGenerationError("Failed to update the Kizlo contract:", error);
26
27
  }
27
28
  }
28
- /** Watches the server directory and regenerates the contract on change. */
29
- async function watch(cfg, credentials) {
29
+ /** Watches the server directory and regenerates the contract on change. Only called when a server is present. */
30
+ async function watch(cfg, server, credentials) {
30
31
  const watcher = new FSWatcher({
31
32
  persistent: true,
32
33
  ignoreInitial: true,
33
- ignored: path.resolve(cfg.cwd, cfg.generatedDir)
34
+ ignored: path.resolve(cfg.cwd, server.contractDir)
34
35
  });
35
36
  const onChange = debounce(() => regenerate(cfg, credentials), 300);
36
- watcher.add(path.resolve(cfg.cwd, cfg.serverDir));
37
+ watcher.add(path.resolve(cfg.cwd, server.dir));
37
38
  watcher.on("all", () => void onChange());
38
39
  return watcher;
39
40
  }
40
41
  /**
41
- * Report one generation's pass under its own name. The poll refreshes two, and one message for both sent
42
- * a client-only workspace looking for a WordPress service its project does not have.
43
- *
44
- * Each reporter carries the last failure it reported, because most of what can fail here cannot clear
45
- * without the user acting — WordPress down, credentials wrong, a document that will not parse, an
46
- * introspection version this package does not speak — and repeating the same line every few seconds says
47
- * nothing the first one did not. A failure that reads differently is a different answer and is reported.
48
- * The state is per reporter rather than per process, so restarting the watcher reports afresh, and one
49
- * generation failing never silences the other.
42
+ * Report the introspection poll's pass. It carries the last failure it reported, because most of what
43
+ * can fail here cannot clear without the user acting (WordPress down, credentials wrong, a document
44
+ * that will not parse, an introspection version this package does not speak), and repeating the same
45
+ * line every few seconds says nothing the first one did not. A failure that reads differently is a
46
+ * different answer and is reported. The state is per reporter rather than per process, so restarting the
47
+ * watcher reports afresh.
50
48
  */
51
- function reportGeneration(subject, stack) {
49
+ function reportGeneration(stack) {
52
50
  let reported;
53
51
  return async (run) => {
54
52
  try {
55
- if (await run() === "generated") log.success(`WordPress ${subject} updated`);
53
+ if (await run() === "generated") log.success("WordPress introspection updated");
56
54
  stack?.answered();
57
55
  if (reported !== void 0) {
58
56
  reported = void 0;
59
- log.success(`Updating the WordPress ${subject} again`);
57
+ log.success("Updating the WordPress introspection again");
60
58
  }
61
59
  } catch (error) {
62
60
  if (error instanceof IntrospectionFetchError && error.unreachable && await stack?.ended()) return;
63
61
  const message = error instanceof Error ? error.message : String(error);
64
62
  if (message === reported) return;
65
63
  reported = message;
66
- reportGenerationError(`Failed to update the WordPress ${subject}:`, error);
64
+ reportGenerationError("Failed to update the WordPress introspection:", error);
67
65
  }
68
66
  };
69
67
  }
@@ -96,26 +94,20 @@ function endOnStoppedStack(stack, now = Date.now) {
96
94
  };
97
95
  }
98
96
  /**
99
- * One pass of the WordPress poll. `cfg` covers an app's client next to its contract; `wordpressClientDir`
100
- * covers a workspace that has only the client. A project has one or the other, but both are refreshed
101
- * together so the plugin's PHP changing is picked up either way. Each is refreshed independently, so a
102
- * service that cannot generate does not cost the pass its client.
103
- *
104
- * Both reporters share one stack guard, so a stopped stack ends the session once rather than once per
105
- * generation the pass refreshes.
97
+ * One pass of the introspection poll: refetch and rewrite `introspection.ts` when WordPress has changed.
98
+ * A server-backed app and a package that ships only procedures both take this same pass, since each has
99
+ * exactly one introspection to keep current; the plugin's PHP changing is picked up either way. The
100
+ * contract is not refreshed here; the file watcher rebuilds it on a server source change.
106
101
  */
107
- function createWordPressRefresh(cwd, cfg, wordpressClientDir, options, stack) {
108
- const guard = stack ? endOnStoppedStack(stack) : void 0;
109
- const service = reportGeneration("service", guard);
110
- const client = reportGeneration("client", guard);
102
+ function createWordPressRefresh(cfg, options, stack) {
103
+ const introspection = reportGeneration(stack ? endOnStoppedStack(stack) : void 0);
111
104
  return async () => {
112
- if (cfg) await service(() => generateWordPressOnce(cfg, options));
113
- if (wordpressClientDir) await client(() => generateWorkspaceClientOnce(cwd, wordpressClientDir, options));
105
+ await introspection(() => generateIntrospectionOnce(cfg, options));
114
106
  };
115
107
  }
116
108
  /** Run {@link createWordPressRefresh} on a timer, skipping a tick while the previous one is still going. */
117
- function refreshWordPress(cwd, cfg, wordpressClientDir, credentials, stack) {
118
- const refresh = createWordPressRefresh(cwd, cfg, wordpressClientDir, { credentials }, stack);
109
+ function refreshWordPress(cfg, credentials, stack) {
110
+ const refresh = createWordPressRefresh(cfg, { credentials }, stack);
119
111
  let refreshing = false;
120
112
  const timer = setInterval(() => {
121
113
  if (refreshing) return;
@@ -128,12 +120,11 @@ function refreshWordPress(cwd, cfg, wordpressClientDir, credentials, stack) {
128
120
  return timer;
129
121
  }
130
122
  /**
131
- * Acquire the single-instance lock, generate the contract once, and start the file
132
- * watcher. Returns a synchronous `stop()` that closes the watcher and releases the
133
- * lock — or `undefined` when another watcher already holds the lock (a framework dev
134
- * script, or a second `kizlo dev`), or when no server `dir` is configured, in which case
135
- * the caller carries on without watching. Used by `kizlo dev`, both when it boots a local
136
- * stack and when it runs the watcher alone, so a single terminal covers the whole dev loop.
123
+ * Acquire the single-instance lock, generate once, and start the file watcher. Returns a synchronous
124
+ * `stop()` that closes the watcher and releases the lock, or `undefined` when another watcher already
125
+ * holds the lock (a framework dev script, or a second `kizlo dev`), or when nothing is configured to
126
+ * generate, in which case the caller carries on without watching. Used by `kizlo dev`, both when it
127
+ * boots a local stack and when it runs the watcher alone, so a single terminal covers the whole dev loop.
137
128
  */
138
129
  async function startWatcher(cwd, opts) {
139
130
  const lock = lockPath(cwd);
@@ -144,26 +135,22 @@ async function startWatcher(cwd, opts) {
144
135
  let handedOver = false;
145
136
  try {
146
137
  const cfg = await resolveConfig(cwd, { dir: opts?.dir });
147
- const wordpressClientDir = await resolveWordPressClientDir(cwd);
148
- if (!cfg && !wordpressClientDir) {
138
+ if (!cfg) {
149
139
  log.info("skipping the contract generation.");
150
140
  return;
151
141
  }
152
142
  loadEnvFiles(cwd);
153
143
  const { credentials } = resolveWordPressConnection(void 0, integrationEnv([node()]));
154
- if (wordpressClientDir) try {
155
- if (await generateWorkspaceClientOnce(cwd, wordpressClientDir, { credentials }) === "generated") log.success("WordPress client generated");
156
- } catch (error) {
157
- reportGenerationError("Failed to generate the WordPress client:", error);
158
- }
159
- if (cfg) try {
160
- if (await generateOnce(cfg, { credentials })) log.success("Contract generated");
161
- else log.warn(`No Kizlo server found in ${cfg.serverEntry}`);
144
+ try {
145
+ const result = await generateOnce(cfg, { credentials });
146
+ if (result.contract === "built") log.success("Contract generated");
147
+ else if (result.contract === "empty" && cfg.server) log.warn(`No Kizlo server found in ${cfg.server.entry}`);
148
+ else if (result.introspection === "generated") log.success("WordPress introspection generated");
162
149
  } catch (error) {
163
150
  reportGenerationError("Failed to generate the Kizlo contract:", error);
164
151
  }
165
- const watcher = cfg ? await watch(cfg, credentials) : void 0;
166
- const wordpressRefresh = refreshWordPress(cwd, cfg, wordpressClientDir, credentials, opts?.stack);
152
+ const watcher = cfg.server ? await watch(cfg, cfg.server, credentials) : void 0;
153
+ const wordpressRefresh = refreshWordPress(cfg, credentials, opts?.stack);
167
154
  let stopped = false;
168
155
  handedOver = true;
169
156
  return () => {
@@ -34,9 +34,7 @@ declare const CREDENTIALS_REL = ".kizlo/test.json";
34
34
  * working dir rather than next to the generated files on purpose: it describes one machine's last
35
35
  * fetch, so a fresh clone has to regenerate rather than trust a cache it never populated.
36
36
  */
37
- declare const WORDPRESS_META_REL = ".kizlo/wordpress.meta.json";
38
- /** Same, for the standalone client written by `wordpressClientDir`. Separate so the two never share one ETag. */
39
- declare const WORDPRESS_CLIENT_META_REL = ".kizlo/wordpress.client.meta.json";
37
+ declare const INTROSPECTION_META_REL = ".kizlo/introspection.meta.json";
40
38
  /**
41
39
  * Fixed folder the local WordPress install lives in, relative to the config root. The whole install
42
40
  * (core, themes, uploads, plugins) is bind-mounted here; `kizlo dev reset` wipes it. It sits under
@@ -56,4 +54,4 @@ declare const DEFAULT_PLUGINS: PluginSource[];
56
54
  declare const TEST_ADMIN: Omit<TestAdminUser, "id" | "applicationPassword">;
57
55
  declare const TEST_USER: Omit<TestUser, "id">;
58
56
  //#endregion
59
- export { COMPOSE_FILE, CONFIG_FILES, CREDENTIALS_REL, DEFAULT_PLUGINS, DEFAULT_PORT, DEFAULT_WORDPRESS_TAG, LOCAL_DIR_REL, OPCACHE_INI, REMAP_ENTRYPOINT, SEED_MARKER_OPTION, SEED_VERSION, TEST_ADMIN, TEST_USER, WORDPRESS_CLIENT_META_REL, WORDPRESS_META_REL, githubRelease, kizloRelease };
57
+ export { COMPOSE_FILE, CONFIG_FILES, CREDENTIALS_REL, DEFAULT_PLUGINS, DEFAULT_PORT, DEFAULT_WORDPRESS_TAG, INTROSPECTION_META_REL, LOCAL_DIR_REL, OPCACHE_INI, REMAP_ENTRYPOINT, SEED_MARKER_OPTION, SEED_VERSION, TEST_ADMIN, TEST_USER, githubRelease, kizloRelease };
@@ -44,9 +44,7 @@ const CREDENTIALS_REL = ".kizlo/test.json";
44
44
  * working dir rather than next to the generated files on purpose: it describes one machine's last
45
45
  * fetch, so a fresh clone has to regenerate rather than trust a cache it never populated.
46
46
  */
47
- const WORDPRESS_META_REL = ".kizlo/wordpress.meta.json";
48
- /** Same, for the standalone client written by `wordpressClientDir`. Separate so the two never share one ETag. */
49
- const WORDPRESS_CLIENT_META_REL = ".kizlo/wordpress.client.meta.json";
47
+ const INTROSPECTION_META_REL = ".kizlo/introspection.meta.json";
50
48
  /**
51
49
  * Fixed folder the local WordPress install lives in, relative to the config root. The whole install
52
50
  * (core, themes, uploads, plugins) is bind-mounted here; `kizlo dev reset` wipes it. It sits under
@@ -83,4 +81,4 @@ const TEST_USER = {
83
81
  };
84
82
 
85
83
  //#endregion
86
- export { COMPOSE_FILE, CONFIG_FILES, CREDENTIALS_REL, DEFAULT_PLUGINS, DEFAULT_PORT, DEFAULT_WORDPRESS_TAG, LOCAL_DIR_REL, OPCACHE_INI, REMAP_ENTRYPOINT, SEED_MARKER_OPTION, SEED_VERSION, TEST_ADMIN, TEST_USER, WORDPRESS_CLIENT_META_REL, WORDPRESS_META_REL, githubRelease, kizloRelease };
84
+ export { COMPOSE_FILE, CONFIG_FILES, CREDENTIALS_REL, DEFAULT_PLUGINS, DEFAULT_PORT, DEFAULT_WORDPRESS_TAG, INTROSPECTION_META_REL, LOCAL_DIR_REL, OPCACHE_INI, REMAP_ENTRYPOINT, SEED_MARKER_OPTION, SEED_VERSION, TEST_ADMIN, TEST_USER, githubRelease, kizloRelease };
@@ -53,13 +53,16 @@ interface SeedContext {
53
53
  /**
54
54
  * A plugin to ensure installed + active during bootstrap. A bare string is a
55
55
  * wp.org slug (the installed name and the install source are the same). The
56
- * object form is for any other source — a zip URL or a local path — where the
57
- * installed slug (`name`, used for activation checks) can't be derived from the
58
- * source. `source` is passed straight to `wp plugin install`.
56
+ * object form names an install source explicitly, either a wp.org slug or a zip
57
+ * URL, when the installed slug (`name`, used for activation checks) cannot be
58
+ * derived from the source or the wp.org release must be pinned. `version` is
59
+ * passed to `wp plugin install --version` when present. `source` is passed
60
+ * straight to `wp plugin install`.
59
61
  */
60
62
  type PluginSource = string | {
61
63
  name: string;
62
64
  source: string;
65
+ version?: string;
63
66
  };
64
67
  /**
65
68
  * A local plugin directory to bind-mount into the dev stack (`kizlo dev`) instead
@@ -14,15 +14,15 @@ import { DevPluginSource, Fixture, PluginSource, SettleContext, TestCredentials
14
14
  * Bumping `SEED_VERSION` invalidates older marks and forces a reseed.
15
15
  */
16
16
  declare function isSeeded(): Promise<boolean>;
17
- /** Resolve a `PluginSource` to the `(name, source)` pair `ensurePlugin` needs. */
18
- declare function resolvePluginSource(plugin: PluginSource): [name: string, source: string];
17
+ /** Resolve a `PluginSource` to the arguments `ensurePlugin` needs. */
18
+ declare function resolvePluginSource(plugin: PluginSource): [name: string, source: string, version?: string];
19
19
  /**
20
20
  * Ensure a plugin is active without ever re-downloading. Plugin files persist in
21
21
  * the `wp_data` volume, so an already-installed plugin is just activated; the
22
22
  * download (`source` = release zip URL or wp.org slug) happens only when the files
23
23
  * are absent (i.e. after `wp reset` wipes the volume).
24
24
  */
25
- declare function ensurePlugin(name: string, source: string): Promise<void>;
25
+ declare function ensurePlugin(name: string, source: string, version?: string): Promise<void>;
26
26
  /**
27
27
  * Activate a bind-mounted local plugin by its directory basename (the slug the
28
28
  * mount lands at under `wp-content/plugins`). Idempotent: the files arrive via the
@@ -29,9 +29,14 @@ async function isSeeded() {
29
29
  ]);
30
30
  return marker.code === 0 && marker.stdout.trim() === SEED_VERSION;
31
31
  }
32
- /** Resolve a `PluginSource` to the `(name, source)` pair `ensurePlugin` needs. */
32
+ /** Resolve a `PluginSource` to the arguments `ensurePlugin` needs. */
33
33
  function resolvePluginSource(plugin) {
34
- return typeof plugin === "string" ? [plugin, plugin] : [plugin.name, plugin.source];
34
+ if (typeof plugin === "string") return [plugin, plugin];
35
+ return plugin.version === void 0 ? [plugin.name, plugin.source] : [
36
+ plugin.name,
37
+ plugin.source,
38
+ plugin.version
39
+ ];
35
40
  }
36
41
  /**
37
42
  * Ensure a plugin is active without ever re-downloading. Plugin files persist in
@@ -39,7 +44,7 @@ function resolvePluginSource(plugin) {
39
44
  * download (`source` = release zip URL or wp.org slug) happens only when the files
40
45
  * are absent (i.e. after `wp reset` wipes the volume).
41
46
  */
42
- async function ensurePlugin(name, source) {
47
+ async function ensurePlugin(name, source, version) {
43
48
  if ((await compose([
44
49
  "exec",
45
50
  "-T",
@@ -69,6 +74,7 @@ async function ensurePlugin(name, source) {
69
74
  "plugin",
70
75
  "install",
71
76
  source,
77
+ ...version === void 0 ? [] : [`--version=${version}`],
72
78
  "--activate"
73
79
  ]);
74
80
  }
@@ -22,7 +22,6 @@ const LIST_COMMENT_ERROR_MAP = defineErrorMap({
22
22
  }
23
23
  });
24
24
  const SUBMIT_COMMENT_ERROR_MAP = defineErrorMap({
25
- COMMENT_USER_NOT_FOUND: {},
26
25
  COMMENT_ID_NOT_FOUND: {},
27
26
  COMMENT_CLOSED: {},
28
27
  COMMENT_LOGIN_REQUIRED: {},
@@ -98,26 +98,26 @@ const COMMENT_PROCEDURES = {
98
98
  const connInfo = await context.getConnInfo();
99
99
  if (!connInfo?.ip) throw new Error("Connection IP is required.");
100
100
  if (!connInfo?.userAgent) throw new Error("Connection user agent is required.");
101
- const user = await context.getAuthUser();
102
- if (!user) {
101
+ const session = await context.getSession();
102
+ if (!session) {
103
103
  if (!input.body.captchaToken) throw errors.COMMENT_CAPTCHA_REQUIRED();
104
104
  if (!await context.verifyCaptcha(input.body.captchaToken)) throw errors.COMMENT_CAPTCHA_INVALID();
105
105
  }
106
- const response = await context.wordpress.kizlo.comments.create({
107
- user_id: user?.id,
106
+ const submission = {
108
107
  author_ip: connInfo.ip,
109
108
  user_agent: connInfo.userAgent,
110
109
  content: input.body.content,
111
110
  post_id: input.body.postId,
111
+ user_email: session?.email,
112
112
  author_email: input.body.authorEmail,
113
113
  author_name: input.body.authorName,
114
114
  author_url: input.body.authorUrl,
115
115
  parent: input.body.parentId
116
- });
116
+ };
117
+ const response = await context.wordpress.kizlo.comments.create(submission);
117
118
  if (response.error) switch (response.error.code) {
118
119
  case "require_name_email": throw errors.COMMENT_NAME_EMAIL_REQUIRED();
119
120
  case "require_valid_email": throw errors.COMMENT_INVALID_EMAIL();
120
- case "kizlo_invalid_user": throw errors.COMMENT_USER_NOT_FOUND();
121
121
  case "require_valid_comment": throw errors.COMMENT_EMPTY_CONTENT();
122
122
  case "not_logged_in": throw errors.COMMENT_LOGIN_REQUIRED();
123
123
  case "comment_closed": throw errors.COMMENT_CLOSED();