@feeef.dev/cli 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -5
- package/dist/bin.js +127 -16
- package/dist/bin.js.map +1 -1
- package/dist/index.js +127 -16
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/scaffolds/blank/.cursor/rules/feeef-theme.mdc +9 -7
- package/scaffolds/blank/.cursor/rules/theme-source.mdc +24 -17
- package/scaffolds/blank/.cursor/skills/feeef-theme/SKILL.md +21 -9
- package/scaffolds/blank/.cursor/skills/feeef-theme/reference.md +35 -30
- package/scaffolds/blank/AGENTS.md +7 -4
- package/scaffolds/blank/README.md +1 -1
- package/scaffolds/blank/docs/03-CUSTOM-COMPONENTS.md +68 -39
- package/scaffolds/blank/docs/04-WORKFLOW.md +7 -4
- package/scaffolds/blank/docs/05-ANTI-PATTERNS.md +7 -6
- package/scaffolds/blank/docs/07-SHARED-COMPONENTS.md +45 -77
- package/scaffolds/blank/docs/08-ORDER-FORM.md +1 -1
- package/scaffolds/blank/docs/10-SCHEMA-LIBRARY.md +21 -69
- package/scaffolds/blank/docs/11-PAGES-CHECKOUT-THANKS-PRODUCT.md +37 -22
- package/scaffolds/blank/docs/16-AUTHORING-TS-IMPORTS.md +38 -14
- package/scaffolds/blank/package-lock.json +4 -1918
- package/vendor/template-kit/lib/build.mjs +1 -1
- package/vendor/template-kit/lib/component-meta.mjs +92 -8
- package/vendor/template-kit/lib/unpack.mjs +295 -135
- package/vendor/template-kit/lib/validate.mjs +1 -1
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Components may be:
|
|
8
8
|
* - Flat page-level: `pages/<page>/components/hero.tsx` → published as `sections.main`
|
|
9
9
|
* - Legacy sections: `pages/<page>/sections/<id>/components/…`
|
|
10
|
-
* - Flat or folder entries under any `components/` dir (`hero.tsx` or `hero/
|
|
10
|
+
* - Flat or folder entries under any `components/` dir (`hero.tsx` or `hero/hero.tsx`)
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import fs from "node:fs";
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Entries may be:
|
|
6
6
|
* - Flat file: `components/hero.tsx` (or `.jsx` / `.json`)
|
|
7
|
-
* - Folder: `components/hero/
|
|
7
|
+
* - Folder: `components/hero/hero.tsx` (+ optional nested slots/children)
|
|
8
|
+
* - Legacy folder: `components/hero/component.tsx`
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import fs from "node:fs";
|
|
@@ -64,6 +65,7 @@ export function isFlatComponentFile(root) {
|
|
|
64
65
|
|
|
65
66
|
/**
|
|
66
67
|
* Resolve the TSX/JSX source file for a component root (dir or flat file).
|
|
68
|
+
* Prefer `<id>/<id>.tsx`; fall back to legacy `component.tsx`.
|
|
67
69
|
* @param {string} root Absolute path to component folder or flat `.tsx`/`.jsx`/`.json`
|
|
68
70
|
* @returns {string | null}
|
|
69
71
|
*/
|
|
@@ -73,6 +75,11 @@ export function resolveComponentEntry(root) {
|
|
|
73
75
|
if (FLAT_CODE_EXT.has(ext)) return root;
|
|
74
76
|
return null;
|
|
75
77
|
}
|
|
78
|
+
const id = path.basename(root);
|
|
79
|
+
const namedTsx = path.join(root, `${id}.tsx`);
|
|
80
|
+
const namedJsx = path.join(root, `${id}.jsx`);
|
|
81
|
+
if (fs.existsSync(namedTsx)) return namedTsx;
|
|
82
|
+
if (fs.existsSync(namedJsx)) return namedJsx;
|
|
76
83
|
const tsx = path.join(root, "component.tsx");
|
|
77
84
|
const jsx = path.join(root, "component.jsx");
|
|
78
85
|
if (fs.existsSync(tsx)) return tsx;
|
|
@@ -80,6 +87,23 @@ export function resolveComponentEntry(root) {
|
|
|
80
87
|
return null;
|
|
81
88
|
}
|
|
82
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Resolve folder meta JSON (`<id>/<id>.json` or legacy `component.json`).
|
|
92
|
+
* @param {string} root Absolute component folder
|
|
93
|
+
* @returns {string | null}
|
|
94
|
+
*/
|
|
95
|
+
export function resolveComponentMetaJson(root) {
|
|
96
|
+
if (isFlatComponentFile(root)) {
|
|
97
|
+
return path.extname(root).toLowerCase() === ".json" ? root : null;
|
|
98
|
+
}
|
|
99
|
+
const id = path.basename(root);
|
|
100
|
+
const named = path.join(root, `${id}.json`);
|
|
101
|
+
if (fs.existsSync(named)) return named;
|
|
102
|
+
const legacy = path.join(root, "component.json");
|
|
103
|
+
if (fs.existsSync(legacy)) return legacy;
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
83
107
|
/**
|
|
84
108
|
* List component entries under a `components/` (or `children/` / `slots/<id>/`) dir.
|
|
85
109
|
* Supports flat `name.tsx|jsx|json` and legacy `name/` folders.
|
|
@@ -454,28 +478,88 @@ export function loadComponentMeta(root) {
|
|
|
454
478
|
}
|
|
455
479
|
}
|
|
456
480
|
|
|
457
|
-
const jsonPath =
|
|
458
|
-
if (
|
|
481
|
+
const jsonPath = resolveComponentMetaJson(root);
|
|
482
|
+
if (jsonPath) {
|
|
459
483
|
const meta = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
|
460
484
|
return { meta, source: "json", entry, jsonPath };
|
|
461
485
|
}
|
|
462
486
|
|
|
463
487
|
throw new Error(
|
|
464
|
-
`Missing component meta in ${rel(root)} — add export const meta in
|
|
488
|
+
`Missing component meta in ${rel(root)} — add export const meta in <name>/<name>.tsx or <name>.json`,
|
|
465
489
|
);
|
|
466
490
|
}
|
|
467
491
|
|
|
468
492
|
/**
|
|
469
|
-
*
|
|
493
|
+
* Indent a multi-line JSON value so it sits inside an object literal.
|
|
494
|
+
* @param {unknown} value
|
|
495
|
+
* @param {number} indent
|
|
496
|
+
*/
|
|
497
|
+
function indentJson(value, indent = 2) {
|
|
498
|
+
const pad = " ".repeat(indent);
|
|
499
|
+
return JSON.stringify(value, null, 2)
|
|
500
|
+
.split("\n")
|
|
501
|
+
.map((line, i) => (i === 0 ? line : pad + line))
|
|
502
|
+
.join("\n");
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Serialize meta as a typed TS preamble for unpack (blank contract).
|
|
507
|
+
* Emits `propsSchema` + `satisfies FeeefComponentMeta` + `FeeefLivePropsOf`.
|
|
470
508
|
* @param {Record<string, any>} meta
|
|
471
509
|
*/
|
|
472
510
|
export function formatMetaExport(meta) {
|
|
473
511
|
const clean = { ...meta };
|
|
474
512
|
delete clean.$schema;
|
|
475
513
|
delete clean.code;
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
514
|
+
delete clean.sourcePath;
|
|
515
|
+
|
|
516
|
+
const propsSchema =
|
|
517
|
+
clean.propsSchema && typeof clean.propsSchema === "object"
|
|
518
|
+
? clean.propsSchema
|
|
519
|
+
: {};
|
|
520
|
+
delete clean.propsSchema;
|
|
521
|
+
|
|
522
|
+
if (!Object.prototype.hasOwnProperty.call(clean, "props")) {
|
|
523
|
+
clean.props = {};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** Preferred key order for readable authoring files. */
|
|
527
|
+
const preferred = [
|
|
528
|
+
"order",
|
|
529
|
+
"type",
|
|
530
|
+
"instanceId",
|
|
531
|
+
"title",
|
|
532
|
+
"props",
|
|
533
|
+
"slotsSchema",
|
|
534
|
+
"slotsLayout",
|
|
535
|
+
"children",
|
|
536
|
+
"slots",
|
|
537
|
+
"refId",
|
|
538
|
+
"refVersion",
|
|
539
|
+
];
|
|
540
|
+
const keys = [
|
|
541
|
+
...preferred.filter((k) => Object.prototype.hasOwnProperty.call(clean, k)),
|
|
542
|
+
...Object.keys(clean).filter((k) => !preferred.includes(k)),
|
|
543
|
+
];
|
|
544
|
+
|
|
545
|
+
const lines = [];
|
|
546
|
+
for (const key of keys) {
|
|
547
|
+
if (key === "props") {
|
|
548
|
+
// propsSchema identifier sits just before props (blank hero order).
|
|
549
|
+
lines.push(` propsSchema,`);
|
|
550
|
+
}
|
|
551
|
+
lines.push(` ${key}: ${indentJson(clean[key], 2)},`);
|
|
552
|
+
}
|
|
553
|
+
if (!keys.includes("props")) {
|
|
554
|
+
lines.push(` propsSchema,`);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return (
|
|
558
|
+
`const propsSchema = ${JSON.stringify(propsSchema, null, 2)} as const;\n\n` +
|
|
559
|
+
`export const meta = {\n${lines.join("\n")}\n` +
|
|
560
|
+
`} as const satisfies FeeefComponentMeta<typeof propsSchema>;\n\n` +
|
|
561
|
+
`type Props = FeeefLivePropsOf<typeof propsSchema>;\n\n`
|
|
562
|
+
);
|
|
479
563
|
}
|
|
480
564
|
|
|
481
565
|
export { COMPONENT_META_KEYS };
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Unpack a Lithium TemplateData JSON blob into a folder-based source tree.
|
|
3
3
|
*
|
|
4
|
-
* Layout:
|
|
4
|
+
* Layout (always flat page components — rebuilds as sections.main):
|
|
5
5
|
* <out>/
|
|
6
6
|
* feeef.template.json
|
|
7
7
|
* props.json
|
|
8
|
+
* shared/components/<id>.tsx | <id>/<id>.tsx (from sourcePath shared/…)
|
|
9
|
+
* library/components/<id>… (mirror of shared)
|
|
8
10
|
* pages/<pageId>/page.json
|
|
9
|
-
* pages/<pageId>/components/
|
|
10
|
-
* <name>.tsx | <name>.json | <name
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* pages/<pageId>/components/
|
|
12
|
+
* <name>.tsx | <name>.json | <name>/<name>.tsx | <name>/<name>.json
|
|
13
|
+
*
|
|
14
|
+
* Legacy multi-section keys in the blob are flattened into one ordered stack
|
|
15
|
+
* under `components/` so marketplace clones match the blank authoring contract.
|
|
16
|
+
*
|
|
17
|
+
* Components whose `sourcePath` starts with `shared/components/<id>` are
|
|
18
|
+
* promoted to the theme shared library; placements become `$ref` stubs.
|
|
13
19
|
*/
|
|
14
20
|
|
|
15
21
|
import fs from "node:fs";
|
|
@@ -60,150 +66,337 @@ function sanitizeComponentSourceForEditor(code) {
|
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
* @param {unknown} sourcePath
|
|
70
|
+
* @returns {string | null}
|
|
71
|
+
*/
|
|
72
|
+
function sharedIdFromSourcePath(sourcePath) {
|
|
73
|
+
if (typeof sourcePath !== "string" || !sourcePath.trim()) return null;
|
|
74
|
+
const norm = sourcePath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
75
|
+
const m = /^shared\/components\/([^/]+)/.exec(norm);
|
|
76
|
+
return m ? m[1] : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
67
80
|
* @param {Record<string, any>} component
|
|
68
|
-
* @param {string} destPath Intended path without extension (`…/components/hero`)
|
|
69
81
|
* @param {number} order
|
|
70
|
-
* @param {
|
|
82
|
+
* @param {string} schemaBase Dir used for relative $schema
|
|
71
83
|
*/
|
|
72
|
-
function
|
|
73
|
-
const children = component.children;
|
|
74
|
-
const hasChildFolders = Array.isArray(children) && children.length > 0;
|
|
75
|
-
|
|
76
|
-
const slots = component.slots;
|
|
77
|
-
const slotEntries =
|
|
78
|
-
slots && typeof slots === "object" && !Array.isArray(slots)
|
|
79
|
-
? Object.entries(slots)
|
|
80
|
-
: [];
|
|
81
|
-
const hasSlotFolders = slotEntries.some(
|
|
82
|
-
([, list]) => Array.isArray(list) && list.length > 0,
|
|
83
|
-
);
|
|
84
|
-
const needsFolder = hasChildFolders || hasSlotFolders;
|
|
85
|
-
|
|
86
|
-
const schemaBase = needsFolder ? destPath : path.dirname(destPath);
|
|
84
|
+
function buildMeta(component, order, schemaBase) {
|
|
87
85
|
/** @type {Record<string, any>} */
|
|
88
86
|
const meta = {
|
|
89
87
|
$schema: path.relative(schemaBase, SCHEMA_ABS).split(path.sep).join("/"),
|
|
90
88
|
order,
|
|
91
89
|
};
|
|
92
90
|
for (const key of COMPONENT_META_KEYS) {
|
|
93
|
-
// Preserve explicit nulls (Dawn/editor blobs often include them).
|
|
94
91
|
if (Object.prototype.hasOwnProperty.call(component, key)) {
|
|
95
92
|
meta[key] = component[key];
|
|
96
93
|
}
|
|
97
94
|
}
|
|
98
|
-
// Preserve unknown top-level keys (forward-compat) except tree/code.
|
|
99
95
|
for (const [key, value] of Object.entries(component)) {
|
|
100
96
|
if (
|
|
101
97
|
key === "code" ||
|
|
102
98
|
key === "children" ||
|
|
103
99
|
key === "slots" ||
|
|
104
100
|
key === "order" ||
|
|
101
|
+
key === "sourcePath" ||
|
|
105
102
|
COMPONENT_META_KEYS.includes(key)
|
|
106
103
|
) {
|
|
107
104
|
continue;
|
|
108
105
|
}
|
|
109
106
|
if (value !== undefined) meta[key] = value;
|
|
110
107
|
}
|
|
108
|
+
return meta;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {Record<string, any>} component
|
|
113
|
+
*/
|
|
114
|
+
function componentNeedsFolder(component) {
|
|
115
|
+
const children = component.children;
|
|
116
|
+
const hasChildFolders = Array.isArray(children) && children.length > 0;
|
|
117
|
+
const slots = component.slots;
|
|
118
|
+
const slotEntries =
|
|
119
|
+
slots && typeof slots === "object" && !Array.isArray(slots)
|
|
120
|
+
? Object.entries(slots)
|
|
121
|
+
: [];
|
|
122
|
+
const hasSlotFolders = slotEntries.some(
|
|
123
|
+
([, list]) => Array.isArray(list) && list.length > 0,
|
|
124
|
+
);
|
|
125
|
+
return {
|
|
126
|
+
needsFolder: hasChildFolders || hasSlotFolders,
|
|
127
|
+
hasChildFolders,
|
|
128
|
+
hasSlotFolders,
|
|
129
|
+
slotEntries,
|
|
130
|
+
children,
|
|
131
|
+
slots,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Write custom TSX body (typed meta + App source).
|
|
137
|
+
* @param {string} filePath
|
|
138
|
+
* @param {Record<string, any>} meta
|
|
139
|
+
* @param {string} code
|
|
140
|
+
* @param {{ files: number }} stats
|
|
141
|
+
*/
|
|
142
|
+
function writeCustomTsx(filePath, meta, code, stats) {
|
|
143
|
+
const metaForExport = { ...meta };
|
|
144
|
+
delete metaForExport.$schema;
|
|
145
|
+
const tsx = `${formatMetaExport(metaForExport)}${sanitizeComponentSourceForEditor(code)}`;
|
|
146
|
+
writeText(filePath, tsx);
|
|
147
|
+
stats.files += 1;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Ensure shared (+ library mirror) definition exists once.
|
|
152
|
+
* Shared leaves are flat `.tsx`; shells with only schemas stay flat too
|
|
153
|
+
* (slot *contents* live on placements).
|
|
154
|
+
*
|
|
155
|
+
* @param {string} sharedId
|
|
156
|
+
* @param {Record<string, any>} component
|
|
157
|
+
* @param {{ outDir: string, sharedWritten: Set<string>, stats: { files: number } }} ctx
|
|
158
|
+
*/
|
|
159
|
+
function ensureSharedDefinition(sharedId, component, ctx) {
|
|
160
|
+
if (ctx.sharedWritten.has(sharedId)) return;
|
|
161
|
+
ctx.sharedWritten.add(sharedId);
|
|
162
|
+
|
|
163
|
+
const hasCode =
|
|
164
|
+
typeof component.code === "string" && component.code.trim().length > 0;
|
|
165
|
+
|
|
166
|
+
/** Canonical shared: no page-local slots/children trees. */
|
|
167
|
+
const canonical = { ...component };
|
|
168
|
+
delete canonical.sourcePath;
|
|
169
|
+
if (Array.isArray(canonical.children) && canonical.children.length > 0) {
|
|
170
|
+
canonical.children = [];
|
|
171
|
+
}
|
|
172
|
+
if (canonical.slots && typeof canonical.slots === "object") {
|
|
173
|
+
/** @type {Record<string, any[]>} */
|
|
174
|
+
const empty = {};
|
|
175
|
+
for (const key of Object.keys(canonical.slots)) {
|
|
176
|
+
empty[key] = [];
|
|
177
|
+
}
|
|
178
|
+
canonical.slots = empty;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const kind of ["shared", "library"]) {
|
|
182
|
+
const destPath = path.join(ctx.outDir, kind, "components", sharedId);
|
|
183
|
+
const schemaBase = path.dirname(destPath);
|
|
184
|
+
const meta = buildMeta(canonical, 0, schemaBase);
|
|
185
|
+
delete meta.instanceId;
|
|
186
|
+
meta.instanceId = null;
|
|
187
|
+
if (Object.prototype.hasOwnProperty.call(canonical, "children")) {
|
|
188
|
+
meta.children = canonical.children ?? null;
|
|
189
|
+
}
|
|
190
|
+
if (Object.prototype.hasOwnProperty.call(canonical, "slots")) {
|
|
191
|
+
meta.slots = canonical.slots ?? null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (hasCode) {
|
|
195
|
+
writeCustomTsx(`${destPath}.tsx`, meta, component.code, ctx.stats);
|
|
196
|
+
} else {
|
|
197
|
+
if (Object.prototype.hasOwnProperty.call(component, "code")) {
|
|
198
|
+
meta.code = component.code ?? null;
|
|
199
|
+
}
|
|
200
|
+
writeJson(`${destPath}.json`, meta);
|
|
201
|
+
ctx.stats.files += 1;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Write nested slots/children under a folder placement.
|
|
208
|
+
* @param {Record<string, any>} component
|
|
209
|
+
* @param {string} dir
|
|
210
|
+
* @param {ReturnType<typeof componentNeedsFolder>} tree
|
|
211
|
+
* @param {{ outDir: string, sharedWritten: Set<string>, stats: { files: number } }} ctx
|
|
212
|
+
*/
|
|
213
|
+
function writeNestedTrees(component, dir, tree, ctx) {
|
|
214
|
+
if (tree.hasChildFolders) {
|
|
215
|
+
const used = new Set();
|
|
216
|
+
tree.children.forEach((child, i) => {
|
|
217
|
+
if (!child || typeof child !== "object") return;
|
|
218
|
+
const name = folderNameForComponent(child, i, used);
|
|
219
|
+
writeComponent(child, path.join(dir, "children", name), i, ctx);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (tree.hasSlotFolders) {
|
|
224
|
+
for (const [slotId, list] of tree.slotEntries) {
|
|
225
|
+
if (!Array.isArray(list) || list.length === 0) continue;
|
|
226
|
+
const used = new Set();
|
|
227
|
+
list.forEach((child, i) => {
|
|
228
|
+
if (!child || typeof child !== "object") return;
|
|
229
|
+
const name = folderNameForComponent(child, i, used);
|
|
230
|
+
writeComponent(child, path.join(dir, "slots", slotId, name), i, ctx);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
const emptySlots = {};
|
|
234
|
+
let anyEmpty = false;
|
|
235
|
+
for (const [slotId, list] of tree.slotEntries) {
|
|
236
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
237
|
+
emptySlots[slotId] = Array.isArray(list) ? list : [];
|
|
238
|
+
anyEmpty = true;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (anyEmpty) {
|
|
242
|
+
writeJson(path.join(dir, "slots.empty.json"), emptySlots);
|
|
243
|
+
ctx.stats.files += 1;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Write one component (and nested slots/children).
|
|
250
|
+
* Leaf customs → `<name>.tsx`; leaf built-ins → `<name>.json`;
|
|
251
|
+
* nested trees → `<name>/<name>.tsx` (or `.json` for `$ref` / built-ins).
|
|
252
|
+
*
|
|
253
|
+
* @param {Record<string, any>} component
|
|
254
|
+
* @param {string} destPath Intended path without extension (`…/components/hero`)
|
|
255
|
+
* @param {number} order
|
|
256
|
+
* @param {{ outDir: string, sharedWritten: Set<string>, stats: { files: number } }} ctx
|
|
257
|
+
*/
|
|
258
|
+
function writeComponent(component, destPath, order, ctx) {
|
|
259
|
+
const tree = componentNeedsFolder(component);
|
|
260
|
+
const sharedId = sharedIdFromSourcePath(component.sourcePath);
|
|
261
|
+
const id = path.basename(destPath);
|
|
262
|
+
|
|
263
|
+
if (sharedId) {
|
|
264
|
+
ensureSharedDefinition(sharedId, component, ctx);
|
|
265
|
+
|
|
266
|
+
/** @type {Record<string, any>} */
|
|
267
|
+
const stub = {
|
|
268
|
+
$schema: path
|
|
269
|
+
.relative(
|
|
270
|
+
tree.needsFolder ? destPath : path.dirname(destPath),
|
|
271
|
+
SCHEMA_ABS,
|
|
272
|
+
)
|
|
273
|
+
.split(path.sep)
|
|
274
|
+
.join("/"),
|
|
275
|
+
order,
|
|
276
|
+
$ref: `shared.${sharedId}`,
|
|
277
|
+
};
|
|
278
|
+
if (component.instanceId != null) stub.instanceId = component.instanceId;
|
|
279
|
+
if (component.title != null) stub.title = component.title;
|
|
280
|
+
if (component.props && typeof component.props === "object") {
|
|
281
|
+
stub.props = component.props;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (!tree.needsFolder) {
|
|
285
|
+
writeJson(`${destPath}.json`, stub);
|
|
286
|
+
ctx.stats.files += 1;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const dir = destPath;
|
|
291
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
292
|
+
writeJson(path.join(dir, `${id}.json`), stub);
|
|
293
|
+
ctx.stats.files += 1;
|
|
294
|
+
writeNestedTrees(component, dir, tree, ctx);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const schemaBase = tree.needsFolder ? destPath : path.dirname(destPath);
|
|
299
|
+
const meta = buildMeta(component, order, schemaBase);
|
|
111
300
|
|
|
112
|
-
// Nested trees: non-empty → folders; empty/null → keep on meta for round-trip.
|
|
113
301
|
if (Object.prototype.hasOwnProperty.call(component, "children")) {
|
|
114
|
-
if (hasChildFolders) {
|
|
302
|
+
if (tree.hasChildFolders) {
|
|
115
303
|
delete meta.children;
|
|
116
304
|
} else {
|
|
117
|
-
meta.children = children ?? null;
|
|
305
|
+
meta.children = tree.children ?? null;
|
|
118
306
|
}
|
|
119
307
|
}
|
|
120
308
|
|
|
121
309
|
if (Object.prototype.hasOwnProperty.call(component, "slots")) {
|
|
122
|
-
if (hasSlotFolders) {
|
|
310
|
+
if (tree.hasSlotFolders) {
|
|
123
311
|
delete meta.slots;
|
|
124
312
|
} else {
|
|
125
|
-
meta.slots = slots ?? null;
|
|
313
|
+
meta.slots = tree.slots ?? null;
|
|
126
314
|
}
|
|
127
315
|
}
|
|
128
316
|
|
|
129
317
|
const hasCode =
|
|
130
318
|
typeof component.code === "string" && component.code.trim().length > 0;
|
|
131
319
|
|
|
132
|
-
if (!needsFolder) {
|
|
133
|
-
// Flat leaf: hero.tsx or hero.json
|
|
320
|
+
if (!tree.needsFolder) {
|
|
134
321
|
if (hasCode) {
|
|
135
|
-
|
|
136
|
-
delete metaForExport.$schema;
|
|
137
|
-
const tsx = `${formatMetaExport(metaForExport)}${sanitizeComponentSourceForEditor(component.code)}`;
|
|
138
|
-
writeText(`${destPath}.tsx`, tsx);
|
|
139
|
-
stats.files += 1;
|
|
322
|
+
writeCustomTsx(`${destPath}.tsx`, meta, component.code, ctx.stats);
|
|
140
323
|
} else {
|
|
141
324
|
if (Object.prototype.hasOwnProperty.call(component, "code")) {
|
|
142
325
|
meta.code = component.code ?? null;
|
|
143
326
|
}
|
|
144
327
|
writeJson(`${destPath}.json`, meta);
|
|
145
|
-
stats.files += 1;
|
|
328
|
+
ctx.stats.files += 1;
|
|
146
329
|
}
|
|
147
330
|
return;
|
|
148
331
|
}
|
|
149
332
|
|
|
150
|
-
// Folder layout
|
|
333
|
+
// Folder layout: `<name>/<name>.tsx` (not legacy component.tsx)
|
|
151
334
|
const dir = destPath;
|
|
152
335
|
fs.mkdirSync(dir, { recursive: true });
|
|
153
336
|
|
|
154
337
|
if (hasCode) {
|
|
155
|
-
|
|
156
|
-
delete metaForExport.$schema;
|
|
157
|
-
const tsx = `${formatMetaExport(metaForExport)}${sanitizeComponentSourceForEditor(component.code)}`;
|
|
158
|
-
writeText(path.join(dir, "component.tsx"), tsx);
|
|
159
|
-
stats.files += 1;
|
|
338
|
+
writeCustomTsx(path.join(dir, `${id}.tsx`), meta, component.code, ctx.stats);
|
|
160
339
|
} else {
|
|
161
340
|
if (Object.prototype.hasOwnProperty.call(component, "code")) {
|
|
162
341
|
meta.code = component.code ?? null;
|
|
163
342
|
}
|
|
164
|
-
writeJson(path.join(dir,
|
|
165
|
-
stats.files += 1;
|
|
343
|
+
writeJson(path.join(dir, `${id}.json`), meta);
|
|
344
|
+
ctx.stats.files += 1;
|
|
166
345
|
}
|
|
167
346
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
children.forEach((child, i) => {
|
|
171
|
-
if (!child || typeof child !== "object") return;
|
|
172
|
-
const name = folderNameForComponent(child, i, used);
|
|
173
|
-
writeComponent(child, path.join(dir, "children", name), i, stats);
|
|
174
|
-
});
|
|
175
|
-
}
|
|
347
|
+
writeNestedTrees(component, dir, tree, ctx);
|
|
348
|
+
}
|
|
176
349
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
350
|
+
/**
|
|
351
|
+
* Stable order when coalescing legacy multi-section blobs into one flat stack.
|
|
352
|
+
* Unknown keys sort alphabetically after these.
|
|
353
|
+
*/
|
|
354
|
+
const SECTION_FLATTEN_ORDER = [
|
|
355
|
+
"header",
|
|
356
|
+
"announcement",
|
|
357
|
+
"top",
|
|
358
|
+
"start",
|
|
359
|
+
"hero",
|
|
360
|
+
"collection_header",
|
|
361
|
+
"sidebar",
|
|
362
|
+
"main",
|
|
363
|
+
"body",
|
|
364
|
+
"form",
|
|
365
|
+
"order_form",
|
|
366
|
+
"products",
|
|
367
|
+
"contact_details",
|
|
368
|
+
"contact_form",
|
|
369
|
+
"map",
|
|
370
|
+
"end",
|
|
371
|
+
"bottom",
|
|
372
|
+
"footer",
|
|
373
|
+
];
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Collect components from all section keys into one ordered list.
|
|
377
|
+
* @param {Record<string, any>} sections
|
|
378
|
+
* @returns {any[]}
|
|
379
|
+
*/
|
|
380
|
+
function flattenSectionComponents(sections) {
|
|
381
|
+
if (!sections || typeof sections !== "object") return [];
|
|
382
|
+
const keys = Object.keys(sections).sort((a, b) => {
|
|
383
|
+
const ia = SECTION_FLATTEN_ORDER.indexOf(a);
|
|
384
|
+
const ib = SECTION_FLATTEN_ORDER.indexOf(b);
|
|
385
|
+
const ra = ia === -1 ? SECTION_FLATTEN_ORDER.length : ia;
|
|
386
|
+
const rb = ib === -1 ? SECTION_FLATTEN_ORDER.length : ib;
|
|
387
|
+
if (ra !== rb) return ra - rb;
|
|
388
|
+
return a.localeCompare(b);
|
|
389
|
+
});
|
|
390
|
+
/** @type {any[]} */
|
|
391
|
+
const out = [];
|
|
392
|
+
for (const key of keys) {
|
|
393
|
+
const list = sections[key]?.components;
|
|
394
|
+
if (!Array.isArray(list)) continue;
|
|
395
|
+
for (const comp of list) {
|
|
396
|
+
if (comp && typeof comp === "object") out.push(comp);
|
|
205
397
|
}
|
|
206
398
|
}
|
|
399
|
+
return out;
|
|
207
400
|
}
|
|
208
401
|
|
|
209
402
|
/**
|
|
@@ -236,6 +429,8 @@ export function unpackTemplate({ inputPath, outDir, force = false }) {
|
|
|
236
429
|
|
|
237
430
|
const pageIds = Object.keys(data.pages);
|
|
238
431
|
const stats = { files: 0, pages: pageIds.length, components: 0 };
|
|
432
|
+
/** @type {{ outDir: string, sharedWritten: Set<string>, stats: { files: number } }} */
|
|
433
|
+
const ctx = { outDir, sharedWritten: new Set(), stats };
|
|
239
434
|
|
|
240
435
|
writeJson(path.join(outDir, "feeef.template.json"), {
|
|
241
436
|
$schema: "../../template-kit/schemas/template.schema.json",
|
|
@@ -268,57 +463,22 @@ export function unpackTemplate({ inputPath, outDir, force = false }) {
|
|
|
268
463
|
const sections =
|
|
269
464
|
page.sections && typeof page.sections === "object" ? page.sections : {};
|
|
270
465
|
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
const
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
466
|
+
// Always unpack to flat `pages/<id>/components/` (→ sections.main on build).
|
|
467
|
+
// Coalesce legacy multi-section keys into one ordered stack.
|
|
468
|
+
const components = flattenSectionComponents(sections);
|
|
469
|
+
const componentsDir = path.join(pageDir, "components");
|
|
470
|
+
fs.mkdirSync(componentsDir, { recursive: true });
|
|
471
|
+
const used = new Set();
|
|
472
|
+
components.forEach((comp, i) => {
|
|
473
|
+
if (!comp || typeof comp !== "object") return;
|
|
474
|
+
const name = folderNameForComponent(comp, i, used);
|
|
475
|
+
writeComponent(comp, path.join(componentsDir, name), i, ctx);
|
|
476
|
+
stats.components += 1;
|
|
277
477
|
});
|
|
278
|
-
const useFlatPageComponents =
|
|
279
|
-
nonEmptyKeys.length === 0
|
|
280
|
-
? sectionKeys.length === 0 ||
|
|
281
|
-
(sectionKeys.length === 1 && sectionKeys[0] === "main")
|
|
282
|
-
: nonEmptyKeys.length === 1 && nonEmptyKeys[0] === "main";
|
|
283
|
-
|
|
284
|
-
if (useFlatPageComponents) {
|
|
285
|
-
const components = Array.isArray(sections.main?.components)
|
|
286
|
-
? sections.main.components
|
|
287
|
-
: [];
|
|
288
|
-
const componentsDir = path.join(pageDir, "components");
|
|
289
|
-
fs.mkdirSync(componentsDir, { recursive: true });
|
|
290
|
-
const used = new Set();
|
|
291
|
-
components.forEach((comp, i) => {
|
|
292
|
-
if (!comp || typeof comp !== "object") return;
|
|
293
|
-
const name = folderNameForComponent(comp, i, used);
|
|
294
|
-
writeComponent(comp, path.join(componentsDir, name), i, stats);
|
|
295
|
-
stats.components += 1;
|
|
296
|
-
});
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
for (const [sectionId, section] of Object.entries(sections)) {
|
|
301
|
-
const components = Array.isArray(section?.components)
|
|
302
|
-
? section.components
|
|
303
|
-
: [];
|
|
304
|
-
const sectionDir = path.join(pageDir, "sections", sectionId);
|
|
305
|
-
// Always create the section folder so empty sections round-trip.
|
|
306
|
-
fs.mkdirSync(path.join(sectionDir, "components"), { recursive: true });
|
|
307
|
-
|
|
308
|
-
const used = new Set();
|
|
309
|
-
components.forEach((comp, i) => {
|
|
310
|
-
if (!comp || typeof comp !== "object") return;
|
|
311
|
-
const name = folderNameForComponent(comp, i, used);
|
|
312
|
-
writeComponent(
|
|
313
|
-
comp,
|
|
314
|
-
path.join(sectionDir, "components", name),
|
|
315
|
-
i,
|
|
316
|
-
stats,
|
|
317
|
-
);
|
|
318
|
-
stats.components += 1;
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
478
|
}
|
|
322
479
|
|
|
323
|
-
return
|
|
480
|
+
return {
|
|
481
|
+
...stats,
|
|
482
|
+
sharedCount: ctx.sharedWritten.size,
|
|
483
|
+
};
|
|
324
484
|
}
|