@feeef.dev/cli 0.2.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/LICENSE +6 -0
- package/README.md +94 -0
- package/dist/bin.js +2752 -0
- package/dist/bin.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +2730 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
- package/scaffolds/blank/.cursor/rules/feeef-theme.mdc +34 -0
- package/scaffolds/blank/.cursor/rules/filterator.mdc +13 -0
- package/scaffolds/blank/.cursor/rules/order-form.mdc +17 -0
- package/scaffolds/blank/.cursor/rules/theme-source.mdc +32 -0
- package/scaffolds/blank/.cursor/skills/feeef-filterator/SKILL.md +15 -0
- package/scaffolds/blank/.cursor/skills/feeef-theme/SKILL.md +56 -0
- package/scaffolds/blank/.cursor/skills/feeef-theme/reference.md +73 -0
- package/scaffolds/blank/.vscode/extensions.json +3 -0
- package/scaffolds/blank/.vscode/settings.json +13 -0
- package/scaffolds/blank/AGENTS.md +104 -0
- package/scaffolds/blank/README.md +21 -0
- package/scaffolds/blank/docs/00-INDEX.md +33 -0
- package/scaffolds/blank/docs/03-CUSTOM-COMPONENTS.md +116 -0
- package/scaffolds/blank/docs/04-WORKFLOW.md +129 -0
- package/scaffolds/blank/docs/05-ANTI-PATTERNS.md +80 -0
- package/scaffolds/blank/docs/07-SHARED-COMPONENTS.md +144 -0
- package/scaffolds/blank/docs/08-ORDER-FORM.md +376 -0
- package/scaffolds/blank/docs/09-THEME-PORTING.md +211 -0
- package/scaffolds/blank/docs/10-SCHEMA-LIBRARY.md +127 -0
- package/scaffolds/blank/docs/11-PAGES-CHECKOUT-THANKS-PRODUCT.md +81 -0
- package/scaffolds/blank/docs/12-DARK-LIGHT-THEME.md +164 -0
- package/scaffolds/blank/docs/13-TEMPLATE-I18N.md +320 -0
- package/scaffolds/blank/docs/14-FILTERATOR.md +433 -0
- package/scaffolds/blank/docs/16-AUTHORING-TS-IMPORTS.md +73 -0
- package/scaffolds/blank/feeef.template.json +7 -0
- package/scaffolds/blank/locales/README.md +4 -0
- package/scaffolds/blank/locales/ar.json +10 -0
- package/scaffolds/blank/locales/en.json +10 -0
- package/scaffolds/blank/package-lock.json +1975 -0
- package/scaffolds/blank/package.json +23 -0
- package/scaffolds/blank/pages/home/components/hero.tsx +34 -0
- package/scaffolds/blank/pages/home/page.json +3 -0
- package/scaffolds/blank/props.json +6 -0
- package/scaffolds/blank/schema.ts +49 -0
- package/scaffolds/blank/tsconfig.json +24 -0
- package/scaffolds/blank/types/feeef-live-scope.d.ts +162 -0
- package/scaffolds/blank/types/feeef-template-schema.d.ts +84 -0
- package/vendor/template-kit/README.md +64 -0
- package/vendor/template-kit/bin/dev.mjs +61 -0
- package/vendor/template-kit/bin/feeef-template.mjs +212 -0
- package/vendor/template-kit/data/lithium-schema.json +4733 -0
- package/vendor/template-kit/lib/build.mjs +377 -0
- package/vendor/template-kit/lib/compile-component.mjs +188 -0
- package/vendor/template-kit/lib/component-meta.mjs +481 -0
- package/vendor/template-kit/lib/library.mjs +168 -0
- package/vendor/template-kit/lib/locales.mjs +68 -0
- package/vendor/template-kit/lib/paths.mjs +84 -0
- package/vendor/template-kit/lib/shared.mjs +268 -0
- package/vendor/template-kit/lib/theme-schema.mjs +300 -0
- package/vendor/template-kit/lib/unpack.mjs +324 -0
- package/vendor/template-kit/lib/validate.mjs +207 -0
- package/vendor/template-kit/schemas/component.schema.json +55 -0
- package/vendor/template-kit/schemas/template.schema.json +18 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path + naming helpers for the Feeef template source tree.
|
|
3
|
+
*
|
|
4
|
+
* Folder names prefer `instanceId`; when missing we fall back to a stable
|
|
5
|
+
* slug from `title` / `type` plus a short hash so siblings never collide.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import crypto from "node:crypto";
|
|
10
|
+
|
|
11
|
+
/** Top-level component fields that live in `export const meta` / component.json. */
|
|
12
|
+
export const COMPONENT_META_KEYS = [
|
|
13
|
+
"type",
|
|
14
|
+
"instanceId",
|
|
15
|
+
"title",
|
|
16
|
+
"propsSchema",
|
|
17
|
+
"slotsSchema",
|
|
18
|
+
"slotsLayout",
|
|
19
|
+
"props",
|
|
20
|
+
"refId",
|
|
21
|
+
"refVersion",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} value
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
export function slugify(value) {
|
|
29
|
+
const s = String(value ?? "")
|
|
30
|
+
.trim()
|
|
31
|
+
.toLowerCase()
|
|
32
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
33
|
+
.replace(/^-+|-+$/g, "");
|
|
34
|
+
return s || "component";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Stable short hash for collision-safe folder names.
|
|
39
|
+
* @param {string} input
|
|
40
|
+
*/
|
|
41
|
+
export function shortHash(input) {
|
|
42
|
+
return crypto.createHash("sha1").update(input).digest("hex").slice(0, 8);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {Record<string, any>} component
|
|
47
|
+
* @param {number} index
|
|
48
|
+
* @param {Set<string>} usedNames
|
|
49
|
+
*/
|
|
50
|
+
export function folderNameForComponent(component, index, usedNames) {
|
|
51
|
+
const base =
|
|
52
|
+
(component.instanceId && slugify(component.instanceId)) ||
|
|
53
|
+
(component.title && slugify(component.title)) ||
|
|
54
|
+
slugify(component.type || "component");
|
|
55
|
+
|
|
56
|
+
let name = base;
|
|
57
|
+
if (usedNames.has(name)) {
|
|
58
|
+
const id = component.instanceId || `${component.type || "c"}-${index}`;
|
|
59
|
+
name = `${base}-${shortHash(String(id))}`;
|
|
60
|
+
}
|
|
61
|
+
// Still colliding (rare) — append index.
|
|
62
|
+
let n = 2;
|
|
63
|
+
while (usedNames.has(name)) {
|
|
64
|
+
name = `${base}-${n++}`;
|
|
65
|
+
}
|
|
66
|
+
usedNames.add(name);
|
|
67
|
+
return name;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve a path relative to cwd, expanding ~ is not needed for this kit.
|
|
72
|
+
* @param {string} p
|
|
73
|
+
*/
|
|
74
|
+
export function resolveFromCwd(p) {
|
|
75
|
+
return path.resolve(process.cwd(), p);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Pretty relative path for logs.
|
|
80
|
+
* @param {string} abs
|
|
81
|
+
*/
|
|
82
|
+
export function rel(abs) {
|
|
83
|
+
return path.relative(process.cwd(), abs) || ".";
|
|
84
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme-local shared component library (compile-time).
|
|
3
|
+
*
|
|
4
|
+
* Placements reference a shared definition with:
|
|
5
|
+
* { "$ref": "shared.footer", "instanceId": "…", "props": { …overrides } }
|
|
6
|
+
* or shorthand:
|
|
7
|
+
* { "shared": "footer", "instanceId": "…", "props": { … } }
|
|
8
|
+
*
|
|
9
|
+
* Build expands the ref into a full TemplateDataComponent (deep-merge props).
|
|
10
|
+
* This is the authoring-time twin of runtime `type:"reference"` (backend library).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { rel } from "./paths.mjs";
|
|
16
|
+
import {
|
|
17
|
+
componentIdFromPath,
|
|
18
|
+
listComponentEntries,
|
|
19
|
+
} from "./component-meta.mjs";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Deep-merge plain objects. Arrays and scalars from `override` replace.
|
|
23
|
+
* @param {any} base
|
|
24
|
+
* @param {any} override
|
|
25
|
+
*/
|
|
26
|
+
export function deepMerge(base, override) {
|
|
27
|
+
if (override === undefined) {
|
|
28
|
+
return base === undefined ? undefined : structuredClone(base);
|
|
29
|
+
}
|
|
30
|
+
if (
|
|
31
|
+
override === null ||
|
|
32
|
+
typeof override !== "object" ||
|
|
33
|
+
Array.isArray(override)
|
|
34
|
+
) {
|
|
35
|
+
return structuredClone(override);
|
|
36
|
+
}
|
|
37
|
+
if (base === null || typeof base !== "object" || Array.isArray(base)) {
|
|
38
|
+
return structuredClone(override);
|
|
39
|
+
}
|
|
40
|
+
/** @type {Record<string, any>} */
|
|
41
|
+
const out = structuredClone(base);
|
|
42
|
+
for (const [key, value] of Object.entries(override)) {
|
|
43
|
+
if (
|
|
44
|
+
value &&
|
|
45
|
+
typeof value === "object" &&
|
|
46
|
+
!Array.isArray(value) &&
|
|
47
|
+
out[key] &&
|
|
48
|
+
typeof out[key] === "object" &&
|
|
49
|
+
!Array.isArray(out[key])
|
|
50
|
+
) {
|
|
51
|
+
out[key] = deepMerge(out[key], value);
|
|
52
|
+
} else {
|
|
53
|
+
out[key] = structuredClone(value);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {Record<string, any>} component
|
|
61
|
+
* @returns {string | null}
|
|
62
|
+
*/
|
|
63
|
+
export function sharedIdFromComponent(component) {
|
|
64
|
+
if (!component || typeof component !== "object") return null;
|
|
65
|
+
if (typeof component.shared === "string" && component.shared.trim()) {
|
|
66
|
+
return component.shared.trim().replace(/^shared\./, "");
|
|
67
|
+
}
|
|
68
|
+
if (typeof component.$ref === "string" && component.$ref.trim()) {
|
|
69
|
+
const ref = component.$ref.trim();
|
|
70
|
+
if (ref.startsWith("shared.")) return ref.slice("shared.".length);
|
|
71
|
+
// Allow bare "$ref": "footer" only when paired with convention — prefer shared.
|
|
72
|
+
if (!ref.includes(".")) return ref;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Load every component under `<srcDir>/shared/components/` (flat or folder).
|
|
79
|
+
* @param {string} srcDir
|
|
80
|
+
* @param {(dir: string, fileStack: string[], ctx: { sharedCatalog: Map<string, any>, resolving: Set<string> }) => Record<string, any>} readComponentFn
|
|
81
|
+
* @returns {Map<string, Record<string, any>>}
|
|
82
|
+
*/
|
|
83
|
+
export function loadSharedCatalog(srcDir, readComponentFn) {
|
|
84
|
+
/** @type {Map<string, Record<string, any>>} */
|
|
85
|
+
const catalog = new Map();
|
|
86
|
+
const sharedRoot = path.join(srcDir, "shared", "components");
|
|
87
|
+
if (!fs.existsSync(sharedRoot)) return catalog;
|
|
88
|
+
|
|
89
|
+
const entries = listComponentEntries(sharedRoot);
|
|
90
|
+
const ids = entries.map((p) => componentIdFromPath(p));
|
|
91
|
+
|
|
92
|
+
const fileStack = [];
|
|
93
|
+
const resolving = new Set();
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < entries.length; i++) {
|
|
96
|
+
const id = ids[i];
|
|
97
|
+
const entryPath = entries[i];
|
|
98
|
+
// First pass: read without resolving nested shared (catalog incomplete).
|
|
99
|
+
catalog.set(
|
|
100
|
+
id,
|
|
101
|
+
readComponentFn(entryPath, fileStack, {
|
|
102
|
+
sharedCatalog: catalog,
|
|
103
|
+
resolving,
|
|
104
|
+
deferShared: true,
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Second pass: resolve $ref inside shared definitions (composable shared).
|
|
110
|
+
for (const id of ids) {
|
|
111
|
+
const raw = catalog.get(id);
|
|
112
|
+
catalog.set(
|
|
113
|
+
id,
|
|
114
|
+
resolveSharedInTree(raw, catalog, resolving, `shared/${id}`),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return catalog;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Expand a placement that points at a shared id into a full component.
|
|
123
|
+
* @param {Record<string, any>} placement
|
|
124
|
+
* @param {Map<string, Record<string, any>>} catalog
|
|
125
|
+
* @param {Set<string>} resolving
|
|
126
|
+
* @param {string} pathHint
|
|
127
|
+
*/
|
|
128
|
+
export function resolveSharedPlacement(
|
|
129
|
+
placement,
|
|
130
|
+
catalog,
|
|
131
|
+
resolving,
|
|
132
|
+
pathHint = "component",
|
|
133
|
+
) {
|
|
134
|
+
const id = sharedIdFromComponent(placement);
|
|
135
|
+
if (!id) return placement;
|
|
136
|
+
|
|
137
|
+
if (resolving.has(id)) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`Shared component cycle detected at ${pathHint} → shared.${id}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (!catalog.has(id)) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Unknown shared component "shared.${id}" at ${pathHint}. ` +
|
|
145
|
+
`Add shared/components/${id}/ or fix the $ref.`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
resolving.add(id);
|
|
150
|
+
const base = structuredClone(catalog.get(id));
|
|
151
|
+
resolving.delete(id);
|
|
152
|
+
|
|
153
|
+
const {
|
|
154
|
+
$ref: _ref,
|
|
155
|
+
shared: _shared,
|
|
156
|
+
order: _order,
|
|
157
|
+
$schema: _schema,
|
|
158
|
+
...overrides
|
|
159
|
+
} = placement;
|
|
160
|
+
|
|
161
|
+
/** Fields that always come from the placement when present */
|
|
162
|
+
const instanceKeys = ["instanceId", "title", "refId", "refVersion"];
|
|
163
|
+
|
|
164
|
+
/** @type {Record<string, any>} */
|
|
165
|
+
const merged = { ...base };
|
|
166
|
+
|
|
167
|
+
for (const key of instanceKeys) {
|
|
168
|
+
if (Object.prototype.hasOwnProperty.call(overrides, key)) {
|
|
169
|
+
merged[key] = overrides[key];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (Object.prototype.hasOwnProperty.call(overrides, "props")) {
|
|
174
|
+
merged.props = deepMerge(base.props || {}, overrides.props || {});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Explicit slot / children overrides replace (not deep-merge trees).
|
|
178
|
+
if (Object.prototype.hasOwnProperty.call(overrides, "slots")) {
|
|
179
|
+
merged.slots = overrides.slots;
|
|
180
|
+
}
|
|
181
|
+
if (Object.prototype.hasOwnProperty.call(overrides, "children")) {
|
|
182
|
+
merged.children = overrides.children;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Allow rare full overrides of schemas/code from placement (escape hatch).
|
|
186
|
+
for (const key of [
|
|
187
|
+
"type",
|
|
188
|
+
"code",
|
|
189
|
+
"propsSchema",
|
|
190
|
+
"slotsSchema",
|
|
191
|
+
"slotsLayout",
|
|
192
|
+
]) {
|
|
193
|
+
if (
|
|
194
|
+
Object.prototype.hasOwnProperty.call(overrides, key) &&
|
|
195
|
+
overrides[key] !== undefined
|
|
196
|
+
) {
|
|
197
|
+
merged[key] = overrides[key];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Strip authoring-only keys from output.
|
|
202
|
+
delete merged.$ref;
|
|
203
|
+
delete merged.shared;
|
|
204
|
+
delete merged.$schema;
|
|
205
|
+
delete merged.order;
|
|
206
|
+
|
|
207
|
+
return merged;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Walk a component tree and expand any shared refs.
|
|
212
|
+
* @param {any} node
|
|
213
|
+
* @param {Map<string, Record<string, any>>} catalog
|
|
214
|
+
* @param {Set<string>} resolving
|
|
215
|
+
* @param {string} pathHint
|
|
216
|
+
*/
|
|
217
|
+
export function resolveSharedInTree(node, catalog, resolving, pathHint) {
|
|
218
|
+
if (!node || typeof node !== "object") return node;
|
|
219
|
+
|
|
220
|
+
let current = node;
|
|
221
|
+
if (sharedIdFromComponent(current)) {
|
|
222
|
+
current = resolveSharedPlacement(current, catalog, resolving, pathHint);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (Array.isArray(current.children)) {
|
|
226
|
+
current.children = current.children.map((c, i) =>
|
|
227
|
+
resolveSharedInTree(c, catalog, resolving, `${pathHint}/children[${i}]`),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (current.slots && typeof current.slots === "object") {
|
|
231
|
+
/** @type {Record<string, any[]>} */
|
|
232
|
+
const next = {};
|
|
233
|
+
for (const [slotId, list] of Object.entries(current.slots)) {
|
|
234
|
+
if (!Array.isArray(list)) {
|
|
235
|
+
next[slotId] = list;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
next[slotId] = list.map((c, i) =>
|
|
239
|
+
resolveSharedInTree(
|
|
240
|
+
c,
|
|
241
|
+
catalog,
|
|
242
|
+
resolving,
|
|
243
|
+
`${pathHint}/slots/${slotId}[${i}]`,
|
|
244
|
+
),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
current.slots = next;
|
|
248
|
+
}
|
|
249
|
+
return current;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* @param {string} srcDir
|
|
254
|
+
* @returns {string[]}
|
|
255
|
+
*/
|
|
256
|
+
export function listSharedIds(srcDir) {
|
|
257
|
+
const sharedRoot = path.join(srcDir, "shared", "components");
|
|
258
|
+
if (!fs.existsSync(sharedRoot)) return [];
|
|
259
|
+
return listComponentEntries(sharedRoot).map((p) => componentIdFromPath(p));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Human-readable path helper for errors.
|
|
264
|
+
* @param {string} abs
|
|
265
|
+
*/
|
|
266
|
+
export function sharedRel(abs) {
|
|
267
|
+
return rel(abs);
|
|
268
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme-owned TemplateSchema: load overlay, merge Lithium built-ins, package.
|
|
3
|
+
*
|
|
4
|
+
* Overlay (preferred): `schema.ts` with `export const schema = … satisfies FeeefThemeSchema`
|
|
5
|
+
* Fallback: `schema.json`
|
|
6
|
+
*
|
|
7
|
+
* Lithium base resolution:
|
|
8
|
+
* FEEEF_LITHIUM_SCHEMA → ../storefront/template/schema.json → vendored data/lithium-schema.json
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import vm from "node:vm";
|
|
14
|
+
import { createRequire } from "node:module";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { rel } from "./paths.mjs";
|
|
17
|
+
import { attachLibraryToSchema } from "./library.mjs";
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @returns {typeof import('esbuild') | null}
|
|
24
|
+
*/
|
|
25
|
+
function loadEsbuild() {
|
|
26
|
+
try {
|
|
27
|
+
return require("esbuild");
|
|
28
|
+
} catch {
|
|
29
|
+
try {
|
|
30
|
+
const cliRoot = path.resolve(here, "../../..");
|
|
31
|
+
return createRequire(path.join(cliRoot, "package.json"))("esbuild");
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} filePath
|
|
40
|
+
* @returns {any}
|
|
41
|
+
*/
|
|
42
|
+
function readJson(filePath) {
|
|
43
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolve Lithium built-in schema JSON path.
|
|
48
|
+
* @returns {string | null}
|
|
49
|
+
*/
|
|
50
|
+
export function resolveLithiumSchemaPath() {
|
|
51
|
+
const env = process.env.FEEEF_LITHIUM_SCHEMA?.trim();
|
|
52
|
+
if (env) {
|
|
53
|
+
const abs = path.isAbsolute(env) ? env : path.resolve(process.cwd(), env);
|
|
54
|
+
if (fs.existsSync(abs)) return abs;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Monorepo: cli/vendor/template-kit/lib → cli → feeef.all → storefront
|
|
58
|
+
const monorepo = path.resolve(here, "../../../../storefront/template/schema.json");
|
|
59
|
+
if (fs.existsSync(monorepo)) return monorepo;
|
|
60
|
+
|
|
61
|
+
// Alternate: storefront/template-kit/lib → storefront/template/schema.json
|
|
62
|
+
const sfSibling = path.resolve(here, "../../template/schema.json");
|
|
63
|
+
if (fs.existsSync(sfSibling)) return sfSibling;
|
|
64
|
+
|
|
65
|
+
const vendored = path.join(here, "../data/lithium-schema.json");
|
|
66
|
+
if (fs.existsSync(vendored)) return vendored;
|
|
67
|
+
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @returns {Record<string, any>}
|
|
73
|
+
*/
|
|
74
|
+
export function loadLithiumBaseSchema() {
|
|
75
|
+
const p = resolveLithiumSchemaPath();
|
|
76
|
+
if (!p) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
"Lithium schema not found. Set FEEEF_LITHIUM_SCHEMA or ensure vendor/template-kit/data/lithium-schema.json exists.",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return readJson(p);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Evaluate `export const schema` from a .ts/.tsx file.
|
|
86
|
+
* @param {string} filePath
|
|
87
|
+
* @returns {Record<string, any>}
|
|
88
|
+
*/
|
|
89
|
+
function evaluateSchemaExport(filePath) {
|
|
90
|
+
const source = fs.readFileSync(filePath, "utf8");
|
|
91
|
+
if (!/\bexport\s+const\s+schema\b/.test(source)) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`${rel(filePath)}: expected \`export const schema\``,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const esbuild = loadEsbuild();
|
|
98
|
+
if (!esbuild) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Found schema.ts but esbuild is not installed (needed to evaluate ${rel(filePath)})`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const loader = filePath.endsWith(".tsx") ? "tsx" : "ts";
|
|
105
|
+
const transformed = esbuild.transformSync(source, {
|
|
106
|
+
loader,
|
|
107
|
+
format: "cjs",
|
|
108
|
+
target: "es2018",
|
|
109
|
+
logLevel: "silent",
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const module = { exports: /** @type {Record<string, any>} */ ({}) };
|
|
113
|
+
const sandbox = {
|
|
114
|
+
module,
|
|
115
|
+
exports: module.exports,
|
|
116
|
+
require: () => ({}),
|
|
117
|
+
console,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
vm.runInNewContext(transformed.code, sandbox, { filename: filePath });
|
|
122
|
+
} catch (err) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Failed to evaluate schema export in ${rel(filePath)}: ${err instanceof Error ? err.message : String(err)}`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const schema =
|
|
129
|
+
module.exports.schema ??
|
|
130
|
+
module.exports.default?.schema ??
|
|
131
|
+
module.exports.default;
|
|
132
|
+
|
|
133
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`${rel(filePath)}: export const schema must be an object`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return { ...schema };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Load theme overlay from schema.ts / schema.tsx / schema.json (or null).
|
|
143
|
+
* @param {string} srcDir
|
|
144
|
+
* @returns {Record<string, any> | null}
|
|
145
|
+
*/
|
|
146
|
+
export function loadThemeSchemaOverlay(srcDir) {
|
|
147
|
+
const ts = path.join(srcDir, "schema.ts");
|
|
148
|
+
const tsx = path.join(srcDir, "schema.tsx");
|
|
149
|
+
const json = path.join(srcDir, "schema.json");
|
|
150
|
+
|
|
151
|
+
if (fs.existsSync(ts)) return evaluateSchemaExport(ts);
|
|
152
|
+
if (fs.existsSync(tsx)) return evaluateSchemaExport(tsx);
|
|
153
|
+
if (fs.existsSync(json)) return readJson(json);
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Structural checks on an overlay (throws on hard errors).
|
|
159
|
+
* @param {Record<string, any>} overlay
|
|
160
|
+
* @param {string} hint
|
|
161
|
+
*/
|
|
162
|
+
export function validateThemeSchemaOverlay(overlay, hint = "schema") {
|
|
163
|
+
if (!overlay || typeof overlay !== "object") {
|
|
164
|
+
throw new Error(`${hint}: overlay must be an object`);
|
|
165
|
+
}
|
|
166
|
+
if (overlay.pages !== undefined) {
|
|
167
|
+
if (!overlay.pages || typeof overlay.pages !== "object" || Array.isArray(overlay.pages)) {
|
|
168
|
+
throw new Error(`${hint}: pages must be an object map`);
|
|
169
|
+
}
|
|
170
|
+
for (const [pageId, page] of Object.entries(overlay.pages)) {
|
|
171
|
+
if (!page || typeof page !== "object") {
|
|
172
|
+
throw new Error(`${hint}: pages.${pageId} must be an object`);
|
|
173
|
+
}
|
|
174
|
+
if (
|
|
175
|
+
page.sections === undefined ||
|
|
176
|
+
typeof page.sections !== "object" ||
|
|
177
|
+
Array.isArray(page.sections)
|
|
178
|
+
) {
|
|
179
|
+
throw new Error(`${hint}: pages.${pageId}.sections is required`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Merge Lithium base + theme overlay.
|
|
187
|
+
*
|
|
188
|
+
* Theme wins for: name, version, schema, propsSchema, pages (when provided).
|
|
189
|
+
* components: { ...base.components, ...overlay.components }
|
|
190
|
+
*
|
|
191
|
+
* @param {Record<string, any>} base
|
|
192
|
+
* @param {Record<string, any> | null} overlay
|
|
193
|
+
* @param {{ name?: string, version?: string }} [manifest]
|
|
194
|
+
* @returns {Record<string, any>}
|
|
195
|
+
*/
|
|
196
|
+
export function mergeThemeSchema(base, overlay, manifest = {}) {
|
|
197
|
+
if (overlay) validateThemeSchemaOverlay(overlay, "theme schema");
|
|
198
|
+
|
|
199
|
+
/** @type {Record<string, any>} */
|
|
200
|
+
const out = {
|
|
201
|
+
...base,
|
|
202
|
+
components: {
|
|
203
|
+
...(base.components && typeof base.components === "object"
|
|
204
|
+
? base.components
|
|
205
|
+
: {}),
|
|
206
|
+
...(overlay?.components && typeof overlay.components === "object"
|
|
207
|
+
? overlay.components
|
|
208
|
+
: {}),
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// Identity: overlay → manifest → base
|
|
213
|
+
if (typeof overlay?.name === "string" && overlay.name.trim()) {
|
|
214
|
+
out.name = overlay.name.trim();
|
|
215
|
+
} else if (typeof manifest.name === "string" && manifest.name.trim()) {
|
|
216
|
+
out.name = manifest.name.trim();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (typeof overlay?.version === "string" && overlay.version.trim()) {
|
|
220
|
+
out.version = overlay.version.trim();
|
|
221
|
+
} else if (typeof manifest.version === "string" && manifest.version.trim()) {
|
|
222
|
+
out.version = manifest.version.trim();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (overlay) {
|
|
226
|
+
if (overlay.schema !== undefined) {
|
|
227
|
+
out.schema = overlay.schema;
|
|
228
|
+
}
|
|
229
|
+
if (overlay.propsSchema && typeof overlay.propsSchema === "object") {
|
|
230
|
+
out.propsSchema = overlay.propsSchema;
|
|
231
|
+
}
|
|
232
|
+
if (overlay.pages && typeof overlay.pages === "object") {
|
|
233
|
+
out.pages = overlay.pages;
|
|
234
|
+
}
|
|
235
|
+
// Forward-compat extras (except reserved merge keys)
|
|
236
|
+
for (const [k, v] of Object.entries(overlay)) {
|
|
237
|
+
if (
|
|
238
|
+
[
|
|
239
|
+
"name",
|
|
240
|
+
"version",
|
|
241
|
+
"schema",
|
|
242
|
+
"propsSchema",
|
|
243
|
+
"pages",
|
|
244
|
+
"components",
|
|
245
|
+
"library",
|
|
246
|
+
].includes(k)
|
|
247
|
+
) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (v !== undefined) out[k] = v;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!out.name) out.name = "theme";
|
|
255
|
+
if (!out.version) out.version = "0.1.0";
|
|
256
|
+
if (!out.schema) out.schema = "1.0";
|
|
257
|
+
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Build packaged schema for dist/schema.json.
|
|
263
|
+
* @param {object} opts
|
|
264
|
+
* @param {string} opts.srcDir
|
|
265
|
+
* @param {{ components?: any[] }} [opts.libraryDoc]
|
|
266
|
+
* @returns {{ schema: Record<string, any>, overlay: Record<string, any> | null, warnings: string[] }}
|
|
267
|
+
*/
|
|
268
|
+
export function buildPackagedSchema({ srcDir, libraryDoc }) {
|
|
269
|
+
/** @type {string[]} */
|
|
270
|
+
const warnings = [];
|
|
271
|
+
/** @type {Record<string, any>} */
|
|
272
|
+
let manifest = {};
|
|
273
|
+
const manifestPath = path.join(srcDir, "feeef.template.json");
|
|
274
|
+
if (fs.existsSync(manifestPath)) {
|
|
275
|
+
try {
|
|
276
|
+
manifest = readJson(manifestPath);
|
|
277
|
+
} catch {
|
|
278
|
+
warnings.push("Could not parse feeef.template.json for schema name/version");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const base = loadLithiumBaseSchema();
|
|
283
|
+
const overlay = loadThemeSchemaOverlay(srcDir);
|
|
284
|
+
if (!overlay) {
|
|
285
|
+
warnings.push(
|
|
286
|
+
"No schema.ts / schema.json — using Lithium pages layout with manifest name/version",
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let schema = mergeThemeSchema(base, overlay, {
|
|
291
|
+
name: typeof manifest.name === "string" ? manifest.name : undefined,
|
|
292
|
+
version: typeof manifest.version === "string" ? manifest.version : undefined,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
if (libraryDoc) {
|
|
296
|
+
schema = attachLibraryToSchema(schema, libraryDoc);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return { schema, overlay, warnings };
|
|
300
|
+
}
|