kizlo 0.17.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.
Files changed (58) hide show
  1. package/dist/adapters/auth.d.ts +17 -16
  2. package/dist/adapters/auth.js +4 -5
  3. package/dist/category/index.js +4 -4
  4. package/dist/category/schema.d.ts +153 -0
  5. package/dist/category/schema.js +4 -3
  6. package/dist/cli/commands/_wiring.js +9 -8
  7. package/dist/cli/commands/check.js +11 -15
  8. package/dist/cli/commands/dev.js +3 -3
  9. package/dist/cli/commands/generate.js +15 -17
  10. package/dist/cli/commands/test.js +3 -3
  11. package/dist/cli/daemon/config.js +148 -82
  12. package/dist/cli/daemon/generate.js +54 -54
  13. package/dist/cli/daemon/watch.js +41 -54
  14. package/dist/cli/wp/constants.d.ts +2 -4
  15. package/dist/cli/wp/constants.js +2 -4
  16. package/dist/cli/wp/types.d.ts +6 -3
  17. package/dist/cli/wp/utils.d.ts +3 -3
  18. package/dist/cli/wp/utils.js +9 -3
  19. package/dist/comment/errors.js +0 -1
  20. package/dist/comment/index.js +6 -6
  21. package/dist/config.d.ts +61 -47
  22. package/dist/context.d.ts +7 -7
  23. package/dist/context.js +4 -4
  24. package/dist/index.d.ts +9 -4
  25. package/dist/index.js +8 -3
  26. package/dist/package.js +1 -1
  27. package/dist/page/index.js +3 -3
  28. package/dist/page/schema.d.ts +226 -0
  29. package/dist/page/schema.js +4 -3
  30. package/dist/post/index.js +3 -3
  31. package/dist/post/schema.d.ts +268 -0
  32. package/dist/post/schema.js +4 -3
  33. package/dist/procedures.d.ts +35 -902
  34. package/dist/seo/index.js +1 -1
  35. package/dist/seo/schema.d.ts +5 -5
  36. package/dist/shared/contract.d.ts +1 -1
  37. package/dist/shared/procedure.d.ts +12 -3
  38. package/dist/shared/procedure.js +4 -0
  39. package/dist/shared/result.d.ts +16 -7
  40. package/dist/shared/types.d.ts +1 -1
  41. package/dist/tag/index.js +3 -3
  42. package/dist/tag/schema.d.ts +151 -0
  43. package/dist/tag/schema.js +4 -3
  44. package/dist/test/auth.js +2 -3
  45. package/dist/test/users.d.ts +2 -4
  46. package/dist/test/users.js +4 -9
  47. package/dist/test.d.ts +2 -2
  48. package/dist/test.js +2 -2
  49. package/dist/wordpress/endpoint.js +2 -1
  50. package/dist/wordpress/error.d.ts +1 -0
  51. package/dist/wordpress/error.js +2 -0
  52. package/dist/wordpress/fetch-introspection.js +1 -1
  53. package/dist/wordpress/generate.js +51 -7
  54. package/dist/wordpress/index.d.ts +1 -1
  55. package/dist/wordpress/transport.js +5 -3
  56. package/dist/wordpress/types.d.ts +38 -12
  57. package/dist/wordpress/utils.d.ts +1 -0
  58. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { DEFAULT_WORDPRESS_TAG, LOCAL_DIR_REL, WORDPRESS_META_REL } from "../wp/constants.js";
1
+ import { DEFAULT_WORDPRESS_TAG, INTROSPECTION_META_REL, LOCAL_DIR_REL } from "../wp/constants.js";
2
2
  import { credentialsPath, findConfigDir } from "../wp/utils.js";
3
3
  import { log } from "./logger.js";
4
4
  import { detectPackageManager } from "../utils.js";
@@ -11,39 +11,82 @@ import fs from "node:fs";
11
11
  //#region src/cli/daemon/config.ts
