kizlo 0.11.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/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 +8 -8
- package/dist/cli/commands/create.js +98 -68
- package/dist/cli/commands/generate.js +4 -0
- package/dist/cli/commands/init.js +109 -59
- package/dist/cli/daemon/config.js +12 -6
- package/dist/cli/daemon/generate.js +2 -3
- package/dist/cli/daemon/jiti.js +101 -0
- package/dist/cli/daemon/watch.js +7 -3
- package/dist/cli/presets/source.js +122 -24
- package/dist/cli/presets/template.js +47 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- 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/seo/utils.d.ts +8 -2
- package/dist/seo/utils.js +9 -1
- package/dist/shared/constants.js +9 -1
- package/package.json +17 -1
- package/dist/cli/presets/base.js +0 -40
- package/dist/cli/presets/index.js +0 -16
- package/dist/cli/presets/nextjs.js +0 -16
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { WEB_MANIFEST_ROUTE } from "../brand/manifest.js";
|
|
2
|
+
import { renderJsonLd } from "../seo/utils.js";
|
|
3
|
+
import { KizloServerOptions, createKizlo } from "../integrations/astro/kizlo.server.js";
|
|
4
|
+
import { AstroRoute, AstroRouteContext, getServerBaseUrl } from "../integrations/astro/utils.js";
|
|
5
|
+
import { createManifestRoute } from "../integrations/astro/manifest.js";
|
|
6
|
+
import { PageHead, RootHead, ThemeColor, resolvePageHead, resolveRootHead } from "../integrations/astro/metadata.js";
|
|
7
|
+
import { createRobotsRoute } from "../integrations/astro/robots.js";
|
|
8
|
+
import { createSitemapRedirectRoute, createSitemapRoute } from "../integrations/astro/sitemap.js";
|
|
9
|
+
export { AstroRoute, AstroRouteContext, KizloServerOptions, PageHead, RootHead, ThemeColor, WEB_MANIFEST_ROUTE, createKizlo, createManifestRoute, createRobotsRoute, createSitemapRedirectRoute, createSitemapRoute, getServerBaseUrl, renderJsonLd, resolvePageHead, resolveRootHead };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { WEB_MANIFEST_ROUTE } from "../brand/manifest.js";
|
|
2
|
+
import { renderJsonLd } from "../seo/utils.js";
|
|
3
|
+
import { getServerBaseUrl } from "../integrations/astro/utils.js";
|
|
4
|
+
import { createKizlo } from "../integrations/astro/kizlo.server.js";
|
|
5
|
+
import { createManifestRoute } from "../integrations/astro/manifest.js";
|
|
6
|
+
import { resolvePageHead, resolveRootHead } from "../integrations/astro/metadata.js";
|
|
7
|
+
import { createRobotsRoute } from "../integrations/astro/robots.js";
|
|
8
|
+
import { createSitemapRedirectRoute, createSitemapRoute } from "../integrations/astro/sitemap.js";
|
|
9
|
+
|
|
10
|
+
export { WEB_MANIFEST_ROUTE, createKizlo, createManifestRoute, createRobotsRoute, createSitemapRedirectRoute, createSitemapRoute, getServerBaseUrl, renderJsonLd, resolvePageHead, resolveRootHead };
|
package/dist/astro.d.ts
ADDED
package/dist/astro.js
ADDED
|
@@ -17,13 +17,13 @@ import getPort, { portNumbers } from "get-port";
|
|
|
17
17
|
|
|
18
18
|
//#region src/cli/commands/_setup.ts
|
|
19
19
|
/**
|
|
20
|
-
* The resolved `.env` key names the scaffold writes: the template's declared `env` when it has one,
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* `
|
|
20
|
+
* The resolved `.env` key names the scaffold writes: the template's declared `env` when it has one, else
|
|
21
|
+
* the {@link DEFAULT_ENV_KEYS} fallback — for an in-repo template before the first stamped release, and
|
|
22
|
+
* for the base no-template fallback (which has no manifest). Threaded into `managedEnv`, `writeEnv`, and
|
|
23
|
+
* `collectConnectionFromEnv` so the scaffold writes the names the pinned runtime reads.
|
|
24
24
|
*/
|
|
25
|
-
function resolveEnvKeys(
|
|
26
|
-
return manifest?.env ??
|
|
25
|
+
function resolveEnvKeys(manifest) {
|
|
26
|
+
return manifest?.env ?? DEFAULT_ENV_KEYS;
|
|
27
27
|
}
|
|
28
28
|
/** Remote connection keys — what a real deploy needs, and what `.env.example` always lists. */
|
|
29
29
|
function remoteKeys(envKeys) {
|
|
@@ -135,11 +135,11 @@ function pickAppPort() {
|
|
|
135
135
|
* `create` scaffolds a brand-new app that has no public URL yet, so it defaults to the local dev
|
|
136
136
|
* origin and lets the user fill in production later.
|
|
137
137
|
*/
|
|
138
|
-
async function collectConnectionInteractively(
|
|
138
|
+
async function collectConnectionInteractively(apiPath, opts = {}) {
|
|
139
139
|
let siteUrl;
|
|
140
140
|
let baseUrl;
|
|
141
141
|
if (opts.baseUrl !== void 0) baseUrl = opts.baseUrl;
|
|
142
|
-
else if (
|
|
142
|
+
else if (apiPath) baseUrl = orCancel(await p.text({
|
|
143
143
|
message: "Public app URL",
|
|
144
144
|
placeholder: "https://your-app.com",
|
|
145
145
|
validate: validate(urlString)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { availablePackageManagers, detectInvokingPackageManager, ensureGitignored, frameworkCreateArgs, installArgs, runCommandAsync, runCommandCaptured } from "../utils.js";
|
|
2
|
-
import {
|
|
3
|
-
import { fetchTemplate } from "../presets/source.js";
|
|
2
|
+
import { fetchRegistry, listTemplates, resolveRegistry } from "../presets/source.js";
|
|
4
3
|
import { adaptFile, changesFor, fileEntries, minCliError, patchEntries, readManifest, resolveDependencies } from "../presets/template.js";
|
|
5
4
|
import { collectConnectionInteractively, nextStepsLines, orCancel, pickAppPort, readEnvExample, resolveEnvKeys, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
|
|
6
5
|
import { applyLayoutPatches, buildScaffoldContext, kizloConfigTemplate, reportScaffold, scaffoldFile, writeGeneratedContract } from "./_wiring.js";
|
|
@@ -11,15 +10,30 @@ import { defineCommand } from "citty";
|
|
|
11
10
|
import * as p from "@clack/prompts";
|
|
12
11
|
|
|
13
12
|
//#region src/cli/commands/create.ts
|
|
13
|
+
/** The final path segment of a project name — the app folder itself; everything before it is where it lands. */
|
|
14
|
+
function folderSegment(value) {
|
|
15
|
+
const segments = value.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
16
|
+
return segments[segments.length - 1] ?? "";
|
|
17
|
+
}
|
|
14
18
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
19
|
+
* Tidy the entered name/path for both the target dir and the display: trim, drop a leading `./`, and drop a
|
|
20
|
+
* trailing separator, so `cd`, the spinner, and the warning lines read as `templates/my-app`, not
|
|
21
|
+
* `./templates/my-app/`. Preserves `../` and absolute paths (only a leading `./` is redundant).
|
|
17
22
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return TEMPLATES.includes(value);
|
|
23
|
+
function normalizeProjectName(value) {
|
|
24
|
+
return value.trim().replace(/^\.\/+/, "").replace(/[/\\]+$/, "");
|
|
21
25
|
}
|
|
22
|
-
|
|
26
|
+
/**
|
|
27
|
+
* A project name may be a bare folder (`my-app`) or a path to scaffold somewhere other than the current
|
|
28
|
+
* dir — relative (`apps/my-app`, `../my-app`) or absolute (`/srv/my-app`). Only the last segment is the
|
|
29
|
+
* app folder, so that's what's validated as a name; the parent dirs (which may not exist yet) are created
|
|
30
|
+
* by the framework's own CLI. `.`/`..` as the final segment, or a name with no real segment, is rejected.
|
|
31
|
+
*/
|
|
32
|
+
const projectNameSchema = z.string().trim().min(1, "Required").refine((value) => {
|
|
33
|
+
const base = folderSegment(value);
|
|
34
|
+
return base.length > 0 && base !== "." && base !== ".." && /^[A-Za-z0-9._-]+$/.test(base);
|
|
35
|
+
}, "The app folder (last path segment) can only contain letters, numbers, dots, and dashes");
|
|
36
|
+
const projectName = validate(projectNameSchema);
|
|
23
37
|
/** Package managers `create` can wire the getting-started steps for, in display order. */
|
|
24
38
|
const PACKAGE_MANAGERS = [
|
|
25
39
|
"pnpm",
|
|
@@ -37,17 +51,25 @@ function recordDependencies(dir, manifest) {
|
|
|
37
51
|
const pkgPath = path.join(dir, "package.json");
|
|
38
52
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
39
53
|
const { dependencies, devDependencies } = resolveDependencies(manifest);
|
|
40
|
-
pkg.dependencies = {
|
|
54
|
+
pkg.dependencies = sortDependencies({
|
|
41
55
|
...pkg.dependencies,
|
|
42
56
|
...dependencies
|
|
43
|
-
};
|
|
44
|
-
if (Object.keys(devDependencies).length) pkg.devDependencies = {
|
|
57
|
+
});
|
|
58
|
+
if (Object.keys(devDependencies).length) pkg.devDependencies = sortDependencies({
|
|
45
59
|
...pkg.devDependencies,
|
|
46
60
|
...devDependencies
|
|
47
|
-
};
|
|
61
|
+
});
|
|
48
62
|
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, " ")}\n`);
|
|
49
63
|
}
|
|
50
64
|
/**
|
|
65
|
+
* Alphabetize a dependency map by package name. The framework CLI writes its deps sorted, but merging
|
|
66
|
+
* Kizlo's pins on top appends them out of order — sherif flags unsorted dependency keys, so re-sort the
|
|
67
|
+
* whole map before writing.
|
|
68
|
+
*/
|
|
69
|
+
function sortDependencies(deps) {
|
|
70
|
+
return Object.fromEntries(Object.entries(deps).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
51
73
|
* The argv `create` runs to bootstrap a fresh base app with the framework's own official CLI, built
|
|
52
74
|
* from the template manifest's `bootstrap` (initializer + flags) — the template is the single source
|
|
53
75
|
* of truth for the framework. The CLI owns only the package-manager mechanics: {@link frameworkCreateArgs}
|
|
@@ -104,54 +126,75 @@ const create = defineCommand({
|
|
|
104
126
|
template: {
|
|
105
127
|
type: "positional",
|
|
106
128
|
required: false,
|
|
107
|
-
description:
|
|
129
|
+
description: "Template to scaffold from (discovered from the registry)"
|
|
108
130
|
},
|
|
109
131
|
name: {
|
|
110
132
|
type: "positional",
|
|
111
133
|
required: false,
|
|
112
|
-
description: "Project folder to create"
|
|
134
|
+
description: "Project folder to create — a bare name or a path (e.g. apps/my-app, ../my-app, /srv/my-app)"
|
|
135
|
+
},
|
|
136
|
+
source: {
|
|
137
|
+
type: "string",
|
|
138
|
+
description: "Where to scaffold from — a local dir or giget source, either a registry of templates or a single template (default: Kizlo's GitHub templates)"
|
|
113
139
|
}
|
|
114
140
|
},
|
|
115
141
|
async run({ args }) {
|
|
116
142
|
const cwd = process.cwd();
|
|
117
143
|
p.intro(`Let's create a new Kizlo application`);
|
|
144
|
+
const registry = resolveRegistry(args.source ? String(args.source) : void 0);
|
|
145
|
+
let fetchedRegistry;
|
|
146
|
+
try {
|
|
147
|
+
fetchedRegistry = await fetchRegistry(registry);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
p.cancel(error instanceof Error ? error.message : String(error));
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
const templates = listTemplates(fetchedRegistry.dir);
|
|
153
|
+
if (templates.length === 0) {
|
|
154
|
+
fetchedRegistry.cleanup();
|
|
155
|
+
p.cancel(`No templates found in the registry (${registry.source}). Each template needs a template.json.`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
const cancelFrom = (message) => {
|
|
159
|
+
fetchedRegistry.cleanup();
|
|
160
|
+
p.cancel(message);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
};
|
|
118
163
|
const requested = args.template;
|
|
119
|
-
let
|
|
164
|
+
let selected;
|
|
120
165
|
if (requested) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
166
|
+
selected = templates.find((entry) => entry.id === requested);
|
|
167
|
+
if (!selected) return cancelFrom(`Unknown template "${requested}". Available: ${templates.map((entry) => entry.id).join(", ")}`);
|
|
168
|
+
} else {
|
|
169
|
+
const id = orCancel(await p.select({
|
|
170
|
+
message: "Template",
|
|
171
|
+
options: templates.map((entry) => ({
|
|
172
|
+
value: entry.id,
|
|
173
|
+
label: entry.label
|
|
174
|
+
}))
|
|
175
|
+
}));
|
|
176
|
+
selected = templates.find((entry) => entry.id === id);
|
|
177
|
+
}
|
|
178
|
+
if (!selected) return cancelFrom("No template selected.");
|
|
179
|
+
const template = selected.id;
|
|
180
|
+
const templateDir = selected.dir;
|
|
181
|
+
const label = selected.label;
|
|
182
|
+
const fail = cancelFrom;
|
|
183
|
+
const manifest = readManifest(templateDir);
|
|
184
|
+
const minErr = minCliError(manifest);
|
|
185
|
+
if (minErr) return fail(minErr);
|
|
186
|
+
const name = normalizeProjectName(args.name ? String(args.name) : orCancel(await p.text({
|
|
187
|
+
message: "What is your project named?",
|
|
135
188
|
placeholder: "my-app",
|
|
136
189
|
validate: projectName
|
|
137
|
-
}));
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
p.cancel(check);
|
|
142
|
-
process.exit(1);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
const dir = path.resolve(cwd, name);
|
|
146
|
-
if (fs.existsSync(dir)) {
|
|
147
|
-
p.cancel(`${name} already exists — pick a different name or remove it.`);
|
|
148
|
-
process.exit(1);
|
|
149
|
-
}
|
|
150
|
-
const preset = getPreset(template);
|
|
151
|
-
if (!preset) {
|
|
152
|
-
p.cancel(`No preset for template "${template}".`);
|
|
190
|
+
})));
|
|
191
|
+
const invalid = projectName(name);
|
|
192
|
+
if (invalid) {
|
|
193
|
+
p.cancel(invalid);
|
|
153
194
|
process.exit(1);
|
|
154
195
|
}
|
|
196
|
+
const dir = path.resolve(cwd, name);
|
|
197
|
+
if (fs.existsSync(dir)) return cancelFrom(`${name} already exists — pick a different name or remove it.`);
|
|
155
198
|
const installed = availablePackageManagers(PACKAGE_MANAGERS);
|
|
156
199
|
const invokingPm = detectInvokingPackageManager();
|
|
157
200
|
const defaultPm = invokingPm && installed.includes(invokingPm) ? invokingPm : installed[0];
|
|
@@ -163,45 +206,32 @@ const create = defineCommand({
|
|
|
163
206
|
})),
|
|
164
207
|
initialValue: defaultPm
|
|
165
208
|
}));
|
|
166
|
-
const conn = await collectConnectionInteractively(
|
|
167
|
-
if (
|
|
209
|
+
const conn = await collectConnectionInteractively(manifest.apiPath, { baseUrl: `http://localhost:${await pickAppPort()}` });
|
|
210
|
+
if (manifest.apiPath && conn.baseUrl) conn.baseUrl = withApiPath(conn.baseUrl, manifest.apiPath);
|
|
168
211
|
const includeExamples = orCancel(await p.confirm({
|
|
169
212
|
message: "Add examples?",
|
|
170
213
|
initialValue: true
|
|
171
214
|
}));
|
|
172
|
-
const fetched = await fetchTemplate(template);
|
|
173
|
-
const fail = (message) => {
|
|
174
|
-
fetched.cleanup();
|
|
175
|
-
p.cancel(message);
|
|
176
|
-
process.exit(1);
|
|
177
|
-
};
|
|
178
|
-
const manifest = readManifest(fetched.dir);
|
|
179
|
-
const minErr = minCliError(manifest);
|
|
180
|
-
if (minErr) fail(minErr);
|
|
181
215
|
const bootstrap = bootstrapArgs(manifest, pm, name);
|
|
182
|
-
if (!bootstrap) {
|
|
183
|
-
fetched.cleanup();
|
|
184
|
-
p.cancel(`Template "${template}" can't be scaffolded — its manifest declares no framework bootstrap.`);
|
|
185
|
-
process.exit(1);
|
|
186
|
-
}
|
|
216
|
+
if (!bootstrap) return fail(`Template "${template}" can't be scaffolded — its manifest declares no framework bootstrap.`);
|
|
187
217
|
const s = p.spinner();
|
|
188
|
-
s.start(`Creating ${name} with the ${
|
|
218
|
+
s.start(`Creating ${name} with the ${label} CLI`);
|
|
189
219
|
const scaffold = runCommandCaptured(bootstrap, cwd);
|
|
190
|
-
s.stop(scaffold.ok ? `Created ${name} with the ${
|
|
220
|
+
s.stop(scaffold.ok ? `Created ${name} with the ${label} CLI` : `${label} setup failed`);
|
|
191
221
|
if (!scaffold.ok) {
|
|
192
222
|
if (scaffold.output) p.log.error(scaffold.output);
|
|
193
|
-
fail(`${
|
|
223
|
+
fail(`${label} setup failed — see the output above and try again.`);
|
|
194
224
|
}
|
|
195
|
-
const exampleTemplate = readEnvExample(
|
|
225
|
+
const exampleTemplate = readEnvExample(templateDir);
|
|
196
226
|
try {
|
|
197
|
-
await applyManifestWiring(dir,
|
|
227
|
+
await applyManifestWiring(dir, templateDir, manifest, {
|
|
198
228
|
includeExamples,
|
|
199
229
|
localDev: conn.mode === "local"
|
|
200
230
|
});
|
|
201
231
|
} catch (error) {
|
|
202
232
|
fail(error instanceof Error ? error.message : String(error));
|
|
203
233
|
}
|
|
204
|
-
|
|
234
|
+
fetchedRegistry.cleanup();
|
|
205
235
|
let depsInstalled = false;
|
|
206
236
|
if (orCancel(await p.confirm({
|
|
207
237
|
message: "Install dependencies now?",
|
|
@@ -214,7 +244,7 @@ const create = defineCommand({
|
|
|
214
244
|
if (!depsInstalled) p.log.warn(`Install them yourself: cd ${name} && ${installArgs(pm).join(" ")}`);
|
|
215
245
|
}
|
|
216
246
|
await setupLocalWordPress(dir, conn);
|
|
217
|
-
await writeEnv(dir, resolveEnvKeys(
|
|
247
|
+
await writeEnv(dir, resolveEnvKeys(manifest), conn, {
|
|
218
248
|
force: true,
|
|
219
249
|
yes: false,
|
|
220
250
|
exampleTemplate
|
|
@@ -15,6 +15,10 @@ const generate = defineCommand({
|
|
|
15
15
|
} },
|
|
16
16
|
async run({ args }) {
|
|
17
17
|
const cfg = await resolveConfig(process.cwd(), { dir: args.dir });
|
|
18
|
+
if (!cfg) {
|
|
19
|
+
log.info("No Kizlo server directory configured, nothing to generate. Set `dir` in kizlo.config.ts or pass --dir.");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
18
22
|
let ok;
|
|
19
23
|
try {
|
|
20
24
|
ok = await generateOnce(cfg);
|
|
@@ -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,73 +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);
|
|
188
|
-
const includeExamples =
|
|
236
|
+
const includeExamples = yes ? true : orCancel(await p.confirm({
|
|
189
237
|
message: "Add example pages?",
|
|
190
238
|
initialValue: true
|
|
191
|
-
}))
|
|
239
|
+
}));
|
|
192
240
|
if (!hasKizlo) {
|
|
193
|
-
const spec = `kizlo@${manifest
|
|
241
|
+
const spec = `kizlo@${manifest.dependencies?.kizlo ?? `^${getVersion()}`}`;
|
|
194
242
|
const s = p.spinner();
|
|
195
243
|
s.start(`Installing kizlo with ${pm}`);
|
|
196
244
|
const ok = await runCommandAsync(addDependencyArgs(pm, spec), cwd, "ignore");
|
|
197
245
|
s.stop(ok ? "Installed kizlo" : "Could not install kizlo automatically");
|
|
198
246
|
if (!ok) p.log.warn(`Install it yourself: ${addDependencyArgs(pm, spec).join(" ")}`);
|
|
199
247
|
}
|
|
200
|
-
if (
|
|
248
|
+
if (apiPath && setup.baseUrl) setup.baseUrl = withApiPath(setup.baseUrl, apiPath);
|
|
201
249
|
await setupLocalWordPress(cwd, setup);
|
|
202
250
|
await writeEnv(cwd, envKeys, setup, {
|
|
203
251
|
force,
|
|
204
252
|
yes,
|
|
205
|
-
exampleTemplate:
|
|
253
|
+
exampleTemplate: readEnvExample(entry.dir)
|
|
206
254
|
});
|
|
207
255
|
await syncRemote(setup);
|
|
208
256
|
const dirRel = setup.dir.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
@@ -210,7 +258,7 @@ const init = defineCommand({
|
|
|
210
258
|
const clientUrl = setup.siteUrl && !sameOrigin(setup.siteUrl, setup.baseUrl) ? setup.baseUrl : void 0;
|
|
211
259
|
const scaffold = buildScaffoldContext(cwd, {
|
|
212
260
|
dirRel,
|
|
213
|
-
appDir: detectAppDir(cwd, hasSrcDir),
|
|
261
|
+
appDir: detectAppDir(cwd, hasSrcDir, manifest.conventions.appDir),
|
|
214
262
|
alias: setup.alias,
|
|
215
263
|
clientUrl
|
|
216
264
|
});
|
|
@@ -219,10 +267,8 @@ const init = defineCommand({
|
|
|
219
267
|
relPath: "kizlo.config.ts",
|
|
220
268
|
contents: kizloConfigTemplate(dirRel, setup.alias, setup.mode === "local")
|
|
221
269
|
}];
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
for (const entry of fileEntries(changesFor(manifest, "init", { includeExamples }))) files.push(adaptFile(fetched.dir, entry, manifest.conventions, scaffold));
|
|
225
|
-
} 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));
|
|
226
272
|
const scaffolded = [];
|
|
227
273
|
for (const file of files) scaffolded.push({
|
|
228
274
|
file,
|
|
@@ -235,12 +281,16 @@ const init = defineCommand({
|
|
|
235
281
|
const gitignore = ensureGitignored(cwd, ".env");
|
|
236
282
|
ensureGitignored(cwd, ".kizlo/");
|
|
237
283
|
for (const { file, result } of scaffolded) reportScaffold(file, result, yes);
|
|
238
|
-
|
|
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
|
+
}
|
|
239
289
|
if (gitignore !== "present") p.log.success(`${gitignore === "created" ? "Created" : "Updated"} .gitignore (ignoring .env)`);
|
|
240
290
|
nextStepsNote(setup);
|
|
241
291
|
p.outro("Kizlo is ready 🎉");
|
|
242
292
|
} finally {
|
|
243
|
-
|
|
293
|
+
fetchedRegistry.cleanup();
|
|
244
294
|
}
|
|
245
295
|
}
|
|
246
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 {
|