kizlo 0.10.0 → 0.12.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/README.md +4 -4
- package/dist/astro/server.d.ts +9 -0
- package/dist/astro/server.js +10 -0
- package/dist/astro.d.ts +2 -0
- package/dist/astro.js +3 -0
- package/dist/cli/commands/_setup.js +19 -10
- package/dist/cli/commands/create.js +102 -72
- package/dist/cli/commands/dev.js +113 -20
- package/dist/cli/commands/generate.js +4 -0
- package/dist/cli/commands/init.js +111 -57
- package/dist/cli/daemon/config.js +13 -7
- package/dist/cli/daemon/generate.js +2 -3
- package/dist/cli/daemon/jiti.js +101 -0
- package/dist/cli/daemon/logger.js +1 -1
- package/dist/cli/daemon/reload.js +66 -0
- package/dist/cli/daemon/watch.js +7 -4
- package/dist/cli/presets/source.js +122 -24
- package/dist/cli/presets/template.js +75 -17
- package/dist/cli/wp/settings.js +8 -2
- package/dist/comment/schema.js +2 -2
- package/dist/context.d.ts +9 -0
- package/dist/context.js +20 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/integrations/astro/kizlo.client.d.ts +15 -0
- package/dist/integrations/astro/kizlo.client.js +13 -0
- package/dist/integrations/astro/kizlo.server.d.ts +10 -0
- package/dist/integrations/astro/kizlo.server.js +12 -0
- package/dist/integrations/astro/manifest.d.ts +9 -0
- package/dist/integrations/astro/manifest.js +11 -0
- package/dist/integrations/astro/metadata.d.ts +64 -0
- package/dist/integrations/astro/metadata.js +71 -0
- package/dist/integrations/astro/robots.d.ts +8 -0
- package/dist/integrations/astro/robots.js +11 -0
- package/dist/integrations/astro/sitemap.d.ts +16 -0
- package/dist/integrations/astro/sitemap.js +20 -0
- package/dist/integrations/astro/utils.d.ts +19 -0
- package/dist/integrations/astro/utils.js +17 -0
- package/dist/integrations/nextjs/metadata.d.ts +1 -0
- package/dist/integrations/nextjs/metadata.js +1 -0
- package/dist/kizlo.d.ts +8 -1
- package/dist/kizlo.js +16 -13
- package/dist/nextjs/server.d.ts +2 -1
- package/dist/nextjs/server.js +2 -1
- package/dist/package.js +1 -1
- package/dist/page/schema.js +2 -2
- package/dist/post/schema.js +2 -2
- package/dist/seo/utils.d.ts +8 -2
- package/dist/seo/utils.js +9 -1
- package/dist/shared/constants.js +9 -1
- package/dist/shared/schema.d.ts +1 -2
- package/dist/shared/schema.js +2 -2
- package/dist/wordpress/service.d.ts +4 -0
- package/dist/wordpress/service.js +5 -0
- package/package.json +18 -2
- package/dist/cli/presets/base.js +0 -40
- package/dist/cli/presets/index.js +0 -16
- package/dist/cli/presets/nextjs.js +0 -16
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { addDependencyArgs, detectImportAlias, detectPackageManager, ensureGitignored, getVersion, loadEnvFiles, runCommandAsync } from "../utils.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { adaptFile, changesFor, fileEntries, isOlderVersion, minCliError, patchEntries, readManifest, resolveDependencies } from "../presets/template.js";
|
|
2
|
+
import { DEFAULT_REGISTRY, detectTemplates, fetchRegistry, isSingleTemplate, listTemplates, resolveRegistry } from "../presets/source.js";
|
|
3
|
+
import { adaptFile, changesFor, fileEntries, isOlderVersion, minCliError, patchEntries, readManifest, renderNote, resolveDependencies } from "../presets/template.js";
|
|
5
4
|
import { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsNote, orCancel, readEnvExample, resolveEnvKeys, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
|
|
6
5
|
import { applyLayoutPatches, buildScaffoldContext, kizloConfigTemplate, reportScaffold, scaffoldFile, writeGeneratedContract } from "./_wiring.js";
|
|
7
6
|
import path from "node:path";
|
|
@@ -25,7 +24,20 @@ function sameOrigin(a, b) {
|
|
|
25
24
|
return false;
|
|
26
25
|
}
|
|
27
26
|
}
|
|
28
|
-
|
|
27
|
+
/**
|
|
28
|
+
* The framework's route directory in the user's project. When a template is present its
|
|
29
|
+
* `conventions.appDir` (e.g. `src/app` for Next, `src/pages` for Astro) is the expected layout; we
|
|
30
|
+
* probe that path and its no-`src` variant so a project that keeps routes at the top level is still
|
|
31
|
+
* found, falling back to whether the project has a `src` dir. With no known template appDir we keep the
|
|
32
|
+
* App Router default.
|
|
33
|
+
*/
|
|
34
|
+
function detectAppDir(cwd, hasSrcDir, templateAppDir) {
|
|
35
|
+
if (templateAppDir) {
|
|
36
|
+
const withoutSrc = templateAppDir.replace(/^src\//, "");
|
|
37
|
+
if (fs.existsSync(path.join(cwd, templateAppDir))) return templateAppDir;
|
|
38
|
+
if (fs.existsSync(path.join(cwd, withoutSrc))) return withoutSrc;
|
|
39
|
+
return hasSrcDir ? templateAppDir : withoutSrc;
|
|
40
|
+
}
|
|
29
41
|
if (fs.existsSync(path.join(cwd, "src", "app"))) return "src/app";
|
|
30
42
|
if (fs.existsSync(path.join(cwd, "app"))) return "app";
|
|
31
43
|
return hasSrcDir ? "src/app" : "app";
|
|
@@ -37,7 +49,7 @@ async function collectInteractively(ctx) {
|
|
|
37
49
|
validate: validate(dirPath)
|
|
38
50
|
}));
|
|
39
51
|
let alias = "";
|
|
40
|
-
if (ctx.
|
|
52
|
+
if (ctx.apiPath) {
|
|
41
53
|
const serverDir = path.join(dir.replace(/^\.\//, "").replace(/\/+$/, ""), "server");
|
|
42
54
|
const detected = detectImportAlias(ctx.cwd, serverDir)?.prefix;
|
|
43
55
|
alias = orCancel(await p.text({
|
|
@@ -47,7 +59,7 @@ async function collectInteractively(ctx) {
|
|
|
47
59
|
})).trim();
|
|
48
60
|
}
|
|
49
61
|
return {
|
|
50
|
-
...await collectConnectionInteractively(ctx.
|
|
62
|
+
...await collectConnectionInteractively(ctx.apiPath),
|
|
51
63
|
dir,
|
|
52
64
|
alias
|
|
53
65
|
};
|
|
@@ -63,7 +75,7 @@ function collectFromEnv(ctx) {
|
|
|
63
75
|
return {
|
|
64
76
|
...conn,
|
|
65
77
|
dir,
|
|
66
|
-
alias: ctx.
|
|
78
|
+
alias: ctx.apiPath ? aliasWithSlash(detectImportAlias(ctx.cwd, path.join(dir, "server"))?.prefix) : ""
|
|
67
79
|
};
|
|
68
80
|
}
|
|
69
81
|
function readPkg(pkgPath) {
|
|
@@ -105,13 +117,13 @@ const init = defineCommand({
|
|
|
105
117
|
description: "Overwrite existing .env values and server entry without asking",
|
|
106
118
|
default: false
|
|
107
119
|
},
|
|
108
|
-
preset: {
|
|
109
|
-
type: "string",
|
|
110
|
-
description: `Force a setup preset (${PRESETS.map((preset) => preset.id).join(", ")})`
|
|
111
|
-
},
|
|
112
120
|
alias: {
|
|
113
121
|
type: "string",
|
|
114
122
|
description: "Import alias prefix for generated imports (e.g. @); blank for relative"
|
|
123
|
+
},
|
|
124
|
+
source: {
|
|
125
|
+
type: "string",
|
|
126
|
+
description: "Where to wire from — a local dir or giget source, either a registry of templates or a single template (default: Kizlo's GitHub templates)"
|
|
115
127
|
}
|
|
116
128
|
},
|
|
117
129
|
async run({ args }) {
|
|
@@ -136,69 +148,109 @@ const init = defineCommand({
|
|
|
136
148
|
const pm = detectPackageManager(cwd);
|
|
137
149
|
const hasKizlo = Boolean(pkg.dependencies?.kizlo) || Boolean(pkg.devDependencies?.kizlo);
|
|
138
150
|
const hasSrcDir = fs.existsSync(path.join(cwd, "src"));
|
|
139
|
-
const initCtx = {
|
|
140
|
-
cwd,
|
|
141
|
-
pkg,
|
|
142
|
-
pm,
|
|
143
|
-
hasSrcDir
|
|
144
|
-
};
|
|
145
|
-
let preset;
|
|
146
|
-
if (args.preset) {
|
|
147
|
-
const chosen = getPreset(String(args.preset));
|
|
148
|
-
if (!chosen) {
|
|
149
|
-
p.cancel(`Unknown preset "${args.preset}". Available: ${PRESETS.map((pr) => pr.id).join(", ")}`);
|
|
150
|
-
process.exit(1);
|
|
151
|
-
}
|
|
152
|
-
preset = chosen;
|
|
153
|
-
} else {
|
|
154
|
-
preset = detectPreset(initCtx);
|
|
155
|
-
if (preset.id !== "base") p.log.success(`${preset.label} detected`);
|
|
156
|
-
}
|
|
157
|
-
if (preset.template === "nextjs" && !fs.existsSync(path.join(cwd, "app")) && !fs.existsSync(path.join(cwd, "src/app"))) {
|
|
158
|
-
p.cancel("Kizlo needs the Next.js App Router — no `app` or `src/app` directory found. The Pages Router isn't supported.");
|
|
159
|
-
process.exit(1);
|
|
160
|
-
}
|
|
161
151
|
if (yes) loadEnvFiles(cwd);
|
|
162
|
-
|
|
163
|
-
let
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
fetched.cleanup();
|
|
170
|
-
p.cancel(minErr);
|
|
171
|
-
process.exit(1);
|
|
172
|
-
}
|
|
152
|
+
const registry = resolveRegistry(args.source ? String(args.source) : void 0);
|
|
153
|
+
let fetchedRegistry;
|
|
154
|
+
try {
|
|
155
|
+
fetchedRegistry = await fetchRegistry(registry);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
p.cancel(error instanceof Error ? error.message : String(error));
|
|
158
|
+
process.exit(1);
|
|
173
159
|
}
|
|
174
|
-
const
|
|
160
|
+
const cancelFrom = (message) => {
|
|
161
|
+
fetchedRegistry.cleanup();
|
|
162
|
+
p.cancel(message);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
};
|
|
175
165
|
try {
|
|
166
|
+
const entries = listTemplates(fetchedRegistry.dir);
|
|
167
|
+
if (entries.length === 0) cancelFrom(`No templates found in ${registry.source}. Each template needs a template.json.`);
|
|
168
|
+
const projectDeps = {
|
|
169
|
+
...pkg.dependencies,
|
|
170
|
+
...pkg.devDependencies
|
|
171
|
+
};
|
|
172
|
+
let entry;
|
|
173
|
+
if (isSingleTemplate(fetchedRegistry.dir)) {
|
|
174
|
+
const [only] = entries;
|
|
175
|
+
entry = only;
|
|
176
|
+
if (only && detectTemplates(entries, projectDeps).length === 0) {
|
|
177
|
+
p.log.warn(`The ${only.label} template doesn't match this project's dependencies — detection may be off on either side.`);
|
|
178
|
+
if (!(yes || orCancel(await p.confirm({
|
|
179
|
+
message: `Apply ${only.label} anyway?`,
|
|
180
|
+
initialValue: false
|
|
181
|
+
})))) cancelFrom("Cancelled. Point --source at a template that matches this project, or omit it to auto-detect.");
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
const matched = detectTemplates(entries, projectDeps);
|
|
185
|
+
const [first] = matched;
|
|
186
|
+
if (matched.length === 0) {
|
|
187
|
+
p.log.warn(`Couldn't find a template matching this project's framework ${registry.source === DEFAULT_REGISTRY ? "in Kizlo templates" : `at your provided source ${registry.source}`}.`);
|
|
188
|
+
if (yes) cancelFrom(`Re-run with --source pointing at the single template you want, or run without --yes to pick one manually.`);
|
|
189
|
+
if (!orCancel(await p.confirm({
|
|
190
|
+
message: "Pick a template manually anyway?",
|
|
191
|
+
initialValue: false
|
|
192
|
+
}))) cancelFrom("Cancelled.");
|
|
193
|
+
const id = orCancel(await p.select({
|
|
194
|
+
message: "Choose a template to apply",
|
|
195
|
+
options: entries.map((e) => ({
|
|
196
|
+
value: e.id,
|
|
197
|
+
label: e.label
|
|
198
|
+
}))
|
|
199
|
+
}));
|
|
200
|
+
entry = entries.find((e) => e.id === id);
|
|
201
|
+
} else if (matched.length === 1 && first) {
|
|
202
|
+
entry = first;
|
|
203
|
+
p.log.success(`${first.label} detected`);
|
|
204
|
+
} else {
|
|
205
|
+
const ids = matched.map((e) => e.id).join(", ");
|
|
206
|
+
if (yes) cancelFrom(`Multiple templates match this project (${ids}). Re-run with --source pointing at the single template you want.`);
|
|
207
|
+
const id = orCancel(await p.select({
|
|
208
|
+
message: "Multiple templates match this project — pick one",
|
|
209
|
+
options: matched.map((e) => ({
|
|
210
|
+
value: e.id,
|
|
211
|
+
label: e.label
|
|
212
|
+
}))
|
|
213
|
+
}));
|
|
214
|
+
entry = matched.find((e) => e.id === id);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (!entry) return cancelFrom("No template selected.");
|
|
218
|
+
const manifest = readManifest(entry.dir);
|
|
219
|
+
const minErr = minCliError(manifest);
|
|
220
|
+
if (minErr) cancelFrom(minErr);
|
|
221
|
+
if (manifest.requires && !manifest.requires.anyDir.some((rel) => fs.existsSync(path.join(cwd, rel)))) cancelFrom(manifest.requires.message);
|
|
222
|
+
const apiPath = manifest.apiPath;
|
|
223
|
+
const envKeys = resolveEnvKeys(manifest);
|
|
176
224
|
const setup = yes ? collectFromEnv({
|
|
177
225
|
cwd,
|
|
178
226
|
hasSrcDir,
|
|
179
|
-
|
|
227
|
+
apiPath,
|
|
180
228
|
envKeys
|
|
181
229
|
}) : await collectInteractively({
|
|
182
230
|
cwd,
|
|
183
231
|
hasSrcDir,
|
|
184
|
-
|
|
232
|
+
apiPath
|
|
185
233
|
});
|
|
186
234
|
if (args.alias !== void 0) setup.alias = String(args.alias).trim();
|
|
187
235
|
setup.alias = aliasWithSlash(setup.alias);
|
|
236
|
+
const includeExamples = yes ? true : orCancel(await p.confirm({
|
|
237
|
+
message: "Add example pages?",
|
|
238
|
+
initialValue: true
|
|
239
|
+
}));
|
|
188
240
|
if (!hasKizlo) {
|
|
189
|
-
const spec = `kizlo@${manifest
|
|
241
|
+
const spec = `kizlo@${manifest.dependencies?.kizlo ?? `^${getVersion()}`}`;
|
|
190
242
|
const s = p.spinner();
|
|
191
243
|
s.start(`Installing kizlo with ${pm}`);
|
|
192
244
|
const ok = await runCommandAsync(addDependencyArgs(pm, spec), cwd, "ignore");
|
|
193
245
|
s.stop(ok ? "Installed kizlo" : "Could not install kizlo automatically");
|
|
194
246
|
if (!ok) p.log.warn(`Install it yourself: ${addDependencyArgs(pm, spec).join(" ")}`);
|
|
195
247
|
}
|
|
196
|
-
if (
|
|
248
|
+
if (apiPath && setup.baseUrl) setup.baseUrl = withApiPath(setup.baseUrl, apiPath);
|
|
197
249
|
await setupLocalWordPress(cwd, setup);
|
|
198
250
|
await writeEnv(cwd, envKeys, setup, {
|
|
199
251
|
force,
|
|
200
252
|
yes,
|
|
201
|
-
exampleTemplate:
|
|
253
|
+
exampleTemplate: readEnvExample(entry.dir)
|
|
202
254
|
});
|
|
203
255
|
await syncRemote(setup);
|
|
204
256
|
const dirRel = setup.dir.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
@@ -206,7 +258,7 @@ const init = defineCommand({
|
|
|
206
258
|
const clientUrl = setup.siteUrl && !sameOrigin(setup.siteUrl, setup.baseUrl) ? setup.baseUrl : void 0;
|
|
207
259
|
const scaffold = buildScaffoldContext(cwd, {
|
|
208
260
|
dirRel,
|
|
209
|
-
appDir: detectAppDir(cwd, hasSrcDir),
|
|
261
|
+
appDir: detectAppDir(cwd, hasSrcDir, manifest.conventions.appDir),
|
|
210
262
|
alias: setup.alias,
|
|
211
263
|
clientUrl
|
|
212
264
|
});
|
|
@@ -215,10 +267,8 @@ const init = defineCommand({
|
|
|
215
267
|
relPath: "kizlo.config.ts",
|
|
216
268
|
contents: kizloConfigTemplate(dirRel, setup.alias, setup.mode === "local")
|
|
217
269
|
}];
|
|
218
|
-
|
|
219
|
-
|
|
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));
|
|
270
|
+
await alignDependencies(cwd, pm, pkg, manifest);
|
|
271
|
+
for (const fileEntry of fileEntries(changesFor(manifest, "init", { includeExamples }))) files.push(adaptFile(entry.dir, fileEntry, manifest.conventions, scaffold));
|
|
222
272
|
const scaffolded = [];
|
|
223
273
|
for (const file of files) scaffolded.push({
|
|
224
274
|
file,
|
|
@@ -231,12 +281,16 @@ const init = defineCommand({
|
|
|
231
281
|
const gitignore = ensureGitignored(cwd, ".env");
|
|
232
282
|
ensureGitignored(cwd, ".kizlo/");
|
|
233
283
|
for (const { file, result } of scaffolded) reportScaffold(file, result, yes);
|
|
234
|
-
|
|
284
|
+
applyLayoutPatches(cwd, patchEntries(changesFor(manifest, "init")), manifest.conventions, scaffold);
|
|
285
|
+
for (const note of manifest.notes ?? []) {
|
|
286
|
+
const rendered = renderNote(note, setup.alias);
|
|
287
|
+
p.note(rendered.body, rendered.title);
|
|
288
|
+
}
|
|
235
289
|
if (gitignore !== "present") p.log.success(`${gitignore === "created" ? "Created" : "Updated"} .gitignore (ignoring .env)`);
|
|
236
290
|
nextStepsNote(setup);
|
|
237
291
|
p.outro("Kizlo is ready 🎉");
|
|
238
292
|
} finally {
|
|
239
|
-
|
|
293
|
+
fetchedRegistry.cleanup();
|
|
240
294
|
}
|
|
241
295
|
}
|
|
242
296
|
});
|
|
@@ -2,10 +2,10 @@ import { 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";
|
|
5
|
+
import { importIgnoringVirtualModules } from "./jiti.js";
|
|
5
6
|
import z from "zod/v4";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import fs from "node:fs";
|
|
8
|
-
import { createJiti } from "jiti";
|
|
9
9
|
|
|
10
10
|
//#region src/cli/daemon/config.ts
|
|
11
11
|
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)" });
|
|
@@ -41,15 +41,12 @@ const CONFIG_FILES = [
|
|
|
41
41
|
const DEFAULT_DEV_PORT = 8080;
|
|
42
42
|
const DEFAULT_DEV_DB_PORT = 3307;
|
|
43
43
|
const DEFAULT_TEST_PORT = 8889;
|
|
44
|
-
function defaultDir(cwd) {
|
|
45
|
-
return fs.existsSync(path.join(cwd, "src")) ? "src/lib/kizlo" : "lib/kizlo";
|
|
46
|
-
}
|
|
47
44
|
async function loadConfigFile(cwd) {
|
|
48
45
|
const file = CONFIG_FILES.map((name) => path.join(cwd, name)).find((p) => fs.existsSync(p));
|
|
49
46
|
if (!file) return void 0;
|
|
50
47
|
let raw;
|
|
51
48
|
try {
|
|
52
|
-
const mod = await
|
|
49
|
+
const mod = await importIgnoringVirtualModules(cwd, file);
|
|
53
50
|
raw = mod.default ?? mod;
|
|
54
51
|
} catch (error) {
|
|
55
52
|
log.error(`Could not load ${path.basename(file)}:`, error);
|
|
@@ -62,9 +59,18 @@ async function loadConfigFile(cwd) {
|
|
|
62
59
|
}
|
|
63
60
|
return result.data;
|
|
64
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Resolve the Kizlo server layout from an explicit `dir` — the `--dir` flag or `dir` in
|
|
64
|
+
* `kizlo.config.*`. Returns `undefined` when neither is set: there's no Kizlo server to
|
|
65
|
+
* generate a contract from, so callers skip generation and (for `dev`) run local WordPress
|
|
66
|
+
* alone. There's intentionally no default path — a missing `dir` means "no server", not
|
|
67
|
+
* "look under `lib/kizlo`".
|
|
68
|
+
*/
|
|
65
69
|
async function resolveConfig(cwd, flags) {
|
|
66
70
|
const fileConfig = await loadConfigFile(cwd);
|
|
67
|
-
const
|
|
71
|
+
const raw = flags?.dir ?? fileConfig?.dir;
|
|
72
|
+
if (!raw) return void 0;
|
|
73
|
+
const dir = raw.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
68
74
|
const serverDir = path.join(dir, "server");
|
|
69
75
|
const generatedDir = path.join(serverDir, "generated");
|
|
70
76
|
return {
|
|
@@ -154,4 +160,4 @@ async function resolveDevConfig(cwd) {
|
|
|
154
160
|
}
|
|
155
161
|
|
|
156
162
|
//#endregion
|
|
157
|
-
export { DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT, resolveConfig, resolveDevConfig, resolveStackName, resolveTestConfig, usesLocalWordPress };
|
|
163
|
+
export { CONFIG_FILES, DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT, resolveConfig, resolveDevConfig, resolveStackName, resolveTestConfig, usesLocalWordPress };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { generateContract } from "../../shared/contract.js";
|
|
2
2
|
import { loadEnvFiles } from "../utils.js";
|
|
3
|
+
import { importIgnoringVirtualModules } from "./jiti.js";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import fs from "node:fs";
|
|
5
|
-
import { createJiti } from "jiti";
|
|
6
6
|
|
|
7
7
|
//#region src/cli/daemon/generate.ts
|
|
8
8
|
/** The generated contract barrel — also written as a stub by `kizlo init`. */
|
|
@@ -25,9 +25,8 @@ function atomicWrite(file, data) {
|
|
|
25
25
|
*/
|
|
26
26
|
async function generateOnce(cfg) {
|
|
27
27
|
loadEnvFiles(cfg.cwd);
|
|
28
|
-
const jiti = createJiti(cfg.cwd, { moduleCache: false });
|
|
29
28
|
const entry = path.resolve(cfg.cwd, cfg.serverEntry);
|
|
30
|
-
const { router } = await
|
|
29
|
+
const { router } = await importIgnoringVirtualModules(cfg.cwd, entry);
|
|
31
30
|
if (!router) return false;
|
|
32
31
|
const contract = JSON.stringify(await generateContract(router));
|
|
33
32
|
fs.mkdirSync(path.resolve(cfg.cwd, cfg.generatedDir), { recursive: true });
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { CONTRACT_GENERATION_ENV } from "../../shared/constants.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { createJiti } from "jiti";
|
|
5
|
+
|
|
6
|
+
//#region src/cli/daemon/jiti.ts
|
|
7
|
+
/**
|
|
8
|
+
* Loads a TypeScript module through jiti while tolerating framework virtual modules
|
|
9
|
+
* (`astro:env/server`, `virtual:*`, `#imports`, `$env/*`, …). These are synthesized by the
|
|
10
|
+
* framework's bundler at build time and don't exist on disk, so jiti can't resolve them — but
|
|
11
|
+
* contract generation only needs the module's exported *shape*, never the runtime values a
|
|
12
|
+
* virtual module carries. We discover them from the project's own source rather than maintaining
|
|
13
|
+
* a per-framework list: import, and whenever jiti reports a missing module, stub it and retry.
|
|
14
|
+
*
|
|
15
|
+
* A virtual module is distinguished from a genuinely missing dependency by the one property that
|
|
16
|
+
* always holds — a virtual module is never a declared dependency, so a bare specifier whose
|
|
17
|
+
* package isn't in `package.json` is treated as virtual, while a declared-but-unresolvable
|
|
18
|
+
* dependency is surfaced as the real error it is.
|
|
19
|
+
*/
|
|
20
|
+
async function importIgnoringVirtualModules(cwd, entry) {
|
|
21
|
+
const deps = declaredDeps(cwd);
|
|
22
|
+
const virtualModules = {};
|
|
23
|
+
const previousGenerationFlag = process.env[CONTRACT_GENERATION_ENV];
|
|
24
|
+
process.env[CONTRACT_GENERATION_ENV] = "1";
|
|
25
|
+
try {
|
|
26
|
+
return await importLoop(cwd, entry, deps, virtualModules);
|
|
27
|
+
} finally {
|
|
28
|
+
if (previousGenerationFlag === void 0) delete process.env[CONTRACT_GENERATION_ENV];
|
|
29
|
+
else process.env[CONTRACT_GENERATION_ENV] = previousGenerationFlag;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function importLoop(cwd, entry, deps, virtualModules) {
|
|
33
|
+
for (;;) {
|
|
34
|
+
const jiti = createJiti(cwd, {
|
|
35
|
+
moduleCache: false,
|
|
36
|
+
tsconfigPaths: true,
|
|
37
|
+
virtualModules
|
|
38
|
+
});
|
|
39
|
+
try {
|
|
40
|
+
return await jiti.import(entry);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
const spec = missingSpecifier(error);
|
|
43
|
+
if (!spec || !isBareSpecifier(spec) || spec in virtualModules || deps.has(packageNameOf(spec))) throw error;
|
|
44
|
+
virtualModules[spec] = makeVirtualStub();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A no-op module namespace that satisfies any import shape (named, default, nested, callable),
|
|
50
|
+
* so a virtual module can be stubbed without knowing what it exports. The `has` trap is required:
|
|
51
|
+
* without it jiti's `interopDefault` sees a truthy `.default` and routes named lookups down a path
|
|
52
|
+
* that misses.
|
|
53
|
+
*
|
|
54
|
+
* Every export — named, default, nested, callable — is a no-op, so contract generation only ever
|
|
55
|
+
* reads the module's shape, never a real value a virtual module would carry at runtime.
|
|
56
|
+
*/
|
|
57
|
+
function makeVirtualStub() {
|
|
58
|
+
const stub = new Proxy(() => {}, {
|
|
59
|
+
apply: () => void 0,
|
|
60
|
+
has: (_t, prop) => typeof prop === "string" && prop !== "then",
|
|
61
|
+
get: (_t, prop) => {
|
|
62
|
+
if (prop === "__esModule") return true;
|
|
63
|
+
if (prop === "then") return void 0;
|
|
64
|
+
if (typeof prop === "symbol") return void 0;
|
|
65
|
+
return stub;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
return stub;
|
|
69
|
+
}
|
|
70
|
+
/** Names of every declared dependency; empty when `package.json` is absent or unreadable. */
|
|
71
|
+
function declaredDeps(cwd) {
|
|
72
|
+
try {
|
|
73
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"));
|
|
74
|
+
return new Set([
|
|
75
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
76
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
77
|
+
...Object.keys(pkg.peerDependencies ?? {}),
|
|
78
|
+
...Object.keys(pkg.optionalDependencies ?? {})
|
|
79
|
+
]);
|
|
80
|
+
} catch {
|
|
81
|
+
return /* @__PURE__ */ new Set();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** The specifier jiti/Node reported unresolvable, or undefined when the error isn't a resolution failure. */
|
|
85
|
+
function missingSpecifier(error) {
|
|
86
|
+
const err = error;
|
|
87
|
+
if (err?.code !== "MODULE_NOT_FOUND" && err?.code !== "ERR_MODULE_NOT_FOUND") return void 0;
|
|
88
|
+
return /Cannot find module '([^']+)'/.exec(err.message ?? "")?.[1];
|
|
89
|
+
}
|
|
90
|
+
/** Virtual specifiers are always bare; a missing relative/absolute path is a real bug, not a virtual module. */
|
|
91
|
+
function isBareSpecifier(spec) {
|
|
92
|
+
return !spec.startsWith(".") && !spec.startsWith("/");
|
|
93
|
+
}
|
|
94
|
+
/** Package name of a bare specifier: `@scope/x/sub` → `@scope/x`, `astro:env/server` → `astro:env`. */
|
|
95
|
+
function packageNameOf(spec) {
|
|
96
|
+
const parts = spec.split("/");
|
|
97
|
+
return spec.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0] ?? spec;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
//#endregion
|
|
101
|
+
export { importIgnoringVirtualModules };
|
|
@@ -2,7 +2,7 @@ import { consola } from "consola";
|
|
|
2
2
|
|
|
3
3
|
//#region src/cli/daemon/logger.ts
|
|
4
4
|
/** Tagged logger for the daemon — every line is prefixed with `kizlo`. */
|
|
5
|
-
const log = consola
|
|
5
|
+
const log = consola;
|
|
6
6
|
log.options.formatOptions.date = false;
|
|
7
7
|
|
|
8
8
|
//#endregion
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { CONFIG_FILES } from "./config.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { FSWatcher } from "chokidar";
|
|
6
|
+
|
|
7
|
+
//#region src/cli/daemon/reload.ts
|
|
8
|
+
/** Env files `loadEnvFiles` reads, in the same order — the runtime values a reload must pick up. */
|
|
9
|
+
const ENV_FILES = [".env", ".env.local"];
|
|
10
|
+
/** A combined hash of the watched files' contents; a missing file counts as empty. */
|
|
11
|
+
function fingerprint(files) {
|
|
12
|
+
const hash = createHash("sha1");
|
|
13
|
+
for (const file of files) {
|
|
14
|
+
hash.update(file);
|
|
15
|
+
hash.update("\0");
|
|
16
|
+
try {
|
|
17
|
+
hash.update(readFileSync(file));
|
|
18
|
+
} catch {}
|
|
19
|
+
hash.update("\0");
|
|
20
|
+
}
|
|
21
|
+
return hash.digest("hex");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Watch the project's `.env` files and `kizlo.config.*` and call `onReload` with the changed file's
|
|
25
|
+
* basename when their *contents* change. `kizlo dev` uses this to restart its whole session — rebooting
|
|
26
|
+
* local WordPress and regenerating the contract — so a changed port, connection, or fixture takes
|
|
27
|
+
* effect without stopping and rerunning the command by hand. Debounced so an editor's multi-write save
|
|
28
|
+
* fires once, and gated on a content hash so a no-op save (`touch`, or writing the file back unchanged)
|
|
29
|
+
* is ignored rather than triggering a pointless reboot. Returns a synchronous `stop()` that closes the
|
|
30
|
+
* watcher.
|
|
31
|
+
*
|
|
32
|
+
* `dev` rewrites `.env` itself while booting (fresh credentials, the current LAN URL); the caller
|
|
33
|
+
* pauses its reload handler around a reboot so that self-write doesn't trigger another reload. The hash
|
|
34
|
+
* is still refreshed for it here, so once the reboot settles the new content is the baseline and won't
|
|
35
|
+
* re-fire.
|
|
36
|
+
*/
|
|
37
|
+
function watchReload(configDir, onReload) {
|
|
38
|
+
const files = [...ENV_FILES, ...CONFIG_FILES].map((name) => path.join(configDir, name));
|
|
39
|
+
const watcher = new FSWatcher({
|
|
40
|
+
persistent: true,
|
|
41
|
+
ignoreInitial: true
|
|
42
|
+
});
|
|
43
|
+
let last = fingerprint(files);
|
|
44
|
+
let timer;
|
|
45
|
+
const schedule = (changed) => {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
timer = setTimeout(() => {
|
|
48
|
+
const next = fingerprint(files);
|
|
49
|
+
if (next === last) return;
|
|
50
|
+
last = next;
|
|
51
|
+
onReload(path.basename(changed));
|
|
52
|
+
}, 300);
|
|
53
|
+
};
|
|
54
|
+
watcher.add(files);
|
|
55
|
+
watcher.on("all", (_event, changed) => schedule(changed));
|
|
56
|
+
let stopped = false;
|
|
57
|
+
return () => {
|
|
58
|
+
if (stopped) return;
|
|
59
|
+
stopped = true;
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
watcher.close();
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
//#endregion
|
|
66
|
+
export { watchReload };
|
package/dist/cli/daemon/watch.js
CHANGED
|
@@ -30,7 +30,6 @@ async function watch(cfg) {
|
|
|
30
30
|
});
|
|
31
31
|
const onChange = debounce(() => regenerate(cfg), 300);
|
|
32
32
|
watcher.add(path.resolve(cfg.cwd, cfg.serverDir));
|
|
33
|
-
watcher.on("ready", () => log.start("Watching for contract changes..."));
|
|
34
33
|
watcher.on("all", () => void onChange());
|
|
35
34
|
return watcher;
|
|
36
35
|
}
|
|
@@ -38,9 +37,9 @@ async function watch(cfg) {
|
|
|
38
37
|
* Acquire the single-instance lock, generate the contract once, and start the file
|
|
39
38
|
* watcher. Returns a synchronous `stop()` that closes the watcher and releases the
|
|
40
39
|
* lock — or `undefined` when another watcher already holds the lock (a framework dev
|
|
41
|
-
* script, or a second `kizlo dev`),
|
|
42
|
-
* watching. Used by `kizlo dev`, both when it boots a local
|
|
43
|
-
* the watcher alone, so a single terminal covers the whole dev loop.
|
|
40
|
+
* script, or a second `kizlo dev`), or when no server `dir` is configured, in which case
|
|
41
|
+
* the caller carries on without watching. Used by `kizlo dev`, both when it boots a local
|
|
42
|
+
* stack and when it runs the watcher alone, so a single terminal covers the whole dev loop.
|
|
44
43
|
*/
|
|
45
44
|
async function startWatcher(cwd, opts) {
|
|
46
45
|
const lock = lockPath(cwd);
|
|
@@ -49,6 +48,10 @@ async function startWatcher(cwd, opts) {
|
|
|
49
48
|
return;
|
|
50
49
|
}
|
|
51
50
|
const cfg = await resolveConfig(cwd, { dir: opts?.dir });
|
|
51
|
+
if (!cfg) {
|
|
52
|
+
log.info("skipping the contract generation.");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
52
55
|
acquire(lock);
|
|
53
56
|
try {
|
|
54
57
|
if (await generateOnce(cfg)) log.success("Contract generated");
|