@navneet_25/tempjs 2.0.0 → 3.0.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/README.md +66 -18
- package/cli/brand-manager.js +183 -96
- package/cli/config.js +9 -1
- package/cli/doctor.js +393 -0
- package/cli/fetch-core-modules.js +130 -0
- package/cli/index.js +141 -10
- package/cli/info.js +28 -1
- package/cli/init-options.js +209 -0
- package/cli/module-manager.js +168 -0
- package/cli/parse-args.js +21 -0
- package/cli/update.js +15 -0
- package/cli/version-manager.js +518 -0
- package/package.json +27 -4
- package/packages/core/modules/README.md +72 -0
- package/packages/core/modules/blog-compose/app/admin/components/blog-compose-form.ts +44 -0
- package/packages/core/modules/blog-compose/app/admin/hooks/useBlogPosts.ts +103 -0
- package/packages/core/modules/blog-compose/app/admin/panels/BlogComposePanel.tsx +388 -0
- package/packages/core/modules/blog-compose/app/api/blog-posts/[id]/route.ts +29 -0
- package/packages/core/modules/blog-compose/app/api/blog-posts/route.ts +21 -0
- package/packages/core/modules/blog-compose/app/blog/[slug]/page.tsx +79 -0
- package/packages/core/modules/blog-compose/app/blog/layout.tsx +22 -0
- package/packages/core/modules/blog-compose/app/blog/page.tsx +102 -0
- package/packages/core/modules/blog-compose/app/components/BlogSection.tsx +74 -0
- package/packages/core/modules/blog-compose/app/components/BlogShell.tsx +36 -0
- package/packages/core/modules/blog-compose/app/components/BlogSidebar.tsx +112 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/annotate-text.tsx +103 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/blocks/media-blocks.tsx +97 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/blocks/text-blocks.tsx +115 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/index.ts +21 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/interpreter.tsx +55 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/registry.ts +276 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/types.ts +128 -0
- package/packages/core/modules/blog-compose/lib/blog/register-sitemap.ts +18 -0
- package/packages/core/modules/blog-compose/lib/controllers/BlogComposeController.ts +2 -0
- package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.controller.ts +78 -0
- package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.repository.ts +78 -0
- package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.service.ts +72 -0
- package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.types.ts +20 -0
- package/packages/core/modules/blog-compose/lib/features/blog-compose/index.ts +7 -0
- package/packages/core/modules/blog-compose/prisma/blog-compose.prisma +13 -0
- package/packages/core/modules/enquiry-modal/app/components/EnquiryModal.tsx +308 -0
- package/packages/core/modules/footer/app/components/Footer.tsx +127 -0
- package/packages/core/modules/gallery/app/admin/components/GalleryFormModal.tsx +132 -0
- package/packages/core/modules/gallery/app/admin/components/GalleryList.tsx +142 -0
- package/packages/core/modules/gallery/app/admin/hooks/useGallery.ts +72 -0
- package/packages/core/modules/gallery/app/api/gallery/[id]/route.ts +29 -0
- package/packages/core/modules/gallery/app/api/gallery/route.ts +22 -0
- package/packages/core/modules/gallery/app/components/GallerySection.tsx +92 -0
- package/packages/core/modules/gallery/app/gallery/layout.tsx +12 -0
- package/packages/core/modules/gallery/app/gallery/page.tsx +179 -0
- package/packages/core/modules/gallery/lib/controllers/GalleryController.ts +2 -0
- package/packages/core/modules/gallery/lib/features/gallery/gallery.controller.ts +78 -0
- package/packages/core/modules/gallery/lib/features/gallery/gallery.repository.ts +55 -0
- package/packages/core/modules/gallery/lib/features/gallery/gallery.service.ts +43 -0
- package/packages/core/modules/gallery/lib/features/gallery/gallery.types.ts +17 -0
- package/packages/core/modules/gallery/lib/features/gallery/index.ts +4 -0
- package/packages/core/modules/gallery/prisma/gallery.prisma +11 -0
- package/packages/core/modules/hero-simple/app/components/Hero.tsx +160 -0
- package/packages/core/modules/legal-pages/app/components/LegalDocumentPage.tsx +61 -0
- package/packages/core/modules/legal-pages/app/privacy-policy/page.tsx +19 -0
- package/packages/core/modules/legal-pages/app/terms-and-conditions/page.tsx +19 -0
- package/packages/core/modules/reviews/app/admin/components/ReviewFormModal.tsx +110 -0
- package/packages/core/modules/reviews/app/admin/components/ReviewsList.tsx +116 -0
- package/packages/core/modules/reviews/app/admin/hooks/useReviews.ts +70 -0
- package/packages/core/modules/reviews/app/api/reviews/[id]/route.ts +29 -0
- package/packages/core/modules/reviews/app/api/reviews/route.ts +17 -0
- package/packages/core/modules/reviews/app/components/ReviewsSection.tsx +100 -0
- package/packages/core/modules/reviews/lib/controllers/ReviewController.ts +2 -0
- package/packages/core/modules/reviews/lib/features/reviews/index.ts +3 -0
- package/packages/core/modules/reviews/lib/features/reviews/review.controller.ts +64 -0
- package/packages/core/modules/reviews/lib/features/reviews/review.repository.ts +50 -0
- package/packages/core/modules/reviews/lib/features/reviews/review.service.ts +38 -0
- package/packages/core/modules/reviews/prisma/reviews.prisma +9 -0
- package/packages/core/modules/seo/app/components/SiteJsonLd.tsx +16 -0
- package/packages/core/modules/seo/app/robots.ts +30 -0
- package/packages/core/modules/seo/app/sitemap.ts +9 -0
- package/packages/core/modules/seo/lib/seo/index.ts +5 -0
- package/packages/core/modules/seo/lib/seo/json-ld.ts +128 -0
- package/packages/core/modules/seo/lib/seo/metadata.ts +65 -0
- package/packages/core/modules/seo/lib/seo/sitemap.ts +82 -0
- package/packages/core/modules/seo/lib/seo/types.ts +30 -0
- package/packages/core/modules/seo/lib/seo/urls.ts +27 -0
- package/packages/core/modules/theme-modes/app/components/ThemeModeInit.tsx +24 -0
- package/packages/core/modules/theme-modes/app/components/ThemeModeToggle.tsx +58 -0
- package/packages/core/modules.json +140 -0
- package/packages/core/prisma/schema.prisma +33 -0
- package/scripts/module-installer-core.mjs +657 -0
- package/scripts/template-modules-admin.mjs +366 -0
- package/scripts/template-modules-home.mjs +120 -0
- package/templates.json +163 -8
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared core module installer — used by monorepo scripts and tempjs add-module.
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
cpSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
|
|
13
|
+
import { buildModulesHomePageSource } from "./template-modules-home.mjs";
|
|
14
|
+
import {
|
|
15
|
+
applyAdminTabRegistry,
|
|
16
|
+
applyAdminDashboard,
|
|
17
|
+
} from "./template-modules-admin.mjs";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {{
|
|
21
|
+
* label: string;
|
|
22
|
+
* description?: string;
|
|
23
|
+
* paths: string[];
|
|
24
|
+
* featureFlag?: string;
|
|
25
|
+
* prismaFragment?: string;
|
|
26
|
+
* default?: boolean;
|
|
27
|
+
* }} CoreModuleDef
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {string} coreDir Absolute path to packages/core
|
|
32
|
+
*/
|
|
33
|
+
export function loadModulesRegistry(coreDir) {
|
|
34
|
+
const modulesRegistryPath = join(coreDir, "modules.json");
|
|
35
|
+
if (!existsSync(modulesRegistryPath)) {
|
|
36
|
+
throw new Error(`Missing modules registry: ${modulesRegistryPath}`);
|
|
37
|
+
}
|
|
38
|
+
return JSON.parse(readFileSync(modulesRegistryPath, "utf8"));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} coreDir
|
|
43
|
+
* @returns {string[]}
|
|
44
|
+
*/
|
|
45
|
+
export function listModuleIds(coreDir) {
|
|
46
|
+
const registry = loadModulesRegistry(coreDir);
|
|
47
|
+
return Object.keys(registry.modules);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} coreDir
|
|
52
|
+
* @param {string[]} moduleIds
|
|
53
|
+
* @returns {string[]}
|
|
54
|
+
*/
|
|
55
|
+
export function resolveModuleIds(coreDir, moduleIds) {
|
|
56
|
+
const registry = loadModulesRegistry(coreDir);
|
|
57
|
+
const resolved = [];
|
|
58
|
+
|
|
59
|
+
for (const id of moduleIds) {
|
|
60
|
+
const trimmed = id.trim();
|
|
61
|
+
if (!trimmed) continue;
|
|
62
|
+
if (!registry.modules[trimmed]) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Unknown core module "${trimmed}". Available: ${listModuleIds(coreDir).join(", ")}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (!resolved.includes(trimmed)) {
|
|
68
|
+
resolved.push(trimmed);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return resolved;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {string} templateRoot
|
|
77
|
+
* @param {string} moduleId
|
|
78
|
+
* @param {string} fragmentRelPath
|
|
79
|
+
* @param {string} modulesRoot
|
|
80
|
+
*/
|
|
81
|
+
function appendPrismaFragment(templateRoot, moduleId, fragmentRelPath, modulesRoot) {
|
|
82
|
+
const fragmentPath = join(modulesRoot, moduleId, fragmentRelPath);
|
|
83
|
+
if (!existsSync(fragmentPath)) {
|
|
84
|
+
throw new Error(`Prisma fragment missing: ${fragmentPath}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const fragment = readFileSync(fragmentPath, "utf8").trim();
|
|
88
|
+
const modelMatch = fragment.match(/model\s+(\w+)\s*\{/);
|
|
89
|
+
const modelName = modelMatch?.[1];
|
|
90
|
+
|
|
91
|
+
const domainPath = join(templateRoot, "prisma", "domain.prisma");
|
|
92
|
+
let domain = existsSync(domainPath) ? readFileSync(domainPath, "utf8") : "";
|
|
93
|
+
|
|
94
|
+
if (modelName && domain.includes(`model ${modelName}`)) {
|
|
95
|
+
console.log(` · prisma model ${modelName} already in domain.prisma`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
domain = domain.trim();
|
|
100
|
+
domain = domain ? `${domain}\n\n${fragment}\n` : `${fragment}\n`;
|
|
101
|
+
mkdirSync(dirname(domainPath), { recursive: true });
|
|
102
|
+
writeFileSync(domainPath, domain, "utf8");
|
|
103
|
+
console.log(` ✓ appended prisma fragment: ${modelName ?? fragmentRelPath}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {string} templateRoot
|
|
108
|
+
* @param {{ skipMetadataReplace?: boolean }} [patchOptions]
|
|
109
|
+
*/
|
|
110
|
+
function applySeoLayoutPatch(templateRoot, patchOptions = {}) {
|
|
111
|
+
const layoutPath = join(templateRoot, "app", "layout.tsx");
|
|
112
|
+
if (!existsSync(layoutPath)) return;
|
|
113
|
+
|
|
114
|
+
let content = readFileSync(layoutPath, "utf8");
|
|
115
|
+
|
|
116
|
+
if (!content.includes("SiteJsonLd")) {
|
|
117
|
+
const cssImport = content.match(/import\s+["']\.\/globals\.css["'];?/);
|
|
118
|
+
if (cssImport) {
|
|
119
|
+
content = content.replace(
|
|
120
|
+
cssImport[0],
|
|
121
|
+
`${cssImport[0]}\nimport SiteJsonLd from "@/app/components/SiteJsonLd";`
|
|
122
|
+
);
|
|
123
|
+
} else {
|
|
124
|
+
content = `import SiteJsonLd from "@/app/components/SiteJsonLd";\n${content}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (content.includes("<QueryProvider>")) {
|
|
128
|
+
content = content.replace("<QueryProvider>", "<SiteJsonLd />\n <QueryProvider>");
|
|
129
|
+
} else if (content.includes("<body")) {
|
|
130
|
+
content = content.replace(/<body([^>]*)>/, "<body$1>\n <SiteJsonLd />");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
writeFileSync(layoutPath, content, "utf8");
|
|
134
|
+
console.log(" ✓ wired SiteJsonLd into app/layout.tsx");
|
|
135
|
+
content = readFileSync(layoutPath, "utf8");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!patchOptions.skipMetadataReplace && !content.includes("buildPageMetadata")) {
|
|
139
|
+
content = content.replace(
|
|
140
|
+
'import type { Metadata } from "next";',
|
|
141
|
+
'import type { Metadata } from "next";\nimport { buildPageMetadata } from "@/lib/seo/metadata";'
|
|
142
|
+
);
|
|
143
|
+
content = content.replace(
|
|
144
|
+
/export const metadata: Metadata = \{[\s\S]*?\};/,
|
|
145
|
+
"export const metadata: Metadata = buildPageMetadata();"
|
|
146
|
+
);
|
|
147
|
+
writeFileSync(layoutPath, content, "utf8");
|
|
148
|
+
console.log(" ✓ upgraded app/layout.tsx metadata via buildPageMetadata()");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param {string} templateRoot
|
|
154
|
+
*/
|
|
155
|
+
function applyBlogComposeSitemapPatch(templateRoot) {
|
|
156
|
+
const sitemapPath = join(templateRoot, "app", "sitemap.ts");
|
|
157
|
+
if (!existsSync(sitemapPath)) return;
|
|
158
|
+
|
|
159
|
+
let content = readFileSync(sitemapPath, "utf8");
|
|
160
|
+
if (content.includes("lib/blog/register-sitemap")) return;
|
|
161
|
+
if (!content.includes("buildAppSitemap")) return;
|
|
162
|
+
|
|
163
|
+
const importLine = 'import "@/lib/blog/register-sitemap";';
|
|
164
|
+
if (content.includes('from "@/lib/seo/sitemap"')) {
|
|
165
|
+
content = content.replace(
|
|
166
|
+
/import\s*\{[^}]*buildAppSitemap[^}]*\}\s*from\s*"@\/lib\/seo\/sitemap";/,
|
|
167
|
+
(match) => `${match}\n${importLine}`
|
|
168
|
+
);
|
|
169
|
+
} else {
|
|
170
|
+
content = `${importLine}\n${content}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
writeFileSync(sitemapPath, content, "utf8");
|
|
174
|
+
console.log(" ✓ wired blog post slugs into app/sitemap.ts");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const THEME_COLOR_KEYS = [
|
|
178
|
+
"primary",
|
|
179
|
+
"primaryHover",
|
|
180
|
+
"accent",
|
|
181
|
+
"accentDark",
|
|
182
|
+
"accentLight",
|
|
183
|
+
"textMain",
|
|
184
|
+
"textMuted",
|
|
185
|
+
"bgMain",
|
|
186
|
+
"bgLight",
|
|
187
|
+
"bgCard",
|
|
188
|
+
"footerBg",
|
|
189
|
+
"ctaPrimary",
|
|
190
|
+
"ctaPrimaryHover",
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
const THEME_CSS_VARS = [
|
|
194
|
+
"--primary",
|
|
195
|
+
"--primary-hover",
|
|
196
|
+
"--accent-gold",
|
|
197
|
+
"--accent-gold-dark",
|
|
198
|
+
"--accent-gold-light",
|
|
199
|
+
"--text-main",
|
|
200
|
+
"--text-muted",
|
|
201
|
+
"--bg-tan",
|
|
202
|
+
"--bg-light",
|
|
203
|
+
"--bg-card",
|
|
204
|
+
"--footer-bg",
|
|
205
|
+
"--cta-primary",
|
|
206
|
+
"--cta-primary-hover",
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* @param {Record<string, string>} light
|
|
211
|
+
*/
|
|
212
|
+
function deriveDarkColorsFromLight(light) {
|
|
213
|
+
return {
|
|
214
|
+
primary: light.primary ?? "#60a5fa",
|
|
215
|
+
primaryHover: light.primaryHover ?? "#3b82f6",
|
|
216
|
+
accent: light.accent ?? "#38bdf8",
|
|
217
|
+
accentDark: light.accentDark ?? "#0ea5e9",
|
|
218
|
+
accentLight: light.accentDark ?? "#1e3a5f",
|
|
219
|
+
textMain: "#f1f5f9",
|
|
220
|
+
textMuted: "#94a3b8",
|
|
221
|
+
bgMain: "#0f172a",
|
|
222
|
+
bgLight: "#1e293b",
|
|
223
|
+
bgCard: "#1e293b",
|
|
224
|
+
footerBg: "#020617",
|
|
225
|
+
ctaPrimary: light.ctaPrimary ?? "#60a5fa",
|
|
226
|
+
ctaPrimaryHover: light.ctaPrimaryHover ?? "#3b82f6",
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* @param {string} siteContent
|
|
232
|
+
*/
|
|
233
|
+
function parseLightColorsFromSiteTs(siteContent) {
|
|
234
|
+
const themeIdx = siteContent.indexOf("theme:");
|
|
235
|
+
if (themeIdx < 0) return null;
|
|
236
|
+
const slice = siteContent.slice(themeIdx);
|
|
237
|
+
const colorsMatch = slice.match(/colors:\s*\{([\s\S]*?)\n\s*\},/);
|
|
238
|
+
if (!colorsMatch?.[1]) return null;
|
|
239
|
+
const inner = colorsMatch[1];
|
|
240
|
+
/** @type {Record<string, string>} */
|
|
241
|
+
const colors = {};
|
|
242
|
+
for (const key of THEME_COLOR_KEYS) {
|
|
243
|
+
const match = inner.match(new RegExp(`${key}\\s*:\\s*"([^"]+)"`));
|
|
244
|
+
if (match?.[1]) colors[key] = match[1];
|
|
245
|
+
}
|
|
246
|
+
return Object.keys(colors).length > 0 ? colors : null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* @param {string} templateRoot
|
|
251
|
+
*/
|
|
252
|
+
function applyThemeModesSitePatch(templateRoot) {
|
|
253
|
+
const sitePath = join(templateRoot, "constants", "site.ts");
|
|
254
|
+
if (!existsSync(sitePath)) return;
|
|
255
|
+
|
|
256
|
+
let content = readFileSync(sitePath, "utf8");
|
|
257
|
+
if (content.includes("colorsDark:")) {
|
|
258
|
+
console.log(" · theme colorsDark already in site.ts");
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const light = parseLightColorsFromSiteTs(content) ?? {};
|
|
263
|
+
const dark = deriveDarkColorsFromLight(light);
|
|
264
|
+
const darkLines = THEME_COLOR_KEYS.map((key) => ` ${key}: "${dark[key]}",`).join("\n");
|
|
265
|
+
|
|
266
|
+
if (!content.includes("appearance:")) {
|
|
267
|
+
content = content.replace(
|
|
268
|
+
/(theme:\s*\{\s*\n)(\s*)colors:/,
|
|
269
|
+
`$1$2appearance: "system" as const,\n$2colors:`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const themeIdx = content.indexOf("theme:");
|
|
274
|
+
if (themeIdx < 0) return;
|
|
275
|
+
const before = content.slice(0, themeIdx);
|
|
276
|
+
const themeSlice = content.slice(themeIdx);
|
|
277
|
+
const patchedTheme = themeSlice.replace(
|
|
278
|
+
/(\n\s*colors:\s*\{[\s\S]*?\n\s*\},)(\n)/,
|
|
279
|
+
`$1\n colorsDark: {\n${darkLines}\n },$2`
|
|
280
|
+
);
|
|
281
|
+
content = before + patchedTheme;
|
|
282
|
+
|
|
283
|
+
writeFileSync(sitePath, content, "utf8");
|
|
284
|
+
console.log(" ✓ added theme.appearance and theme.colorsDark to constants/site.ts");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* @param {string} templateRoot
|
|
289
|
+
* @param {Record<string, string>} dark
|
|
290
|
+
*/
|
|
291
|
+
function applyThemeModesCssPatch(templateRoot, dark) {
|
|
292
|
+
const globalsPath = join(templateRoot, "app", "globals.css");
|
|
293
|
+
if (!existsSync(globalsPath)) return;
|
|
294
|
+
|
|
295
|
+
let css = readFileSync(globalsPath, "utf8");
|
|
296
|
+
if (css.includes("[data-theme=\"dark\"]")) {
|
|
297
|
+
console.log(" · [data-theme=\"dark\"] already in globals.css");
|
|
298
|
+
} else {
|
|
299
|
+
const lines = THEME_CSS_VARS.map((cssVar, index) => {
|
|
300
|
+
const key = THEME_COLOR_KEYS[index];
|
|
301
|
+
return ` ${cssVar}: ${dark[key]};`;
|
|
302
|
+
});
|
|
303
|
+
css = `${css.trim()}\n\n[data-theme="dark"] {\n${lines.join("\n")}\n}\n`;
|
|
304
|
+
console.log(" ✓ appended [data-theme=\"dark\"] CSS variables to app/globals.css");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!css.includes("color-scheme:")) {
|
|
308
|
+
css = css.replace(
|
|
309
|
+
/:root\s*\{/,
|
|
310
|
+
":root {\n color-scheme: light;"
|
|
311
|
+
);
|
|
312
|
+
if (css.includes("[data-theme=\"dark\"]")) {
|
|
313
|
+
css = css.replace(
|
|
314
|
+
/\[data-theme="dark"\]\s*\{/,
|
|
315
|
+
"[data-theme=\"dark\"] {\n color-scheme: dark;"
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
console.log(" ✓ set color-scheme on :root and [data-theme=\"dark\"] for native UI");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
writeFileSync(globalsPath, css, "utf8");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* @param {string} templateRoot
|
|
326
|
+
*/
|
|
327
|
+
function applyThemeModesLayoutPatch(templateRoot) {
|
|
328
|
+
const layoutPath = join(templateRoot, "app", "layout.tsx");
|
|
329
|
+
if (!existsSync(layoutPath)) return;
|
|
330
|
+
|
|
331
|
+
let content = readFileSync(layoutPath, "utf8");
|
|
332
|
+
|
|
333
|
+
if (content.includes("SITE.theme.colors") && content.includes("style={{")) {
|
|
334
|
+
content = content.replace(/\s*style=\{\{[\s\S]*?SITE\.theme\.colors[\s\S]*?\}\s*as React\.CSSProperties\}\}/, "");
|
|
335
|
+
console.log(" ✓ removed inline theme CSS vars from <html> (uses data-theme CSS instead)");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (!content.includes("ThemeModeInit")) {
|
|
339
|
+
const cssImport = content.match(/import\s+["']\.\/globals\.css["'];?/);
|
|
340
|
+
if (cssImport) {
|
|
341
|
+
content = content.replace(
|
|
342
|
+
cssImport[0],
|
|
343
|
+
`${cssImport[0]}\nimport ThemeModeInit from "@/app/components/ThemeModeInit";`
|
|
344
|
+
);
|
|
345
|
+
} else {
|
|
346
|
+
content = `import ThemeModeInit from "@/app/components/ThemeModeInit";\n${content}`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (content.includes("<body")) {
|
|
350
|
+
content = content.replace(/<body([^>]*)>/, "<body$1>\n <ThemeModeInit />");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
writeFileSync(layoutPath, content, "utf8");
|
|
354
|
+
console.log(" ✓ wired ThemeModeInit into app/layout.tsx");
|
|
355
|
+
content = readFileSync(layoutPath, "utf8");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (!/<html[^>]*suppressHydrationWarning/.test(content)) {
|
|
359
|
+
content = content.replace(/<html([^>]*)>/, "<html$1 suppressHydrationWarning>");
|
|
360
|
+
writeFileSync(layoutPath, content, "utf8");
|
|
361
|
+
console.log(" ✓ added suppressHydrationWarning on <html> for theme mode init");
|
|
362
|
+
content = readFileSync(layoutPath, "utf8");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (!/<body[^>]*suppressHydrationWarning/.test(content)) {
|
|
366
|
+
content = content.replace(/<body([^>]*)>/, (match, attrs) => {
|
|
367
|
+
if (attrs.includes("suppressHydrationWarning")) return match;
|
|
368
|
+
return `<body${attrs} suppressHydrationWarning>`;
|
|
369
|
+
});
|
|
370
|
+
writeFileSync(layoutPath, content, "utf8");
|
|
371
|
+
console.log(" ✓ added suppressHydrationWarning on <body> for theme mode init");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* @param {string} templateRoot
|
|
377
|
+
*/
|
|
378
|
+
function applyThemeModesInstall(templateRoot) {
|
|
379
|
+
const sitePath = join(templateRoot, "constants", "site.ts");
|
|
380
|
+
const light = existsSync(sitePath)
|
|
381
|
+
? parseLightColorsFromSiteTs(readFileSync(sitePath, "utf8")) ?? {}
|
|
382
|
+
: {};
|
|
383
|
+
const dark = deriveDarkColorsFromLight(light);
|
|
384
|
+
applyThemeModesSitePatch(templateRoot);
|
|
385
|
+
applyThemeModesCssPatch(templateRoot, dark);
|
|
386
|
+
applyThemeModesLayoutPatch(templateRoot);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* @param {string} templateRoot
|
|
391
|
+
* @param {string} featureFlag
|
|
392
|
+
*/
|
|
393
|
+
function enableFeatureFlag(templateRoot, featureFlag) {
|
|
394
|
+
const sitePath = join(templateRoot, "constants", "site.ts");
|
|
395
|
+
if (!existsSync(sitePath)) return;
|
|
396
|
+
|
|
397
|
+
let content = readFileSync(sitePath, "utf8");
|
|
398
|
+
const pattern = new RegExp(`(${featureFlag}\\s*:\\s*)false`);
|
|
399
|
+
if (!pattern.test(content)) {
|
|
400
|
+
console.warn(` ⚠ feature flag ${featureFlag} not found in constants/site.ts`);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
content = content.replace(pattern, "$1true");
|
|
404
|
+
writeFileSync(sitePath, content, "utf8");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* @param {string} templateRoot
|
|
409
|
+
* @param {string} displayName
|
|
410
|
+
*/
|
|
411
|
+
function applyHeroSimpleDefaults(templateRoot, displayName) {
|
|
412
|
+
const sitePath = join(templateRoot, "constants", "site.ts");
|
|
413
|
+
if (!existsSync(sitePath)) return;
|
|
414
|
+
|
|
415
|
+
let content = readFileSync(sitePath, "utf8");
|
|
416
|
+
const replacements = [
|
|
417
|
+
['eyebrow: "WELCOME"', `eyebrow: "WELCOME TO ${displayName.toUpperCase()}"`],
|
|
418
|
+
['headline: "Your headline here"', `headline: "${displayName}"`],
|
|
419
|
+
[
|
|
420
|
+
'subheadline: "Your subheadline here."',
|
|
421
|
+
'subheadline: "Edit constants/site.ts to customize hero copy and add hero images."',
|
|
422
|
+
],
|
|
423
|
+
[
|
|
424
|
+
"features: [] as { icon: string; label: string }[]",
|
|
425
|
+
"features: [{ icon: \"sparkles\", label: \"Quality\" }, { icon: \"shield\", label: \"Trusted\" }] as { icon: string; label: string }[]",
|
|
426
|
+
],
|
|
427
|
+
[
|
|
428
|
+
"ctaButtons: [] as { label: string; enquiryLabel: string }[]",
|
|
429
|
+
"ctaButtons: [{ label: \"Contact us\", enquiryLabel: \"General enquiry\" }] as { label: string; enquiryLabel: string }[]",
|
|
430
|
+
],
|
|
431
|
+
];
|
|
432
|
+
|
|
433
|
+
for (const [find, replace] of replacements) {
|
|
434
|
+
if (content.includes(find)) {
|
|
435
|
+
content = content.replace(find, replace);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
writeFileSync(sitePath, content, "utf8");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* @param {string} templateRoot
|
|
444
|
+
* @param {string} displayName
|
|
445
|
+
*/
|
|
446
|
+
function applyEnquiryModalDefaults(templateRoot, displayName) {
|
|
447
|
+
const sitePath = join(templateRoot, "constants", "site.ts");
|
|
448
|
+
if (!existsSync(sitePath)) return;
|
|
449
|
+
|
|
450
|
+
let content = readFileSync(sitePath, "utf8");
|
|
451
|
+
const replacements = [
|
|
452
|
+
['modalTitle: "Demo Client Site"', `modalTitle: "${displayName}"`],
|
|
453
|
+
[
|
|
454
|
+
"selectionOptions: [] as { value: string; label: string }[]",
|
|
455
|
+
"selectionOptions: [{ value: \"General enquiry\", label: \"General enquiry\" }] as { value: string; label: string }[]",
|
|
456
|
+
],
|
|
457
|
+
];
|
|
458
|
+
|
|
459
|
+
for (const [find, replace] of replacements) {
|
|
460
|
+
if (content.includes(find)) {
|
|
461
|
+
content = content.replace(find, replace);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
writeFileSync(sitePath, content, "utf8");
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* @param {string} templateRoot
|
|
470
|
+
* @param {string[]} moduleIds
|
|
471
|
+
*/
|
|
472
|
+
export function writeInstalledModulesManifest(templateRoot, moduleIds) {
|
|
473
|
+
const manifestPath = join(templateRoot, ".tempjs-modules.json");
|
|
474
|
+
let existing = { coreModules: [] };
|
|
475
|
+
|
|
476
|
+
if (existsSync(manifestPath)) {
|
|
477
|
+
try {
|
|
478
|
+
existing = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
479
|
+
} catch {
|
|
480
|
+
existing = { coreModules: [] };
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const merged = [...new Set([...(existing.coreModules ?? []), ...moduleIds])];
|
|
485
|
+
writeFileSync(
|
|
486
|
+
manifestPath,
|
|
487
|
+
JSON.stringify({ coreModules: merged, updatedAt: new Date().toISOString() }, null, 2) + "\n",
|
|
488
|
+
"utf8"
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* @param {string} templateRoot
|
|
494
|
+
* @param {string[]} moduleIds
|
|
495
|
+
*/
|
|
496
|
+
export function applyModulesHomePage(templateRoot, moduleIds) {
|
|
497
|
+
const uiModules = [
|
|
498
|
+
"enquiry-modal",
|
|
499
|
+
"footer",
|
|
500
|
+
"hero-simple",
|
|
501
|
+
"gallery",
|
|
502
|
+
"reviews",
|
|
503
|
+
"blog-compose",
|
|
504
|
+
];
|
|
505
|
+
const installedUi = moduleIds.filter((id) => uiModules.includes(id));
|
|
506
|
+
|
|
507
|
+
if (installedUi.length === 0) return;
|
|
508
|
+
|
|
509
|
+
const destPage = join(templateRoot, "app", "page.tsx");
|
|
510
|
+
mkdirSync(dirname(destPage), { recursive: true });
|
|
511
|
+
writeFileSync(destPage, buildModulesHomePageSource(moduleIds), "utf8");
|
|
512
|
+
console.log(" ✓ wired app/page.tsx with optional UI modules");
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Copy module files into a project and enable feature flags.
|
|
517
|
+
* @param {string} templateRoot Absolute path to project/template directory
|
|
518
|
+
* @param {string[]} moduleIds Module ids (resolved or raw)
|
|
519
|
+
* @param {{
|
|
520
|
+
* coreDir: string;
|
|
521
|
+
* displayName?: string;
|
|
522
|
+
* excludePaths?: string[];
|
|
523
|
+
* skipHomePage?: boolean;
|
|
524
|
+
* skipAdminTabRegistry?: boolean;
|
|
525
|
+
* skipAdminContentPage?: boolean;
|
|
526
|
+
* skipSeoMetadataReplace?: boolean;
|
|
527
|
+
* installOptions?: { blogSidebar?: boolean };
|
|
528
|
+
* writePrismaSchema?: (root: string) => void;
|
|
529
|
+
* }} options
|
|
530
|
+
* @returns {string[]} Installed module ids
|
|
531
|
+
*/
|
|
532
|
+
export function copyModulesIntoProject(templateRoot, moduleIds, options) {
|
|
533
|
+
const { coreDir } = options;
|
|
534
|
+
const modulesRoot = join(coreDir, "modules");
|
|
535
|
+
const registry = loadModulesRegistry(coreDir);
|
|
536
|
+
const resolved = resolveModuleIds(coreDir, moduleIds);
|
|
537
|
+
const displayName = options.displayName ?? "Your Site";
|
|
538
|
+
const exclude = new Set(options.excludePaths ?? []);
|
|
539
|
+
|
|
540
|
+
for (const id of resolved) {
|
|
541
|
+
const mod = registry.modules[id];
|
|
542
|
+
const moduleDir = join(modulesRoot, id);
|
|
543
|
+
|
|
544
|
+
if (!existsSync(moduleDir)) {
|
|
545
|
+
throw new Error(`Module folder missing: ${moduleDir}`);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
for (const relPath of mod.paths) {
|
|
549
|
+
if (exclude.has(relPath)) {
|
|
550
|
+
console.log(` · skipped path (excluded): ${relPath}`);
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const src = join(moduleDir, relPath);
|
|
555
|
+
const dest = join(templateRoot, relPath);
|
|
556
|
+
|
|
557
|
+
if (!existsSync(src)) {
|
|
558
|
+
throw new Error(`Module file missing: ${join(moduleDir, relPath)}`);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
562
|
+
cpSync(src, dest, { force: true, recursive: true });
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (mod.featureFlag) {
|
|
566
|
+
enableFeatureFlag(templateRoot, mod.featureFlag);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (mod.prismaFragment) {
|
|
570
|
+
appendPrismaFragment(templateRoot, id, mod.prismaFragment, modulesRoot);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (id === "hero-simple") {
|
|
574
|
+
applyHeroSimpleDefaults(templateRoot, displayName);
|
|
575
|
+
}
|
|
576
|
+
if (id === "enquiry-modal") {
|
|
577
|
+
applyEnquiryModalDefaults(templateRoot, displayName);
|
|
578
|
+
}
|
|
579
|
+
if (id === "theme-modes") {
|
|
580
|
+
applyThemeModesInstall(templateRoot);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
console.log(` ✓ module: ${id} (${mod.label})`);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (resolved.includes("blog-compose")) {
|
|
587
|
+
applyBlogComposeSitemapPatch(templateRoot);
|
|
588
|
+
if (options.installOptions?.blogSidebar) {
|
|
589
|
+
enableFeatureFlag(templateRoot, "blogSidebar");
|
|
590
|
+
console.log(" ✓ enabled SITE.features.blogSidebar (blog article sidebar)");
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (options.writePrismaSchema) {
|
|
595
|
+
options.writePrismaSchema(templateRoot);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
writeInstalledModulesManifest(templateRoot, resolved);
|
|
599
|
+
|
|
600
|
+
if (resolved.includes("seo")) {
|
|
601
|
+
applySeoLayoutPatch(templateRoot, {
|
|
602
|
+
skipMetadataReplace: options.skipSeoMetadataReplace,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (!options.skipHomePage) {
|
|
607
|
+
applyModulesHomePage(templateRoot, resolved);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const skipAdminTabRegistry =
|
|
611
|
+
options.skipAdminTabRegistry ?? options.skipAdminContentPage ?? false;
|
|
612
|
+
|
|
613
|
+
if (
|
|
614
|
+
!skipAdminTabRegistry &&
|
|
615
|
+
resolved.some((id) => id === "gallery" || id === "reviews" || id === "blog-compose")
|
|
616
|
+
) {
|
|
617
|
+
applyAdminTabRegistry(templateRoot, resolved);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return resolved;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* @param {string[] | string | undefined} input
|
|
625
|
+
* @returns {{ id: string, extras: string[] }[]}
|
|
626
|
+
*/
|
|
627
|
+
export function parseModuleSpecs(input) {
|
|
628
|
+
if (!input) return [];
|
|
629
|
+
const raw = Array.isArray(input) ? input.join(",") : input;
|
|
630
|
+
const specs = [];
|
|
631
|
+
for (const part of raw.split(",")) {
|
|
632
|
+
const trimmed = part.trim();
|
|
633
|
+
if (!trimmed) continue;
|
|
634
|
+
const pieces = trimmed.split("+").map((s) => s.trim()).filter(Boolean);
|
|
635
|
+
if (pieces.length === 0) continue;
|
|
636
|
+
specs.push({ id: pieces[0], extras: pieces.slice(1) });
|
|
637
|
+
}
|
|
638
|
+
return specs;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* @param {{ id: string, extras: string[] }[]} specs
|
|
643
|
+
*/
|
|
644
|
+
export function moduleInstallOptionsFromSpecs(specs) {
|
|
645
|
+
return {
|
|
646
|
+
blogSidebar:
|
|
647
|
+
specs.some((s) => s.id === "blog-compose" && s.extras.includes("sidebar")),
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* @param {string[] | string | undefined} input
|
|
653
|
+
* @returns {string[]}
|
|
654
|
+
*/
|
|
655
|
+
export function parseModulesArg(input) {
|
|
656
|
+
return parseModuleSpecs(input).map((s) => s.id);
|
|
657
|
+
}
|