12
12
  const fixtureSchema = z.custom((value) => typeof value === "object" && value !== null && typeof value.name === "string", { message: "must be a fixture object with a string `name` (use defineFixture)" });
13
13
  /**
14
- * `dev.version` / `test.version`. Validated against Docker's tag grammar here rather than at boot,
15
- * so `"wordpress:7.1.0"`, which would resolve to `wordpress:wordpress:7.1.0`, is named as a config
16
- * mistake instead of surfacing as a pull failure seconds into starting a stack.
14
+ * `local.dev.version` / `local.test.version`. Validated against Docker's tag grammar here rather than
15
+ * at boot, so `"wordpress:7.1.0"`, which would resolve to `wordpress:wordpress:7.1.0`, is named as a
16
+ * config mistake instead of surfacing as a pull failure seconds into starting a stack.
17
17
  */
18
18
  const versionSchema = z.string().regex(WORDPRESS_TAG_PATTERN, "must be a WordPress image tag, like \"7.1.0\" or \"7.1.0-php8.3-apache\" (no \"wordpress:\" prefix)");
19
+ /**
20
+ * A key that moved in the config redesign. It accepts only `undefined`, so an absent key passes and a
21
+ * present one fails validation with a message naming where it went: a hard cutover with no silent
22
+ * fallback. {@link loadConfigFile} surfaces the message through `z.prettifyError`.
23
+ */
24
+ const removedKey = (message) => z.undefined({ error: message }).optional();
25
+ const dirSchema = z.union([z.string(), z.object({
26
+ server: z.string().optional(),
27
+ contract: z.string().optional(),
28
+ introspection: z.string().optional()
29
+ })]);
30
+ const devStackSchema = z.object({
31
+ enable: z.boolean().optional(),
32
+ port: z.number().int().positive().optional(),
33
+ version: versionSchema.optional(),
34
+ dbPort: z.number().int().positive().optional(),
35
+ fixtures: z.array(fixtureSchema).optional()
36
+ });
37
+ const testStackSchema = z.object({
38
+ enable: z.boolean().optional(),
39
+ inherit: z.boolean().optional(),
40
+ port: z.number().int().positive().optional(),
41
+ version: versionSchema.optional(),
42
+ fixtures: z.array(fixtureSchema).optional(),
43
+ packageManager: z.enum([
44
+ "npm",
45
+ "pnpm",
46
+ "yarn",
47
+ "bun"
48
+ ]).optional(),
49
+ command: z.string().optional()
50
+ });
51
+ const localSchema = z.union([z.boolean(), z.object({
52
+ enable: z.boolean().optional(),
53
+ name: z.string().optional(),
54
+ worktrees: z.boolean().optional(),
55
+ dev: devStackSchema.optional(),
56
+ test: testStackSchema.optional()
57
+ })]);
19
58
  /** Runtime shape of `kizlo.config.*` — mirrors {@link KizloGlobalConfig}. */
20
59
  const configSchema = z.object({
21
- dir: z.string().optional(),
60
+ dir: dirSchema.optional(),
22
61
  alias: z.string().optional(),
23
- name: z.string().optional(),
24
- worktrees: z.boolean().optional(),
25
- wordpressClientDir: z.string().optional(),
26
- dev: z.object({
27
- local: z.boolean().optional(),
28
- port: z.number().int().positive().optional(),
29
- version: versionSchema.optional(),
30
- dbPort: z.number().int().positive().optional(),
31
- fixtures: z.array(fixtureSchema).optional()
32
- }).optional(),
33
- test: z.object({
34
- local: z.boolean().optional(),
35
- port: z.number().int().positive().optional(),
36
- version: versionSchema.optional(),
37
- fixtures: z.array(fixtureSchema).optional(),
38
- packageManager: z.enum([
39
- "npm",
40
- "pnpm",
41
- "yarn",
42
- "bun"
43
- ]).optional(),
44
- command: z.string().optional()
45
- }).optional()
62
+ local: localSchema.optional(),
63
+ wordpressClientDir: removedKey("`wordpressClientDir` has been removed. Use `dir: { introspection }` instead."),
64
+ name: removedKey("`name` is no longer a config root key. Move it under `local.name`."),
65
+ worktrees: removedKey("`worktrees` is no longer a config root key. Move it under `local.worktrees`."),
66
+ dev: removedKey("`dev` is no longer a config root key. Move it under `local.dev`."),
67
+ test: removedKey("`test` is no longer a config root key. Move it under `local.test`.")
46
68
  });
