@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.
- package/bin/storefront-build.js +1016 -0
- package/package.json +24 -0
- package/templates/render-page.tsx +61 -0
|
@@ -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
|
+
}
|