kizlo 0.8.1 → 0.8.3
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/cli/commands/_setup.js +10 -6
- package/dist/cli/commands/_wiring.js +116 -0
- package/dist/cli/commands/create.js +114 -47
- package/dist/cli/commands/init.js +64 -89
- package/dist/cli/presets/nextjs.js +1 -5
- package/dist/cli/presets/patch.js +27 -39
- package/dist/cli/presets/source.js +50 -0
- package/dist/cli/presets/template.js +156 -20
- package/dist/cli/utils.js +102 -2
- package/dist/cli/wp/dev.js +1 -1
- package/dist/package.js +1 -1
- package/dist/router.d.ts +4 -4
- package/dist/seo/schema.d.ts +5 -5
- package/package.json +1 -1
- package/dist/cli/presets/generated/templates.js +0 -59
|
@@ -321,11 +321,11 @@ async function syncRemote(conn) {
|
|
|
321
321
|
});
|
|
322
322
|
if (!sync.ok) p.log.warn(`Could not sync the site settings to WordPress (${sync.error}) — make sure the kizlo plugin is active, then set them from the Kizlo settings.`);
|
|
323
323
|
}
|
|
324
|
-
/** The
|
|
325
|
-
function
|
|
326
|
-
|
|
324
|
+
/** The "Next steps" lines, branched on whether a local dev stack was set up. */
|
|
325
|
+
function nextStepsLines(mode, prefix = "") {
|
|
326
|
+
return [
|
|
327
327
|
...mode === "local" ? [
|
|
328
|
-
`Start your local WordPress dev stack
|
|
328
|
+
`Start your local WordPress dev stack:`,
|
|
329
329
|
` ${prefix}npx kizlo dev`,
|
|
330
330
|
``
|
|
331
331
|
] : [
|
|
@@ -335,8 +335,12 @@ function nextStepsNote(mode, prefix = "") {
|
|
|
335
335
|
],
|
|
336
336
|
`Generate the contract once for production builds:`,
|
|
337
337
|
` ${prefix}npx kizlo generate`
|
|
338
|
-
]
|
|
338
|
+
];
|
|
339
|
+
}
|
|
340
|
+
/** The shared "Next steps" note, branched on whether a local dev stack was set up. */
|
|
341
|
+
function nextStepsNote(mode, prefix = "") {
|
|
342
|
+
p.note(nextStepsLines(mode, prefix).join("\n"), "Next steps");
|
|
339
343
|
}
|
|
340
344
|
|
|
341
345
|
//#endregion
|
|
342
|
-
export { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsNote, orCancel, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv };
|
|
346
|
+
export { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsLines, nextStepsNote, orCancel, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { resolveModuleImport, writeFileIfAbsent } from "../utils.js";
|
|
2
|
+
import { applyPatchToSource, patchChanged, renderPatchCode } from "../presets/patch.js";
|
|
3
|
+
import { findRootLayout, patchEntries, resolvePatch } from "../presets/template.js";
|
|
4
|
+
import { orCancel } from "./_setup.js";
|
|
5
|
+
import { CONTRACT_BARREL } from "../daemon/generate.js";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import * as p from "@clack/prompts";
|
|
9
|
+
|
|
10
|
+
//#region src/cli/commands/_wiring.ts
|
|
11
|
+
/** The kizlo.config.ts a scaffolded project gets: the Kizlo directory, optional import alias, and —
|
|
12
|
+
* for a local dev setup — the WordPress install folder so `kizlo dev` knows where it lives. */
|
|
13
|
+
function kizloConfigTemplate(dir, alias, devPath) {
|
|
14
|
+
return `import { defineConfig } from "kizlo/config"
|
|
15
|
+
|
|
16
|
+
export default defineConfig({
|
|
17
|
+
dir: "${dir}",${alias ? `\n\talias: "${alias}",` : ""}${devPath ? `\n\tdev: { path: "${devPath}" },` : ""}
|
|
18
|
+
})
|
|
19
|
+
`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Build the {@link ScaffoldContext} that adapts a template's tokenized paths and imports to a real
|
|
23
|
+
* project. `dirRel` is the Kizlo home directory, `appDir` the App Router directory, and `alias` the
|
|
24
|
+
* import-alias prefix (empty for relative imports). The server-entry import is resolved per calling
|
|
25
|
+
* file so each scaffolded file references the server through the right specifier.
|
|
26
|
+
*/
|
|
27
|
+
function buildScaffoldContext(cwd, { dirRel, appDir, alias, clientUrl }) {
|
|
28
|
+
const serverDirRel = path.join(dirRel, "server");
|
|
29
|
+
return {
|
|
30
|
+
kizloDir: dirRel,
|
|
31
|
+
serverDirName: path.basename(serverDirRel),
|
|
32
|
+
serverEntryPath: path.join(serverDirRel, "index.ts"),
|
|
33
|
+
clientPath: path.join(dirRel, "client.ts"),
|
|
34
|
+
appDir,
|
|
35
|
+
serverImport: (fromDir) => resolveModuleImport(cwd, serverDirRel, fromDir, alias),
|
|
36
|
+
clientUrl
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The single overwrite policy for every scaffolded file: create it when absent, overwrite on
|
|
41
|
+
* `--force`, and otherwise ask before clobbering an existing file (keeping it when the user
|
|
42
|
+
* declines or when running non-interactively with `--yes`). Every scaffolded file routes through
|
|
43
|
+
* here rather than deciding for itself, so new files inherit the same behavior for free.
|
|
44
|
+
*/
|
|
45
|
+
async function scaffoldFile(cwd, file, opts) {
|
|
46
|
+
const absPath = path.join(cwd, file.relPath);
|
|
47
|
+
const existed = fs.existsSync(absPath);
|
|
48
|
+
if (existed) {
|
|
49
|
+
let overwrite = opts.force;
|
|
50
|
+
if (!opts.force && !opts.yes) {
|
|
51
|
+
p.log.warn(`${file.label} already exists at ${file.relPath}`);
|
|
52
|
+
overwrite = orCancel(await p.confirm({
|
|
53
|
+
message: "Overwrite it?",
|
|
54
|
+
initialValue: true
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
if (!overwrite) return "kept";
|
|
58
|
+
}
|
|
59
|
+
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
|
60
|
+
fs.writeFileSync(absPath, file.contents);
|
|
61
|
+
return existed ? "overwritten" : "created";
|
|
62
|
+
}
|
|
63
|
+
/** Report a {@link scaffoldFile} outcome in the setup commands' usual voice. */
|
|
64
|
+
function reportScaffold(file, result, yes) {
|
|
65
|
+
if (result === "kept") p.log.info(`Kept existing ${file.label} (${file.relPath})${yes ? " — pass --force to overwrite" : ""}`);
|
|
66
|
+
else p.log.success(`${result === "overwritten" ? "Overwrote" : "Created"} ${file.label} (${file.relPath})`);
|
|
67
|
+
}
|
|
68
|
+
/** Seed the generated-contract directory so imports resolve before the first `kizlo watch`/`generate`. */
|
|
69
|
+
function writeGeneratedContract(cwd, serverDirRel) {
|
|
70
|
+
const generatedDirRel = path.join(serverDirRel, "generated");
|
|
71
|
+
writeFileIfAbsent(path.join(cwd, generatedDirRel, "contract.json"), "{}\n");
|
|
72
|
+
writeFileIfAbsent(path.join(cwd, generatedDirRel, "index.ts"), CONTRACT_BARREL);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Merge Kizlo wiring into files the project already owns (the root layout's SEO exports). Never
|
|
76
|
+
* aborts: the target is resolved by identity (the layout that renders `<html>`), and on any doubt —
|
|
77
|
+
* not found, not parseable — the resolved payload is printed at the end with placement instructions
|
|
78
|
+
* rather than written to a guessed-at file. A confident apply is an idempotent upsert: it replaces
|
|
79
|
+
* our exports if present, adds them if not.
|
|
80
|
+
*/
|
|
81
|
+
function applyLayoutPatches(cwd, manifest, scaffold) {
|
|
82
|
+
const manualSteps = [];
|
|
83
|
+
for (const entry of patchEntries(manifest)) {
|
|
84
|
+
const resolved = resolvePatch(entry, manifest.tokens, scaffold);
|
|
85
|
+
const target = findRootLayout(cwd, scaffold.appDir, resolved.relPath);
|
|
86
|
+
if (!target) {
|
|
87
|
+
manualSteps.push(resolved);
|
|
88
|
+
p.log.info(`Couldn't find your ${resolved.label} to wire Kizlo into — see the code to add below`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const relTarget = path.relative(cwd, target);
|
|
92
|
+
const src = fs.readFileSync(target, "utf8");
|
|
93
|
+
let applied;
|
|
94
|
+
try {
|
|
95
|
+
applied = applyPatchToSource(src, resolved);
|
|
96
|
+
} catch {
|
|
97
|
+
manualSteps.push(resolved);
|
|
98
|
+
p.log.warn(`Couldn't parse your ${resolved.label} (${relTarget}) — see the code to add below`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const { text, changes } = applied;
|
|
102
|
+
if (patchChanged(changes)) {
|
|
103
|
+
fs.writeFileSync(target, text);
|
|
104
|
+
const parts = [
|
|
105
|
+
...changes.replacedExports.length ? [`replaced ${changes.replacedExports.join(", ")}`] : [],
|
|
106
|
+
...changes.addedExports.length ? [`added ${changes.addedExports.join(", ")}`] : [],
|
|
107
|
+
...changes.addedImports.length ? ["imports"] : []
|
|
108
|
+
];
|
|
109
|
+
p.log.success(`Wired Kizlo into your ${resolved.label} (${relTarget}) — ${parts.join(", ")}`);
|
|
110
|
+
} else p.log.info(`Your ${resolved.label} is already wired (${relTarget})`);
|
|
111
|
+
}
|
|
112
|
+
for (const resolved of manualSteps) p.note(renderPatchCode(resolved), `Add these to your ${resolved.label} (the layout that renders <html>)`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
//#endregion
|
|
116
|
+
export { applyLayoutPatches, buildScaffoldContext, kizloConfigTemplate, reportScaffold, scaffoldFile, writeGeneratedContract };
|
|
@@ -1,65 +1,90 @@
|
|
|
1
1
|
import { dockerAvailable } from "../wp/docker.js";
|
|
2
|
-
import {
|
|
2
|
+
import { availablePackageManagers, detectInvokingPackageManager, ensureGitignored, frameworkCreateArgs, getVersion, installArgs, runCommandAsync, runCommandCaptured } from "../utils.js";
|
|
3
3
|
import { printBanner } from "../banner.js";
|
|
4
4
|
import { getPreset } from "../presets/index.js";
|
|
5
|
-
import {
|
|
5
|
+
import { fetchTemplate } from "../presets/source.js";
|
|
6
|
+
import { adaptOwnFile, ownEntries, readManifest } from "../presets/template.js";
|
|
7
|
+
import { collectConnectionInteractively, nextStepsLines, orCancel, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
|
|
8
|
+
import { applyLayoutPatches, buildScaffoldContext, kizloConfigTemplate, reportScaffold, scaffoldFile, writeGeneratedContract } from "./_wiring.js";
|
|
6
9
|
import z from "zod/v4";
|
|
7
10
|
import path from "node:path";
|
|
8
11
|
import fs from "node:fs";
|
|
9
12
|
import { defineCommand } from "citty";
|
|
10
13
|
import * as p from "@clack/prompts";
|
|
11
|
-
import { downloadTemplate } from "giget";
|
|
12
14
|
|
|
13
15
|
//#region src/cli/commands/create.ts
|
|
14
|
-
/** GitHub repo the full templates are fetched from, matching the release-tag scheme. */
|
|
15
|
-
const TEMPLATE_REPO = "github:kizlo-io/kizlo";
|
|
16
16
|
/**
|
|
17
17
|
* The templates `create` can scaffold from. Each id is both the `templates/<id>` folder and the
|
|
18
|
-
* preset that drives its WordPress setup.
|
|
18
|
+
* preset that drives its WordPress setup and framework bootstrap.
|
|
19
19
|
*/
|
|
20
20
|
const TEMPLATES = ["nextjs"];
|
|
21
21
|
function isTemplate(value) {
|
|
22
22
|
return TEMPLATES.includes(value);
|
|
23
23
|
}
|
|
24
24
|
const projectName = validate(z.string().trim().min(1, "Required").refine((value) => !value.startsWith(".") && !value.includes("/") && !value.includes("\\"), "Enter a folder name (letters, numbers, dashes)"));
|
|
25
|
+
/** Package managers `create` can wire the getting-started steps for, in display order. */
|
|
26
|
+
const PACKAGE_MANAGERS = [
|
|
27
|
+
"pnpm",
|
|
28
|
+
"npm",
|
|
29
|
+
"yarn",
|
|
30
|
+
"bun"
|
|
31
|
+
];
|
|
25
32
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
33
|
+
* Pin the `kizlo` dependency in the freshly bootstrapped `package.json`. The template is
|
|
34
|
+
* authoritative for the version (`kizloVersion` in its manifest, stamped at release), falling back to
|
|
35
|
+
* the running CLI's version in-repo before the first stamp. The dependency is only recorded, not
|
|
36
|
+
* installed — the getting-started note tells the user to run install.
|
|
30
37
|
*/
|
|
31
|
-
function
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
function recordKizloDependency(dir, manifest) {
|
|
39
|
+
const pkgPath = path.join(dir, "package.json");
|
|
40
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
41
|
+
pkg.dependencies ??= {};
|
|
42
|
+
pkg.dependencies.kizlo = manifest.kizloVersion ?? `^${getVersion()}`;
|
|
43
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, " ")}\n`);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The argv `create` runs to bootstrap a fresh base app with the framework's own official CLI, built
|
|
47
|
+
* from the template manifest's `bootstrap` (initializer + flags) — the template is the single source
|
|
48
|
+
* of truth for the framework. The CLI owns only the package-manager mechanics: {@link frameworkCreateArgs}
|
|
49
|
+
* assembles the `<pm> create …` invocation, and the `{{pm}}` token in the flags is substituted with the
|
|
50
|
+
* chosen manager (e.g. `--use-{{pm}}` → `--use-pnpm`).
|
|
51
|
+
*/
|
|
52
|
+
function bootstrapArgs(manifest, pm, name) {
|
|
53
|
+
if (!manifest.bootstrap) return void 0;
|
|
54
|
+
const flags = manifest.bootstrap.flags.map((flag) => flag.replaceAll("{{pm}}", pm));
|
|
55
|
+
return frameworkCreateArgs(pm, manifest.bootstrap.initializer, name, flags);
|
|
35
56
|
}
|
|
36
57
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
58
|
+
* Layer Kizlo's wiring onto a freshly bootstrapped app in `dir`, from an already-fetched template
|
|
59
|
+
* directory + manifest. The framework CLI has produced the base project (package.json, config,
|
|
60
|
+
* tsconfig, the root layout); this drives the manifest — the same engine `init` uses — to record the
|
|
61
|
+
* `kizlo` dependency, write `kizlo.config.ts`, scaffold the manifest's own files (wiring plus the demo
|
|
62
|
+
* starter pages), seed the generated contract, ignore `.env`, and patch the root layout's SEO exports.
|
|
63
|
+
* The target directory layout comes straight from the manifest tokens, which the bootstrap flags are
|
|
64
|
+
* chosen to match, so files land where the manifest expects. Fresh files are written with `force`,
|
|
65
|
+
* silently replacing the framework defaults.
|
|
40
66
|
*/
|
|
41
|
-
async function
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
forceClean: true
|
|
67
|
+
async function applyManifestWiring(dir, templateDir, manifest, opts) {
|
|
68
|
+
const { kizloDir, appDir, alias } = manifest.tokens;
|
|
69
|
+
const scaffold = buildScaffoldContext(dir, {
|
|
70
|
+
dirRel: kizloDir,
|
|
71
|
+
appDir,
|
|
72
|
+
alias,
|
|
73
|
+
clientUrl: void 0
|
|
49
74
|
});
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
75
|
+
recordKizloDependency(dir, manifest);
|
|
76
|
+
const files = [{
|
|
77
|
+
label: "Kizlo config",
|
|
78
|
+
relPath: "kizlo.config.ts",
|
|
79
|
+
contents: kizloConfigTemplate(kizloDir, alias, opts.devPath)
|
|
80
|
+
}, ...ownEntries(manifest, { includeStarter: true }).map((entry) => adaptOwnFile(templateDir, entry, manifest.tokens, scaffold))];
|
|
81
|
+
for (const file of files) reportScaffold(file, await scaffoldFile(dir, file, {
|
|
82
|
+
force: true,
|
|
83
|
+
yes: false
|
|
84
|
+
}), false);
|
|
85
|
+
writeGeneratedContract(dir, path.join(kizloDir, "server"));
|
|
86
|
+
ensureGitignored(dir, ".env");
|
|
87
|
+
applyLayoutPatches(dir, manifest, scaffold);
|
|
63
88
|
}
|
|
64
89
|
const create = defineCommand({
|
|
65
90
|
meta: {
|
|
@@ -119,21 +144,60 @@ const create = defineCommand({
|
|
|
119
144
|
p.cancel(`No preset for template "${template}".`);
|
|
120
145
|
process.exit(1);
|
|
121
146
|
}
|
|
147
|
+
const installed = availablePackageManagers(PACKAGE_MANAGERS);
|
|
148
|
+
const invokingPm = detectInvokingPackageManager();
|
|
149
|
+
const defaultPm = invokingPm && installed.includes(invokingPm) ? invokingPm : installed[0];
|
|
150
|
+
const pm = orCancel(await p.select({
|
|
151
|
+
message: "Package manager",
|
|
152
|
+
options: installed.map((id) => ({
|
|
153
|
+
value: id,
|
|
154
|
+
label: id
|
|
155
|
+
})),
|
|
156
|
+
initialValue: defaultPm
|
|
157
|
+
}));
|
|
122
158
|
const conn = await collectConnectionInteractively(preset);
|
|
123
159
|
if (preset.apiPath && conn.baseUrl) conn.baseUrl = withApiPath(conn.baseUrl, preset.apiPath);
|
|
124
160
|
if (conn.mode === "local" && !await dockerAvailable()) {
|
|
125
161
|
p.cancel("Docker isn't available — start Docker (or install it) and re-run, or choose “Use my own WordPress”.");
|
|
126
162
|
process.exit(1);
|
|
127
163
|
}
|
|
164
|
+
const fetched = await fetchTemplate(template);
|
|
165
|
+
const fail = (message) => {
|
|
166
|
+
fetched.cleanup();
|
|
167
|
+
p.cancel(message);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
};
|
|
170
|
+
const manifest = readManifest(fetched.dir);
|
|
171
|
+
const bootstrap = bootstrapArgs(manifest, pm, name);
|
|
172
|
+
if (!bootstrap) {
|
|
173
|
+
fetched.cleanup();
|
|
174
|
+
p.cancel(`Template "${template}" can't be scaffolded — its manifest declares no framework bootstrap.`);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
128
177
|
const s = p.spinner();
|
|
129
|
-
s.start(`Creating ${name}
|
|
178
|
+
s.start(`Creating ${name} with the ${preset.label} CLI`);
|
|
179
|
+
const scaffold = runCommandCaptured(bootstrap, cwd);
|
|
180
|
+
s.stop(scaffold.ok ? `Created ${name} with the ${preset.label} CLI` : `${preset.label} setup failed`);
|
|
181
|
+
if (!scaffold.ok) {
|
|
182
|
+
if (scaffold.output) p.log.error(scaffold.output);
|
|
183
|
+
fail(`${preset.label} setup failed — see the output above and try again.`);
|
|
184
|
+
}
|
|
130
185
|
try {
|
|
131
|
-
await
|
|
132
|
-
s.stop(`Created ${name}`);
|
|
186
|
+
await applyManifestWiring(dir, fetched.dir, manifest, { devPath: conn.devPath });
|
|
133
187
|
} catch (error) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
188
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
189
|
+
}
|
|
190
|
+
fetched.cleanup();
|
|
191
|
+
let depsInstalled = false;
|
|
192
|
+
if (orCancel(await p.confirm({
|
|
193
|
+
message: "Install dependencies now?",
|
|
194
|
+
initialValue: true
|
|
195
|
+
}))) {
|
|
196
|
+
const is = p.spinner();
|
|
197
|
+
is.start(`Installing dependencies with ${pm}`);
|
|
198
|
+
depsInstalled = await runCommandAsync(installArgs(pm), dir, "ignore");
|
|
199
|
+
is.stop(depsInstalled ? "Installed dependencies" : "Could not install dependencies");
|
|
200
|
+
if (!depsInstalled) p.log.warn(`Install them yourself: cd ${name} && ${installArgs(pm).join(" ")}`);
|
|
137
201
|
}
|
|
138
202
|
await setupLocalWordPress(dir, conn);
|
|
139
203
|
await writeEnv(dir, preset, conn, {
|
|
@@ -141,9 +205,12 @@ const create = defineCommand({
|
|
|
141
205
|
yes: false
|
|
142
206
|
});
|
|
143
207
|
await syncRemote(conn);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
208
|
+
p.note([
|
|
209
|
+
`cd ${name}`,
|
|
210
|
+
...depsInstalled ? [] : [`${pm} install`],
|
|
211
|
+
``,
|
|
212
|
+
...nextStepsLines(conn.mode)
|
|
213
|
+
].join("\n"), "Next steps");
|
|
147
214
|
p.outro("Kizlo is ready 🎉");
|
|
148
215
|
}
|
|
149
216
|
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { dockerAvailable } from "../wp/docker.js";
|
|
2
|
-
import { addDependencyArgs, detectImportAlias, detectPackageManager, ensureGitignored, getVersion, loadEnvFiles,
|
|
2
|
+
import { addDependencyArgs, detectImportAlias, detectPackageManager, ensureGitignored, getVersion, loadEnvFiles, runCommand } from "../utils.js";
|
|
3
3
|
import { printBanner } from "../banner.js";
|
|
4
4
|
import { PRESETS, detectPreset, getPreset } from "../presets/index.js";
|
|
5
|
+
import { fetchTemplate } from "../presets/source.js";
|
|
6
|
+
import { adaptOwnFile, ownEntries, readManifest } from "../presets/template.js";
|
|
5
7
|
import { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsNote, orCancel, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
|
|
6
|
-
import {
|
|
7
|
-
import { applyPatchToSource, patchChanged, resolvePatch, resolvePatchTargetPath, templatePatches } from "../presets/patch.js";
|
|
8
|
+
import { applyLayoutPatches, buildScaffoldContext, kizloConfigTemplate, reportScaffold, scaffoldFile, writeGeneratedContract } from "./_wiring.js";
|
|
8
9
|
import path from "node:path";
|
|
9
10
|
import fs from "node:fs";
|
|
10
11
|
import { defineCommand } from "citty";
|
|
@@ -18,14 +19,6 @@ function defaultDir(hasSrcDir) {
|
|
|
18
19
|
function aliasWithSlash(alias) {
|
|
19
20
|
return alias ? `${alias.replace(/\/+$/, "")}/` : "";
|
|
20
21
|
}
|
|
21
|
-
function kizloConfigTemplate(dir, alias, devPath) {
|
|
22
|
-
return `import { defineConfig } from "kizlo/config"
|
|
23
|
-
|
|
24
|
-
export default defineConfig({
|
|
25
|
-
dir: "${dir}",${alias ? `\n\talias: "${alias}",` : ""}${devPath ? `\n\tdev: { path: "${devPath}" },` : ""}
|
|
26
|
-
})
|
|
27
|
-
`;
|
|
28
|
-
}
|
|
29
22
|
/** Whether two URLs share an origin (scheme + host + port); false when either can't be parsed. */
|
|
30
23
|
function sameOrigin(a, b) {
|
|
31
24
|
try {
|
|
@@ -39,35 +32,6 @@ function detectAppDir(cwd, hasSrcDir) {
|
|
|
39
32
|
if (fs.existsSync(path.join(cwd, "app"))) return "app";
|
|
40
33
|
return hasSrcDir ? "src/app" : "app";
|
|
41
34
|
}
|
|
42
|
-
/**
|
|
43
|
-
* The single overwrite policy for every scaffolded file: create it when absent, overwrite on
|
|
44
|
-
* `--force`, and otherwise ask before clobbering an existing file (keeping it when the user
|
|
45
|
-
* declines or when running non-interactively with `--yes`). Every scaffolded file routes through
|
|
46
|
-
* here rather than deciding for itself, so new files inherit the same behavior for free.
|
|
47
|
-
*/
|
|
48
|
-
async function scaffoldFile(cwd, file, opts) {
|
|
49
|
-
const absPath = path.join(cwd, file.relPath);
|
|
50
|
-
const existed = fs.existsSync(absPath);
|
|
51
|
-
if (existed) {
|
|
52
|
-
let overwrite = opts.force;
|
|
53
|
-
if (!opts.force && !opts.yes) {
|
|
54
|
-
p.log.warn(`${file.label} already exists at ${file.relPath}`);
|
|
55
|
-
overwrite = orCancel(await p.confirm({
|
|
56
|
-
message: "Overwrite it?",
|
|
57
|
-
initialValue: true
|
|
58
|
-
}));
|
|
59
|
-
}
|
|
60
|
-
if (!overwrite) return "kept";
|
|
61
|
-
}
|
|
62
|
-
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
|
63
|
-
fs.writeFileSync(absPath, file.contents);
|
|
64
|
-
return existed ? "overwritten" : "created";
|
|
65
|
-
}
|
|
66
|
-
/** Report a {@link scaffoldFile} outcome in init's usual voice. */
|
|
67
|
-
function reportScaffold(file, result, yes) {
|
|
68
|
-
if (result === "kept") p.log.info(`Kept existing ${file.label} (${file.relPath})${yes ? " — pass --force to overwrite" : ""}`);
|
|
69
|
-
else p.log.success(`${result === "overwritten" ? "Overwrote" : "Created"} ${file.label} (${file.relPath})`);
|
|
70
|
-
}
|
|
71
35
|
async function collectInteractively(ctx) {
|
|
72
36
|
const conn = await collectConnectionInteractively(ctx.preset);
|
|
73
37
|
const dir = orCancel(await p.text({
|
|
@@ -108,6 +72,41 @@ function collectFromEnv(ctx) {
|
|
|
108
72
|
function readPkg(pkgPath) {
|
|
109
73
|
return JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
110
74
|
}
|
|
75
|
+
function kizloDep(pkg) {
|
|
76
|
+
return pkg.dependencies?.kizlo ?? pkg.devDependencies?.kizlo;
|
|
77
|
+
}
|
|
78
|
+
/** A bare `x.y.z` from a version spec (range prefix and pre-release/build metadata dropped); undefined if none. */
|
|
79
|
+
function coerceVersion(spec) {
|
|
80
|
+
const match = /(\d+)\.(\d+)\.(\d+)/.exec(spec);
|
|
81
|
+
return match ? [
|
|
82
|
+
Number(match[1]),
|
|
83
|
+
Number(match[2]),
|
|
84
|
+
Number(match[3])
|
|
85
|
+
] : void 0;
|
|
86
|
+
}
|
|
87
|
+
/** Whether `have` is a strictly older release than `want`; false when either can't be compared. */
|
|
88
|
+
function isOlder(have, want) {
|
|
89
|
+
const a = coerceVersion(have);
|
|
90
|
+
const b = coerceVersion(want);
|
|
91
|
+
if (!a || !b) return false;
|
|
92
|
+
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] < b[i];
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The template is authoritative for the kizlo version: it declares `kizloVersion` in its manifest
|
|
97
|
+
* (stamped at release), falling back to the running CLI's version in-repo before the first stamp.
|
|
98
|
+
* When the project's kizlo is older, upgrade the project up to it and say so — a deliberate pin is
|
|
99
|
+
* never changed silently. Never downgrades.
|
|
100
|
+
*/
|
|
101
|
+
function alignKizloVersion(cwd, pm, pkg, manifest) {
|
|
102
|
+
const want = manifest.kizloVersion ?? `^${getVersion()}`;
|
|
103
|
+
const have = kizloDep(pkg);
|
|
104
|
+
if (!have || !isOlder(have, want)) return;
|
|
105
|
+
const s = p.spinner();
|
|
106
|
+
s.start(`Upgrading kizlo from ${have} to ${want}`);
|
|
107
|
+
const ok = runCommand(addDependencyArgs(pm, `kizlo@${want}`), cwd, "ignore");
|
|
108
|
+
s.stop(ok ? `Upgraded kizlo to ${want}` : `Could not upgrade kizlo automatically — install kizlo@${want} yourself`);
|
|
109
|
+
}
|
|
111
110
|
const init = defineCommand({
|
|
112
111
|
meta: {
|
|
113
112
|
name: "init",
|
|
@@ -168,6 +167,10 @@ const init = defineCommand({
|
|
|
168
167
|
preset = detectPreset(initCtx);
|
|
169
168
|
if (preset.id !== "base") p.log.success(`${preset.label} detected`);
|
|
170
169
|
}
|
|
170
|
+
if (preset.template === "nextjs" && !fs.existsSync(path.join(cwd, "app")) && !fs.existsSync(path.join(cwd, "src/app"))) {
|
|
171
|
+
p.cancel("Kizlo needs the Next.js App Router — no `app` or `src/app` directory found. The Pages Router isn't supported.");
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
171
174
|
if (yes) loadEnvFiles(cwd);
|
|
172
175
|
const setup = yes ? collectFromEnv({
|
|
173
176
|
cwd,
|
|
@@ -185,11 +188,12 @@ const init = defineCommand({
|
|
|
185
188
|
process.exit(1);
|
|
186
189
|
}
|
|
187
190
|
if (!hasKizlo) {
|
|
191
|
+
const spec = `kizlo@^${getVersion()}`;
|
|
188
192
|
const s = p.spinner();
|
|
189
193
|
s.start(`Installing kizlo with ${pm}`);
|
|
190
|
-
const ok = runCommand(addDependencyArgs(pm,
|
|
194
|
+
const ok = runCommand(addDependencyArgs(pm, spec), cwd, "ignore");
|
|
191
195
|
s.stop(ok ? "Installed kizlo" : "Could not install kizlo automatically");
|
|
192
|
-
if (!ok) p.log.warn(`Install it yourself: ${addDependencyArgs(pm,
|
|
196
|
+
if (!ok) p.log.warn(`Install it yourself: ${addDependencyArgs(pm, spec).join(" ")}`);
|
|
193
197
|
}
|
|
194
198
|
if (preset.apiPath && setup.baseUrl) setup.baseUrl = withApiPath(setup.baseUrl, preset.apiPath);
|
|
195
199
|
await setupLocalWordPress(cwd, setup);
|
|
@@ -201,20 +205,28 @@ const init = defineCommand({
|
|
|
201
205
|
const dirRel = setup.dir.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
202
206
|
const serverDirRel = path.join(dirRel, "server");
|
|
203
207
|
const clientUrl = setup.siteUrl && !sameOrigin(setup.siteUrl, setup.baseUrl) ? setup.baseUrl : void 0;
|
|
204
|
-
const scaffold = {
|
|
205
|
-
|
|
206
|
-
serverDirName: path.basename(serverDirRel),
|
|
207
|
-
serverEntryPath: path.join(serverDirRel, "index.ts"),
|
|
208
|
-
clientPath: path.join(dirRel, "client.ts"),
|
|
208
|
+
const scaffold = buildScaffoldContext(cwd, {
|
|
209
|
+
dirRel,
|
|
209
210
|
appDir: detectAppDir(cwd, hasSrcDir),
|
|
210
|
-
|
|
211
|
+
alias: setup.alias,
|
|
211
212
|
clientUrl
|
|
212
|
-
};
|
|
213
|
+
});
|
|
213
214
|
const files = [{
|
|
214
215
|
label: "Kizlo config",
|
|
215
216
|
relPath: "kizlo.config.ts",
|
|
216
217
|
contents: kizloConfigTemplate(dirRel, setup.alias, setup.devPath)
|
|
217
|
-
}
|
|
218
|
+
}];
|
|
219
|
+
let manifest;
|
|
220
|
+
if (preset.template) {
|
|
221
|
+
const fetched = await fetchTemplate(preset.template);
|
|
222
|
+
try {
|
|
223
|
+
manifest = readManifest(fetched.dir);
|
|
224
|
+
alignKizloVersion(cwd, pm, pkg, manifest);
|
|
225
|
+
for (const entry of ownEntries(manifest)) files.push(adaptOwnFile(fetched.dir, entry, manifest.tokens, scaffold));
|
|
226
|
+
} finally {
|
|
227
|
+
fetched.cleanup();
|
|
228
|
+
}
|
|
229
|
+
} else if (preset.scaffolds) files.push(...preset.scaffolds(scaffold));
|
|
218
230
|
const scaffolded = [];
|
|
219
231
|
for (const file of files) scaffolded.push({
|
|
220
232
|
file,
|
|
@@ -223,47 +235,10 @@ const init = defineCommand({
|
|
|
223
235
|
yes
|
|
224
236
|
})
|
|
225
237
|
});
|
|
226
|
-
|
|
227
|
-
writeFileIfAbsent(path.join(cwd, generatedDirRel, "contract.json"), "{}\n");
|
|
228
|
-
writeFileIfAbsent(path.join(cwd, generatedDirRel, "index.ts"), CONTRACT_BARREL);
|
|
238
|
+
writeGeneratedContract(cwd, serverDirRel);
|
|
229
239
|
const gitignore = ensureGitignored(cwd, ".env");
|
|
230
240
|
for (const { file, result } of scaffolded) reportScaffold(file, result, yes);
|
|
231
|
-
|
|
232
|
-
const resolved = resolvePatch(patch, scaffold);
|
|
233
|
-
const abs = resolvePatchTargetPath(cwd, resolved.relPath);
|
|
234
|
-
if (!abs) {
|
|
235
|
-
p.log.info(`Skipped ${resolved.label} wiring — ${resolved.relPath} not found (add it yourself)`);
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
const relTarget = path.relative(cwd, abs);
|
|
239
|
-
const src = fs.readFileSync(abs, "utf8");
|
|
240
|
-
let text;
|
|
241
|
-
let changes;
|
|
242
|
-
try {
|
|
243
|
-
({text, changes} = applyPatchToSource(src, resolved, { replaceConflicts: force }));
|
|
244
|
-
} catch (err) {
|
|
245
|
-
p.log.error(`Could not parse ${resolved.label} (${relTarget}) to wire Kizlo into it: ${err instanceof Error ? err.message : String(err)}`);
|
|
246
|
-
throw err;
|
|
247
|
-
}
|
|
248
|
-
if (!force && !yes && changes.keptExports.length) {
|
|
249
|
-
p.log.warn(`${resolved.label} (${relTarget}) already defines ${changes.keptExports.join(", ")}`);
|
|
250
|
-
if (orCancel(await p.confirm({
|
|
251
|
-
message: "Replace with Kizlo's version?",
|
|
252
|
-
initialValue: true
|
|
253
|
-
}))) ({text, changes} = applyPatchToSource(src, resolved, { replaceConflicts: true }));
|
|
254
|
-
}
|
|
255
|
-
if (patchChanged(changes)) {
|
|
256
|
-
fs.writeFileSync(abs, text);
|
|
257
|
-
const parts = [
|
|
258
|
-
...changes.replacedExports.length ? [`replaced ${changes.replacedExports.join(", ")}`] : [],
|
|
259
|
-
...changes.addedExports.length ? [`added ${changes.addedExports.join(", ")}`] : [],
|
|
260
|
-
...changes.addedImports.length ? ["imports"] : []
|
|
261
|
-
];
|
|
262
|
-
p.log.success(`Wired Kizlo into your ${resolved.label} (${relTarget}) — ${parts.join(", ")}`);
|
|
263
|
-
}
|
|
264
|
-
if (changes.keptExports.length) p.log.info(`Kept your existing ${changes.keptExports.join(", ")} in ${resolved.label} — pass --force to replace`);
|
|
265
|
-
else if (!patchChanged(changes)) p.log.info(`Your ${resolved.label} is already wired (${relTarget})`);
|
|
266
|
-
}
|
|
241
|
+
if (manifest) applyLayoutPatches(cwd, manifest, scaffold);
|
|
267
242
|
if (gitignore !== "present") p.log.success(`${gitignore === "created" ? "Created" : "Updated"} .gitignore (ignoring .env)`);
|
|
268
243
|
nextStepsNote(setup.mode);
|
|
269
244
|
p.outro("Kizlo is ready 🎉");
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { materializeTemplate } from "./template.js";
|
|
2
|
-
|
|
3
1
|
//#region src/cli/presets/nextjs.ts
|
|
4
2
|
const nextjs = {
|
|
5
3
|
id: "nextjs",
|
|
@@ -12,9 +10,7 @@ const nextjs = {
|
|
|
12
10
|
},
|
|
13
11
|
baseUrlEnvKey: "NEXT_PUBLIC_KIZLO_BACKEND_URL",
|
|
14
12
|
apiPath: "/api/kizlo",
|
|
15
|
-
|
|
16
|
-
return materializeTemplate("nextjs", ctx);
|
|
17
|
-
}
|
|
13
|
+
template: "nextjs"
|
|
18
14
|
};
|
|
19
15
|
|
|
20
16
|
//#endregion
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { TEMPLATE_PATCHES } from "./generated/templates.js";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
import fs from "node:fs";
|
|
4
3
|
import { builders, detectCodeFormat, generateCode, parseModule } from "magicast";
|
|
@@ -28,26 +27,7 @@ function resolvePatchTargetPath(cwd, relPath) {
|
|
|
28
27
|
if (fs.existsSync(candidate)) return candidate;
|
|
29
28
|
}
|
|
30
29
|
}
|
|
31
|
-
/**
|
|
32
|
-
function templatePatches(framework) {
|
|
33
|
-
return TEMPLATE_PATCHES[framework] ?? [];
|
|
34
|
-
}
|
|
35
|
-
/** Substitute the path/import tokens for a patch against the project's real directories. */
|
|
36
|
-
function resolvePatch(patch, ctx) {
|
|
37
|
-
const relPath = patch.tokenizedFile.replaceAll("{{kizloDir}}", ctx.kizloDir).replaceAll("{{appDir}}", ctx.appDir);
|
|
38
|
-
const fromDir = path.posix.dirname(relPath);
|
|
39
|
-
const imports = patch.imports.map((imp) => ({
|
|
40
|
-
module: imp.module.replaceAll("{{serverImport}}", ctx.serverImport(fromDir)),
|
|
41
|
-
names: imp.names
|
|
42
|
-
}));
|
|
43
|
-
return {
|
|
44
|
-
label: patch.label,
|
|
45
|
-
relPath,
|
|
46
|
-
imports,
|
|
47
|
-
exports: patch.exports
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
/** Whether a patch actually modified the file (vs. a no-op / kept outcome). */
|
|
30
|
+
/** Whether a patch actually modified the file (vs. a no-op / already-wired outcome). */
|
|
51
31
|
function patchChanged(changes) {
|
|
52
32
|
return Boolean(changes.addedImports.length || changes.addedExports.length || changes.replacedExports.length);
|
|
53
33
|
}
|
|
@@ -69,15 +49,18 @@ function sameExpr(a, b) {
|
|
|
69
49
|
}
|
|
70
50
|
/**
|
|
71
51
|
* Merge a resolved patch into `source` by parsing it with magicast (a real TS/JS/JSX parser via Babel,
|
|
72
|
-
* so declarations are bounded exactly — never by a text heuristic).
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
52
|
+
* so declarations are bounded exactly — never by a text heuristic). Missing imports are added; each
|
|
53
|
+
* export is upserted — set with `builders`, which replaces it if present and adds it if not — and a
|
|
54
|
+
* static Next counterpart (`metadata` for `generateMetadata`) is dropped as part of a replace. The
|
|
55
|
+
* upsert is deliberate replace-always: we cannot distinguish a user's own `generateMetadata` from a
|
|
56
|
+
* stale copy of ours, and those values come from WordPress (edited in wp-admin, not the file), so the
|
|
57
|
+
* export body isn't meant to be hand-customized. Idempotent: an export already equal to ours is left
|
|
58
|
+
* untouched, so re-running never churns the file.
|
|
76
59
|
*
|
|
77
|
-
* Throws if the target cannot be parsed or regenerated. Callers must NOT swallow that — a file we
|
|
78
|
-
* parse is one we must not guess at, so wiring
|
|
60
|
+
* Throws if the target cannot be parsed or regenerated. Callers must NOT swallow that — a file we
|
|
61
|
+
* can't parse is one we must not guess at, so wiring falls back to a printed instruction instead.
|
|
79
62
|
*/
|
|
80
|
-
function applyPatchToSource(source, patch
|
|
63
|
+
function applyPatchToSource(source, patch) {
|
|
81
64
|
const format = detectCodeFormat(source);
|
|
82
65
|
const mod = parseModule(source, {
|
|
83
66
|
sourceFileName: "layout.tsx",
|
|
@@ -86,8 +69,7 @@ function applyPatchToSource(source, patch, opts) {
|
|
|
86
69
|
const changes = {
|
|
87
70
|
addedImports: [],
|
|
88
71
|
addedExports: [],
|
|
89
|
-
replacedExports: []
|
|
90
|
-
keptExports: []
|
|
72
|
+
replacedExports: []
|
|
91
73
|
};
|
|
92
74
|
for (const imp of patch.imports) for (const name of imp.names) {
|
|
93
75
|
if (mod.imports.$items.some((item) => item.from === imp.module && item.imported === name)) continue;
|
|
@@ -101,20 +83,12 @@ function applyPatchToSource(source, patch, opts) {
|
|
|
101
83
|
const exportNames = Object.keys(mod.exports);
|
|
102
84
|
if (exportNames.includes(exp.name)) {
|
|
103
85
|
if (sameExpr(generateCode(mod.exports[exp.name]).code, exp.value)) continue;
|
|
104
|
-
if (!opts.replaceConflicts) {
|
|
105
|
-
changes.keptExports.push(exp.name);
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
86
|
mod.exports[exp.name] = builders.raw(exp.value);
|
|
109
87
|
changes.replacedExports.push(exp.name);
|
|
110
88
|
continue;
|
|
111
89
|
}
|
|
112
90
|
const counterpart = staticCounterpart(exp.name);
|
|
113
91
|
if (counterpart && exportNames.includes(counterpart)) {
|
|
114
|
-
if (!opts.replaceConflicts) {
|
|
115
|
-
changes.keptExports.push(exp.name);
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
92
|
delete mod.exports[counterpart];
|
|
119
93
|
mod.exports[exp.name] = builders.raw(exp.value);
|
|
120
94
|
changes.replacedExports.push(exp.name);
|
|
@@ -128,6 +102,20 @@ function applyPatchToSource(source, patch, opts) {
|
|
|
128
102
|
changes
|
|
129
103
|
};
|
|
130
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Render a resolved patch as source the user can paste in, for the printed fallback when the target
|
|
107
|
+
* can't be resolved or parsed. The same declared payload the confident apply upserts, so what we tell
|
|
108
|
+
* the user to add never drifts from what we would have written.
|
|
109
|
+
*/
|
|
110
|
+
function renderPatchCode(patch) {
|
|
111
|
+
const imports = patch.imports.map((imp) => `import { ${imp.names.join(", ")} } from "${imp.module}"`);
|
|
112
|
+
const exports = patch.exports.map((exp) => `export const ${exp.name} = ${exp.value}`);
|
|
113
|
+
return `${[
|
|
114
|
+
...imports,
|
|
115
|
+
"",
|
|
116
|
+
...exports
|
|
117
|
+
].join("\n")}\n`;
|
|
118
|
+
}
|
|
131
119
|
|
|
132
120
|
//#endregion
|
|
133
|
-
export { applyPatchToSource, patchChanged,
|
|
121
|
+
export { applyPatchToSource, patchChanged, renderPatchCode, resolvePatchTargetPath };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { downloadTemplate } from "giget";
|
|
5
|
+
|
|
6
|
+
//#region src/cli/presets/source.ts
|
|
7
|
+
/** GitHub repo the templates are fetched from. */
|
|
8
|
+
const TEMPLATE_REPO = "github:kizlo-io/kizlo";
|
|
9
|
+
/**
|
|
10
|
+
* Resolve where a template is fetched from. Defaults to the templates on the repo's default branch:
|
|
11
|
+
* the template is decoupled from the CLI version — it declares its own `kizlo` dependency — so there
|
|
12
|
+
* is no tag to match and no release to cut. `KIZLO_TEMPLATE_LOCAL_DIR` points at a local `templates/`
|
|
13
|
+
* directory so development (and `create`/`init` tests) never touch the network.
|
|
14
|
+
*/
|
|
15
|
+
function templateSource(template) {
|
|
16
|
+
const localDir = process.env.KIZLO_TEMPLATE_LOCAL_DIR;
|
|
17
|
+
if (localDir) return { local: path.resolve(localDir, template) };
|
|
18
|
+
return { remote: `${TEMPLATE_REPO}/templates/${template}` };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Get the template as a readable directory both `create` and `init` can copy from or read. With
|
|
22
|
+
* `KIZLO_TEMPLATE_LOCAL_DIR` set it's the local folder (returned as-is, cleanup is a no-op). Otherwise
|
|
23
|
+
* it's downloaded once into a fresh temp directory the caller cleans up. The single fetch path shared
|
|
24
|
+
* by both commands, so they can never source different bytes.
|
|
25
|
+
*/
|
|
26
|
+
async function fetchTemplate(template) {
|
|
27
|
+
const source = templateSource(template);
|
|
28
|
+
if (source.local) {
|
|
29
|
+
if (!fs.existsSync(source.local)) throw new Error(`Local template not found: ${source.local}`);
|
|
30
|
+
return {
|
|
31
|
+
dir: source.local,
|
|
32
|
+
cleanup: () => {}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "kizlo-template-"));
|
|
36
|
+
await downloadTemplate(source.remote, {
|
|
37
|
+
dir,
|
|
38
|
+
forceClean: true
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
dir,
|
|
42
|
+
cleanup: () => fs.rmSync(dir, {
|
|
43
|
+
recursive: true,
|
|
44
|
+
force: true
|
|
45
|
+
})
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
//#endregion
|
|
50
|
+
export { fetchTemplate };
|
|
@@ -1,8 +1,59 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolvePatchTargetPath } from "./patch.js";
|
|
2
|
+
import z from "zod/v4";
|
|
2
3
|
import path from "node:path";
|
|
4
|
+
import fs from "node:fs";
|
|
3
5
|
|
|
4
6
|
//#region src/cli/presets/template.ts
|
|
5
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* The template's `template.json`. It is the single declaration of which files Kizlo lays onto a
|
|
9
|
+
* project and how each is applied — the same engine drives both `create` (onto a fresh app the
|
|
10
|
+
* framework CLI just bootstrapped) and `init` (onto the user's existing project). Every entry is one
|
|
11
|
+
* of a small closed set — deliberately no "patch an arbitrary source file" kind, so a dangerous
|
|
12
|
+
* strategy is unrepresentable:
|
|
13
|
+
*
|
|
14
|
+
* - `own` — a whole file Kizlo writes, adapted to the project's directories and written through the
|
|
15
|
+
* overwrite policy. `wiring`-scope files (the default) are laid down by both commands; `starter`
|
|
16
|
+
* demo files are laid down only by `create` (see {@link ownEntries}).
|
|
17
|
+
* - `patch` — a partial injection into a file the framework owns (the root layout's SEO exports).
|
|
18
|
+
* Both commands try a confident apply, otherwise print the payload with placement instructions.
|
|
19
|
+
*/
|
|
20
|
+
const tokensSchema = z.object({
|
|
21
|
+
kizloDir: z.string(),
|
|
22
|
+
appDir: z.string(),
|
|
23
|
+
alias: z.string()
|
|
24
|
+
});
|
|
25
|
+
const ownSchema = z.object({
|
|
26
|
+
kind: z.literal("own"),
|
|
27
|
+
role: z.string(),
|
|
28
|
+
path: z.string(),
|
|
29
|
+
scope: z.enum(["wiring", "starter"]).optional()
|
|
30
|
+
});
|
|
31
|
+
const patchSchema = z.object({
|
|
32
|
+
kind: z.literal("patch"),
|
|
33
|
+
role: z.string(),
|
|
34
|
+
label: z.string(),
|
|
35
|
+
path: z.string(),
|
|
36
|
+
imports: z.array(z.object({
|
|
37
|
+
module: z.string(),
|
|
38
|
+
names: z.array(z.string())
|
|
39
|
+
})),
|
|
40
|
+
exports: z.array(z.object({
|
|
41
|
+
name: z.string(),
|
|
42
|
+
value: z.string()
|
|
43
|
+
}))
|
|
44
|
+
});
|
|
45
|
+
const bootstrapSchema = z.object({
|
|
46
|
+
initializer: z.string(),
|
|
47
|
+
flags: z.array(z.string()).default([])
|
|
48
|
+
});
|
|
49
|
+
const manifestSchema = z.object({
|
|
50
|
+
framework: z.string(),
|
|
51
|
+
kizloVersion: z.string().optional(),
|
|
52
|
+
bootstrap: bootstrapSchema.optional(),
|
|
53
|
+
tokens: tokensSchema,
|
|
54
|
+
files: z.array(z.discriminatedUnion("kind", [ownSchema, patchSchema]))
|
|
55
|
+
});
|
|
56
|
+
/** Role → the human label init uses in prompts and logs. */
|
|
6
57
|
const ROLE_LABELS = {
|
|
7
58
|
"server-entry": "Kizlo server instance",
|
|
8
59
|
client: "Browser client",
|
|
@@ -10,27 +61,112 @@ const ROLE_LABELS = {
|
|
|
10
61
|
robots: "robots.txt route",
|
|
11
62
|
sitemap: "sitemap route",
|
|
12
63
|
"sitemap-redirect": "sitemap.xml redirect route",
|
|
13
|
-
manifest: "web manifest route"
|
|
64
|
+
manifest: "web manifest route",
|
|
65
|
+
"home-page": "home page",
|
|
66
|
+
"blog-post": "blog post page",
|
|
67
|
+
styles: "global styles"
|
|
14
68
|
};
|
|
69
|
+
/** Read and validate the template's manifest from a fetched template directory. */
|
|
70
|
+
function readManifest(templateDir) {
|
|
71
|
+
const manifestPath = path.join(templateDir, "template.json");
|
|
72
|
+
if (!fs.existsSync(manifestPath)) throw new Error(`Template manifest not found at ${manifestPath}`);
|
|
73
|
+
return manifestSchema.parse(JSON.parse(fs.readFileSync(manifestPath, "utf8")));
|
|
74
|
+
}
|
|
15
75
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* each file's location (tsconfig alias or relative). The single source of these bodies is the
|
|
20
|
-
* template folder, so init and `create` can never drift.
|
|
76
|
+
* The `own` files to scaffold. By default only `wiring` files (the Kizlo plumbing) — this is what
|
|
77
|
+
* `init` lays onto an existing project, so it never touches the user's own pages. With
|
|
78
|
+
* `includeStarter`, demo `starter` files are added too, which is what `create` lays onto a fresh app.
|
|
21
79
|
*/
|
|
22
|
-
function
|
|
23
|
-
return
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
80
|
+
function ownEntries(manifest, opts = {}) {
|
|
81
|
+
return manifest.files.filter((file) => file.kind === "own" && (opts.includeStarter || file.scope !== "starter"));
|
|
82
|
+
}
|
|
83
|
+
function patchEntries(manifest) {
|
|
84
|
+
return manifest.files.filter((file) => file.kind === "patch");
|
|
85
|
+
}
|
|
86
|
+
/** The specifier the template's files use to reach the server entry, e.g. `@/lib/kizlo/server`. */
|
|
87
|
+
function serverImportSpecifier(tokens) {
|
|
88
|
+
const withoutSrc = tokens.kizloDir.replace(/^src\//, "");
|
|
89
|
+
return `${tokens.alias.replace(/\/+$/, "")}/${withoutSrc}/server`;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Adapt a template path prefix (`src/app/...`, `src/lib/kizlo/...`) to the project's real directories.
|
|
93
|
+
* Only the leading token dir is swapped; the rest of the path is preserved verbatim.
|
|
94
|
+
*/
|
|
95
|
+
function adaptTemplatePath(relPath, tokens, ctx) {
|
|
96
|
+
const normalized = relPath.split(path.sep).join("/");
|
|
97
|
+
if (normalized === tokens.appDir || normalized.startsWith(`${tokens.appDir}/`)) return `${ctx.appDir}${normalized.slice(tokens.appDir.length)}`;
|
|
98
|
+
if (normalized === tokens.kizloDir || normalized.startsWith(`${tokens.kizloDir}/`)) return `${ctx.kizloDir}${normalized.slice(tokens.kizloDir.length)}`;
|
|
99
|
+
return normalized;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Turn an `own` manifest entry into the {@link ScaffoldFile} init writes: read the template's real
|
|
103
|
+
* file, rewrite its path prefix to the project's directories, and swap the server-entry import
|
|
104
|
+
* specifier for the one resolved at the file's new location (tsconfig alias or relative). The swap is
|
|
105
|
+
* surgical — it replaces the exact specifier the template uses, not a loose directory substring — so
|
|
106
|
+
* an unrelated occurrence can't be corrupted. This is the job the old build-time extractor did; it
|
|
107
|
+
* moves to runtime and stays as precise.
|
|
108
|
+
*/
|
|
109
|
+
function adaptOwnFile(templateDir, entry, tokens, ctx) {
|
|
110
|
+
const abs = path.join(templateDir, entry.path);
|
|
111
|
+
if (!fs.existsSync(abs)) throw new Error(`Template file "${entry.path}" is listed in the manifest but does not exist in the template.`);
|
|
112
|
+
const body = fs.readFileSync(abs, "utf8");
|
|
113
|
+
const relPath = adaptTemplatePath(entry.path, tokens, ctx);
|
|
114
|
+
const fromDir = path.posix.dirname(relPath);
|
|
115
|
+
const contents = body.split(serverImportSpecifier(tokens)).join(ctx.serverImport(fromDir));
|
|
116
|
+
return {
|
|
117
|
+
label: ROLE_LABELS[entry.role] ?? entry.role,
|
|
118
|
+
relPath,
|
|
119
|
+
contents
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** Substitute the path/import tokens for a patch against the project's real directories. */
|
|
123
|
+
function resolvePatch(entry, tokens, ctx) {
|
|
124
|
+
const relPath = adaptTemplatePath(entry.path, tokens, ctx);
|
|
125
|
+
const fromDir = path.posix.dirname(relPath);
|
|
126
|
+
const imports = entry.imports.map((imp) => ({
|
|
127
|
+
module: imp.module.replaceAll("{{serverImport}}", ctx.serverImport(fromDir)),
|
|
128
|
+
names: imp.names
|
|
129
|
+
}));
|
|
130
|
+
return {
|
|
131
|
+
label: entry.label,
|
|
132
|
+
relPath,
|
|
133
|
+
imports,
|
|
134
|
+
exports: entry.exports
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const LAYOUT_FILE = /^layout\.(tsx|jsx|ts|js|mjs|cjs)$/;
|
|
138
|
+
/** A root layout is the one that renders `<html>` — its identity, not its path. */
|
|
139
|
+
const RENDERS_HTML = /<html[\s/>]/;
|
|
140
|
+
/** Every file under `dir`, recursively (files only). Missing dir yields nothing. */
|
|
141
|
+
function walk(dir) {
|
|
142
|
+
if (!fs.existsSync(dir)) return [];
|
|
143
|
+
const out = [];
|
|
144
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
145
|
+
const full = path.join(dir, entry.name);
|
|
146
|
+
if (entry.isDirectory()) out.push(...walk(full));
|
|
147
|
+
else out.push(full);
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Resolve the root layout by identity, not path. Try the manifest hint first; if it exists and
|
|
153
|
+
* renders `<html>`, use it. Otherwise scan the App Router directory for a layout that renders `<html>`
|
|
154
|
+
* and use it only when there is exactly one. On zero, many, or an unreadable file, return undefined so
|
|
155
|
+
* the caller prints the wiring for the user to place. Never guesses.
|
|
156
|
+
*/
|
|
157
|
+
function findRootLayout(cwd, appDir, hintRelPath) {
|
|
158
|
+
const hint = resolvePatchTargetPath(cwd, hintRelPath);
|
|
159
|
+
if (hint && rendersHtml(hint)) return hint;
|
|
160
|
+
const matches = walk(path.join(cwd, appDir)).filter((file) => LAYOUT_FILE.test(path.basename(file)) && rendersHtml(file));
|
|
161
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
162
|
+
}
|
|
163
|
+
function rendersHtml(file) {
|
|
164
|
+
try {
|
|
165
|
+
return RENDERS_HTML.test(fs.readFileSync(file, "utf8"));
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
33
169
|
}
|
|
34
170
|
|
|
35
171
|
//#endregion
|
|
36
|
-
export {
|
|
172
|
+
export { adaptOwnFile, findRootLayout, ownEntries, patchEntries, readManifest, resolvePatch };
|
package/dist/cli/utils.js
CHANGED
|
@@ -2,7 +2,7 @@ import { log } from "./daemon/logger.js";
|
|
|
2
2
|
import { PortInUseError, resolveHostPort } from "./wp/ports.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
6
6
|
import fs from "node:fs";
|
|
7
7
|
import * as p from "@clack/prompts";
|
|
8
8
|
|
|
@@ -107,6 +107,31 @@ function detectPackageManager(cwd) {
|
|
|
107
107
|
if (fs.existsSync(path.join(cwd, "bun.lockb")) || fs.existsSync(path.join(cwd, "bun.lock"))) return "bun";
|
|
108
108
|
return "npm";
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* The package manager that invoked the current process, read from `npm_config_user_agent`
|
|
112
|
+
* (e.g. `pnpm/9.0.0 npm/? node/v20`). Used to pre-select the most likely choice when a fresh
|
|
113
|
+
* project has no lockfile to detect from. Returns undefined when the agent is absent or unknown.
|
|
114
|
+
*/
|
|
115
|
+
function detectInvokingPackageManager() {
|
|
116
|
+
const name = process.env.npm_config_user_agent?.split("/")[0];
|
|
117
|
+
return name === "pnpm" || name === "yarn" || name === "bun" || name === "npm" ? name : void 0;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Whether `command` resolves to a runnable executable, probed with `--version`. Used to hide or
|
|
121
|
+
* disable package-manager choices the user can't actually run. A non-zero exit or a spawn error
|
|
122
|
+
* (the binary isn't on PATH) both count as unavailable.
|
|
123
|
+
*/
|
|
124
|
+
function isCommandAvailable(command) {
|
|
125
|
+
const result = spawnSync(command, ["--version"], {
|
|
126
|
+
stdio: "ignore",
|
|
127
|
+
shell: process.platform === "win32"
|
|
128
|
+
});
|
|
129
|
+
return !result.error && result.status === 0;
|
|
130
|
+
}
|
|
131
|
+
/** The package managers currently installed on the host, in the given display order. */
|
|
132
|
+
function availablePackageManagers(order) {
|
|
133
|
+
return order.filter((pm) => isCommandAvailable(pm));
|
|
134
|
+
}
|
|
110
135
|
function addDependencyArgs(pm, pkg) {
|
|
111
136
|
if (pm === "npm") return [
|
|
112
137
|
"npm",
|
|
@@ -119,6 +144,42 @@ function addDependencyArgs(pm, pkg) {
|
|
|
119
144
|
pkg
|
|
120
145
|
];
|
|
121
146
|
}
|
|
147
|
+
/** The argv to install a project's dependencies with `pm` — every supported manager spells it `<pm> install`. */
|
|
148
|
+
function installArgs(pm) {
|
|
149
|
+
return [pm, "install"];
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The argv to run a framework's `create-*` initializer through a package manager, e.g.
|
|
153
|
+
* `pnpm create next-app@latest my-app --ts …`. The mechanics differ per manager: npm forwards
|
|
154
|
+
* flags to the initializer only after a `--` separator, and `yarn create` resolves the latest
|
|
155
|
+
* version itself so the `@latest` tag is dropped. `flags` are the initializer's own options.
|
|
156
|
+
*/
|
|
157
|
+
function frameworkCreateArgs(pm, initializer, name, flags) {
|
|
158
|
+
switch (pm) {
|
|
159
|
+
case "npm": return [
|
|
160
|
+
"npm",
|
|
161
|
+
"create",
|
|
162
|
+
initializer,
|
|
163
|
+
name,
|
|
164
|
+
"--",
|
|
165
|
+
...flags
|
|
166
|
+
];
|
|
167
|
+
case "yarn": return [
|
|
168
|
+
"yarn",
|
|
169
|
+
"create",
|
|
170
|
+
initializer.replace(/@latest$/, ""),
|
|
171
|
+
name,
|
|
172
|
+
...flags
|
|
173
|
+
];
|
|
174
|
+
default: return [
|
|
175
|
+
pm,
|
|
176
|
+
"create",
|
|
177
|
+
initializer,
|
|
178
|
+
name,
|
|
179
|
+
...flags
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
122
183
|
function runCommand(args, cwd, stdio = "inherit") {
|
|
123
184
|
const [command, ...rest] = args;
|
|
124
185
|
if (!command) return false;
|
|
@@ -128,6 +189,45 @@ function runCommand(args, cwd, stdio = "inherit") {
|
|
|
128
189
|
shell: process.platform === "win32"
|
|
129
190
|
}).status === 0;
|
|
130
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Async sibling of {@link runCommand}: runs a command without blocking the event loop, so a clack
|
|
194
|
+
* spinner can keep animating while it works (a synchronous `spawnSync` would freeze it on the first
|
|
195
|
+
* frame). Resolves whether it exited cleanly; a spawn error (binary off PATH) resolves false.
|
|
196
|
+
*/
|
|
197
|
+
function runCommandAsync(args, cwd, stdio = "inherit") {
|
|
198
|
+
const [command, ...rest] = args;
|
|
199
|
+
if (!command) return Promise.resolve(false);
|
|
200
|
+
return new Promise((resolve$1) => {
|
|
201
|
+
const child = spawn(command, rest, {
|
|
202
|
+
cwd,
|
|
203
|
+
stdio,
|
|
204
|
+
shell: process.platform === "win32"
|
|
205
|
+
});
|
|
206
|
+
child.on("error", () => resolve$1(false));
|
|
207
|
+
child.on("close", (code) => resolve$1(code === 0));
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Run a command capturing its combined output instead of streaming it, for a step that should stay
|
|
212
|
+
* quiet on success but whose logs are worth surfacing when it fails (e.g. the framework's scaffolder
|
|
213
|
+
* behind a spinner). Returns whether it succeeded and the trimmed stdout+stderr.
|
|
214
|
+
*/
|
|
215
|
+
function runCommandCaptured(args, cwd) {
|
|
216
|
+
const [command, ...rest] = args;
|
|
217
|
+
if (!command) return {
|
|
218
|
+
ok: false,
|
|
219
|
+
output: ""
|
|
220
|
+
};
|
|
221
|
+
const result = spawnSync(command, rest, {
|
|
222
|
+
cwd,
|
|
223
|
+
encoding: "utf8",
|
|
224
|
+
shell: process.platform === "win32"
|
|
225
|
+
});
|
|
226
|
+
return {
|
|
227
|
+
ok: result.status === 0,
|
|
228
|
+
output: `${result.stdout ?? ""}${result.stderr ?? ""}`.trim()
|
|
229
|
+
};
|
|
230
|
+
}
|
|
131
231
|
function writeFileIfAbsent(filePath, contents) {
|
|
132
232
|
if (fs.existsSync(filePath)) return false;
|
|
133
233
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
@@ -263,4 +363,4 @@ function groupDefault(subCommandNames, run) {
|
|
|
263
363
|
}
|
|
264
364
|
|
|
265
365
|
//#endregion
|
|
266
|
-
export { addDependencyArgs, detectImportAlias, detectPackageManager, ensureGitignored, envGroups, envKeysPresent, getVersion, groupDefault, loadEnvFiles, mergeEnv, pickStackPort, resolveModuleImport, runCommand, withSpinner, writeFileIfAbsent };
|
|
366
|
+
export { addDependencyArgs, availablePackageManagers, detectImportAlias, detectInvokingPackageManager, detectPackageManager, ensureGitignored, envGroups, envKeysPresent, frameworkCreateArgs, getVersion, groupDefault, installArgs, loadEnvFiles, mergeEnv, pickStackPort, resolveModuleImport, runCommand, runCommandAsync, runCommandCaptured, withSpinner, writeFileIfAbsent };
|
package/dist/cli/wp/dev.js
CHANGED
|
@@ -6,8 +6,8 @@ import { createAdminAppPassword, seedUsers } from "./bootstrap.js";
|
|
|
6
6
|
import { prepareByo } from "./byo.js";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { existsSync, rmSync } from "node:fs";
|
|
9
|
-
import { randomBytes } from "node:crypto";
|
|
10
9
|
import { networkInterfaces } from "node:os";
|
|
10
|
+
import { randomBytes } from "node:crypto";
|
|
11
11
|
|
|
12
12
|
//#region src/cli/wp/dev.ts
|
|
13
13
|
/** A strong random admin password (144 bits, URL/JSON/CLI-safe characters). */
|
package/dist/package.js
CHANGED
package/dist/router.d.ts
CHANGED
|
@@ -764,7 +764,7 @@ declare const ROUTER_MAP: {
|
|
|
764
764
|
exclude?: string | number | (string | number)[] | undefined;
|
|
765
765
|
include?: string | number | (string | number)[] | undefined;
|
|
766
766
|
order?: "asc" | "desc" | undefined;
|
|
767
|
-
orderBy?: "id" | "slug" | "name" | "description" | "include" | "include_slugs" | "term_group" |
|
|
767
|
+
orderBy?: "count" | "id" | "slug" | "name" | "description" | "include" | "include_slugs" | "term_group" | undefined;
|
|
768
768
|
hideEmpty?: string | boolean | undefined;
|
|
769
769
|
parent?: string | number | undefined;
|
|
770
770
|
post?: string | number | undefined;
|
|
@@ -976,7 +976,7 @@ declare const ROUTER_MAP: {
|
|
|
976
976
|
exclude?: string | number | (string | number)[] | undefined;
|
|
977
977
|
include?: string | number | (string | number)[] | undefined;
|
|
978
978
|
order?: "asc" | "desc" | undefined;
|
|
979
|
-
orderBy?: "id" | "slug" | "name" | "description" | "include" | "include_slugs" | "term_group" |
|
|
979
|
+
orderBy?: "count" | "id" | "slug" | "name" | "description" | "include" | "include_slugs" | "term_group" | undefined;
|
|
980
980
|
hideEmpty?: string | boolean | undefined;
|
|
981
981
|
post?: string | number | undefined;
|
|
982
982
|
slug?: string | string[] | undefined;
|
|
@@ -1135,7 +1135,7 @@ declare const ROUTER_MAP: {
|
|
|
1135
1135
|
sitemaps: {
|
|
1136
1136
|
list: Procedure<"internal", {}, {
|
|
1137
1137
|
key: string;
|
|
1138
|
-
type: "
|
|
1138
|
+
type: "post_type" | "taxonomy" | "author";
|
|
1139
1139
|
pages: number;
|
|
1140
1140
|
lastmod: string;
|
|
1141
1141
|
}[], DefinedErrorMapLike>;
|
|
@@ -1143,7 +1143,7 @@ declare const ROUTER_MAP: {
|
|
|
1143
1143
|
origin: string;
|
|
1144
1144
|
sitemaps: {
|
|
1145
1145
|
key: string;
|
|
1146
|
-
type: "
|
|
1146
|
+
type: "post_type" | "taxonomy" | "author";
|
|
1147
1147
|
pages: number;
|
|
1148
1148
|
lastmod: string;
|
|
1149
1149
|
}[];
|
package/dist/seo/schema.d.ts
CHANGED
|
@@ -108,17 +108,17 @@ declare const Seo: z.ZodObject<{
|
|
|
108
108
|
type Seo = z.infer<typeof Seo>;
|
|
109
109
|
declare const CONTENT_TYPES: readonly ["post_type", "taxonomy", "author"];
|
|
110
110
|
declare const ContentType: z.ZodEnum<{
|
|
111
|
-
author: "author";
|
|
112
111
|
post_type: "post_type";
|
|
113
112
|
taxonomy: "taxonomy";
|
|
113
|
+
author: "author";
|
|
114
114
|
}>;
|
|
115
115
|
type ContentType = z.infer<typeof ContentType>;
|
|
116
116
|
declare const Sitemap: z.ZodObject<{
|
|
117
117
|
key: z.ZodString;
|
|
118
118
|
type: z.ZodEnum<{
|
|
119
|
-
author: "author";
|
|
120
119
|
post_type: "post_type";
|
|
121
120
|
taxonomy: "taxonomy";
|
|
121
|
+
author: "author";
|
|
122
122
|
}>;
|
|
123
123
|
pages: z.ZodNumber;
|
|
124
124
|
lastmod: z.ZodString;
|
|
@@ -127,9 +127,9 @@ type Sitemap = z.infer<typeof Sitemap>;
|
|
|
127
127
|
declare const SitemapList: z.ZodArray<z.ZodObject<{
|
|
128
128
|
key: z.ZodString;
|
|
129
129
|
type: z.ZodEnum<{
|
|
130
|
-
author: "author";
|
|
131
130
|
post_type: "post_type";
|
|
132
131
|
taxonomy: "taxonomy";
|
|
132
|
+
author: "author";
|
|
133
133
|
}>;
|
|
134
134
|
pages: z.ZodNumber;
|
|
135
135
|
lastmod: z.ZodString;
|
|
@@ -140,9 +140,9 @@ declare const SitemapIndex: z.ZodObject<{
|
|
|
140
140
|
sitemaps: z.ZodArray<z.ZodObject<{
|
|
141
141
|
key: z.ZodString;
|
|
142
142
|
type: z.ZodEnum<{
|
|
143
|
-
author: "author";
|
|
144
143
|
post_type: "post_type";
|
|
145
144
|
taxonomy: "taxonomy";
|
|
145
|
+
author: "author";
|
|
146
146
|
}>;
|
|
147
147
|
pages: z.ZodNumber;
|
|
148
148
|
lastmod: z.ZodString;
|
|
@@ -174,9 +174,9 @@ declare const SitemapUrlList: z.ZodArray<z.ZodObject<{
|
|
|
174
174
|
type SitemapUrlList = z.infer<typeof SitemapUrlList>;
|
|
175
175
|
declare const ListSitemapUrlInputBase: z.ZodObject<{
|
|
176
176
|
type: z.ZodEnum<{
|
|
177
|
-
author: "author";
|
|
178
177
|
post_type: "post_type";
|
|
179
178
|
taxonomy: "taxonomy";
|
|
179
|
+
author: "author";
|
|
180
180
|
}>;
|
|
181
181
|
page: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodCoercedNumber<string | number>>>;
|
|
182
182
|
}, z.core.$strip>;
|
package/package.json
CHANGED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
//#region src/cli/presets/generated/templates.ts
|
|
2
|
-
const TEMPLATE_SUBSETS = { "nextjs": [
|
|
3
|
-
{
|
|
4
|
-
"role": "server-entry",
|
|
5
|
-
"tokenizedPath": "{{kizloDir}}/server/index.ts",
|
|
6
|
-
"body": "import { createKizlo } from \"kizlo/nextjs/server\"\n\nexport const { router, client, context, handler } = createKizlo()\n"
|
|
7
|
-
},
|
|
8
|
-
{
|
|
9
|
-
"role": "client",
|
|
10
|
-
"tokenizedPath": "{{kizloDir}}/client.ts",
|
|
11
|
-
"body": "import { createKizloClient } from \"kizlo/nextjs\"\nimport { contract } from \"./server/generated\"\n\nexport const client = createKizloClient(contract)\n"
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
"role": "api-route",
|
|
15
|
-
"tokenizedPath": "{{appDir}}/api/kizlo/[[...rest]]/route.ts",
|
|
16
|
-
"body": "import { handler } from \"{{serverImport}}\"\n\nexport { handler as GET, handler as POST, handler as PUT, handler as PATCH, handler as DELETE, handler as OPTIONS }\n"
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
"role": "robots",
|
|
20
|
-
"tokenizedPath": "{{appDir}}/robots.txt/route.ts",
|
|
21
|
-
"body": "import { createRobotsRoute } from \"kizlo/nextjs/server\"\nimport { client } from \"{{serverImport}}\"\n\n// Run on the edge: cheaper and faster than Node for a response this small.\nexport const runtime = \"edge\"\n\n// Vercel treats special metadata files like robots.txt and sitemap.xml as completely static\n// files at build time, bypassing normal Incremental Static Regeneration (ISR) and on-demand\n// revalidatePath rules.\n//\n// While your revalidation logic executes perfectly in a local Node.js server, Vercel uploads\n// these routes directly to its Edge CDN as immutable assets. Subsequent on-demand revalidation\n// triggers will successfully clear the Data Cache but fail to purge the CDN-level Edge cache\n// for that file.\n//\n// force-dynamic opts out of that static treatment so CMS edits show up; the WordPress call is\n// cached, so requests stay cheap.\nexport const dynamic = \"force-dynamic\"\n\nexport const GET = createRobotsRoute(client)\n"
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
"role": "sitemap",
|
|
25
|
-
"tokenizedPath": "{{appDir}}/sitemaps/[sitemap]/route.ts",
|
|
26
|
-
"body": "import { createSitemapRoute } from \"kizlo/nextjs/server\"\nimport { client } from \"{{serverImport}}\"\n\n// Run on the edge: cheaper and faster than Node for a response this small.\nexport const runtime = \"edge\"\n\n// Like robots.txt, Vercel bakes sitemap routes into immutable Edge CDN assets at build time,\n// bypassing ISR and on-demand revalidatePath. The index gets frozen with whatever WordPress\n// returned at build (often nothing, since the self-referential backend isn't reachable yet),\n// and revalidation can't purge that CDN copy.\n//\n// force-dynamic opts out of that static treatment. createSitemapRoute caches the WordPress calls\n// per sitemap and refreshes them on content changes (via revalidateTag), so requests stay cheap.\nexport const dynamic = \"force-dynamic\"\n\nexport const GET = createSitemapRoute(client)\n"
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
"role": "sitemap-redirect",
|
|
30
|
-
"tokenizedPath": "{{appDir}}/sitemap.xml/route.ts",
|
|
31
|
-
"body": "import { createSitemapRedirectRoute } from \"kizlo/nextjs/server\"\n\n// Many crawlers ignore robots.txt and probe the well-known /sitemap.xml directly. This route\n// permanently redirects (308) to the generated index at /sitemaps/index.xml. The response is a\n// fixed constant with no WordPress call or request input, so Next statically generates it at\n// build time (no runtime/edge function invoked per request).\nexport const GET = createSitemapRedirectRoute()\n"
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
"role": "manifest",
|
|
35
|
-
"tokenizedPath": "{{appDir}}/site.webmanifest/route.ts",
|
|
36
|
-
"body": "import { createManifestRoute } from \"kizlo/nextjs/server\"\nimport { client } from \"{{serverImport}}\"\n\n// Run on the edge: cheaper and faster than Node for a response this small.\nexport const runtime = \"edge\"\n\n// Like robots.txt, Vercel bakes this route into an immutable Edge CDN asset at build time,\n// bypassing ISR and on-demand revalidatePath. force-dynamic opts out of that static treatment\n// so brand-settings edits show up; createManifestRoute caches the WordPress call, so requests\n// stay cheap.\nexport const dynamic = \"force-dynamic\"\n\nexport const GET = createManifestRoute(client)\n"
|
|
37
|
-
}
|
|
38
|
-
] };
|
|
39
|
-
const TEMPLATE_PATCHES = { "nextjs": [{
|
|
40
|
-
"label": "root layout",
|
|
41
|
-
"tokenizedFile": "{{appDir}}/layout.tsx",
|
|
42
|
-
"imports": [{
|
|
43
|
-
"module": "kizlo/nextjs/server",
|
|
44
|
-
"names": ["createRootMetadata", "createRootViewport"]
|
|
45
|
-
}, {
|
|
46
|
-
"module": "{{serverImport}}",
|
|
47
|
-
"names": ["client"]
|
|
48
|
-
}],
|
|
49
|
-
"exports": [{
|
|
50
|
-
"name": "generateMetadata",
|
|
51
|
-
"value": "createRootMetadata(client)"
|
|
52
|
-
}, {
|
|
53
|
-
"name": "generateViewport",
|
|
54
|
-
"value": "createRootViewport(client)"
|
|
55
|
-
}]
|
|
56
|
-
}] };
|
|
57
|
-
|
|
58
|
-
//#endregion
|
|
59
|
-
export { TEMPLATE_PATCHES, TEMPLATE_SUBSETS };
|