@pradip1995/framework-compiler 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.
@@ -0,0 +1,1016 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * storefront build — reads client config, generates manifests + Next.js app shell.
4
+ */
5
+ import { mkdirSync, writeFileSync, readFileSync, cpSync, existsSync, readdirSync } from "fs"
6
+ import { join, dirname, resolve, relative } from "path"
7
+ import { fileURLToPath } from "url"
8
+ import { createRequire } from "module"
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url))
11
+ const require = createRequire(import.meta.url)
12
+
13
+ const clientDir = resolve(process.cwd())
14
+ const outDir = join(clientDir, ".generated-app")
15
+
16
+ function loadClientTheme(clientDir) {
17
+ return loadClientStorefrontConfig(clientDir).theme
18
+ }
19
+
20
+ function loadClientStorefrontConfig(clientDir) {
21
+ const envPath = join(clientDir, ".env.local")
22
+ let envTheme = "valero"
23
+ if (existsSync(envPath)) {
24
+ const text = readFileSync(envPath, "utf8")
25
+ const match = text.match(/NEXT_PUBLIC_STOREFRONT_THEME=(.+)/)
26
+ const raw = match?.[1]?.trim().replace(/^["']|["']$/g, "") || "valero"
27
+ const map = { sahsha: "impulse", impulse: "impulse", valero: "valero" }
28
+ envTheme = map[raw] || "valero"
29
+ }
30
+
31
+ let raw = {}
32
+ const jsonPath = join(clientDir, "storefront.config.json")
33
+ const tsPath = join(clientDir, "storefront.config.ts")
34
+ if (existsSync(jsonPath)) {
35
+ raw = JSON.parse(readFileSync(jsonPath, "utf8"))
36
+ } else if (existsSync(tsPath)) {
37
+ try {
38
+ raw = JSON.parse(readFileSync(tsPath, "utf8"))
39
+ } catch {
40
+ raw = {}
41
+ }
42
+ }
43
+
44
+ const envLoaderVariant = process.env.NEXT_PUBLIC_LOADER_VARIANT?.trim()
45
+ const loader = {
46
+ enabled: raw.loader?.enabled !== false,
47
+ variant: envLoaderVariant || raw.loader?.variant || "spinner",
48
+ }
49
+
50
+ return {
51
+ theme: envTheme,
52
+ defaultCountryCode: raw.defaultCountryCode || "in",
53
+ loader,
54
+ }
55
+ }
56
+
57
+ function generateRootLayout(config) {
58
+ const loaderJson = JSON.stringify(config.loader)
59
+ return `import "@pradip1995/segment-tokens/themes/${config.theme}.css"
60
+ import "./globals.css"
61
+ import { LoaderProvider, GlobalLoader } from "@pradip1995/segment-loader"
62
+
63
+ const loaderConfig = ${loaderJson} as const
64
+
65
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
66
+ return (
67
+ <html lang="en">
68
+ <body className="min-h-screen bg-page-bg font-sans text-body antialiased">
69
+ <LoaderProvider config={loaderConfig}>
70
+ <GlobalLoader />
71
+ {children}
72
+ </LoaderProvider>
73
+ </body>
74
+ </html>
75
+ )
76
+ }
77
+ `
78
+ }
79
+
80
+ function main() {
81
+ console.log("storefront build")
82
+ console.log(" client:", clientDir)
83
+ console.log(" output:", outDir)
84
+
85
+ const pagesConfigPath = join(clientDir, "pages.config.json")
86
+ const pagesConfigTs = join(clientDir, "pages.config.ts")
87
+ if (!existsSync(pagesConfigPath) && !existsSync(pagesConfigTs)) {
88
+ console.error("Missing pages.config.json or pages.config.ts in", clientDir)
89
+ process.exit(1)
90
+ }
91
+
92
+ // Dynamic import of compiled config — use jiti or read JSON fallback
93
+ const pagesConfig = loadPagesConfig(clientDir)
94
+ const packages = collectPackages(pagesConfig)
95
+
96
+ mkdirSync(join(outDir, "generated"), { recursive: true })
97
+
98
+ writeFileSync(
99
+ join(outDir, "generated", "route.manifest.ts"),
100
+ generateRouteManifest(pagesConfig)
101
+ )
102
+ writeFileSync(
103
+ join(outDir, "generated", "pages.config.json"),
104
+ JSON.stringify(pagesConfig, null, 2)
105
+ )
106
+
107
+ generateRoutePages(pagesConfig, outDir, process.env.NEXT_PUBLIC_DEFAULT_REGION || "in")
108
+ generateRootRedirects(pagesConfig, outDir, process.env.NEXT_PUBLIC_DEFAULT_REGION || "in")
109
+
110
+ if (packages.segments.includes("@pradip1995/segment-google-login")) {
111
+ generateGoogleAuthRoutes(outDir, process.env.NEXT_PUBLIC_DEFAULT_REGION || "in")
112
+ }
113
+
114
+ // Remove legacy catch-all if present
115
+ const legacyCatchAll = join(outDir, "app", "[countryCode]", "[[...slug]]")
116
+ if (existsSync(legacyCatchAll)) {
117
+ cpSync(legacyCatchAll, join(outDir, ".legacy-slug-bak"), { recursive: true })
118
+ require("fs").rmSync(legacyCatchAll, { recursive: true, force: true })
119
+ }
120
+
121
+ writeFileSync(join(outDir, "app", "layout.tsx"), generateRootLayout(loadClientStorefrontConfig(clientDir)))
122
+ writeFileSync(join(outDir, "app", "globals.css"), GLOBALS_CSS)
123
+ writeFileSync(join(outDir, "app", "page.tsx"), ROOT_PAGE)
124
+ writeFileSync(join(outDir, "app", "error.tsx"), ERROR_PAGE)
125
+ writeFileSync(join(outDir, "middleware.ts"), MIDDLEWARE)
126
+ writeFileSync(join(outDir, "tailwind.config.js"), TAILWIND_CONFIG)
127
+ writeFileSync(join(outDir, "postcss.config.js"), POSTCSS_CONFIG)
128
+ writeFileSync(join(outDir, "next.config.js"), generateNextConfig(packages))
129
+ writeFileSync(join(outDir, "tsconfig.json"), TSCONFIG)
130
+ writeFileSync(join(outDir, "package.json"), generatePackageJson(packages, clientDir))
131
+ writeFileSync(join(outDir, ".npmrc"), "legacy-peer-deps=true\n")
132
+
133
+ // Copy env from client if present
134
+ const envLocal = join(clientDir, ".env.local")
135
+ if (existsSync(envLocal)) {
136
+ cpSync(envLocal, join(outDir, ".env.local"))
137
+ }
138
+
139
+ // Copy static assets: plugin public/ dirs, then client overrides
140
+ copyPublicAssets(packages, clientDir, outDir)
141
+
142
+ generateDynamicConfigSchema(clientDir, outDir)
143
+
144
+ console.log(" generated:", packages.segments.length, "segments,", packages.layouts.length, "layouts,", packages.workflows.length, "workflows")
145
+ console.log("Done.")
146
+ }
147
+
148
+ function readClientPackageJson(clientDir) {
149
+ const pkgPath = join(clientDir, "package.json")
150
+ if (!existsSync(pkgPath)) return {}
151
+ return JSON.parse(readFileSync(pkgPath, "utf8"))
152
+ }
153
+
154
+ /** Resolve npm semver from client deps, with optional file: override for local dev. */
155
+ function resolveNpmDep(clientDeps, name, defaultVersion) {
156
+ const val = clientDeps[name]
157
+ if (typeof val === "string" && val.startsWith("file:")) {
158
+ return val
159
+ }
160
+ if (typeof val === "string" && val !== "workspace:*") {
161
+ return val
162
+ }
163
+ return defaultVersion
164
+ }
165
+
166
+ /** Resolve commerce-core from client package.json (semver or file: override). */
167
+ function resolveCommerceCoreDep(_clientDir, clientDeps = {}) {
168
+ return resolveNpmDep(clientDeps, "@pradip1995/commerce-core", "^4.0.0")
169
+ }
170
+
171
+ /** Resolve installed package root from client package.json file: or node_modules paths */
172
+ function resolvePackageRoot(clientDir, clientPkg, pkgName) {
173
+ const dep =
174
+ clientPkg.dependencies?.[pkgName] ||
175
+ clientPkg.devDependencies?.[pkgName]
176
+
177
+ if (typeof dep === "string" && dep.startsWith("file:")) {
178
+ const resolved = resolve(clientDir, dep.slice(5))
179
+ if (existsSync(resolved)) return resolved
180
+ }
181
+
182
+ const fromNodeModules = join(clientDir, "node_modules", pkgName)
183
+ if (existsSync(fromNodeModules)) return fromNodeModules
184
+
185
+ // Monorepo fallback: walk up to find storefront-components/packages/<short-name>
186
+ const shortName = pkgName.includes("/") ? pkgName.split("/").pop() : pkgName
187
+ let dir = clientDir
188
+ for (let depth = 0; depth < 10; depth++) {
189
+ const candidate = join(dir, "storefront-components", "packages", shortName)
190
+ if (existsSync(candidate)) return candidate
191
+ const parent = dirname(dir)
192
+ if (parent === dir) break
193
+ dir = parent
194
+ }
195
+
196
+ return null
197
+ }
198
+
199
+ /** Infer @pradip1995/segment-assets sibling path from any file: segment dependency */
200
+ function inferSegmentAssetsRoot(clientDir, clientPkg) {
201
+ const deps = clientPkg.dependencies || {}
202
+ for (const [name, depPath] of Object.entries(deps)) {
203
+ if (!name.startsWith("@pradip1995/segment-") || typeof depPath !== "string" || !depPath.startsWith("file:")) {
204
+ continue
205
+ }
206
+ const segmentRoot = resolve(clientDir, depPath.slice(5))
207
+ const assetsRoot = join(dirname(segmentRoot), "segment-assets")
208
+ if (existsSync(join(assetsRoot, "public"))) return assetsRoot
209
+ }
210
+ return null
211
+ }
212
+
213
+ function copyDirectoryContents(srcDir, destDir) {
214
+ mkdirSync(destDir, { recursive: true })
215
+ for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
216
+ const src = join(srcDir, entry.name)
217
+ const dest = join(destDir, entry.name)
218
+ if (entry.isDirectory()) {
219
+ copyDirectoryContents(src, dest)
220
+ } else if (entry.isFile()) {
221
+ cpSync(src, dest, { force: true })
222
+ }
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Merge public/ folders into the generated app:
228
+ * 1. @pradip1995/segment-assets (shared baseline)
229
+ * 2. Each layout + segment package that defines public/
230
+ * 3. Client project public/ (brand overrides win)
231
+ */
232
+ function copyPublicAssets(packages, clientDir, outDir) {
233
+ const clientPkg = readClientPackageJson(clientDir)
234
+ const outPublic = join(outDir, "public")
235
+ mkdirSync(outPublic, { recursive: true })
236
+ const sources = []
237
+
238
+ const assetsRoot =
239
+ resolvePackageRoot(clientDir, clientPkg, "@pradip1995/segment-assets") ||
240
+ inferSegmentAssetsRoot(clientDir, clientPkg)
241
+
242
+ if (assetsRoot) {
243
+ sources.push({ label: "@pradip1995/segment-assets", path: join(assetsRoot, "public") })
244
+ }
245
+
246
+ const pluginPackages = [
247
+ ...new Set([
248
+ ...packages.layouts,
249
+ ...packages.segments,
250
+ "@pradip1995/segment-nav",
251
+ "@pradip1995/segment-footer",
252
+ "@pradip1995/segment-promo-bar",
253
+ ]),
254
+ ].sort()
255
+
256
+ for (const pkgName of pluginPackages) {
257
+ const root = resolvePackageRoot(clientDir, clientPkg, pkgName)
258
+ if (!root) continue
259
+ sources.push({ label: pkgName, path: join(root, "public") })
260
+ }
261
+
262
+ let copied = 0
263
+ for (const source of sources) {
264
+ if (!existsSync(source.path)) continue
265
+ copyDirectoryContents(source.path, outPublic)
266
+ copied++
267
+ console.log(" assets:", relative(clientDir, source.path), `(${source.label})`)
268
+ }
269
+
270
+ const clientPublic = join(clientDir, "public")
271
+ if (existsSync(clientPublic)) {
272
+ copyDirectoryContents(clientPublic, outPublic)
273
+ copied++
274
+ console.log(" assets:", "client public/ (overrides)")
275
+ }
276
+
277
+ if (copied === 0) {
278
+ console.warn(" assets: no public/ folders found — add @pradip1995/segment-assets or client public/")
279
+ }
280
+ }
281
+
282
+ function resolveDynamicConfigSchemaDir() {
283
+ const candidates = [
284
+ join(__dirname, "..", "dynamic-config-schema"),
285
+ join(__dirname, "..", "..", "dynamic-config-schema"),
286
+ ]
287
+ for (const candidate of candidates) {
288
+ if (existsSync(join(candidate, "schemas", "homepage-config.json"))) return candidate
289
+ }
290
+ let dir = dirname(__dirname)
291
+ for (let depth = 0; depth < 6; depth++) {
292
+ const candidate = join(dir, "packages", "dynamic-config-schema")
293
+ if (existsSync(join(candidate, "schemas", "homepage-config.json"))) return candidate
294
+ const parent = dirname(dir)
295
+ if (parent === dir) break
296
+ dir = parent
297
+ }
298
+ return null
299
+ }
300
+
301
+ /**
302
+ * Emit Medusa dynamic-config schema for backend registration.
303
+ * Output: dynamic-config.generated.json with { configs: { "homepage-config": ... } }
304
+ *
305
+ * Client override: place dynamic-config.schema.json in client root (full { configs } or bare config map).
306
+ */
307
+ function generateDynamicConfigSchema(clientDir, outDir) {
308
+ const clientOverride = join(clientDir, "dynamic-config.schema.json")
309
+ let payload
310
+
311
+ if (existsSync(clientOverride)) {
312
+ payload = JSON.parse(readFileSync(clientOverride, "utf8"))
313
+ if (!payload.configs) {
314
+ payload = { configs: payload }
315
+ }
316
+ console.log(" dynamic-config: using client dynamic-config.schema.json")
317
+ } else {
318
+ const schemaDir = resolveDynamicConfigSchemaDir()
319
+ if (!schemaDir) {
320
+ console.warn(" dynamic-config: schema package not found — skip generation")
321
+ return
322
+ }
323
+ const homepage = JSON.parse(
324
+ readFileSync(join(schemaDir, "schemas", "homepage-config.json"), "utf8")
325
+ )
326
+ payload = { configs: { "homepage-config": homepage } }
327
+ console.log(" dynamic-config: from packages/dynamic-config-schema")
328
+ }
329
+
330
+ const schemaDir = resolveDynamicConfigSchemaDir()
331
+ const homepageSchema = payload.configs?.["homepage-config"]
332
+ let homepageDefaults = {}
333
+
334
+ if (homepageSchema && schemaDir) {
335
+ const { buildHomepageConfigDefaults } = require(join(schemaDir, "build-defaults.js"))
336
+ homepageDefaults = buildHomepageConfigDefaults(homepageSchema)
337
+ }
338
+
339
+ payload.defaults = {
340
+ ...(payload.defaults || {}),
341
+ "homepage-config": homepageDefaults,
342
+ }
343
+
344
+ const json = `${JSON.stringify(payload, null, 2)}\n`
345
+ writeFileSync(join(clientDir, "dynamic-config.generated.json"), json)
346
+ writeFileSync(join(outDir, "dynamic-config.generated.json"), json)
347
+
348
+ const defaultsJson = `${JSON.stringify({ "homepage-config": homepageDefaults }, null, 2)}\n`
349
+ writeFileSync(join(clientDir, "dynamic-config.defaults.json"), defaultsJson)
350
+ writeFileSync(join(outDir, "dynamic-config.defaults.json"), defaultsJson)
351
+ console.log(" dynamic-config: wrote dynamic-config.generated.json")
352
+ console.log(" dynamic-config: wrote dynamic-config.defaults.json")
353
+ }
354
+
355
+ function loadPagesConfig(clientDir) {
356
+ const jsonPath = join(clientDir, "pages.config.json")
357
+ if (existsSync(jsonPath)) {
358
+ return JSON.parse(readFileSync(jsonPath, "utf8"))
359
+ }
360
+ // Fallback: require transpiled config
361
+ try {
362
+ const jiti = require("jiti")(clientDir, { interopDefault: true })
363
+ return jiti("./pages.config.ts")
364
+ } catch {
365
+ console.error("Install jiti or provide pages.config.json")
366
+ process.exit(1)
367
+ }
368
+ }
369
+
370
+ function packageBaseName(pkg) {
371
+ if (!pkg.includes("/")) return pkg
372
+ const parts = pkg.split("/")
373
+ if (parts[0].startsWith("@") && parts.length >= 2) {
374
+ return `${parts[0]}/${parts[1]}`
375
+ }
376
+ return pkg
377
+ }
378
+
379
+ function collectPackages(pages) {
380
+ const segments = new Set()
381
+ const layouts = new Set()
382
+ const workflows = new Set()
383
+
384
+ for (const page of pages) {
385
+ workflows.add(page.workflow)
386
+ layouts.add(packageBaseName(page.layout))
387
+ for (const s of page.segments) segments.add(s)
388
+ }
389
+
390
+ // Transitive segment dependencies
391
+ if (segments.has("@pradip1995/segment-product-grid")) {
392
+ segments.add("@pradip1995/segment-product-card")
393
+ }
394
+ if (
395
+ segments.has("@pradip1995/segment-new-arrivals") ||
396
+ segments.has("@pradip1995/segment-loved-by-moms") ||
397
+ segments.has("@pradip1995/segment-bestsellers-carousel") ||
398
+ segments.has("@pradip1995/segment-collections-showcase")
399
+ ) {
400
+ segments.add("@pradip1995/segment-product-card")
401
+ }
402
+
403
+ return {
404
+ segments: [...segments],
405
+ layouts: [...layouts],
406
+ workflows: [...workflows],
407
+ }
408
+ }
409
+
410
+ function routeToAppPath(route, region = "in") {
411
+ if (route === "/") return join("app", region, "page.tsx")
412
+ if (route.endsWith("/*")) {
413
+ const base = route.slice(0, -2).replace(/^\//, "")
414
+ return join("app", region, base, "[[...path]]", "page.tsx")
415
+ }
416
+ const segments = route.replace(/^\//, "").split("/")
417
+ return join("app", region, ...segments, "page.tsx")
418
+ }
419
+
420
+ function generateRoutePages(pagesConfig, outDir, region = "in") {
421
+ for (const page of pagesConfig) {
422
+ const appPath = routeToAppPath(page.route, region)
423
+ const fullPath = join(outDir, appPath)
424
+ mkdirSync(dirname(fullPath), { recursive: true })
425
+
426
+ const workflowPkg = page.workflow
427
+ const layoutPkg = page.layout
428
+ const segmentImports = page.segments.map((s) => {
429
+ const name = s.replace("@pradip1995/", "").replace(/-/g, "_")
430
+ return { pkg: s, name }
431
+ })
432
+
433
+ let slugCode = "[]"
434
+ if (page.route === "/") {
435
+ slugCode = "[]"
436
+ } else if (page.route.includes("[handle]")) {
437
+ slugCode = '`products/${params.handle}`'.replace("params.handle", "${params.handle}")
438
+ slugCode = '["products", params.handle].filter(Boolean) as string[]'
439
+ } else if (page.route.includes("[id]")) {
440
+ slugCode = '["orders", params.id].filter(Boolean) as string[]'
441
+ } else if (page.route.endsWith("/*")) {
442
+ slugCode = '["account", ...(params.path ? (Array.isArray(params.path) ? params.path : [params.path]) : [])]'
443
+ } else {
444
+ const parts = page.route.replace(/^\//, "").split("/")
445
+ slugCode = `[${parts.map((p) => `"${p}"`).join(", ")}]`
446
+ }
447
+
448
+ const content = `import { renderConfiguredPage } from "@pradip1995/framework-runtime"
449
+ import workflow from "${workflowPkg}"
450
+ import Layout from "${layoutPkg}"
451
+ ${segmentImports.map((s) => `import ${s.name} from "${s.pkg}"`).join("\n")}
452
+
453
+ export const dynamic = "force-dynamic"
454
+
455
+ const pageConfig = {
456
+ workflow,
457
+ layout: Layout,
458
+ segments: [
459
+ ${segmentImports.map((s) => ` { pkg: "${s.pkg}", Component: ${s.name} },`).join("\n")}
460
+ ],
461
+ }
462
+
463
+ export default async function Page(props: {
464
+ params: Promise<Record<string, string | string[] | undefined>>
465
+ searchParams: Promise<Record<string, string | string[] | undefined>>
466
+ }) {
467
+ const params = await props.params
468
+ const searchParams = await props.searchParams
469
+ const countryCode = "${region}"
470
+ const slug = ${slugCode}
471
+
472
+ return renderConfiguredPage({
473
+ page: pageConfig,
474
+ countryCode,
475
+ slug,
476
+ searchParams,
477
+ })
478
+ }
479
+ `
480
+ writeFileSync(fullPath, content)
481
+ }
482
+ }
483
+
484
+ function searchParamsRedirectHelper() {
485
+ return ` const sp = await searchParams
486
+ const qs = new URLSearchParams()
487
+ for (const [key, value] of Object.entries(sp)) {
488
+ if (Array.isArray(value)) {
489
+ value.forEach((v) => qs.append(key, v))
490
+ } else if (value != null) {
491
+ qs.set(key, value)
492
+ }
493
+ }
494
+ const query = qs.toString()`
495
+ }
496
+
497
+ function generateStaticRedirectPage(route, region) {
498
+ return `import { redirect } from "next/navigation"
499
+
500
+ const REGION = process.env.NEXT_PUBLIC_DEFAULT_REGION || "${region}"
501
+
502
+ export default async function Page({
503
+ searchParams,
504
+ }: {
505
+ searchParams: Promise<Record<string, string | string[] | undefined>>
506
+ }) {
507
+ ${searchParamsRedirectHelper()}
508
+ redirect(\`/\${REGION}${route}\${query ? \`?\${query}\` : ""}\`)
509
+ }
510
+ `
511
+ }
512
+
513
+ function generateGoogleAuthRoutes(outDir, region = "in") {
514
+ const callbackPage = `"use client"
515
+
516
+ import { GoogleCallbackPage } from "@pradip1995/commerce-auth"
517
+ import {
518
+ handleGoogleAuthCallback,
519
+ handleGoogleCallback,
520
+ } from "@pradip1995/commerce-core/data/customer"
521
+
522
+ export default function GoogleCallbackRoute() {
523
+ return (
524
+ <GoogleCallbackPage
525
+ handleAuthCallback={handleGoogleAuthCallback}
526
+ handleCustomerCallback={handleGoogleCallback}
527
+ />
528
+ )
529
+ }
530
+ `
531
+
532
+ const regionPath = join(outDir, "app", region, "auth", "customer", "google", "callback", "page.tsx")
533
+ mkdirSync(dirname(regionPath), { recursive: true })
534
+ writeFileSync(regionPath, callbackPage)
535
+
536
+ const rootRedirect = `import { redirect } from "next/navigation"
537
+
538
+ const REGION = process.env.NEXT_PUBLIC_DEFAULT_REGION || "${region}"
539
+
540
+ export default async function Page({
541
+ searchParams,
542
+ }: {
543
+ searchParams: Promise<Record<string, string | string[] | undefined>>
544
+ }) {
545
+ const sp = await searchParams
546
+ const qs = new URLSearchParams()
547
+ for (const [key, value] of Object.entries(sp)) {
548
+ if (Array.isArray(value)) {
549
+ value.forEach((v) => qs.append(key, v))
550
+ } else if (value != null) {
551
+ qs.set(key, value)
552
+ }
553
+ }
554
+ const query = qs.toString()
555
+ redirect(\`/\${REGION}/auth/customer/google/callback\${query ? \`?\${query}\` : ""}\`)
556
+ }
557
+ `
558
+
559
+ const rootPath = join(outDir, "app", "auth", "customer", "google", "callback", "page.tsx")
560
+ mkdirSync(dirname(rootPath), { recursive: true })
561
+ writeFileSync(rootPath, rootRedirect)
562
+ }
563
+
564
+ function generateDynamicRedirectPage(base, param, region) {
565
+ return `import { redirect } from "next/navigation"
566
+
567
+ const REGION = process.env.NEXT_PUBLIC_DEFAULT_REGION || "${region}"
568
+
569
+ export default async function Page({
570
+ params,
571
+ searchParams,
572
+ }: {
573
+ params: Promise<Record<string, string | undefined>>
574
+ searchParams: Promise<Record<string, string | string[] | undefined>>
575
+ }) {
576
+ const { ${param} } = await params
577
+ ${searchParamsRedirectHelper()}
578
+ redirect(\`/\${REGION}/${base}/\${${param}}\${query ? \`?\${query}\` : ""}\`)
579
+ }
580
+ `
581
+ }
582
+
583
+ function generateCatchAllRedirectPage(base, region) {
584
+ return `import { redirect } from "next/navigation"
585
+
586
+ const REGION = process.env.NEXT_PUBLIC_DEFAULT_REGION || "${region}"
587
+
588
+ export default async function Page({
589
+ params,
590
+ searchParams,
591
+ }: {
592
+ params: Promise<{ path?: string[] }>
593
+ searchParams: Promise<Record<string, string | string[] | undefined>>
594
+ }) {
595
+ const { path } = await params
596
+ const subpath = path?.length ? \`/\${path.join("/")}\` : ""
597
+ ${searchParamsRedirectHelper()}
598
+ redirect(\`/\${REGION}/${base}\${subpath}\${query ? \`?\${query}\` : ""}\`)
599
+ }
600
+ `
601
+ }
602
+
603
+ /** Region-less URLs (/cart) → /{region}/cart for direct links and client nav */
604
+ function generateRootRedirects(pagesConfig, outDir, region = "in") {
605
+ for (const page of pagesConfig) {
606
+ if (page.route === "/") continue
607
+
608
+ if (page.route.endsWith("/*")) {
609
+ const base = page.route.slice(0, -2).replace(/^\//, "")
610
+ const fullPath = join(outDir, "app", base, "[[...path]]", "page.tsx")
611
+ mkdirSync(dirname(fullPath), { recursive: true })
612
+ writeFileSync(fullPath, generateCatchAllRedirectPage(base, region))
613
+ continue
614
+ }
615
+
616
+ if (page.route.includes("[handle]")) {
617
+ const fullPath = join(outDir, "app", "products", "[handle]", "page.tsx")
618
+ mkdirSync(dirname(fullPath), { recursive: true })
619
+ writeFileSync(fullPath, generateDynamicRedirectPage("products", "handle", region))
620
+ continue
621
+ }
622
+
623
+ if (page.route.includes("[id]")) {
624
+ const fullPath = join(outDir, "app", "orders", "[id]", "page.tsx")
625
+ mkdirSync(dirname(fullPath), { recursive: true })
626
+ writeFileSync(fullPath, generateDynamicRedirectPage("orders", "id", region))
627
+ continue
628
+ }
629
+
630
+ const segments = page.route.replace(/^\//, "").split("/")
631
+ const fullPath = join(outDir, "app", ...segments, "page.tsx")
632
+ mkdirSync(dirname(fullPath), { recursive: true })
633
+ writeFileSync(fullPath, generateStaticRedirectPage(page.route, region))
634
+ }
635
+ }
636
+
637
+ function generateRouteManifest(pages) {
638
+ return `export const routeManifest = ${JSON.stringify(pages, null, 2)} as const
639
+ export type RouteManifest = typeof routeManifest
640
+ `
641
+ }
642
+
643
+ function generateImportMap(packages) {
644
+ const all = [...packages.workflows, ...packages.layouts, ...packages.segments]
645
+ const entries = all
646
+ .map((pkg, i) => {
647
+ const key = pkg.replace(/[@/]/g, "_").replace(/-/g, "_")
648
+ return ` "${pkg}": () => import("${pkg}"),`
649
+ })
650
+ .join("\n")
651
+
652
+ return `export const importMap: Record<string, () => Promise<{ default: unknown }>> = {
653
+ ${entries}
654
+ }
655
+
656
+ export async function resolvePlugin(pkg: string) {
657
+ const loader = importMap[pkg]
658
+ if (!loader) throw new Error(\`Plugin not in import map: \${pkg}\`)
659
+ return loader()
660
+ }
661
+ `
662
+ }
663
+
664
+ function generatePackageJson(packages, clientDir) {
665
+ const clientPkg = existsSync(join(clientDir, "package.json"))
666
+ ? JSON.parse(readFileSync(join(clientDir, "package.json"), "utf8"))
667
+ : {}
668
+
669
+ const clientDeps = clientPkg.dependencies || {}
670
+
671
+ const deps = {
672
+ next: "15.3.8",
673
+ react: "19.0.3",
674
+ "react-dom": "19.0.3",
675
+ tailwindcss: clientDeps.tailwindcss || "^3.4.17",
676
+ postcss: clientDeps.postcss || "^8.4.49",
677
+ autoprefixer: clientDeps.autoprefixer || "^10.4.20",
678
+ color: "^5.0.3",
679
+ lodash: "^4.17.21",
680
+ qs: "^6.12.1",
681
+ "server-only": "^0.0.1",
682
+ }
683
+
684
+ const frameworkPkgNames = [
685
+ "@pradip1995/plugin-sdk",
686
+ "@pradip1995/framework-core",
687
+ "@pradip1995/framework-runtime",
688
+ "@pradip1995/medusa-connector",
689
+ "@pradip1995/workflow-home",
690
+ "@pradip1995/workflow-layout",
691
+ "@pradip1995/workflow-store",
692
+ "@pradip1995/workflow-product",
693
+ "@pradip1995/workflow-cart",
694
+ "@pradip1995/workflow-checkout",
695
+ "@pradip1995/workflow-account",
696
+ "@pradip1995/workflow-order",
697
+ "@pradip1995/workflow-wishlist",
698
+ "@pradip1995/workflow-help",
699
+ ]
700
+
701
+ for (const name of frameworkPkgNames) {
702
+ deps[name] = resolveNpmDep(clientDeps, name, "^0.2.0")
703
+ }
704
+
705
+ deps["@pradip1995/commerce-core"] = resolveCommerceCoreDep(clientDir, clientDeps)
706
+ deps["@medusajs/js-sdk"] = "^2.8.0"
707
+ deps["@medusajs/types"] = "^2.8.0"
708
+
709
+ // Always include tokens/primitives/loader for layout CSS and global loader
710
+ for (const pkg of [
711
+ "@pradip1995/segment-tokens",
712
+ "@pradip1995/segment-primitives",
713
+ "@pradip1995/segment-loader",
714
+ "@pradip1995/segment-nav",
715
+ "@pradip1995/segment-footer",
716
+ "@pradip1995/segment-promo-bar",
717
+ "@pradip1995/segment-product-card",
718
+ ]) {
719
+ if (clientDeps[pkg]) {
720
+ deps[pkg] = resolveNpmDep(clientDeps, pkg, "^0.2.0")
721
+ }
722
+ }
723
+
724
+ for (const pkg of [...packages.workflows, ...packages.layouts, ...packages.segments]) {
725
+ if (frameworkPkgNames.includes(pkg)) continue
726
+ if (clientDeps[pkg]) {
727
+ deps[pkg] = resolveNpmDep(clientDeps, pkg, "^0.2.0")
728
+ }
729
+ }
730
+
731
+ if (packages.segments.includes("@pradip1995/segment-google-login")) {
732
+ deps["@pradip1995/commerce-auth"] = resolveNpmDep(clientDeps, "@pradip1995/commerce-auth", "^4.0.0")
733
+ }
734
+
735
+ return JSON.stringify(
736
+ {
737
+ name: "generated-storefront",
738
+ private: true,
739
+ scripts: {
740
+ dev: "next dev",
741
+ build: "next build",
742
+ start: "next start",
743
+ },
744
+ dependencies: deps,
745
+ devDependencies: {
746
+ "@types/node": "^22",
747
+ "@types/react": "^19",
748
+ "@types/react-dom": "^19",
749
+ typescript: "^5.7.2",
750
+ },
751
+ },
752
+ null,
753
+ 2
754
+ )
755
+ }
756
+
757
+ const ROOT_LAYOUT = `import "@pradip1995/segment-tokens/theme.css"
758
+ import "./globals.css"
759
+
760
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
761
+ return (
762
+ <html lang="en">
763
+ <body className="min-h-screen">{children}</body>
764
+ </html>
765
+ )
766
+ }
767
+ `
768
+
769
+ const GLOBALS_CSS = `@tailwind base;
770
+ @tailwind components;
771
+ @tailwind utilities;
772
+ `
773
+
774
+ const TAILWIND_CONFIG = `/** @type {import('tailwindcss').Config} */
775
+ module.exports = {
776
+ content: [
777
+ "./app/**/*.{js,ts,jsx,tsx}",
778
+ "./node_modules/@pradip1995/segment-*/src/**/*.{js,ts,jsx,tsx}",
779
+ "./node_modules/@pradip1995/layout-*/src/**/*.{js,ts,jsx,tsx}",
780
+ "./node_modules/@pradip1995/segment-primitives/src/**/*.{js,ts,jsx,tsx}",
781
+ "./node_modules/@pradip1995/commerce-core/src/**/*.{js,ts,jsx,tsx}",
782
+ ],
783
+ theme: {
784
+ extend: {
785
+ colors: {
786
+ page: { bg: "var(--color-page-bg)" },
787
+ surface: { DEFAULT: "var(--color-surface)", muted: "var(--color-surface-muted)" },
788
+ hero: { fallback: "var(--color-hero-fallback)" },
789
+ heading: { DEFAULT: "var(--color-text-heading)", sub: "var(--color-text-subheading)" },
790
+ brand: {
791
+ accent: "var(--color-brand-accent)",
792
+ "accent-hover": "var(--color-brand-accent-hover)",
793
+ pink: "var(--color-brand-pink)",
794
+ primary: "var(--color-brand-primary)",
795
+ sale: "var(--color-brand-sale)",
796
+ success: "var(--color-brand-success)",
797
+ footer: "var(--color-footer-bg)",
798
+ },
799
+ body: "var(--color-text-body)",
800
+ muted: "var(--color-text-muted)",
801
+ inverse: "var(--color-text-inverse)",
802
+ },
803
+ fontFamily: {
804
+ sans: ["var(--font-sans)"],
805
+ heading: ["var(--font-heading)"],
806
+ display: ["var(--font-display)"],
807
+ quote: ["var(--font-quote)"],
808
+ },
809
+ lineHeight: {
810
+ heading: "var(--line-height-heading)",
811
+ body: "var(--line-height-body)",
812
+ },
813
+ letterSpacing: {
814
+ nav: "var(--letter-spacing-nav)",
815
+ },
816
+ borderColor: {
817
+ "cart-border": "var(--color-border)",
818
+ },
819
+ boxShadow: {
820
+ brand: "var(--shadow-brand)",
821
+ "brand-sm": "var(--shadow-brand-sm)",
822
+ card: "var(--shadow-card)",
823
+ },
824
+ backgroundImage: {
825
+ "promo-gradient": "var(--gradient-promo)",
826
+ "hero-gradient": "var(--gradient-hero)",
827
+ },
828
+ },
829
+ },
830
+ plugins: [],
831
+ }
832
+ `
833
+
834
+ const POSTCSS_CONFIG = `module.exports = {
835
+ plugins: { tailwindcss: {}, autoprefixer: {} },
836
+ }
837
+ `
838
+
839
+ const CATCH_ALL_PAGE = `import { renderPage } from "@pradip1995/framework-runtime"
840
+ import { routeManifest } from "@generated/route.manifest"
841
+ import { importMap } from "@generated/import-map"
842
+
843
+ export const dynamic = "force-dynamic"
844
+
845
+ export default async function Page(props: {
846
+ params: Promise<{ countryCode: string; slug?: string[] }>
847
+ searchParams: Promise<Record<string, string | string[] | undefined>>
848
+ }) {
849
+ const params = await props.params
850
+ const searchParams = await props.searchParams
851
+ return renderPage({
852
+ countryCode: params.countryCode,
853
+ slug: params.slug ?? [],
854
+ searchParams,
855
+ routeManifest: routeManifest as import("@pradip1995/plugin-sdk").PageConfigEntry[],
856
+ importMap,
857
+ })
858
+ }
859
+ `
860
+
861
+ const ROOT_PAGE = `import { redirect } from "next/navigation"
862
+
863
+ export default function RootPage() {
864
+ redirect(\`/\${process.env.NEXT_PUBLIC_DEFAULT_REGION || "in"}\`)
865
+ }
866
+ `
867
+
868
+ const MIDDLEWARE = `import { NextRequest, NextResponse } from "next/server"
869
+
870
+ const DEFAULT_REGION = process.env.NEXT_PUBLIC_DEFAULT_REGION || "in"
871
+
872
+ /** Paths served under /{region}/… in the generated app */
873
+ const REGION_SCOPED = new Set([
874
+ "store",
875
+ "cart",
876
+ "checkout",
877
+ "account",
878
+ "products",
879
+ "orders",
880
+ "collections",
881
+ "auth",
882
+ ])
883
+
884
+ function isRegionCode(segment: string) {
885
+ return segment.length >= 2 && segment.length <= 3 && /^[a-z]{2,3}$/i.test(segment)
886
+ }
887
+
888
+ export async function middleware(request: NextRequest) {
889
+ const pathname = request.nextUrl.pathname
890
+
891
+ if (pathname.includes(".") || pathname.startsWith("/api") || pathname.startsWith("/_next")) {
892
+ return NextResponse.next()
893
+ }
894
+
895
+ const segments = pathname.split("/").filter(Boolean)
896
+ const firstSegment = segments[0]?.toLowerCase() ?? ""
897
+
898
+ if (pathname === "/" || !firstSegment) {
899
+ return NextResponse.redirect(new URL(\`/\${DEFAULT_REGION}\`, request.url))
900
+ }
901
+
902
+ if (isRegionCode(firstSegment)) {
903
+ return NextResponse.next()
904
+ }
905
+
906
+ if (REGION_SCOPED.has(firstSegment)) {
907
+ const url = request.nextUrl.clone()
908
+ url.pathname = \`/\${DEFAULT_REGION}\${pathname}\`
909
+ return NextResponse.redirect(url)
910
+ }
911
+
912
+ return NextResponse.next()
913
+ }
914
+
915
+ export const config = {
916
+ matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
917
+ }
918
+ `
919
+
920
+ const ERROR_PAGE = `"use client"
921
+
922
+ export default function Error({
923
+ error,
924
+ reset,
925
+ }: {
926
+ error: Error & { digest?: string }
927
+ reset: () => void
928
+ }) {
929
+ return (
930
+ <main className="min-h-screen p-8">
931
+ <h2 className="text-xl font-semibold mb-2">Something went wrong</h2>
932
+ <p className="text-sm text-muted mb-4">{error.message}</p>
933
+ <button type="button" onClick={() => reset()} className="px-4 py-2 bg-brand-accent text-inverse text-sm">
934
+ Try again
935
+ </button>
936
+ </main>
937
+ )
938
+ }
939
+ `
940
+
941
+ function generateNextConfig(packages) {
942
+ const uiPackages = [
943
+ ...packages.workflows,
944
+ ...packages.layouts,
945
+ ...packages.segments,
946
+ "@pradip1995/segment-nav",
947
+ "@pradip1995/segment-footer",
948
+ "@pradip1995/segment-promo-bar",
949
+ "@pradip1995/segment-primitives",
950
+ "@pradip1995/segment-tokens",
951
+ "@pradip1995/framework-runtime",
952
+ "@pradip1995/framework-core",
953
+ "@pradip1995/plugin-sdk",
954
+ "@pradip1995/medusa-connector",
955
+ "@pradip1995/commerce-core",
956
+ ]
957
+ if (packages.segments.includes("@pradip1995/segment-google-login")) {
958
+ uiPackages.push("@pradip1995/commerce-auth")
959
+ }
960
+ const transpile = [...new Set(uiPackages)]
961
+
962
+ return `const path = require("path")
963
+
964
+ /** @type {import('next').NextConfig} */
965
+ module.exports = {
966
+ transpilePackages: ${JSON.stringify(transpile, null, 4)},
967
+ images: { remotePatterns: [{ protocol: "https", hostname: "**" }] },
968
+ webpack: (config) => {
969
+ config.resolve.modules = [
970
+ path.resolve(__dirname, "node_modules"),
971
+ ...(config.resolve.modules || []),
972
+ ]
973
+ try {
974
+ const commerceCoreEntry = require.resolve("@pradip1995/commerce-core/config")
975
+ config.resolve.alias = {
976
+ ...config.resolve.alias,
977
+ "@core": path.dirname(commerceCoreEntry),
978
+ }
979
+ } catch {}
980
+ return config
981
+ },
982
+ }
983
+ `
984
+ }
985
+
986
+ const NEXT_CONFIG = generateNextConfig({ workflows: [], layouts: [], segments: [] })
987
+
988
+ const TSCONFIG = JSON.stringify(
989
+ {
990
+ compilerOptions: {
991
+ target: "ES2022",
992
+ lib: ["dom", "dom.iterable", "esnext"],
993
+ allowJs: true,
994
+ skipLibCheck: true,
995
+ strict: true,
996
+ noEmit: true,
997
+ esModuleInterop: true,
998
+ module: "esnext",
999
+ moduleResolution: "bundler",
1000
+ resolveJsonModule: true,
1001
+ isolatedModules: true,
1002
+ jsx: "preserve",
1003
+ incremental: true,
1004
+ plugins: [{ name: "next" }],
1005
+ paths: {
1006
+ "@generated/*": ["./generated/*"],
1007
+ },
1008
+ },
1009
+ include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
1010
+ exclude: ["node_modules"],
1011
+ },
1012
+ null,
1013
+ 2
1014
+ )
1015
+
1016
+ main()
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@pradip1995/framework-compiler",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "bin": {
10
+ "storefront-build": "./bin/storefront-build.js"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "templates"
15
+ ],
16
+ "dependencies": {
17
+ "jiti": "^2.4.2"
18
+ },
19
+ "scripts": {
20
+ "build": "echo ok",
21
+ "typecheck": "echo ok",
22
+ "lint": "echo ok"
23
+ }
24
+ }
@@ -0,0 +1,61 @@
1
+ import { matchRoute, buildRouteContext, container } from "@pradip1995/framework-core"
2
+ import { resolvePlugin } from "@generated/import-map"
3
+ import { routeManifest } from "@generated/route.manifest"
4
+ import { registerServices } from "./register-services"
5
+ import { getSegmentProps } from "./segment-props"
6
+ import type { ComponentType } from "react"
7
+
8
+ type RenderPageInput = {
9
+ countryCode: string
10
+ slug: string[]
11
+ searchParams: Record<string, string | string[] | undefined>
12
+ }
13
+
14
+ export async function renderPage(input: RenderPageInput) {
15
+ registerServices()
16
+
17
+ const pathname =
18
+ input.slug.length > 0 ? `/${input.slug.join("/")}` : "/"
19
+ const page = matchRoute(routeManifest as Parameters<typeof matchRoute>[0], pathname)
20
+
21
+ if (!page) {
22
+ const { notFound } = await import("next/navigation")
23
+ notFound()
24
+ }
25
+
26
+ const route = buildRouteContext(input.countryCode, input.slug, input.searchParams)
27
+
28
+ const workflowMod = (await resolvePlugin(page!.workflow)) as {
29
+ default: { execute: (ctx: unknown) => Promise<Record<string, unknown>> }
30
+ }
31
+ const services: Record<string, unknown> = {}
32
+ for (const [key] of container["factories"] ?? new Map()) {
33
+ if (container.has(key)) {
34
+ services[key] = container.resolve(key)
35
+ }
36
+ }
37
+
38
+ const data = await workflowMod.default.execute({ route, services })
39
+
40
+ const layoutMod = (await resolvePlugin(page!.layout)) as {
41
+ default: ComponentType<{ data: Record<string, unknown>; children: React.ReactNode }>
42
+ }
43
+ const Layout = layoutMod.default
44
+
45
+ const segmentElements = await Promise.all(
46
+ page!.segments.map(async (segmentPkg) => {
47
+ const mod = (await resolvePlugin(segmentPkg)) as {
48
+ default: ComponentType<Record<string, unknown>>
49
+ }
50
+ const props = getSegmentProps(segmentPkg, data)
51
+ const Segment = mod.default
52
+ return <Segment key={segmentPkg} {...props} />
53
+ })
54
+ )
55
+
56
+ return (
57
+ <Layout data={data}>
58
+ {segmentElements}
59
+ </Layout>
60
+ )
61
+ }