@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.
Files changed (61) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +94 -0
  3. package/dist/bin.js +2752 -0
  4. package/dist/bin.js.map +1 -0
  5. package/dist/index.d.ts +6 -0
  6. package/dist/index.js +2730 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +67 -0
  9. package/scaffolds/blank/.cursor/rules/feeef-theme.mdc +34 -0
  10. package/scaffolds/blank/.cursor/rules/filterator.mdc +13 -0
  11. package/scaffolds/blank/.cursor/rules/order-form.mdc +17 -0
  12. package/scaffolds/blank/.cursor/rules/theme-source.mdc +32 -0
  13. package/scaffolds/blank/.cursor/skills/feeef-filterator/SKILL.md +15 -0
  14. package/scaffolds/blank/.cursor/skills/feeef-theme/SKILL.md +56 -0
  15. package/scaffolds/blank/.cursor/skills/feeef-theme/reference.md +73 -0
  16. package/scaffolds/blank/.vscode/extensions.json +3 -0
  17. package/scaffolds/blank/.vscode/settings.json +13 -0
  18. package/scaffolds/blank/AGENTS.md +104 -0
  19. package/scaffolds/blank/README.md +21 -0
  20. package/scaffolds/blank/docs/00-INDEX.md +33 -0
  21. package/scaffolds/blank/docs/03-CUSTOM-COMPONENTS.md +116 -0
  22. package/scaffolds/blank/docs/04-WORKFLOW.md +129 -0
  23. package/scaffolds/blank/docs/05-ANTI-PATTERNS.md +80 -0
  24. package/scaffolds/blank/docs/07-SHARED-COMPONENTS.md +144 -0
  25. package/scaffolds/blank/docs/08-ORDER-FORM.md +376 -0
  26. package/scaffolds/blank/docs/09-THEME-PORTING.md +211 -0
  27. package/scaffolds/blank/docs/10-SCHEMA-LIBRARY.md +127 -0
  28. package/scaffolds/blank/docs/11-PAGES-CHECKOUT-THANKS-PRODUCT.md +81 -0
  29. package/scaffolds/blank/docs/12-DARK-LIGHT-THEME.md +164 -0
  30. package/scaffolds/blank/docs/13-TEMPLATE-I18N.md +320 -0
  31. package/scaffolds/blank/docs/14-FILTERATOR.md +433 -0
  32. package/scaffolds/blank/docs/16-AUTHORING-TS-IMPORTS.md +73 -0
  33. package/scaffolds/blank/feeef.template.json +7 -0
  34. package/scaffolds/blank/locales/README.md +4 -0
  35. package/scaffolds/blank/locales/ar.json +10 -0
  36. package/scaffolds/blank/locales/en.json +10 -0
  37. package/scaffolds/blank/package-lock.json +1975 -0
  38. package/scaffolds/blank/package.json +23 -0
  39. package/scaffolds/blank/pages/home/components/hero.tsx +34 -0
  40. package/scaffolds/blank/pages/home/page.json +3 -0
  41. package/scaffolds/blank/props.json +6 -0
  42. package/scaffolds/blank/schema.ts +49 -0
  43. package/scaffolds/blank/tsconfig.json +24 -0
  44. package/scaffolds/blank/types/feeef-live-scope.d.ts +162 -0
  45. package/scaffolds/blank/types/feeef-template-schema.d.ts +84 -0
  46. package/vendor/template-kit/README.md +64 -0
  47. package/vendor/template-kit/bin/dev.mjs +61 -0
  48. package/vendor/template-kit/bin/feeef-template.mjs +212 -0
  49. package/vendor/template-kit/data/lithium-schema.json +4733 -0
  50. package/vendor/template-kit/lib/build.mjs +377 -0
  51. package/vendor/template-kit/lib/compile-component.mjs +188 -0
  52. package/vendor/template-kit/lib/component-meta.mjs +481 -0
  53. package/vendor/template-kit/lib/library.mjs +168 -0
  54. package/vendor/template-kit/lib/locales.mjs +68 -0
  55. package/vendor/template-kit/lib/paths.mjs +84 -0
  56. package/vendor/template-kit/lib/shared.mjs +268 -0
  57. package/vendor/template-kit/lib/theme-schema.mjs +300 -0
  58. package/vendor/template-kit/lib/unpack.mjs +324 -0
  59. package/vendor/template-kit/lib/validate.mjs +207 -0
  60. package/vendor/template-kit/schemas/component.schema.json +55 -0
  61. package/vendor/template-kit/schemas/template.schema.json +18 -0
