kizlo 0.6.1 → 0.8.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/brand/icons.d.ts +10 -12
- package/dist/brand/icons.js +19 -17
- package/dist/category/error.js +14 -0
- package/dist/category/index.js +77 -0
- package/dist/category/schema.js +40 -0
- package/dist/category/utils.js +20 -0
- package/dist/cli/banner.js +1 -1
- package/dist/cli/commands/_setup.js +342 -0
- package/dist/cli/commands/create.js +146 -0
- package/dist/cli/commands/dev.js +26 -15
- package/dist/cli/commands/init.js +53 -300
- package/dist/cli/index.js +2 -1
- package/dist/cli/presets/generated/templates.js +59 -0
- package/dist/cli/presets/nextjs.js +3 -95
- package/dist/cli/presets/patch.js +133 -0
- package/dist/cli/presets/template.js +36 -0
- package/dist/cli/wp/dev.js +16 -1
- package/dist/comment/index.js +4 -4
- package/dist/context.d.ts +25 -8
- package/dist/context.js +15 -5
- package/dist/email/service.d.ts +4 -3
- package/dist/email/service.js +4 -4
- package/dist/index.d.ts +15 -11
- package/dist/index.js +10 -4
- package/dist/integrations/nextjs/metadata.js +7 -5
- package/dist/integrations/nextjs/sitemap.d.ts +8 -7
- package/dist/integrations/nextjs/sitemap.js +3 -3
- package/dist/menu/index.js +19 -14
- package/dist/menu/schema.d.ts +14 -0
- package/dist/menu/schema.js +35 -40
- package/dist/menu/utils.js +23 -14
- package/dist/nextjs/server.d.ts +2 -1
- package/dist/nextjs/server.js +2 -1
- package/dist/package.js +1 -1
- package/dist/page/error.js +20 -0
- package/dist/page/index.js +128 -0
- package/dist/page/schema.js +75 -0
- package/dist/page/utils.js +43 -0
- package/dist/post/error.js +4 -14
- package/dist/post/index.js +17 -9
- package/dist/post/schema.js +37 -66
- package/dist/post/utils.js +27 -24
- package/dist/router.d.ts +1006 -187
- package/dist/router.js +6 -0
- package/dist/seo/index.js +7 -44
- package/dist/seo/schema.d.ts +10 -6
- package/dist/seo/schema.js +7 -5
- package/dist/seo/sitemap.d.ts +9 -1
- package/dist/seo/sitemap.js +17 -1
- package/dist/seo/types.d.ts +17 -20
- package/dist/seo/utils.js +9 -7
- package/dist/settings/index.js +27 -24
- package/dist/settings/service.d.ts +14 -13
- package/dist/settings/service.interface.d.ts +2 -2
- package/dist/settings/service.js +9 -9
- package/dist/shared/middleware.d.ts +4 -4
- package/dist/shared/procedure.d.ts +6 -7
- package/dist/tag/error.js +14 -0
- package/dist/tag/index.js +76 -0
- package/dist/tag/schema.js +38 -0
- package/dist/tag/utils.js +19 -0
- package/dist/taxonomy/service.js +48 -0
- package/dist/test/fixtures.d.ts +1 -1
- package/dist/test/fixtures.js +1 -1
- package/dist/webhook/index.d.ts +2 -2
- package/dist/wordpress/category/service.d.ts +28 -0
- package/dist/wordpress/category/service.js +55 -0
- package/dist/wordpress/category/types.d.ts +120 -0
- package/dist/wordpress/category/types.js +15 -0
- package/dist/wordpress/comment/service.d.ts +1 -1
- package/dist/wordpress/index.d.ts +9 -4
- package/dist/wordpress/menu/items/types.d.ts +7 -4
- package/dist/wordpress/menu/items/types.js +22 -0
- package/dist/wordpress/page/service.d.ts +28 -0
- package/dist/wordpress/page/service.js +55 -0
- package/dist/wordpress/page/types.d.ts +270 -0
- package/dist/wordpress/page/types.js +27 -0
- package/dist/wordpress/post/types.d.ts +9 -5
- package/dist/wordpress/post/types.js +41 -0
- package/dist/wordpress/service.d.ts +7 -1
- package/dist/wordpress/service.js +6 -0
- package/dist/wordpress/tag/service.d.ts +28 -0
- package/dist/wordpress/tag/service.js +55 -0
- package/dist/wordpress/tag/types.d.ts +113 -0
- package/dist/wordpress/tag/types.js +15 -0
- package/package.json +5 -3
- package/dist/service.d.ts +0 -19
- package/dist/service.js +0 -20
|
@@ -0,0 +1,59 @@
|
|
|
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 };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { materializeTemplate } from "./template.js";
|
|
2
|
+
|
|
1
3
|
//#region src/cli/presets/nextjs.ts
|
|
2
4
|
const nextjs = {
|
|
3
5
|
id: "nextjs",
|
|
@@ -11,101 +13,7 @@ const nextjs = {
|
|
|
11
13
|
baseUrlEnvKey: "NEXT_PUBLIC_KIZLO_BACKEND_URL",
|
|
12
14
|
apiPath: "/api/kizlo",
|
|
13
15
|
scaffolds(ctx) {
|
|
14
|
-
|
|
15
|
-
const robotsDir = `${ctx.appDir}/robots.txt`;
|
|
16
|
-
const sitemapDir = `${ctx.appDir}/sitemaps/[sitemap]`;
|
|
17
|
-
const manifestDir = `${ctx.appDir}/site.webmanifest`;
|
|
18
|
-
return [
|
|
19
|
-
{
|
|
20
|
-
label: "Kizlo server instance",
|
|
21
|
-
relPath: ctx.serverEntryPath,
|
|
22
|
-
contents: `import { createKizlo } from "kizlo/nextjs/server"
|
|
23
|
-
|
|
24
|
-
export const { router, client, context, handler } = createKizlo()
|
|
25
|
-
`
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
label: "Browser client",
|
|
29
|
-
relPath: ctx.clientPath,
|
|
30
|
-
contents: `import { createKizloClient } from "kizlo/nextjs"
|
|
31
|
-
import { contract } from "./${ctx.serverDirName}/generated"
|
|
32
|
-
|
|
33
|
-
export const client = createKizloClient(contract)
|
|
34
|
-
`
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
label: "API route",
|
|
38
|
-
relPath: `${apiDir}/route.ts`,
|
|
39
|
-
contents: `import { handler } from "${ctx.serverImport(apiDir)}"
|
|
40
|
-
|
|
41
|
-
export { handler as GET, handler as POST, handler as PUT, handler as PATCH, handler as DELETE, handler as OPTIONS }
|
|
42
|
-
`
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
label: "robots.txt route",
|
|
46
|
-
relPath: `${robotsDir}/route.ts`,
|
|
47
|
-
contents: `import { createRobotsRoute } from "kizlo/nextjs/server"
|
|
48
|
-
import { client } from "${ctx.serverImport(robotsDir)}"
|
|
49
|
-
|
|
50
|
-
// Run on the edge: cheaper and faster than Node for a response this small.
|
|
51
|
-
export const runtime = "edge"
|
|
52
|
-
|
|
53
|
-
// Vercel treats special metadata files like robots.txt and sitemap.xml as completely static
|
|
54
|
-
// files at build time, bypassing normal Incremental Static Regeneration (ISR) and on-demand
|
|
55
|
-
// revalidatePath rules.
|
|
56
|
-
//
|
|
57
|
-
// While your revalidation logic executes perfectly in a local Node.js server, Vercel uploads
|
|
58
|
-
// these routes directly to its Edge CDN as immutable assets. Subsequent on-demand revalidation
|
|
59
|
-
// triggers will successfully clear the Data Cache but fail to purge the CDN-level Edge cache
|
|
60
|
-
// for that file.
|
|
61
|
-
//
|
|
62
|
-
// force-dynamic opts out of that static treatment so CMS edits show up; the WordPress call is
|
|
63
|
-
// cached, so requests stay cheap.
|
|
64
|
-
export const dynamic = "force-dynamic"
|
|
65
|
-
|
|
66
|
-
export const GET = createRobotsRoute(client)
|
|
67
|
-
`
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
label: "sitemap route",
|
|
71
|
-
relPath: `${sitemapDir}/route.ts`,
|
|
72
|
-
contents: `import { createSitemapRoute } from "kizlo/nextjs/server"
|
|
73
|
-
import { client } from "${ctx.serverImport(sitemapDir)}"
|
|
74
|
-
|
|
75
|
-
// Run on the edge: cheaper and faster than Node for a response this small.
|
|
76
|
-
export const runtime = "edge"
|
|
77
|
-
|
|
78
|
-
// Like robots.txt, Vercel bakes sitemap routes into immutable Edge CDN assets at build time,
|
|
79
|
-
// bypassing ISR and on-demand revalidatePath. The index gets frozen with whatever WordPress
|
|
80
|
-
// returned at build (often nothing, since the self-referential backend isn't reachable yet),
|
|
81
|
-
// and revalidation can't purge that CDN copy.
|
|
82
|
-
//
|
|
83
|
-
// force-dynamic opts out of that static treatment. createSitemapRoute caches the WordPress calls
|
|
84
|
-
// per sitemap and refreshes them on content changes (via revalidateTag), so requests stay cheap.
|
|
85
|
-
export const dynamic = "force-dynamic"
|
|
86
|
-
|
|
87
|
-
export const GET = createSitemapRoute(client)
|
|
88
|
-
`
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
label: "web manifest route",
|
|
92
|
-
relPath: `${manifestDir}/route.ts`,
|
|
93
|
-
contents: `import { createManifestRoute } from "kizlo/nextjs/server"
|
|
94
|
-
import { client } from "${ctx.serverImport(manifestDir)}"
|
|
95
|
-
|
|
96
|
-
// Run on the edge: cheaper and faster than Node for a response this small.
|
|
97
|
-
export const runtime = "edge"
|
|
98
|
-
|
|
99
|
-
// Like robots.txt, Vercel bakes this route into an immutable Edge CDN asset at build time,
|
|
100
|
-
// bypassing ISR and on-demand revalidatePath. force-dynamic opts out of that static treatment
|
|
101
|
-
// so brand-settings edits show up; createManifestRoute caches the WordPress call, so requests
|
|
102
|
-
// stay cheap.
|
|
103
|
-
export const dynamic = "force-dynamic"
|
|
104
|
-
|
|
105
|
-
export const GET = createManifestRoute(client)
|
|
106
|
-
`
|
|
107
|
-
}
|
|
108
|
-
];
|
|
16
|
+
return materializeTemplate("nextjs", ctx);
|
|
109
17
|
}
|
|
110
18
|
};
|
|
111
19
|
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { TEMPLATE_PATCHES } from "./generated/templates.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { builders, detectCodeFormat, generateCode, parseModule } from "magicast";
|
|
5
|
+
|
|
6
|
+
//#region src/cli/presets/patch.ts
|
|
7
|
+
/** Extensions a patch target might really use, in preference order. The template names files `.tsx`,
|
|
8
|
+
* but a JS project's layout may be `.js`/`.jsx`, so init probes these to find the actual file. */
|
|
9
|
+
const PATCH_TARGET_EXTS = [
|
|
10
|
+
".tsx",
|
|
11
|
+
".jsx",
|
|
12
|
+
".ts",
|
|
13
|
+
".js",
|
|
14
|
+
".mjs",
|
|
15
|
+
".cjs"
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a patch's tokenized target to the real file on disk, trying the common component
|
|
19
|
+
* extensions so a JS project's `layout.js`/`layout.jsx` is found — not just the template's `.tsx`.
|
|
20
|
+
* Returns undefined when no candidate exists.
|
|
21
|
+
*/
|
|
22
|
+
function resolvePatchTargetPath(cwd, relPath) {
|
|
23
|
+
const direct = path.join(cwd, relPath);
|
|
24
|
+
if (fs.existsSync(direct)) return direct;
|
|
25
|
+
const base = relPath.replace(/\.[^./]+$/, "");
|
|
26
|
+
for (const ext of PATCH_TARGET_EXTS) {
|
|
27
|
+
const candidate = path.join(cwd, `${base}${ext}`);
|
|
28
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** The Kizlo-wiring patches for a framework (files init merges into rather than writes). */
|
|
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). */
|
|
51
|
+
function patchChanged(changes) {
|
|
52
|
+
return Boolean(changes.addedImports.length || changes.addedExports.length || changes.replacedExports.length);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Next.js pairs a dynamic `generateX` export with a static `x` export (e.g. `generateMetadata` and
|
|
56
|
+
* `metadata`) and rejects a file that declares both. When we add a `generateX`, its static
|
|
57
|
+
* counterpart is a conflict too — return `metadata` for `generateMetadata`, `viewport` for
|
|
58
|
+
* `generateViewport`, and so on; undefined for anything that isn't a `generate*` name.
|
|
59
|
+
*/
|
|
60
|
+
function staticCounterpart(name) {
|
|
61
|
+
const match = /^generate([A-Z]\w*)$/.exec(name);
|
|
62
|
+
if (!match) return void 0;
|
|
63
|
+
const rest = match[1] ?? "";
|
|
64
|
+
return rest.charAt(0).toLowerCase() + rest.slice(1);
|
|
65
|
+
}
|
|
66
|
+
/** Whitespace-insensitive equality, for telling "already our wiring" apart from a user's own export. */
|
|
67
|
+
function sameExpr(a, b) {
|
|
68
|
+
return a.replace(/\s+/g, "") === b.replace(/\s+/g, "");
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 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). Imports and absent exports are
|
|
73
|
+
* added; an export the file already defines is replaced surgically when `replaceConflicts` is set, else
|
|
74
|
+
* kept. A static Next counterpart (`metadata` for `generateMetadata`) is dropped as part of a replace.
|
|
75
|
+
* Idempotent: an export whose current value already equals ours is left untouched.
|
|
76
|
+
*
|
|
77
|
+
* Throws if the target cannot be parsed or regenerated. Callers must NOT swallow that — a file we can't
|
|
78
|
+
* parse is one we must not guess at, so wiring stops rather than writing a corrupted file.
|
|
79
|
+
*/
|
|
80
|
+
function applyPatchToSource(source, patch, opts) {
|
|
81
|
+
const format = detectCodeFormat(source);
|
|
82
|
+
const mod = parseModule(source, {
|
|
83
|
+
sourceFileName: "layout.tsx",
|
|
84
|
+
...format
|
|
85
|
+
});
|
|
86
|
+
const changes = {
|
|
87
|
+
addedImports: [],
|
|
88
|
+
addedExports: [],
|
|
89
|
+
replacedExports: [],
|
|
90
|
+
keptExports: []
|
|
91
|
+
};
|
|
92
|
+
for (const imp of patch.imports) for (const name of imp.names) {
|
|
93
|
+
if (mod.imports.$items.some((item) => item.from === imp.module && item.imported === name)) continue;
|
|
94
|
+
mod.imports.$append({
|
|
95
|
+
imported: name,
|
|
96
|
+
from: imp.module
|
|
97
|
+
});
|
|
98
|
+
changes.addedImports.push(name);
|
|
99
|
+
}
|
|
100
|
+
for (const exp of patch.exports) {
|
|
101
|
+
const exportNames = Object.keys(mod.exports);
|
|
102
|
+
if (exportNames.includes(exp.name)) {
|
|
103
|
+
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
|
+
mod.exports[exp.name] = builders.raw(exp.value);
|
|
109
|
+
changes.replacedExports.push(exp.name);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const counterpart = staticCounterpart(exp.name);
|
|
113
|
+
if (counterpart && exportNames.includes(counterpart)) {
|
|
114
|
+
if (!opts.replaceConflicts) {
|
|
115
|
+
changes.keptExports.push(exp.name);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
delete mod.exports[counterpart];
|
|
119
|
+
mod.exports[exp.name] = builders.raw(exp.value);
|
|
120
|
+
changes.replacedExports.push(exp.name);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
mod.exports[exp.name] = builders.raw(exp.value);
|
|
124
|
+
changes.addedExports.push(exp.name);
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
text: generateCode(mod, { format }).code,
|
|
128
|
+
changes
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
//#endregion
|
|
133
|
+
export { applyPatchToSource, patchChanged, resolvePatch, resolvePatchTargetPath, templatePatches };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { TEMPLATE_SUBSETS } from "./generated/templates.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
//#region src/cli/presets/template.ts
|
|
5
|
+
/** Role → the human label init uses in prompts and logs. Mirrors the old hardcoded labels. */
|
|
6
|
+
const ROLE_LABELS = {
|
|
7
|
+
"server-entry": "Kizlo server instance",
|
|
8
|
+
client: "Browser client",
|
|
9
|
+
"api-route": "API route",
|
|
10
|
+
robots: "robots.txt route",
|
|
11
|
+
sitemap: "sitemap route",
|
|
12
|
+
"sitemap-redirect": "sitemap.xml redirect route",
|
|
13
|
+
manifest: "web manifest route"
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Turn a framework's extracted template subset into the {@link ScaffoldFile}s init writes, by
|
|
17
|
+
* substituting the tokens the extractor left behind: the path prefixes (`{{kizloDir}}`, `{{appDir}}`)
|
|
18
|
+
* with the project's real directories, and `{{serverImport}}` with the import specifier resolved for
|
|
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.
|
|
21
|
+
*/
|
|
22
|
+
function materializeTemplate(framework, ctx) {
|
|
23
|
+
return (TEMPLATE_SUBSETS[framework] ?? []).map((file) => {
|
|
24
|
+
const relPath = file.tokenizedPath.replaceAll("{{kizloDir}}", ctx.kizloDir).replaceAll("{{appDir}}", ctx.appDir);
|
|
25
|
+
const fromDir = path.posix.dirname(relPath);
|
|
26
|
+
const contents = file.body.replaceAll("{{serverImport}}", ctx.serverImport(fromDir));
|
|
27
|
+
return {
|
|
28
|
+
label: ROLE_LABELS[file.role] ?? file.role,
|
|
29
|
+
relPath,
|
|
30
|
+
contents
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
export { materializeTemplate };
|
package/dist/cli/wp/dev.js
CHANGED
|
@@ -7,12 +7,27 @@ import { prepareByo } from "./byo.js";
|
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { existsSync, rmSync } from "node:fs";
|
|
9
9
|
import { randomBytes } from "node:crypto";
|
|
10
|
+
import { networkInterfaces } from "node:os";
|
|
10
11
|
|
|
11
12
|
//#region src/cli/wp/dev.ts
|
|
12
13
|
/** A strong random admin password (144 bits, URL/JSON/CLI-safe characters). */
|
|
13
14
|
function generatePassword() {
|
|
14
15
|
return randomBytes(18).toString("base64url");
|
|
15
16
|
}
|
|
17
|
+
/** First non-internal IPv4 address — the router-assigned LAN address (undefined when offline). */
|
|
18
|
+
function lanAddress() {
|
|
19
|
+
for (const ifaces of Object.values(networkInterfaces())) for (const iface of ifaces ?? []) if (iface.family === "IPv4" && !iface.internal) return iface.address;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The URL the dev stack is provisioned at. We prefer the router-assigned LAN address over `localhost`
|
|
23
|
+
* so it's reachable from off the host (the app in a container or on another device). Serving WordPress
|
|
24
|
+
* under the request host is already handled by the container's `WORDPRESS_CONFIG_EXTRA` (it derives
|
|
25
|
+
* `WP_HOME`/`WP_SITEURL` from `HTTP_HOST` per request), so this is only the address we write into `.env`
|
|
26
|
+
* and print. Falls back to `localhost` when the machine is offline, so bare local dev still works.
|
|
27
|
+
*/
|
|
28
|
+
function devUrl(port) {
|
|
29
|
+
return `http://${lanAddress() ?? "localhost"}:${port}`;
|
|
30
|
+
}
|
|
16
31
|
/**
|
|
17
32
|
* Seed `dev.fixtures` into a freshly installed dev stack, reusing the test seeding
|
|
18
33
|
* primitives: seed the default subscriber so `ctx.userId` exists, then run each `seed`
|
|
@@ -58,7 +73,7 @@ async function seedDevFixtures(cfg, url) {
|
|
|
58
73
|
* concern.
|
|
59
74
|
*/
|
|
60
75
|
async function bootstrapDev(cfg) {
|
|
61
|
-
const url =
|
|
76
|
+
const url = devUrl(cfg.port);
|
|
62
77
|
const byo = !existsSync(join(cfg.wordpressDir, "wp-includes", "version.php")) && cfg.byo ? await prepareByo(cfg.byo, cfg.wordpressDir, cfg.configDir) : void 0;
|
|
63
78
|
await composeUp();
|
|
64
79
|
const installed = (await compose([
|
package/dist/comment/index.js
CHANGED
|
@@ -16,7 +16,7 @@ const COMMENT_ROUTER_MAP = {
|
|
|
16
16
|
errors: GET_COMMENT_ERROR_MAP,
|
|
17
17
|
output: Comment
|
|
18
18
|
}, async ({ context, errors, input }) => {
|
|
19
|
-
const response = await context.
|
|
19
|
+
const response = await context.wordpress.get(`/comments/${input.params.id}`, {
|
|
20
20
|
base: WP_CORE_BASE,
|
|
21
21
|
searchParams: { password: input.query?.password }
|
|
22
22
|
});
|
|
@@ -42,7 +42,7 @@ const COMMENT_ROUTER_MAP = {
|
|
|
42
42
|
errors: LIST_COMMENT_ERROR_MAP,
|
|
43
43
|
output: CommentList
|
|
44
44
|
}, async ({ context, errors, input }) => {
|
|
45
|
-
const response = await context.
|
|
45
|
+
const response = await context.wordpress.get(`/comments`, {
|
|
46
46
|
base: WP_CORE_BASE,
|
|
47
47
|
searchParams: {
|
|
48
48
|
context: "edit",
|
|
@@ -74,7 +74,7 @@ const COMMENT_ROUTER_MAP = {
|
|
|
74
74
|
context.logger.error("List comments unhandled error", response.error, { code: response.error.code });
|
|
75
75
|
throw errors.INTERNAL_SERVER_ERROR();
|
|
76
76
|
}
|
|
77
|
-
const list = context.
|
|
77
|
+
const list = context.wordpress.resolveList({
|
|
78
78
|
data: response.data,
|
|
79
79
|
headers: response.headers,
|
|
80
80
|
searchParams: input.query
|
|
@@ -100,7 +100,7 @@ const COMMENT_ROUTER_MAP = {
|
|
|
100
100
|
if (!input.body.captchaToken) throw errors.COMMENT_CAPTCHA_REQUIRED();
|
|
101
101
|
if (!await context.verifyCaptcha(input.body.captchaToken)) throw errors.COMMENT_CAPTCHA_INVALID();
|
|
102
102
|
}
|
|
103
|
-
const response = await context.
|
|
103
|
+
const response = await context.wordpress.post(`/comments`, {
|
|
104
104
|
body: {
|
|
105
105
|
user_id: user?.id,
|
|
106
106
|
author_ip: connInfo.ip,
|
package/dist/context.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { WordPressCredentials } from "./wordpress/types.js";
|
|
2
|
+
import { WordPressService } from "./wordpress/service.js";
|
|
2
3
|
import "./wordpress/index.js";
|
|
3
4
|
import { AuthUser } from "./adapters/auth.js";
|
|
4
5
|
import { PreviewTokenPayload } from "./shared/schema.js";
|
|
5
6
|
import { ConnInfo } from "./adapters/geo.js";
|
|
6
7
|
import { Logger } from "./adapters/logger.js";
|
|
7
8
|
import { CookiesStorage } from "./cookie.js";
|
|
8
|
-
import {
|
|
9
|
+
import { EmailService } from "./email/service.js";
|
|
10
|
+
import { SettingsService } from "./settings/service.js";
|
|
9
11
|
import { ServiceAdapters } from "./kizlo.js";
|
|
10
12
|
|
|
11
13
|
//#region src/context.d.ts
|
|
@@ -18,15 +20,24 @@ type AuthUserFn = () => Promise<AuthUser | null>;
|
|
|
18
20
|
type ConnInfoFn = () => Promise<ConnInfo | null>;
|
|
19
21
|
type VerifyCaptchaFn = (token: string) => Promise<boolean>;
|
|
20
22
|
type VerifyPreviewTokenFn = (token: string) => Promise<PreviewTokenPayload | null>;
|
|
21
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The context every procedure, middleware, event, and webhook handler receives — the fixed base
|
|
25
|
+
* the server builds. A procedure's handler receives this base plus whatever its middleware injected
|
|
26
|
+
* via `next({ context })`.
|
|
27
|
+
*/
|
|
28
|
+
interface ProcedureContext {
|
|
22
29
|
/** The incoming HTTP `Request`, or `null` for server-side (non-HTTP) invocations. */
|
|
23
30
|
request: Request | null;
|
|
24
31
|
/** Response headers to be sent (e.g. `Set-Cookie`), or `null` server-side. */
|
|
25
32
|
headers: Headers | null;
|
|
26
33
|
/** The configured logger adapter. */
|
|
27
34
|
logger: Logger;
|
|
28
|
-
/** Typed
|
|
29
|
-
|
|
35
|
+
/** Typed WordPress REST client. */
|
|
36
|
+
wordpress: WordPressService;
|
|
37
|
+
/** Kizlo settings client. */
|
|
38
|
+
settings: SettingsService;
|
|
39
|
+
/** Transactional email client. */
|
|
40
|
+
email: EmailService;
|
|
30
41
|
/** The resolved context config: site secret, adapters, and WordPress credentials. */
|
|
31
42
|
config: ContextConfig;
|
|
32
43
|
/** Resolve the caller's connection info (IP, geo) via the geo adapter, or `null`. */
|
|
@@ -45,14 +56,18 @@ interface ServerContext {
|
|
|
45
56
|
declare class Context {
|
|
46
57
|
private readonly config;
|
|
47
58
|
private readonly logger;
|
|
48
|
-
private readonly
|
|
59
|
+
private readonly wordpress;
|
|
60
|
+
private readonly settings;
|
|
61
|
+
private readonly email;
|
|
49
62
|
constructor(config: ContextConfig);
|
|
50
63
|
createRestContext(request: Request): {
|
|
51
64
|
headers: Headers;
|
|
52
65
|
request: Request;
|
|
53
66
|
config: ContextConfig;
|
|
54
67
|
logger: Logger;
|
|
55
|
-
|
|
68
|
+
wordpress: WordPressService;
|
|
69
|
+
settings: SettingsService;
|
|
70
|
+
email: EmailService;
|
|
56
71
|
getConnInfo: ConnInfoFn;
|
|
57
72
|
verifyCaptcha: VerifyCaptchaFn;
|
|
58
73
|
getAuthUser: AuthUserFn;
|
|
@@ -66,7 +81,9 @@ declare class Context {
|
|
|
66
81
|
request: null;
|
|
67
82
|
config: ContextConfig;
|
|
68
83
|
logger: Logger;
|
|
69
|
-
|
|
84
|
+
wordpress: WordPressService;
|
|
85
|
+
settings: SettingsService;
|
|
86
|
+
email: EmailService;
|
|
70
87
|
getAuthUser: AuthUserFn;
|
|
71
88
|
getConnInfo: ConnInfoFn;
|
|
72
89
|
verifyCaptcha: VerifyCaptchaFn;
|
|
@@ -81,4 +98,4 @@ declare class Context {
|
|
|
81
98
|
private createLogger;
|
|
82
99
|
}
|
|
83
100
|
//#endregion
|
|
84
|
-
export { AuthUserFn, ConnInfoFn, Context, ContextConfig,
|
|
101
|
+
export { AuthUserFn, ConnInfoFn, Context, ContextConfig, ProcedureContext, VerifyCaptchaFn, VerifyPreviewTokenFn };
|
package/dist/context.js
CHANGED
|
@@ -2,7 +2,9 @@ import { compare, hmac } from "./shared/crypto.js";
|
|
|
2
2
|
import { noopAdapter } from "./adapters/logger.js";
|
|
3
3
|
import { PreviewTokenData } from "./shared/schema.js";
|
|
4
4
|
import { CookiesStorage } from "./cookie.js";
|
|
5
|
-
import {
|
|
5
|
+
import { WordPressService } from "./wordpress/service.js";
|
|
6
|
+
import { EmailService } from "./email/service.js";
|
|
7
|
+
import { SettingsService } from "./settings/service.js";
|
|
6
8
|
import { base64Decode, tryCatchSync } from "@kizlo/shared";
|
|
7
9
|
import { serialize } from "cookie";
|
|
8
10
|
|
|
@@ -10,10 +12,14 @@ import { serialize } from "cookie";
|
|
|
10
12
|
var Context = class {
|
|
11
13
|
config;
|
|
12
14
|
logger;
|
|
13
|
-
|
|
15
|
+
wordpress;
|
|
16
|
+
settings;
|
|
17
|
+
email;
|
|
14
18
|
constructor(config) {
|
|
15
19
|
this.config = config;
|
|
16
|
-
this.
|
|
20
|
+
this.wordpress = new WordPressService(config);
|
|
21
|
+
this.settings = new SettingsService(this.wordpress);
|
|
22
|
+
this.email = new EmailService(this.wordpress);
|
|
17
23
|
this.logger = this.createLogger();
|
|
18
24
|
}
|
|
19
25
|
createRestContext(request) {
|
|
@@ -23,7 +29,9 @@ var Context = class {
|
|
|
23
29
|
request: request.clone(),
|
|
24
30
|
config: this.config,
|
|
25
31
|
logger: this.logger,
|
|
26
|
-
|
|
32
|
+
wordpress: this.wordpress,
|
|
33
|
+
settings: this.settings,
|
|
34
|
+
email: this.email,
|
|
27
35
|
getConnInfo: this.createConnInfoFn(request),
|
|
28
36
|
verifyCaptcha: this.createVerifyCaptchaFn(request),
|
|
29
37
|
getAuthUser: this.createGetUserFn(request),
|
|
@@ -39,7 +47,9 @@ var Context = class {
|
|
|
39
47
|
request: null,
|
|
40
48
|
config: this.config,
|
|
41
49
|
logger: this.logger,
|
|
42
|
-
|
|
50
|
+
wordpress: this.wordpress,
|
|
51
|
+
settings: this.settings,
|
|
52
|
+
email: this.email,
|
|
43
53
|
getAuthUser: this.createGetUserFn(null),
|
|
44
54
|
getConnInfo: this.createConnInfoFn(null),
|
|
45
55
|
verifyCaptcha: this.createVerifyCaptchaFn(null),
|
package/dist/email/service.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { WordPressService } from "../wordpress/service.js";
|
|
2
|
+
import "../wordpress/index.js";
|
|
1
3
|
import { EmailSendParams } from "./service.interface.js";
|
|
2
|
-
import { Service } from "../service.js";
|
|
3
4
|
|
|
4
5
|
//#region src/email/service.d.ts
|
|
5
6
|
declare class EmailService {
|
|
6
|
-
private readonly
|
|
7
|
-
constructor(
|
|
7
|
+
private readonly wordpress;
|
|
8
|
+
constructor(wordpress: WordPressService);
|
|
8
9
|
send(params: EmailSendParams): Promise<void>;
|
|
9
10
|
}
|
|
10
11
|
//#endregion
|
package/dist/email/service.js
CHANGED
|
@@ -2,12 +2,12 @@ import { WP_KIZLO_BASE } from "../wordpress/constants.js";
|
|
|
2
2
|
|
|
3
3
|
//#region src/email/service.ts
|
|
4
4
|
var EmailService = class {
|
|
5
|
-
|
|
6
|
-
constructor(
|
|
7
|
-
this.
|
|
5
|
+
wordpress;
|
|
6
|
+
constructor(wordpress) {
|
|
7
|
+
this.wordpress = wordpress;
|
|
8
8
|
}
|
|
9
9
|
async send(params) {
|
|
10
|
-
const { data, error } = await this.
|
|
10
|
+
const { data, error } = await this.wordpress.post("/email/send", {
|
|
11
11
|
body: params,
|
|
12
12
|
base: WP_KIZLO_BASE
|
|
13
13
|
});
|