@decocms/blocks-cli 7.9.0 → 7.11.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/blocks-cli",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco codegen (generate-blocks, generate-schema, generate-invoke) and Fresh-to-TanStack migration tooling",
|
|
6
6
|
"repository": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"deco-sync-blocks-to-kv": "./scripts/sync-blocks-to-kv.ts"
|
|
20
20
|
},
|
|
21
21
|
"exports": {
|
|
22
|
+
"./generate-app-schemas": "./scripts/generate-app-schemas.ts",
|
|
22
23
|
"./generate-blocks": "./scripts/generate-blocks.ts",
|
|
23
24
|
"./generate-blocks-manifest": "./scripts/generate-blocks-manifest.ts",
|
|
24
25
|
"./generate-schema": "./scripts/generate-schema.ts",
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
"lint:unused": "knip"
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {
|
|
38
|
-
"@decocms/blocks": "7.
|
|
39
|
+
"@decocms/blocks": "7.11.0",
|
|
39
40
|
"ts-morph": "^27.0.0",
|
|
40
41
|
"tsx": "^4.22.5"
|
|
41
42
|
},
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for generate-app-schemas.ts — the build-time extractor that gives app
|
|
3
|
+
* packages (apps-vtex, …) real loader/action props schemas in the admin meta,
|
|
4
|
+
* replacing the `__resolveType`-only stubs auto-registered at runtime.
|
|
5
|
+
*
|
|
6
|
+
* Drives generateAppSchemas() directly (the script is import-safe, guarded by
|
|
7
|
+
* isMainModule()) against a tmp fixture app package covering both key
|
|
8
|
+
* universes: file-path loader keys and manifest-flattened module keys.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as os from "node:os";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
14
|
+
import {
|
|
15
|
+
type AppSchemasResult,
|
|
16
|
+
generateAppSchemas,
|
|
17
|
+
renderSchemasModule,
|
|
18
|
+
} from "./generate-app-schemas";
|
|
19
|
+
|
|
20
|
+
let tmpDir: string;
|
|
21
|
+
let result: AppSchemasResult;
|
|
22
|
+
|
|
23
|
+
function write(rel: string, content: string) {
|
|
24
|
+
const full = path.join(tmpDir, rel);
|
|
25
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
26
|
+
fs.writeFileSync(full, content);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
beforeAll(() => {
|
|
30
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "generate-app-schemas-"));
|
|
31
|
+
|
|
32
|
+
write(
|
|
33
|
+
"package.json",
|
|
34
|
+
JSON.stringify({ name: "@decocms/apps-fixture", version: "0.0.0", type: "module" }),
|
|
35
|
+
);
|
|
36
|
+
write(
|
|
37
|
+
"tsconfig.json",
|
|
38
|
+
JSON.stringify({
|
|
39
|
+
compilerOptions: {
|
|
40
|
+
target: "ES2022",
|
|
41
|
+
module: "ESNext",
|
|
42
|
+
moduleResolution: "bundler",
|
|
43
|
+
lib: ["ES2022", "DOM"],
|
|
44
|
+
strict: true,
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// File-path-keyed loader with a Props interface + JSDoc metadata.
|
|
50
|
+
write(
|
|
51
|
+
"src/loaders/detail/page.ts",
|
|
52
|
+
`export interface Props {
|
|
53
|
+
/** @title Slug */
|
|
54
|
+
slug?: string;
|
|
55
|
+
/** @description How many variants to fetch */
|
|
56
|
+
variantCount: number;
|
|
57
|
+
}
|
|
58
|
+
export default async function loader(props: Props): Promise<string | null> {
|
|
59
|
+
return props.slug ?? null;
|
|
60
|
+
}
|
|
61
|
+
`,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Loader that takes no input at all.
|
|
65
|
+
write(
|
|
66
|
+
"src/loaders/noProps.ts",
|
|
67
|
+
`export default async function loader(): Promise<number> {
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
`,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Loader whose props are untyped — input exists but can't be described.
|
|
74
|
+
write(
|
|
75
|
+
"src/loaders/anyProps.ts",
|
|
76
|
+
`export default async function loader(props: any): Promise<unknown> {
|
|
77
|
+
return props;
|
|
78
|
+
}
|
|
79
|
+
`,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Runtime-injected platform types (url: URL) must be hidden, not expanded,
|
|
83
|
+
// and must not stay required.
|
|
84
|
+
write(
|
|
85
|
+
"src/loaders/listing.ts",
|
|
86
|
+
`export interface Props {
|
|
87
|
+
url: URL;
|
|
88
|
+
sort?: "asc" | "desc";
|
|
89
|
+
count: number;
|
|
90
|
+
}
|
|
91
|
+
export default async function loader(props: Props): Promise<unknown[]> {
|
|
92
|
+
return [props];
|
|
93
|
+
}
|
|
94
|
+
`,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Barrel — must not become a loader key.
|
|
98
|
+
write("src/loaders/index.ts", `export { default as page } from "./detail/page";\n`);
|
|
99
|
+
|
|
100
|
+
// Manifest module with named exports → flattened keys.
|
|
101
|
+
write(
|
|
102
|
+
"src/actions/cart.ts",
|
|
103
|
+
`export interface AddItemProps {
|
|
104
|
+
id: string;
|
|
105
|
+
qty?: number;
|
|
106
|
+
}
|
|
107
|
+
/** @title Add item to cart */
|
|
108
|
+
export async function addItem(props: AddItemProps): Promise<string> {
|
|
109
|
+
return props.id;
|
|
110
|
+
}
|
|
111
|
+
export async function clearCart(): Promise<void> {}
|
|
112
|
+
export type IgnoredType = { nope: true };
|
|
113
|
+
export const IGNORED_CONST = 42;
|
|
114
|
+
`,
|
|
115
|
+
);
|
|
116
|
+
write(
|
|
117
|
+
"src/loaders/things.ts",
|
|
118
|
+
`export async function listThings(props: { filter?: string }): Promise<string[]> {
|
|
119
|
+
return [props.filter ?? ""];
|
|
120
|
+
}
|
|
121
|
+
export default async function defaultThings(props: { limit: number }): Promise<string[]> {
|
|
122
|
+
return [String(props.limit)];
|
|
123
|
+
}
|
|
124
|
+
`,
|
|
125
|
+
);
|
|
126
|
+
write(
|
|
127
|
+
"src/manifest.gen.ts",
|
|
128
|
+
`import * as actions_cart from "./actions/cart";
|
|
129
|
+
import * as loaders_things from "./loaders/things";
|
|
130
|
+
|
|
131
|
+
const manifest = {
|
|
132
|
+
\tname: "fixture",
|
|
133
|
+
\tloaders: {
|
|
134
|
+
\t\t"fixture/loaders/things": loaders_things,
|
|
135
|
+
\t},
|
|
136
|
+
\tactions: {
|
|
137
|
+
\t\t"fixture/actions/cart": actions_cart,
|
|
138
|
+
\t},
|
|
139
|
+
\tsections: {},
|
|
140
|
+
} as const;
|
|
141
|
+
|
|
142
|
+
export default manifest;
|
|
143
|
+
`,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
result = generateAppSchemas(tmpDir, "fixture");
|
|
147
|
+
}, 120_000);
|
|
148
|
+
|
|
149
|
+
afterAll(() => {
|
|
150
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("file-path-keyed loaders", () => {
|
|
154
|
+
it("emits real props from the Props interface, under both bare and .ts keys", () => {
|
|
155
|
+
const withTs = result.loaders["fixture/loaders/detail/page.ts"];
|
|
156
|
+
const bare = result.loaders["fixture/loaders/detail/page"];
|
|
157
|
+
expect(withTs).toBeDefined();
|
|
158
|
+
expect(bare).toBe(withTs);
|
|
159
|
+
|
|
160
|
+
expect(withTs.properties?.slug).toMatchObject({ type: "string", title: "Slug" });
|
|
161
|
+
expect(withTs.properties?.variantCount).toMatchObject({
|
|
162
|
+
type: "number",
|
|
163
|
+
description: "How many variants to fetch",
|
|
164
|
+
});
|
|
165
|
+
expect(withTs.required).toEqual(["variantCount"]);
|
|
166
|
+
expect(withTs.additionalProperties).toBeUndefined();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("emits an empty-props schema WITHOUT additionalProperties for no-input loaders", () => {
|
|
170
|
+
const schema = result.loaders["fixture/loaders/noProps.ts"];
|
|
171
|
+
expect(schema).toEqual({ type: "object", properties: {} });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("falls back to the JSON-editor shape (additionalProperties) for untyped props", () => {
|
|
175
|
+
const schema = result.loaders["fixture/loaders/anyProps.ts"];
|
|
176
|
+
expect(schema).toEqual({ type: "object", additionalProperties: true });
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("hides runtime-injected platform types and drops them from required", () => {
|
|
180
|
+
const schema = result.loaders["fixture/loaders/listing.ts"];
|
|
181
|
+
expect(schema.properties?.url).toMatchObject({ type: "object", hide: "true" });
|
|
182
|
+
expect(schema.properties?.url.properties).toBeUndefined();
|
|
183
|
+
expect(schema.properties?.sort).toMatchObject({ type: "string", enum: ["asc", "desc"] });
|
|
184
|
+
expect(schema.required).toEqual(["count"]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("skips barrel index files", () => {
|
|
188
|
+
expect(result.loaders["fixture/loaders/index.ts"]).toBeUndefined();
|
|
189
|
+
expect(result.loaders["fixture/loaders/index"]).toBeUndefined();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("manifest-flattened keys", () => {
|
|
194
|
+
it("flattens named function exports to <moduleKey>/<fnName>", () => {
|
|
195
|
+
const addItem = result.actions["fixture/actions/cart/addItem"];
|
|
196
|
+
expect(addItem).toBeDefined();
|
|
197
|
+
expect(result.actions["fixture/actions/cart/addItem.ts"]).toBe(addItem);
|
|
198
|
+
expect(addItem.title).toBe("Add item to cart");
|
|
199
|
+
expect(addItem.properties?.id).toMatchObject({ type: "string" });
|
|
200
|
+
expect(addItem.required).toEqual(["id"]);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("emits no-props actions without additionalProperties", () => {
|
|
204
|
+
expect(result.actions["fixture/actions/cart/clearCart"]).toEqual({
|
|
205
|
+
type: "object",
|
|
206
|
+
properties: {},
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("ignores type and constant exports", () => {
|
|
211
|
+
const keys = Object.keys(result.actions);
|
|
212
|
+
expect(keys.filter((k) => k.includes("IgnoredType"))).toEqual([]);
|
|
213
|
+
expect(keys.filter((k) => k.includes("IGNORED_CONST"))).toEqual([]);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("registers a module's default export at the moduleKey itself", () => {
|
|
217
|
+
const def = result.loaders["fixture/loaders/things"];
|
|
218
|
+
expect(def.properties?.limit).toMatchObject({ type: "number" });
|
|
219
|
+
expect(result.loaders["fixture/loaders/things/listThings"].properties?.filter).toMatchObject({
|
|
220
|
+
type: "string",
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe("renderSchemasModule", () => {
|
|
226
|
+
it("emits a typed module with deduplicated schema consts shared across alias keys", () => {
|
|
227
|
+
const out = renderSchemasModule(result);
|
|
228
|
+
expect(out).toContain('import type { BlockPropsSchema } from "@decocms/blocks/cms/client";');
|
|
229
|
+
expect(out).toContain("export const loaderSchemas: Record<string, BlockPropsSchema>");
|
|
230
|
+
expect(out).toContain("export const actionSchemas: Record<string, BlockPropsSchema>");
|
|
231
|
+
|
|
232
|
+
// Alias keys must point at the same const (dedup by content).
|
|
233
|
+
const bare = out.match(/"fixture\/loaders\/detail\/page": (s\d+),/)?.[1];
|
|
234
|
+
const withTs = out.match(/"fixture\/loaders\/detail\/page\.ts": (s\d+),/)?.[1];
|
|
235
|
+
expect(bare).toBeDefined();
|
|
236
|
+
expect(bare).toBe(withTs);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
/**
|
|
6
|
+
* App Schema Generator — real loader/action props schemas for app packages.
|
|
7
|
+
*
|
|
8
|
+
* The admin builds a block's props form from `/deco/meta`'s
|
|
9
|
+
* `schema.definitions[b64(key)]`. Site loaders get real schemas from
|
|
10
|
+
* generate-schema.ts, but app loaders/actions (@decocms/apps-vtex, ...) were
|
|
11
|
+
* only ever auto-registered as `__resolveType`-only stubs by
|
|
12
|
+
* registerCommerceLoaders() — the admin couldn't render any form for them.
|
|
13
|
+
*
|
|
14
|
+
* This script runs at app-package build time (Props are TS types, erased at
|
|
15
|
+
* runtime — this CANNOT run in the site) and emits a committed
|
|
16
|
+
* `src/schemas.gen.ts` artifact mapping every CMS-reachable key to its real
|
|
17
|
+
* props schema. The app's entrypoints feed it to registerAppSchemas()
|
|
18
|
+
* (@decocms/blocks/cms), which beats the runtime stubs by key.
|
|
19
|
+
*
|
|
20
|
+
* Two key universes are covered, matching how keys reach the CMS at runtime:
|
|
21
|
+
*
|
|
22
|
+
* 1. File-path keys — `<ns>/loaders/<relpath>.ts` (+ bare alias): what
|
|
23
|
+
* commerceLoaders maps and site decofiles reference. Every file under
|
|
24
|
+
* src/loaders/ with a default export.
|
|
25
|
+
* 2. Manifest-flattened keys — what setupApps() registers from
|
|
26
|
+
* src/manifest.gen.ts: `<moduleKey>` for a module's default export,
|
|
27
|
+
* `<moduleKey>/<fnName>` for named function exports (+ `.ts` siblings).
|
|
28
|
+
* e.g. "vtex/loaders/legacy" × `productListingPage` →
|
|
29
|
+
* "vtex/loaders/legacy/productListingPage". Covers actions too.
|
|
30
|
+
*
|
|
31
|
+
* Usage (from the app package root):
|
|
32
|
+
* bun ../blocks-cli/scripts/generate-app-schemas.ts [options]
|
|
33
|
+
*
|
|
34
|
+
* Options:
|
|
35
|
+
* --namespace CMS namespace (default: derived from package.json name,
|
|
36
|
+
* "@decocms/apps-<ns>" → "<ns>")
|
|
37
|
+
* --pkg App package root (default: cwd)
|
|
38
|
+
* --out Output file (default: src/schemas.gen.ts, relative to --pkg)
|
|
39
|
+
*/
|
|
40
|
+
import { type Symbol as MorphSymbol, Node, Project, type SourceFile, type Type } from "ts-morph";
|
|
41
|
+
import { getJsDocTags, typeToJsonSchema } from "./generate-schema";
|
|
42
|
+
import { isExcludedCodegenFile } from "./lib/codegenExclusions";
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Types
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/** Mirrors BlockPropsSchema in @decocms/blocks/cms — kept structural so this
|
|
49
|
+
* script has no runtime dependency on the blocks package. */
|
|
50
|
+
export interface AppBlockPropsSchema {
|
|
51
|
+
type?: "object";
|
|
52
|
+
title?: string;
|
|
53
|
+
description?: string;
|
|
54
|
+
properties?: Record<string, any>;
|
|
55
|
+
required?: string[];
|
|
56
|
+
additionalProperties?: boolean;
|
|
57
|
+
/** Any other JSON Schema keyword or JSDoc passthrough tag (nullable, …). */
|
|
58
|
+
[keyword: string]: any;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface AppSchemasResult {
|
|
62
|
+
loaders: Record<string, AppBlockPropsSchema>;
|
|
63
|
+
actions: Record<string, AppBlockPropsSchema>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Schema extraction helpers
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
/** Props whose declared type is any/unknown — the block DOES take input, we
|
|
71
|
+
* just can't describe it. The stub shape keeps the admin's JSON editor. */
|
|
72
|
+
const UNKNOWN_PROPS: AppBlockPropsSchema = { type: "object", additionalProperties: true };
|
|
73
|
+
/** The block takes no input at all. Deliberately WITHOUT additionalProperties
|
|
74
|
+
* so the admin can say "takes no input". */
|
|
75
|
+
const NO_PROPS: AppBlockPropsSchema = { type: "object", properties: {} };
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Normalize whatever typeToJsonSchema produced into a props-object schema.
|
|
79
|
+
* Handlers are invoked with a single props object; a first parameter that
|
|
80
|
+
* isn't object-shaped (e.g. `getCategoryTree(levels: number)`) can't be
|
|
81
|
+
* described as a props form — fall back to the JSON-editor shape.
|
|
82
|
+
*/
|
|
83
|
+
function toPropsSchema(schema: any): AppBlockPropsSchema {
|
|
84
|
+
if (!schema || typeof schema !== "object") return UNKNOWN_PROPS;
|
|
85
|
+
// any/unknown → {} from typeToJsonSchema
|
|
86
|
+
if (Object.keys(schema).length === 0) return UNKNOWN_PROPS;
|
|
87
|
+
if (schema.type === "object" && schema.properties) return schema;
|
|
88
|
+
// Record<string, T> → { type: "object", additionalProperties: <T> }
|
|
89
|
+
if (schema.type === "object" && schema.additionalProperties !== undefined) {
|
|
90
|
+
return { type: "object", additionalProperties: true };
|
|
91
|
+
}
|
|
92
|
+
if (schema.type === "object") return { ...schema, properties: schema.properties ?? {} };
|
|
93
|
+
return UNKNOWN_PROPS;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Schema for a function's first parameter (a loader/action handler). */
|
|
97
|
+
function functionInputSchema(fnType: Type, location: Node): AppBlockPropsSchema | null {
|
|
98
|
+
const callSigs = fnType.getCallSignatures();
|
|
99
|
+
if (callSigs.length === 0) return null;
|
|
100
|
+
const params = callSigs[0].getParameters();
|
|
101
|
+
if (params.length === 0) return NO_PROPS;
|
|
102
|
+
const paramType = params[0].getTypeAtLocation(location);
|
|
103
|
+
if (paramType.isAny() || paramType.isUnknown()) return UNKNOWN_PROPS;
|
|
104
|
+
return toPropsSchema(typeToJsonSchema(paramType));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Apply a symbol's JSDoc @title (slash-free only) and description. */
|
|
108
|
+
function applyDocMeta(schema: AppBlockPropsSchema, symbol: MorphSymbol): AppBlockPropsSchema {
|
|
109
|
+
const tags = getJsDocTags(symbol);
|
|
110
|
+
const title = tags.title && !tags.title.includes("/") ? tags.title : undefined;
|
|
111
|
+
const description = tags.description;
|
|
112
|
+
if (!title && !description) return schema;
|
|
113
|
+
return {
|
|
114
|
+
...(title ? { title } : {}),
|
|
115
|
+
...(description ? { description } : {}),
|
|
116
|
+
...schema,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Discovery 1: file-path-keyed loaders (src/loaders/**)
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
function walkTsFiles(dir: string): string[] {
|
|
125
|
+
const results: string[] = [];
|
|
126
|
+
if (!fs.existsSync(dir)) return results;
|
|
127
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
128
|
+
const full = path.join(dir, entry.name);
|
|
129
|
+
if (entry.isDirectory()) {
|
|
130
|
+
if (entry.name === "__tests__" || entry.name === "__test__") continue;
|
|
131
|
+
results.push(...walkTsFiles(full));
|
|
132
|
+
} else if (
|
|
133
|
+
!isExcludedCodegenFile(entry.name) &&
|
|
134
|
+
(entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) &&
|
|
135
|
+
entry.name !== "index.ts" &&
|
|
136
|
+
!entry.name.startsWith("_")
|
|
137
|
+
) {
|
|
138
|
+
results.push(full);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return results;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function extractFileLoaderSchema(sourceFile: SourceFile): AppBlockPropsSchema | null {
|
|
145
|
+
const defaultSym = sourceFile.getDefaultExportSymbol();
|
|
146
|
+
if (!defaultSym) return null; // barrel/utility module, not a loader file
|
|
147
|
+
|
|
148
|
+
// Same extraction order as generate-schema.ts's app-loaders pass: a named
|
|
149
|
+
// Props interface/alias wins over the default export's first parameter.
|
|
150
|
+
let schema: any = null;
|
|
151
|
+
const propsInterface = sourceFile.getInterface("Props");
|
|
152
|
+
if (propsInterface) schema = typeToJsonSchema(propsInterface.getType());
|
|
153
|
+
|
|
154
|
+
if (!schema) {
|
|
155
|
+
const propsAlias = sourceFile.getTypeAlias("Props");
|
|
156
|
+
if (propsAlias) schema = typeToJsonSchema(propsAlias.getType());
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (schema) return applyDocMeta(toPropsSchema(schema), defaultSym);
|
|
160
|
+
|
|
161
|
+
const fnSchema = functionInputSchema(defaultSym.getTypeAtLocation(sourceFile), sourceFile);
|
|
162
|
+
return fnSchema ? applyDocMeta(fnSchema, defaultSym) : NO_PROPS;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Discovery 2: manifest-flattened keys (src/manifest.gen.ts)
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
/** moduleKey → absolute module file path, per manifest category. */
|
|
170
|
+
function parseManifestModules(manifestFile: SourceFile): {
|
|
171
|
+
loaders: Map<string, string>;
|
|
172
|
+
actions: Map<string, string>;
|
|
173
|
+
} {
|
|
174
|
+
const result = { loaders: new Map<string, string>(), actions: new Map<string, string>() };
|
|
175
|
+
|
|
176
|
+
// identifier → module specifier, from `import * as loaders_x from "./loaders/x"`
|
|
177
|
+
const importMap = new Map<string, string>();
|
|
178
|
+
for (const imp of manifestFile.getImportDeclarations()) {
|
|
179
|
+
const ns = imp.getNamespaceImport();
|
|
180
|
+
if (ns) importMap.set(ns.getText(), imp.getModuleSpecifierValue());
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const manifestVar = manifestFile.getVariableDeclaration("manifest");
|
|
184
|
+
let init = manifestVar?.getInitializer();
|
|
185
|
+
while (init && Node.isAsExpression(init)) init = init.getExpression();
|
|
186
|
+
if (!init || !Node.isObjectLiteralExpression(init)) return result;
|
|
187
|
+
|
|
188
|
+
const manifestDir = path.dirname(manifestFile.getFilePath());
|
|
189
|
+
for (const category of ["loaders", "actions"] as const) {
|
|
190
|
+
const prop = init.getProperty(category);
|
|
191
|
+
if (!prop || !Node.isPropertyAssignment(prop)) continue;
|
|
192
|
+
const value = prop.getInitializer();
|
|
193
|
+
if (!value || !Node.isObjectLiteralExpression(value)) continue;
|
|
194
|
+
|
|
195
|
+
for (const entry of value.getProperties()) {
|
|
196
|
+
if (!Node.isPropertyAssignment(entry)) continue;
|
|
197
|
+
const keyNode = entry.getNameNode();
|
|
198
|
+
const moduleKey = Node.isStringLiteral(keyNode) ? keyNode.getLiteralValue() : entry.getName();
|
|
199
|
+
const ident = entry.getInitializer()?.getText();
|
|
200
|
+
const spec = ident ? importMap.get(ident) : undefined;
|
|
201
|
+
if (!spec?.startsWith(".")) continue;
|
|
202
|
+
|
|
203
|
+
const base = path.resolve(manifestDir, spec);
|
|
204
|
+
for (const candidate of [base, `${base}.ts`, `${base}.tsx`, path.join(base, "index.ts")]) {
|
|
205
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
206
|
+
result[category].set(moduleKey, candidate);
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Flatten a manifest module's function exports, mirroring setupApps():
|
|
216
|
+
* default → moduleKey; named fn → `${moduleKey}/${fnName}`. */
|
|
217
|
+
function flattenModuleSchemas(
|
|
218
|
+
moduleKey: string,
|
|
219
|
+
sourceFile: SourceFile,
|
|
220
|
+
into: Record<string, AppBlockPropsSchema>,
|
|
221
|
+
) {
|
|
222
|
+
for (const sym of sourceFile.getExportSymbols()) {
|
|
223
|
+
const name = sym.getName();
|
|
224
|
+
const fnType = sym.getTypeAtLocation(sourceFile);
|
|
225
|
+
const schema = functionInputSchema(fnType, sourceFile);
|
|
226
|
+
if (!schema) continue; // type/constant export, not a handler
|
|
227
|
+
|
|
228
|
+
const key = name === "default" ? moduleKey : `${moduleKey}/${name}`;
|
|
229
|
+
into[key] = applyDocMeta(schema, sym);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Main generation
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
/** Add the `.ts` sibling for every bare key and vice versa, both pointing at
|
|
238
|
+
* the same schema object — mirrors runtime alias registration. */
|
|
239
|
+
function withKeyAliases(
|
|
240
|
+
schemas: Record<string, AppBlockPropsSchema>,
|
|
241
|
+
): Record<string, AppBlockPropsSchema> {
|
|
242
|
+
const out: Record<string, AppBlockPropsSchema> = {};
|
|
243
|
+
for (const [key, schema] of Object.entries(schemas)) {
|
|
244
|
+
const bare = key.endsWith(".ts") ? key.slice(0, -3) : key;
|
|
245
|
+
// Bare-key entry wins ties with a .ts entry for the same block — both
|
|
246
|
+
// describe the same file, so any difference is extraction noise.
|
|
247
|
+
out[bare] = out[bare] ?? schema;
|
|
248
|
+
out[`${bare}.ts`] = out[`${bare}.ts`] ?? schema;
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function generateAppSchemas(pkgDir: string, namespace: string): AppSchemasResult {
|
|
254
|
+
const project = new Project({
|
|
255
|
+
tsConfigFilePath: path.join(pkgDir, "tsconfig.json"),
|
|
256
|
+
skipAddingFilesFromTsConfig: true,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const loaders: Record<string, AppBlockPropsSchema> = {};
|
|
260
|
+
const actions: Record<string, AppBlockPropsSchema> = {};
|
|
261
|
+
|
|
262
|
+
// Manifest-flattened keys first; file-path keys second so the more
|
|
263
|
+
// path-accurate extraction wins any (same-file) collision.
|
|
264
|
+
const manifestPath = path.join(pkgDir, "src", "manifest.gen.ts");
|
|
265
|
+
if (fs.existsSync(manifestPath)) {
|
|
266
|
+
const manifestFile = project.addSourceFileAtPath(manifestPath);
|
|
267
|
+
const modules = parseManifestModules(manifestFile);
|
|
268
|
+
for (const [category, into] of [
|
|
269
|
+
["loaders", loaders],
|
|
270
|
+
["actions", actions],
|
|
271
|
+
] as const) {
|
|
272
|
+
for (const [moduleKey, filePath] of modules[category]) {
|
|
273
|
+
try {
|
|
274
|
+
flattenModuleSchemas(moduleKey, project.addSourceFileAtPath(filePath), into);
|
|
275
|
+
} catch (e) {
|
|
276
|
+
console.warn(` ✗ manifest module ${moduleKey}: ${(e as Error).message}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const loadersDir = path.join(pkgDir, "src", "loaders");
|
|
283
|
+
for (const filePath of walkTsFiles(loadersDir)) {
|
|
284
|
+
const rel = path.relative(loadersDir, filePath).replaceAll("\\", "/");
|
|
285
|
+
const cmsKey = `${namespace}/loaders/${rel}`;
|
|
286
|
+
try {
|
|
287
|
+
const schema = extractFileLoaderSchema(project.addSourceFileAtPath(filePath));
|
|
288
|
+
if (schema) loaders[cmsKey] = schema;
|
|
289
|
+
} catch (e) {
|
|
290
|
+
console.warn(` ✗ loader ${cmsKey}: ${(e as Error).message}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
loaders: withKeyAliases(loaders),
|
|
296
|
+
actions: withKeyAliases(actions),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
// Module rendering
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
/** Render the schemas.gen.ts module. Schemas are deduplicated: each unique
|
|
305
|
+
* schema is emitted once and shared by all keys aliasing it. */
|
|
306
|
+
export function renderSchemasModule(result: AppSchemasResult): string {
|
|
307
|
+
const constByJson = new Map<string, string>();
|
|
308
|
+
const constDecls: string[] = [];
|
|
309
|
+
|
|
310
|
+
const constFor = (schema: AppBlockPropsSchema): string => {
|
|
311
|
+
const json = JSON.stringify(schema);
|
|
312
|
+
let name = constByJson.get(json);
|
|
313
|
+
if (!name) {
|
|
314
|
+
name = `s${constByJson.size}`;
|
|
315
|
+
constByJson.set(json, name);
|
|
316
|
+
constDecls.push(`const ${name}: BlockPropsSchema = ${JSON.stringify(schema, null, "\t")};`);
|
|
317
|
+
}
|
|
318
|
+
return name;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const renderRecord = (schemas: Record<string, AppBlockPropsSchema>): string => {
|
|
322
|
+
const entries = Object.keys(schemas)
|
|
323
|
+
.sort()
|
|
324
|
+
.map((key) => `\t${JSON.stringify(key)}: ${constFor(schemas[key])},`);
|
|
325
|
+
return `{\n${entries.join("\n")}\n}`;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// Records must render before constDecls is final, but constDecls prints first.
|
|
329
|
+
const loadersRecord = renderRecord(result.loaders);
|
|
330
|
+
const actionsRecord = renderRecord(result.actions);
|
|
331
|
+
|
|
332
|
+
return [
|
|
333
|
+
"// AUTO-GENERATED by @decocms/blocks-cli scripts/generate-app-schemas.ts — DO NOT EDIT",
|
|
334
|
+
"// Regenerate from this package's root: bun run generate:schemas",
|
|
335
|
+
"//",
|
|
336
|
+
"// Real props schemas for this app's loaders/actions, keyed by CMS key",
|
|
337
|
+
"// (both bare and `.ts` forms). Registered into the admin meta via",
|
|
338
|
+
"// registerAppSchemas() so the Studio can render real props forms instead",
|
|
339
|
+
"// of the __resolveType-only stubs from registerCommerceLoaders().",
|
|
340
|
+
'import type { BlockPropsSchema } from "@decocms/blocks/cms/client";',
|
|
341
|
+
"",
|
|
342
|
+
...constDecls,
|
|
343
|
+
"",
|
|
344
|
+
`export const loaderSchemas: Record<string, BlockPropsSchema> = ${loadersRecord};`,
|
|
345
|
+
"",
|
|
346
|
+
`export const actionSchemas: Record<string, BlockPropsSchema> = ${actionsRecord};`,
|
|
347
|
+
"",
|
|
348
|
+
].join("\n");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
// CLI entry
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
function isMainModule(): boolean {
|
|
356
|
+
const entry = process.argv[1];
|
|
357
|
+
if (!entry) return false;
|
|
358
|
+
try {
|
|
359
|
+
return fs.realpathSync(path.resolve(entry)) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
360
|
+
} catch {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (isMainModule()) {
|
|
366
|
+
const argv = process.argv.slice(2);
|
|
367
|
+
const arg = (name: string, fallback: string): string => {
|
|
368
|
+
const idx = argv.indexOf(`--${name}`);
|
|
369
|
+
return idx !== -1 && argv[idx + 1] ? argv[idx + 1] : fallback;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const pkgDir = path.resolve(arg("pkg", process.cwd()));
|
|
373
|
+
const pkgJsonPath = path.join(pkgDir, "package.json");
|
|
374
|
+
const pkgName: string = fs.existsSync(pkgJsonPath)
|
|
375
|
+
? (JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8")).name ?? "")
|
|
376
|
+
: "";
|
|
377
|
+
const defaultNamespace = pkgName.match(/^@decocms\/apps-(.+)$/)?.[1] ?? "";
|
|
378
|
+
const namespace = arg("namespace", defaultNamespace);
|
|
379
|
+
if (!namespace) {
|
|
380
|
+
console.error(
|
|
381
|
+
"Could not derive --namespace from package.json (expected @decocms/apps-<ns>); pass --namespace explicitly.",
|
|
382
|
+
);
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const outPath = path.resolve(pkgDir, arg("out", "src/schemas.gen.ts"));
|
|
387
|
+
const result = generateAppSchemas(pkgDir, namespace);
|
|
388
|
+
fs.writeFileSync(outPath, renderSchemasModule(result));
|
|
389
|
+
|
|
390
|
+
const count = (r: Record<string, unknown>) => Object.keys(r).length;
|
|
391
|
+
console.log(
|
|
392
|
+
`Generated ${count(result.loaders)} loader keys, ${count(result.actions)} action keys → ${path.relative(pkgDir, outPath)}`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
@@ -284,6 +284,23 @@ const REACT_INTERNAL_PROPS = new Set([
|
|
|
284
284
|
"_store",
|
|
285
285
|
]);
|
|
286
286
|
|
|
287
|
+
// Platform types injected at runtime, never configured through the CMS form.
|
|
288
|
+
// Collapsed to a hidden object in typeToJsonSchema (guarded by "is it really
|
|
289
|
+
// the lib declaration" so same-named site types still expand).
|
|
290
|
+
const RUNTIME_INJECTED_TYPES = new Set([
|
|
291
|
+
"URL",
|
|
292
|
+
"URLSearchParams",
|
|
293
|
+
"Request",
|
|
294
|
+
"Response",
|
|
295
|
+
"Headers",
|
|
296
|
+
"AbortSignal",
|
|
297
|
+
"ReadableStream",
|
|
298
|
+
"WritableStream",
|
|
299
|
+
"Blob",
|
|
300
|
+
"File",
|
|
301
|
+
"FormData",
|
|
302
|
+
]);
|
|
303
|
+
|
|
287
304
|
interface GenerationContext {
|
|
288
305
|
outputTypeToLoaderKeys: Map<string, string[]>;
|
|
289
306
|
}
|
|
@@ -409,6 +426,18 @@ export function typeToJsonSchema(type: Type, visited = new Set<string>(), ctx?:
|
|
|
409
426
|
}
|
|
410
427
|
|
|
411
428
|
if (type.isObject() || type.isInterface()) {
|
|
429
|
+
// Runtime-injected platform types (loader `url: URL`, `req: Request`, …)
|
|
430
|
+
// are not CMS-configurable — hide instead of expanding the whole DOM
|
|
431
|
+
// interface into the form. Only collapse the real lib types: a site
|
|
432
|
+
// interface merely NAMED `Request` should still be expanded.
|
|
433
|
+
const symName = type.getSymbol()?.getName();
|
|
434
|
+
if (symName && RUNTIME_INJECTED_TYPES.has(symName)) {
|
|
435
|
+
const declFile = type.getSymbol()?.getDeclarations()?.[0]?.getSourceFile().getFilePath();
|
|
436
|
+
if (declFile?.includes("typescript/lib/") || declFile?.includes("@types/")) {
|
|
437
|
+
return { type: "object", hide: "true" };
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
412
441
|
// Record<K,V> → { type: "object", additionalProperties: V-schema }
|
|
413
442
|
const stringIdx = type.getStringIndexType();
|
|
414
443
|
const numberIdx = type.getNumberIndexType();
|
|
@@ -435,6 +464,12 @@ export function typeToJsonSchema(type: Type, visited = new Set<string>(), ctx?:
|
|
|
435
464
|
if (!decl) continue;
|
|
436
465
|
const propType = prop.getTypeAtLocation(decl);
|
|
437
466
|
|
|
467
|
+
// Methods (e.g. URL#toString on platform-ish objects) aren't data —
|
|
468
|
+
// a pure function type has no place in a props form.
|
|
469
|
+
if (propType.getCallSignatures().length > 0 && propType.getProperties().length === 0) {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
|
|
438
473
|
const tags = getJsDocTags(prop);
|
|
439
474
|
if (tags.ignore) continue;
|
|
440
475
|
|
|
@@ -527,7 +562,9 @@ export function typeToJsonSchema(type: Type, visited = new Set<string>(), ctx?:
|
|
|
527
562
|
if (!schema.title) schema.title = name.charAt(0).toUpperCase() + name.slice(1);
|
|
528
563
|
|
|
529
564
|
properties[name] = schema;
|
|
530
|
-
|
|
565
|
+
// A hidden prop (runtime-injected type, ReactNode, @hide) can never be
|
|
566
|
+
// filled in by the CMS user — requiring it would deadlock the form.
|
|
567
|
+
if (!prop.isOptional() && schema.hide !== "true") required.push(name);
|
|
531
568
|
}
|
|
532
569
|
|
|
533
570
|
const result: any = { type: "object", properties };
|
|
@@ -548,7 +585,7 @@ export function typeToJsonSchema(type: Type, visited = new Set<string>(), ctx?:
|
|
|
548
585
|
}
|
|
549
586
|
}
|
|
550
587
|
|
|
551
|
-
function getJsDocTags(symbol: MorphSymbol): Record<string, string> {
|
|
588
|
+
export function getJsDocTags(symbol: MorphSymbol): Record<string, string> {
|
|
552
589
|
const tags: Record<string, string> = {};
|
|
553
590
|
for (const decl of symbol.getDeclarations()) {
|
|
554
591
|
const jsDocs = Node.isJSDocable(decl) ? decl.getJsDocs() : [];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { MigrationContext } from "../types";
|
|
3
|
+
import { createContext } from "../types";
|
|
4
|
+
import { generateRoutes } from "./routes";
|
|
5
|
+
|
|
6
|
+
function makeCtx(platform: MigrationContext["platform"]): MigrationContext {
|
|
7
|
+
const ctx = createContext("/tmp/routes-template-fixture-site");
|
|
8
|
+
ctx.siteName = "acme-storefront";
|
|
9
|
+
ctx.platform = platform;
|
|
10
|
+
ctx.vtexAccount = platform === "vtex" ? "acme" : null;
|
|
11
|
+
return ctx;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Regression guard: the scaffolded deco admin route files must use the
|
|
16
|
+
* dev-HMR-safe `*RouteConfig()` factories — the only form @decocms/tanstack
|
|
17
|
+
* exports since 7.10.0 — never the removed pre-7.10.0 module-scope literals
|
|
18
|
+
* passed by reference. router-core's `update()` mutates the options object it
|
|
19
|
+
* receives (injects id/path); a shared literal got polluted on first
|
|
20
|
+
* execution, and any dev-HMR re-execution of the route file then threw
|
|
21
|
+
* "Route cannot have both an 'id' and a 'path' option", 500ing every route
|
|
22
|
+
* until the dev server restarted.
|
|
23
|
+
*/
|
|
24
|
+
describe("scaffolded deco admin routes use HMR-safe factories", () => {
|
|
25
|
+
const routeCases = [
|
|
26
|
+
{ file: "src/routes/deco/meta.ts", factory: "decoMetaRouteConfig", literal: "decoMetaRoute" },
|
|
27
|
+
{
|
|
28
|
+
file: "src/routes/deco/render.ts",
|
|
29
|
+
factory: "decoRenderRouteConfig",
|
|
30
|
+
literal: "decoRenderRoute",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
file: "src/routes/deco/invoke.$.ts",
|
|
34
|
+
factory: "decoInvokeRouteConfig",
|
|
35
|
+
literal: "decoInvokeRoute",
|
|
36
|
+
},
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
for (const platform of ["vtex", "custom"] as const) {
|
|
40
|
+
describe(`platform: ${platform}`, () => {
|
|
41
|
+
const files = generateRoutes(makeCtx(platform));
|
|
42
|
+
|
|
43
|
+
for (const { file, factory, literal } of routeCases) {
|
|
44
|
+
it(`${file} calls ${factory}() and never passes ${literal} by reference`, () => {
|
|
45
|
+
const content = files[file];
|
|
46
|
+
expect(content, `${file} must be emitted`).toBeTypeOf("string");
|
|
47
|
+
|
|
48
|
+
// Factory form: createFileRoute("...")(decoXRouteConfig())
|
|
49
|
+
expect(content).toContain(`${factory}()`);
|
|
50
|
+
expect(content).toContain(`import { ${factory} } from "@decocms/tanstack"`);
|
|
51
|
+
|
|
52
|
+
// Forbidden form: createFileRoute("...")(decoXRoute) — shared
|
|
53
|
+
// literal by reference. `(?!Config)` keeps the factory call legal.
|
|
54
|
+
expect(content).not.toMatch(new RegExp(`\\)\\(${literal}(?!Config)\\s*\\)`));
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
});
|
|
@@ -210,26 +210,34 @@ function NotFoundPage() {
|
|
|
210
210
|
`;
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
// The deco admin routes use the `*RouteConfig()` factories — the only form
|
|
214
|
+
// @decocms/tanstack exports since 7.10.0. The pre-7.10.0 module-scope
|
|
215
|
+
// literals (decoMetaRoute/decoRenderRoute/decoInvokeRoute) were removed:
|
|
216
|
+
// passed by reference, router-core's update() mutated the shared object
|
|
217
|
+
// (injecting id/path), and any dev-HMR re-execution of the route file then
|
|
218
|
+
// threw "Route cannot have both an 'id' and a 'path' option", 500ing every
|
|
219
|
+
// route until dev restart.
|
|
220
|
+
|
|
213
221
|
function generateDecoMeta(): string {
|
|
214
222
|
return `import { createFileRoute } from "@tanstack/react-router";
|
|
215
|
-
import {
|
|
223
|
+
import { decoMetaRouteConfig } from "@decocms/tanstack";
|
|
216
224
|
|
|
217
|
-
export const Route = createFileRoute("/deco/meta")(
|
|
225
|
+
export const Route = createFileRoute("/deco/meta")(decoMetaRouteConfig());
|
|
218
226
|
`;
|
|
219
227
|
}
|
|
220
228
|
|
|
221
229
|
function generateDecoInvoke(): string {
|
|
222
230
|
return `import { createFileRoute } from "@tanstack/react-router";
|
|
223
|
-
import {
|
|
231
|
+
import { decoInvokeRouteConfig } from "@decocms/tanstack";
|
|
224
232
|
|
|
225
|
-
export const Route = createFileRoute("/deco/invoke/$")(
|
|
233
|
+
export const Route = createFileRoute("/deco/invoke/$")(decoInvokeRouteConfig());
|
|
226
234
|
`;
|
|
227
235
|
}
|
|
228
236
|
|
|
229
237
|
function generateDecoRender(): string {
|
|
230
238
|
return `import { createFileRoute } from "@tanstack/react-router";
|
|
231
|
-
import {
|
|
239
|
+
import { decoRenderRouteConfig } from "@decocms/tanstack";
|
|
232
240
|
|
|
233
|
-
export const Route = createFileRoute("/deco/render")(
|
|
241
|
+
export const Route = createFileRoute("/deco/render")(decoRenderRouteConfig());
|
|
234
242
|
`;
|
|
235
243
|
}
|