@@ -0,0 +1,377 @@
1
+ /**
2
+ * Build a folder-based template source tree into Lithium TemplateData (data.json).
3
+ *
4
+ * Resolves `shared/components/*` placements (`$ref: "shared.foo"`) at compile time
5
+ * so every page that references the same shared id stays design-consistent.
6
+ *
7
+ * Components may be:
8
+ * - Flat page-level: `pages/<page>/components/hero.tsx` → published as `sections.main`
9
+ * - Legacy sections: `pages/<page>/sections/<id>/components/…`
10
+ * - Flat or folder entries under any `components/` dir (`hero.tsx` or `hero/component.tsx`)
11
+ */
12
+
13
+ import fs from "node:fs";
14
+ import path from "node:path";
15
+ import { validateTemplateData } from "./validate.mjs";
16
+ import {
17
+ loadSharedCatalog,
18
+ resolveSharedInTree,
19
+ sharedIdFromComponent,
20
+ } from "./shared.mjs";
21
+ import { buildSchemaLibrary } from "./library.mjs";
22
+ import { buildThemeI18n, writeLocaleFiles } from "./locales.mjs";
23
+ import { compileComponentSource } from "./compile-component.mjs";
24
+ import {
25
+ componentIdFromPath,
26
+ listComponentEntries,
27
+ loadComponentMeta,
28
+ isFlatComponentFile,
29
+ } from "./component-meta.mjs";
30
+ import { buildPackagedSchema } from "./theme-schema.mjs";
31
+
32
+ /**
33
+ * @param {string} dir
34
+ * @returns {string[]}
35
+ */
36
+ function listDirs(dir) {
37
+ if (!fs.existsSync(dir)) return [];
38
+ return fs
39
+ .readdirSync(dir, { withFileTypes: true })
40
+ .filter((d) => d.isDirectory() && !d.name.startsWith("."))
41
+ .map((d) => d.name)
42
+ .sort();
43
+ }
44
+
45
+ /**
46
+ * @param {string} filePath
47
+ * @returns {any}
48
+ */
49
+ function readJson(filePath) {
50
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
51
+ }
52
+
53
+ /**
54
+ * Read one component entry from disk (does not expand shared refs).
55
+ * Meta: `export const meta` in .tsx/.jsx, or legacy .json / component.json.
56
+ * Nested slots/children only apply to folder entries.
57
+ * @param {string} root Absolute folder or flat file path
58
+ * @param {string[]} fileStack
59
+ * @returns {Record<string, any>}
60
+ */
61
+ function readComponentRaw(root, fileStack) {
62
+ const loaded = loadComponentMeta(root);
63
+ const track = loaded.jsonPath || loaded.entry || root;
64
+ fileStack.push(track);
65
+ const meta = loaded.meta;
66
+ fileStack.pop();
67
+
68
+ /** @type {Record<string, any>} */
69
+ const component = { ...meta };
70
+ delete component.order;
71
+ delete component.$schema;
72
+
73
+ const compiled = compileComponentSource(root);
74
+ if (compiled) {
75
+ component.code = compiled.code;
76
+ }
77
+
78
+ // Flat leaves cannot host children/slots folders.
79
+ if (isFlatComponentFile(root)) {
80
+ return component;
81
+ }
82
+
83
+ const childrenDir = path.join(root, "children");
84
+ if (fs.existsSync(childrenDir)) {
85
+ const childEntries = listComponentEntries(childrenDir);
86
+ if (childEntries.length > 0) {
87
+ component.children = childEntries.map((d) =>
88
+ readComponentRaw(d, fileStack),
89
+ );
90
+ }
91
+ }
92
+
93
+ const slotsDir = path.join(root, "slots");
94
+ /** @type {Record<string, any[]>} */
95
+ const slotsFromDirs = {};
96
+ if (fs.existsSync(slotsDir)) {
97
+ for (const slotId of listDirs(slotsDir)) {
98
+ const slotChildren = listComponentEntries(path.join(slotsDir, slotId));
99
+ slotsFromDirs[slotId] = slotChildren.map((d) =>
100
+ readComponentRaw(d, fileStack),
101
+ );
102
+ }
103
+ }
104
+
105
+ const emptySlotsPath = path.join(root, "slots.empty.json");
106
+ /** @type {Record<string, any[]>} */
107
+ let emptySlots = {};
108
+ if (fs.existsSync(emptySlotsPath)) {
109
+ emptySlots = readJson(emptySlotsPath);
110
+ }
111
+
112
+ if (
113
+ Object.keys(slotsFromDirs).length > 0 ||
114
+ Object.keys(emptySlots).length > 0
115
+ ) {
116
+ component.slots = { ...emptySlots, ...slotsFromDirs };
117
+ }
118
+
119
+ return component;
120
+ }
121
+
122
+ /**
123
+ * @param {string} srcDir
124
+ * @returns {Map<string, Record<string, any>>}
125
+ */
126
+ function buildSharedCatalog(srcDir) {
127
+ const sharedRoot = path.join(srcDir, "shared", "components");
128
+ /** @type {Map<string, Record<string, any>>} */
129
+ const catalog = new Map();
130
+ if (!fs.existsSync(sharedRoot)) return catalog;
131
+
132
+ const fileStack = [];
133
+ const entries = listComponentEntries(sharedRoot);
134
+ const ids = entries.map((p) => componentIdFromPath(p));
135
+ for (let i = 0; i < entries.length; i++) {
136
+ catalog.set(ids[i], readComponentRaw(entries[i], fileStack));
137
+ }
138
+
139
+ // Expand nested shared refs inside library definitions.
140
+ const resolving = new Set();
141
+ for (const id of ids) {
142
+ catalog.set(
143
+ id,
144
+ resolveSharedInTree(
145
+ structuredClone(catalog.get(id)),
146
+ catalog,
147
+ resolving,
148
+ `shared/${id}`,
149
+ ),
150
+ );
151
+ }
152
+ return catalog;
153
+ }
154
+
155
+ /**
156
+ * @param {object} opts
157
+ * @param {string} opts.srcDir
158
+ * @param {string} [opts.outPath]
159
+ * @param {boolean} [opts.write]
160
+ */
161
+ export function buildTemplate({ srcDir, outPath, write = true }) {
162
+ if (!fs.existsSync(srcDir)) {
163
+ throw new Error(`Source not found: ${srcDir}`);
164
+ }
165
+
166
+ const manifestPath = path.join(srcDir, "feeef.template.json");
167
+ /** @type {Record<string, any>} */
168
+ let manifest = {};
169
+ if (fs.existsSync(manifestPath)) {
170
+ manifest = readJson(manifestPath);
171
+ }
172
+
173
+ const propsPath = path.join(srcDir, "props.json");
174
+ const props = fs.existsSync(propsPath) ? readJson(propsPath) : undefined;
175
+
176
+ const sharedCatalog = buildSharedCatalog(srcDir);
177
+ const resolving = new Set();
178
+
179
+ const pagesDir = path.join(srcDir, "pages");
180
+ const pageIdsFromDisk = listDirs(pagesDir);
181
+ const pageIds =
182
+ Array.isArray(manifest.pages) && manifest.pages.length > 0
183
+ ? manifest.pages.filter((id) => pageIdsFromDisk.includes(id))
184
+ : pageIdsFromDisk;
185
+
186
+ for (const id of pageIdsFromDisk) {
187
+ if (!pageIds.includes(id)) pageIds.push(id);
188
+ }
189
+
190
+ /** @type {Record<string, any>} */
191
+ const pages = {};
192
+ const fileStack = [];
193
+ let sharedPlacementCount = 0;
194
+
195
+ for (const pageId of pageIds) {
196
+ const pageDir = path.join(pagesDir, pageId);
197
+ const pageJsonPath = path.join(pageDir, "page.json");
198
+ let pageProps = {};
199
+ if (fs.existsSync(pageJsonPath)) {
200
+ const pageJson = readJson(pageJsonPath);
201
+ pageProps =
202
+ pageJson.props && typeof pageJson.props === "object"
203
+ ? pageJson.props
204
+ : pageJson;
205
+ }
206
+
207
+ /** @type {Record<string, any>} */
208
+ const sections = {};
209
+ const pageComponentsDir = path.join(pageDir, "components");
210
+ const sectionsDir = path.join(pageDir, "sections");
211
+ const flatEntries = listComponentEntries(pageComponentsDir);
212
+
213
+ /** Count legacy section component entries (any section with ≥1 component). */
214
+ let legacyComponentCount = 0;
215
+ const sectionIds = listDirs(sectionsDir);
216
+ for (const sectionId of sectionIds) {
217
+ legacyComponentCount += listComponentEntries(
218
+ path.join(sectionsDir, sectionId, "components"),
219
+ ).length;
220
+ }
221
+
222
+ if (flatEntries.length > 0 && legacyComponentCount > 0) {
223
+ throw new Error(
224
+ `Page "${pageId}" has both pages/${pageId}/components/ and pages/${pageId}/sections/…/components/. ` +
225
+ `Use one layout: flat page components (→ sections.main) or legacy sections folders.`,
226
+ );
227
+ }
228
+
229
+ if (flatEntries.length > 0) {
230
+ sections.main = {
231
+ components: flatEntries.map((entryPath) => {
232
+ const raw = readComponentRaw(entryPath, fileStack);
233
+ if (sharedIdFromComponent(raw)) sharedPlacementCount += 1;
234
+ return resolveSharedInTree(
235
+ raw,
236
+ sharedCatalog,
237
+ resolving,
238
+ `${pageId}/main/${componentIdFromPath(entryPath)}`,
239
+ );
240
+ }),
241
+ };
242
+ } else {
243
+ for (const sectionId of sectionIds) {
244
+ const componentsDir = path.join(sectionsDir, sectionId, "components");
245
+ const componentEntries = listComponentEntries(componentsDir);
246
+ sections[sectionId] = {
247
+ components: componentEntries.map((entryPath) => {
248
+ const raw = readComponentRaw(entryPath, fileStack);
249
+ if (sharedIdFromComponent(raw)) sharedPlacementCount += 1;
250
+ return resolveSharedInTree(
251
+ raw,
252
+ sharedCatalog,
253
+ resolving,
254
+ `${pageId}/${sectionId}/${componentIdFromPath(entryPath)}`,
255
+ );
256
+ }),
257
+ };
258
+ }
259
+ }
260
+
261
+ pages[pageId] = { props: pageProps, sections };
262
+ }
263
+
264
+ /** @type {Record<string, any>} */
265
+ const data = { pages };
266
+ if (props !== undefined) data.props = props;
267
+
268
+ const metaPath = path.join(srcDir, "feeef.template.json");
269
+ let defaultLocale;
270
+ if (fs.existsSync(metaPath)) {
271
+ try {
272
+ const meta = readJson(metaPath);
273
+ if (typeof meta.defaultLocale === "string") {
274
+ defaultLocale = meta.defaultLocale;
275
+ }
276
+ } catch {
277
+ // ignore
278
+ }
279
+ }
280
+
281
+ const i18n = buildThemeI18n(srcDir, { defaultLocale });
282
+ if (i18n) {
283
+ data.i18n = i18n;
284
+ }
285
+
286
+ const issues = validateTemplateData(data, { srcDir });
287
+ const errors = issues.filter((i) => i.level === "error");
288
+ if (errors.length > 0) {
289
+ const msg = errors.map((e) => ` [${e.path}] ${e.message}`).join("\n");
290
+ throw new Error(`Template validation failed:\n${msg}`);
291
+ }
292
+
293
+ const libraryDoc = buildSchemaLibrary({
294
+ srcDir,
295
+ readComponentRaw,
296
+ resolveSharedInTree,
297
+ sharedCatalog,
298
+ });
299
+
300
+ const resolvedOut = outPath || path.join(srcDir, "dist", "data.json");
301
+ const libraryOut = path.join(path.dirname(resolvedOut), "schema.library.json");
302
+ const schemaOut = path.join(path.dirname(resolvedOut), "schema.json");
303
+
304
+ const { schema: packagedSchema, warnings: schemaWarnings } = buildPackagedSchema({
305
+ srcDir,
306
+ libraryDoc,
307
+ });
308
+ for (const w of schemaWarnings) {
309
+ issues.push({ level: "warning", path: "schema", message: w });
310
+ }
311
+
312
+ if (write) {
313
+ fs.mkdirSync(path.dirname(resolvedOut), { recursive: true });
314
+ fs.writeFileSync(
315
+ resolvedOut,
316
+ `${JSON.stringify(data, null, 2)}\n`,
317
+ "utf8",
318
+ );
319
+ fs.writeFileSync(
320
+ libraryOut,
321
+ `${JSON.stringify(libraryDoc, null, 2)}\n`,
322
+ "utf8",
323
+ );
324
+ if (i18n) {
325
+ writeLocaleFiles(path.dirname(resolvedOut), i18n);
326
+ }
327
+ fs.writeFileSync(
328
+ schemaOut,
329
+ `${JSON.stringify(packagedSchema, null, 2)}\n`,
330
+ "utf8",
331
+ );
332
+ }
333
+
334
+ return {
335
+ data,
336
+ library: libraryDoc,
337
+ schema: packagedSchema,
338
+ i18n,
339
+ outPath: resolvedOut,
340
+ libraryOutPath: libraryOut,
341
+ schemaOutPath: schemaOut,
342
+ warnings: issues.filter((i) => i.level === "warning"),
343
+ pageCount: Object.keys(pages).length,
344
+ componentCount: countComponents(data),
345
+ sharedCount: sharedCatalog.size,
346
+ sharedPlacementCount,
347
+ libraryCount: libraryDoc.components.length,
348
+ localeCount: i18n?.locales?.length ?? 0,
349
+ };
350
+ }
351
+
352
+ // Re-export for tests / CLI introspection
353
+ export { loadSharedCatalog };
354
+
355
+ /**
356
+ * @param {Record<string, any>} data
357
+ */
358
+ function countComponents(data) {
359
+ let n = 0;
360
+ /** @param {any} c */
361
+ function walk(c) {
362
+ if (!c || typeof c !== "object") return;
363
+ n += 1;
364
+ if (Array.isArray(c.children)) c.children.forEach(walk);
365
+ if (c.slots && typeof c.slots === "object") {
366
+ for (const list of Object.values(c.slots)) {
367
+ if (Array.isArray(list)) list.forEach(walk);
368
+ }
369
+ }
370
+ }
371
+ for (const page of Object.values(data.pages || {})) {
372
+ for (const section of Object.values(page.sections || {})) {
373
+ (section.components || []).forEach(walk);
374
+ }
375
+ }
376
+ return n;
377
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Compile theme component source (JSX/TSX) into a react-live `code` string.
3
+ *
4
+ * - Plain `.jsx` without imports → returned as-is (legacy path).
5
+ * - `.tsx` and/or `import`/`export` → esbuild transpile + optional bundle.
6
+ * - `react` / `react-dom` stay external (injected by CustomLive scope).
7
+ * - Result always exposes top-level `function App` for react-live.
8
+ * - npm packages resolve from the theme root `node_modules` (walk up to
9
+ * `feeef.template.json`).
10
+ *
11
+ * `root` may be a component folder (`…/hero/`) or a flat file (`…/hero.tsx`).
12
+ */
13
+
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+ import { createRequire } from "node:module";
17
+ import { fileURLToPath } from "node:url";
18
+ import {
19
+ isFlatComponentFile,
20
+ resolveComponentEntry,
21
+ stripMetaExport,
22
+ } from "./component-meta.mjs";
23
+
24
+ const require = createRequire(import.meta.url);
25
+
26
+ /**
27
+ * @returns {typeof import('esbuild') | null}
28
+ */
29
+ function loadEsbuild() {
30
+ try {
31
+ return require("esbuild");
32
+ } catch {
33
+ try {
34
+ const here = path.dirname(fileURLToPath(import.meta.url));
35
+ const cliRoot = path.resolve(here, "../../..");
36
+ return createRequire(path.join(cliRoot, "package.json"))("esbuild");
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Find theme root (`feeef.template.json`) walking up from a component folder.
45
+ * @param {string} startDir
46
+ * @returns {string}
47
+ */
48
+ export function findThemeRoot(startDir) {
49
+ let dir = path.resolve(startDir);
50
+ for (;;) {
51
+ if (fs.existsSync(path.join(dir, "feeef.template.json"))) {
52
+ return dir;
53
+ }
54
+ const parent = path.dirname(dir);
55
+ if (parent === dir) return path.resolve(startDir);
56
+ dir = parent;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * @param {string} source
62
+ */
63
+ export function sourceNeedsCompile(source) {
64
+ return (
65
+ /^\s*import\s/m.test(source) ||
66
+ /^\s*export\s/m.test(source) ||
67
+ /:\s*[A-Za-z<{]/.test(source)
68
+ );
69
+ }
70
+
71
+ /**
72
+ * @param {string} root Absolute component folder or flat `.tsx`/`.jsx` file
73
+ * @returns {{ code: string, entry: string, bundled: boolean } | null}
74
+ */
75
+ export function compileComponentSource(root) {
76
+ /** @type {string | null} */
77
+ let entry = null;
78
+ /** @type {string} */
79
+ let resolveDir;
80
+
81
+ if (isFlatComponentFile(root)) {
82
+ const ext = path.extname(root).toLowerCase();
83
+ if (ext === ".json") return null;
84
+ entry = root;
85
+ resolveDir = path.dirname(root);
86
+ } else {
87
+ entry = resolveComponentEntry(root);
88
+ resolveDir = root;
89
+ }
90
+ if (!entry) return null;
91
+
92
+ const source = stripMetaExport(
93
+ fs.readFileSync(entry, "utf8").replace(/\s+$/, ""),
94
+ );
95
+ const isTsx = entry.endsWith(".tsx");
96
+ const hasModuleSyntax =
97
+ /^\s*import\s/m.test(source) || /^\s*export\s/m.test(source);
98
+
99
+ if (!isTsx && !hasModuleSyntax) {
100
+ return { code: source, entry, bundled: false };
101
+ }
102
+
103
+ const esbuild = loadEsbuild();
104
+ if (!esbuild) {
105
+ throw new Error(
106
+ `Component ${path.basename(root)} uses TypeScript or imports, but esbuild is not installed. ` +
107
+ `Install it in the Feeef CLI package (or the theme) and retry.`,
108
+ );
109
+ }
110
+
111
+ const themeRoot = findThemeRoot(resolveDir);
112
+ const themeNodeModules = path.join(themeRoot, "node_modules");
113
+
114
+ if (!hasModuleSyntax) {
115
+ const result = esbuild.transformSync(source, {
116
+ loader: "tsx",
117
+ format: "esm",
118
+ target: "es2018",
119
+ jsx: "preserve",
120
+ });
121
+ let code = result.code.replace(/\s+$/, "");
122
+ code = code.replace(/^\s*export\s+/gm, "");
123
+ if (!/\bfunction\s+App\s*\(/.test(code)) {
124
+ throw new Error(
125
+ `${path.relative(process.cwd(), entry)}: expected function App() after transpile`,
126
+ );
127
+ }
128
+ return { code, entry, bundled: false };
129
+ }
130
+
131
+ let stdinContents = source;
132
+ if (
133
+ /\bfunction\s+App\s*\(/.test(source) &&
134
+ !/\bexport\s+(async\s+)?function\s+App\b/.test(source) &&
135
+ !/\bexport\s*\{[^}]*\bApp\b/.test(source)
136
+ ) {
137
+ stdinContents = `${source}\nexport { App };\n`;
138
+ }
139
+
140
+ /** @type {import('esbuild').BuildOptions} */
141
+ const buildOpts = {
142
+ stdin: {
143
+ contents: stdinContents,
144
+ resolveDir,
145
+ sourcefile: path.basename(entry),
146
+ loader: isTsx ? "tsx" : "jsx",
147
+ },
148
+ bundle: true,
149
+ write: false,
150
+ format: "iife",
151
+ globalName: "__FeeefThemeMod",
152
+ platform: "browser",
153
+ target: "es2018",
154
+ jsx: "transform",
155
+ absWorkingDir: themeRoot,
156
+ external: [
157
+ "react",
158
+ "react-dom",
159
+ "react/jsx-runtime",
160
+ "react/jsx-dev-runtime",
161
+ ],
162
+ logLevel: "silent",
163
+ };
164
+
165
+ if (fs.existsSync(themeNodeModules)) {
166
+ buildOpts.nodePaths = [themeNodeModules];
167
+ }
168
+
169
+ const result = esbuild.buildSync(buildOpts);
170
+
171
+ const bundled = result.outputFiles?.[0]?.text;
172
+ if (!bundled) {
173
+ throw new Error(`esbuild produced no output for ${entry}`);
174
+ }
175
+
176
+ const code = `${bundled.replace(/\s+$/, "")}
177
+ function App() {
178
+ var m = (typeof __FeeefThemeMod !== "undefined" && __FeeefThemeMod) || {};
179
+ var C = m.App || m.default;
180
+ if (typeof C !== "function") {
181
+ throw new Error("Bundled component did not export App");
182
+ }
183
+ return C.apply(this, arguments);
184
+ }
185
+ `;
186
+
187
+ return { code, entry, bundled: true };
188
+ }