69
+ /** Normalize `local` (absent, `true`, `false`, or the object form) into one shape the resolvers read. */
70
+ function resolveLocal(fileConfig) {
71
+ const local = fileConfig?.local;
72
+ if (local === true) return {
73
+ enabled: true,
74
+ dev: {},
75
+ test: {}
76
+ };
77
+ if (!local) return {
78
+ enabled: false,
79
+ dev: {},
80
+ test: {}
81
+ };
82
+ return {
83
+ enabled: local.enable !== false,
84
+ name: local.name,
85
+ worktrees: local.worktrees,
86
+ dev: local.dev ?? {},
87
+ test: local.test ?? {}
88
+ };
89
+ }
47
90
  const CONFIG_FILES = [
48
91
  "kizlo.config.ts",
49
92
  "kizlo.config.js",
@@ -52,6 +95,12 @@ const CONFIG_FILES = [
52
95
  const DEFAULT_DEV_PORT = 8080;
53
96
  const DEFAULT_DEV_DB_PORT = 3307;
54
97
  const DEFAULT_TEST_PORT = 8889;
98
+ /** The generated introspection artifact's filename, written under its resolved directory. */
99
+ const INTROSPECTION_FILE = "introspection.ts";
100
+ /** Strip a leading `./` and trailing slashes so a config path joins cleanly. */
101
+ function normalizeDir(value) {
102
+ return value.replace(/^\.\//, "").replace(/\/+$/, "");
103
+ }
55
104
  async function loadConfigFile(cwd) {
56
105
  const file = CONFIG_FILES.map((name) => path.join(cwd, name)).find((p) => fs.existsSync(p));
57
106
  if (!file) return void 0;
@@ -71,40 +120,56 @@ async function loadConfigFile(cwd) {
71
120
  return result.data;
72
121
  }
73
122
  /**
74
- * Resolve the Kizlo server layout from an explicit `dir` — the `--dir` flag or `dir` in
75
- * `kizlo.config.*`. Returns `undefined` when neither is set: there's no Kizlo server to
76
- * generate a contract from, so callers skip generation and (for `dev`) run local WordPress
77
- * alone. There's intentionally no default path — a missing `dir` means "no server", not
78
- * "look under `lib/kizlo`".
123
+ * Resolve what Kizlo generates and watches from `dir`: the `--dir` flag or `dir` in `kizlo.config.*`.
124
+ * A string is the home Kizlo owns the layout under (sources in `<dir>/server`, generated files under
125
+ * `<dir>/server/generated`); the object form sets `server` / `contract` / `introspection` independently.
126
+ *
127
+ * The introspection is always generated at its resolved path, so a project with no server still gets it.
128
+ * `server` is present only when `dir` resolves a server path (the string form always does, the object
129
+ * form only when `server` is set), and its absence means there is nothing to watch and no contract to
130
+ * build. Returns `undefined` only when nothing at all is configured (no `dir`, or an object naming
131
+ * neither a server nor an introspection path): there is then nothing to generate.
79
132
  */
80
133
  async function resolveConfig(cwd, flags) {
81
134
  const fileConfig = await loadConfigFile(cwd);
82
135
  const raw = flags?.dir ?? fileConfig?.dir;
83
- if (!raw) return void 0;
84
- const dir = raw.replace(/^\.\//, "").replace(/\/+$/, "");
85
- const serverDir = path.join(dir, "server");
86
- const generatedDir = path.join(serverDir, "generated");
136
+ if (raw === void 0) return void 0;
137
+ if (typeof raw === "string") {
138
+ const home = normalizeDir(raw);
139
+ const serverDir$1 = path.join(home, "server");
140
+ const contractDir$1 = path.join(serverDir$1, "generated");
141
+ return {
142
+ cwd,
143
+ server: {
144
+ dir: serverDir$1,
145
+ entry: path.join(serverDir$1, "index.ts"),
146
+ contractDir: contractDir$1,
147
+ contractPath: path.join(contractDir$1, "contract.json"),
148
+ barrelPath: path.join(contractDir$1, "index.ts")
149
+ },
150
+ introspectionPath: path.join(contractDir$1, INTROSPECTION_FILE),
151
+ introspectionMetaPath: INTROSPECTION_META_REL
152
+ };
153
+ }
154
+ const serverDir = raw.server ? normalizeDir(raw.server) : void 0;
155
+ const contractDir = serverDir ? raw.contract ? normalizeDir(raw.contract) : path.join(serverDir, "generated") : void 0;
156
+ const server = serverDir && contractDir ? {
157
+ dir: serverDir,
158
+ entry: path.join(serverDir, "index.ts"),
159
+ contractDir,
160
+ contractPath: path.join(contractDir, "contract.json"),
161
+ barrelPath: path.join(contractDir, "index.ts")
162
+ } : void 0;
163
+ const introspectionDir = raw.introspection ? normalizeDir(raw.introspection) : contractDir;
164
+ if (!introspectionDir) return void 0;
87
165
  return {
88
166
  cwd,
89
- dir,
90
- serverDir,
91
- serverEntry: path.join(serverDir, "index.ts"),
92
- generatedDir,
93
- contractPath: path.join(generatedDir, "contract.json"),
94
- barrelPath: path.join(generatedDir, "index.ts"),
95
- wordpressPath: path.join(generatedDir, "wordpress.ts"),
96
- wordpressMetaPath: WORDPRESS_META_REL
167
+ server,
168
+ introspectionPath: path.join(introspectionDir, INTROSPECTION_FILE),
169
+ introspectionMetaPath: INTROSPECTION_META_REL
97
170
  };
98
171
  }
99
172
  /**
100
- * The directory `wordpress.ts` is written to for a workspace with no Kizlo server, from `wordpressClientDir`
101
- * in `kizlo.config.*`. Resolved on its own rather than through {@link resolveConfig}, which describes a
102
- * server layout and returns nothing without `dir` — the two are independent.
103
- */
104
- async function resolveWordPressClientDir(cwd) {
105
- return (await loadConfigFile(cwd))?.wordpressClientDir;
106
- }
107
- /**
108
173
  * Sanitize an arbitrary package/dir name into a valid Docker compose project id:
109
174
  * lowercase, `@scope/pkg` → `scope-pkg`, dropping anything outside `[a-z0-9_-]`.
110
175
  */
@@ -164,61 +229,62 @@ function stackProject(baseName, kind) {
164
229
  return `kizlo-${baseName}-${kind}`;
165
230
  }
166
231
  /**
167
- * Resolve the `test` block from `kizlo.config.*` into concrete values for the
168
- * `test` command, applying defaults (port 8889, auto-detected package manager)
169
- * and anchoring the credentials artifact to the config directory.
232
+ * Resolve the test stack (`local.test`) into concrete values for the `test` command, applying defaults
233
+ * (port 8889, auto-detected package manager) and anchoring the credentials artifact to the config
234
+ * directory. `version` and `fixtures` fall back to the dev stack unless `test.inherit` is `false`;
235
+ * `port`, `packageManager`, and `command` are never inherited.
170
236
  */
171
237
  async function resolveTestConfig(cwd) {
172
238
  const configDir = findConfigDir(cwd);
173
- const fileConfig = await loadConfigFile(configDir);
174
- const test = fileConfig?.test ?? {};
239
+ const local = resolveLocal(await loadConfigFile(configDir));
240
+ const { dev, test } = local;
241
+ const inherit = test.inherit !== false;
175
242
  return {
176
243
  configDir,
177
- local: Boolean(test.local),
244
+ local: local.enabled && test.enable !== false,
178
245
  project: stackProject(resolveStackName(configDir, {
179
- name: fileConfig?.name,
180
- worktrees: fileConfig?.worktrees
246
+ name: local.name,
247
+ worktrees: local.worktrees
181
248
  }), "test"),
182
249
  command: test.command,
183
250
  port: test.port ?? DEFAULT_TEST_PORT,
184
- portExplicit: test.port !== void 0 && !fileConfig?.worktrees,
185
- wordpressTag: test.version ?? DEFAULT_WORDPRESS_TAG,
186
- fixtures: test.fixtures ?? [],
251
+ portExplicit: test.port !== void 0 && !local.worktrees,
252
+ wordpressTag: test.version ?? (inherit ? dev.version : void 0) ?? DEFAULT_WORDPRESS_TAG,
253
+ fixtures: test.fixtures ?? (inherit ? dev.fixtures : void 0) ?? [],
187
254
  credentialsPath: credentialsPath(cwd),
188
255
  packageManager: test.packageManager ?? await detectPackageManager(configDir) ?? "npm"
189
256
  };
190
257
  }
191
258
  /**
192
- * Whether this project runs local WordPress under `kizlo dev` — `dev.local` is `true` in
193
- * `kizlo.config.*`. When false, `kizlo dev` has nothing to boot and runs the contract watcher alone,
194
- * the path a project pointing at its own WordPress takes. The flag is written by `create`/`init` when
195
- * local WordPress is chosen, and lives next to the rest of the `dev` config, so it's committed and
196
- * survives `kizlo dev reset`.
259
+ * Whether this project runs local WordPress under `kizlo dev`: local is enabled and the dev stack is
260
+ * on (`local.dev.enable !== false`) in `kizlo.config.*`. When false, `kizlo dev` has nothing to boot and
261
+ * runs the contract watcher alone, the path a project pointing at its own WordPress takes. Written by
262
+ * `create`/`init` when local WordPress is chosen, so it's committed and survives `kizlo dev reset`.
197
263
  */
198
264
  async function usesLocalWordPress(cwd) {
199
- const fileConfig = await loadConfigFile(findConfigDir(cwd));
200
- return Boolean(fileConfig?.dev?.local);
265
+ const local = resolveLocal(await loadConfigFile(findConfigDir(cwd)));
266
+ return local.enabled && local.dev.enable !== false;
201
267
  }
202
268
  /**
203
- * Resolve the `dev` block from `kizlo.config.*` into concrete values for the `dev` command, applying
204
- * defaults (port 8080). The install folder is fixed at `.kizlo/local` — no longer a config choice — so
205
- * there's nothing required here. Callers gate on {@link usesLocalWordPress} first, since a project
206
- * without local WordPress runs the watcher alone rather than reaching here.
269
+ * Resolve the dev stack (`local.dev`) into concrete values for the `dev` command, applying defaults
270
+ * (port 8080). The install folder is fixed at `.kizlo/local` (no longer a config choice), so there's
271
+ * nothing required here. Callers gate on {@link usesLocalWordPress} first, since a project without
272
+ * local WordPress runs the watcher alone rather than reaching here.
207
273
  */
208
274
  async function resolveDevConfig(cwd) {
209
275
  const configDir = findConfigDir(cwd);
210
- const fileConfig = await loadConfigFile(configDir);
211
- const dev = fileConfig?.dev ?? {};
276
+ const local = resolveLocal(await loadConfigFile(configDir));
277
+ const { dev } = local;
212
278
  return {
213
279
  configDir,
214
280
  project: stackProject(resolveStackName(configDir, {
215
- name: fileConfig?.name,
216
- worktrees: fileConfig?.worktrees
281
+ name: local.name,
282
+ worktrees: local.worktrees
217
283
  }), "dev"),
218
284
  port: dev.port ?? DEFAULT_DEV_PORT,
219
- portExplicit: dev.port !== void 0 && !fileConfig?.worktrees,
285
+ portExplicit: dev.port !== void 0 && !local.worktrees,
220
286
  dbPort: dev.dbPort ?? DEFAULT_DEV_DB_PORT,
221
- dbPortExplicit: dev.dbPort !== void 0 && !fileConfig?.worktrees,
287
+ dbPortExplicit: dev.dbPort !== void 0 && !local.worktrees,
222
288
  wordpressTag: dev.version ?? DEFAULT_WORDPRESS_TAG,
223
289
  fixtures: dev.fixtures ?? [],
224
290
  wordpressPath: LOCAL_DIR_REL,
@@ -227,4 +293,4 @@ async function resolveDevConfig(cwd) {
227
293
  }
228
294
 
229
295
  //#endregion
230
- export { CONFIG_FILES, DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT, resolveConfig, resolveDevConfig, resolveStackName, resolveTestConfig, resolveWordPressClientDir, stackProject, usesLocalWordPress };
296
+ export { CONFIG_FILES, DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT, resolveConfig, resolveDevConfig, resolveStackName, resolveTestConfig, stackProject, usesLocalWordPress };
@@ -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`,
@@ -51,6 +50,12 @@ const WORDPRESS_STUB = [
51
50
  `\tinterface WordPressClientRegistry {`,
52
51
  `\t\tendpoints: typeof endpoints`,
53
52
  `\t}`,
53
+ `\tinterface WordPressEndpointRegistry {`,
54
+ `\t\t[path: string]: any`,
55
+ `\t}`,
56
+ `\tinterface WordPressCustomFieldsRegistry {`,
57
+ `\t\t[path: string]: Record<string, unknown>`,
58
+ `\t}`,
54
59
  `}`,
55
60
  ``
56
61
  ].join("\n");
@@ -117,8 +122,8 @@ function formatDiagnostic(diagnostic) {
117
122
  * partial contract is worth is the generator's call rather than the transport's. Taking it is the
118
123
  * default: a third-party plugin's broken declaration then costs its own routes and nothing else,
119
124
  * where refusing the document costs the project every route that still works, for a mistake in code
120
- * it does not own. Strict generation makes the opposite trade for CI, where a client silently short
121
- * 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.
122
127
  */
123
128
  var PartialContractError = class extends Error {
124
129
  excluded;
@@ -149,7 +154,7 @@ function reportDiagnostics(document, strict) {
149
154
  const excluded = new Set(document.diagnostics.filter((diagnostic) => diagnostic.type === "error").map(diagnosticLocation));
150
155
  if (!excluded.size) return;
151
156
  if (strict) throw new PartialContractError(excluded.size);
152
- 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.`);
153
158
  }
154
159
  /**
155
160
  * Plugin versions already named as outdated. `kizlo dev` refetches every few seconds, and the answer
@@ -183,74 +188,69 @@ async function fetchDocument(cwd, options, etag) {
183
188
  return result;
184
189
  }
185
190
  /**
186
- * 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
187
192
  * the same generator as `kizlo generate` what should be on disk, while leaving a contributor's
188
193
  * checkout untouched. Callers choose strictness just as generation does.
189
194
  */
190
- async function generateWordPressSource(cwd, options = {}) {
195
+ async function generateIntrospectionSource(cwd, options = {}) {
191
196
  const result = await fetchDocument(cwd, options);
192
197
  if (!result?.document) throw new Error("WordPress introspection returned no document.");
193
198
  const generated = generateWordPressModule(result.document);
194
199
  await assertGeneratedClientCompiles(generated);
195
200
  return generated.source;
196
201
  }
197
- async function generateWordPressOnce(cfg, options = {}) {
198
- const etag = readWordPressMeta(path.resolve(cfg.cwd, cfg.wordpressMetaPath), path.resolve(cfg.cwd, cfg.wordpressPath))?.etag;
199
- const result = await fetchDocument(cfg.cwd, options, etag);
200
- if (!result?.document) return "unchanged";
201
- const generated = generateWordPressModule(result.document);
202
- await assertGeneratedClientCompiles(generated);
203
- writeGeneratedWordPress(cfg, generated.source, wordPressMeta(result.etag, result.document.hash));
204
- return "generated";
205
- }
206
202
  /**
207
- * Write `<dir>/wordpress.ts` once for a whole workspace. Every package that ships procedures compiles
208
- * against the same file, so there is one description of WordPress in the repo rather than one per
209
- * package drifting apart. One shape too: a package that only wants the types today still gets the
210
- * 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`.
211
206
  */
212
- async function generateWorkspaceClientOnce(cwd, dir, options = {}) {
213
- const file = path.resolve(cwd, dir, "wordpress.ts");
214
- const metaPath = path.resolve(cwd, WORDPRESS_CLIENT_META_REL);
215
- 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);
216
211
  if (!result?.document) return "unchanged";
217
212
  const generated = generateWordPressModule(result.document);
218
213
  await assertGeneratedClientCompiles(generated);
219
- const contents = generated.source;
214
+ fs.mkdirSync(path.dirname(file), { recursive: true });
220
215
  fs.mkdirSync(path.dirname(metaPath), { recursive: true });
216
+ atomicWrite(file, generated.source);
221
217
  atomicWrite(metaPath, wordPressMeta(result.etag, result.document.hash));
222
- if (fs.existsSync(file) && fs.readFileSync(file, "utf8") === contents) return "unchanged";
223
- fs.mkdirSync(path.dirname(file), { recursive: true });
224
- atomicWrite(file, contents);
225
218
  return "generated";
226
219
  }
227
- function writeGeneratedWordPress(cfg, client, meta) {
228
- const metaPath = path.resolve(cfg.cwd, cfg.wordpressMetaPath);
229
- fs.mkdirSync(path.resolve(cfg.cwd, cfg.generatedDir), { recursive: true });
230
- fs.mkdirSync(path.dirname(metaPath), { recursive: true });
231
- atomicWrite(path.resolve(cfg.cwd, cfg.wordpressPath), client);
232
- atomicWrite(metaPath, meta);
233
- }
234
220
  /**
235
- * Loads the Kizlo server, builds its contract, and writes `contract.json` and
236
- * 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"`.
237
225
  */
238
226
  async function generateOnce(cfg, options = {}) {
239
227
  loadEnvFiles(cfg.cwd);
240
- const entry = path.resolve(cfg.cwd, cfg.serverEntry);
241
- const server = await importIgnoringVirtualModules(cfg.cwd, entry);
242
- if (!server.procedures) {
243
- if (server.router) throw new LegacyRouterExportError(cfg.serverEntry);
244
- 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;
245
234
  }
246
- const contract = JSON.stringify(await generateContract(server.procedures));
247
- const wordpress = await generateWordPressOnce(cfg, options);
248
- fs.mkdirSync(path.resolve(cfg.cwd, cfg.generatedDir), { recursive: true });
249
- atomicWrite(path.resolve(cfg.cwd, cfg.contractPath), contract);
250
- atomicWrite(path.resolve(cfg.cwd, cfg.barrelPath), CONTRACT_BARREL);
251
- if (wordpress === "generated") log.success("WordPress client generated");
252
- 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
+ };
253
253
  }
254
254
 
255
255
  //#endregion
256
- export { CONTRACT_BARREL, PartialContractError, WORDPRESS_STUB, generateOnce, generateWordPressOnce, generateWordPressSource, generateWorkspaceClientOnce, reportGenerationError };
256
+ export { CONTRACT_BARREL, INTROSPECTION_STUB, PartialContractError, generateIntrospectionOnce, generateIntrospectionSource, generateOnce, reportGenerationError };