@fecommunity/reactpress-theme-starter 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/.env.example +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +286 -0
  4. package/app/api/[[...path]]/route.ts +69 -0
  5. package/app/archives/page.tsx +28 -0
  6. package/app/article/[id]/page.tsx +92 -0
  7. package/app/category/[category]/page.tsx +60 -0
  8. package/app/globals.css +4969 -0
  9. package/app/knowledge/[pId]/[id]/page.tsx +54 -0
  10. package/app/knowledge/[pId]/page.tsx +47 -0
  11. package/app/knowledge/page.tsx +29 -0
  12. package/app/layout.tsx +76 -0
  13. package/app/llms.txt/route.ts +26 -0
  14. package/app/login/page.tsx +17 -0
  15. package/app/nav/[id]/page.tsx +44 -0
  16. package/app/nav/page.tsx +5 -0
  17. package/app/not-found.tsx +21 -0
  18. package/app/page/[id]/page.tsx +57 -0
  19. package/app/page.tsx +61 -0
  20. package/app/register/page.tsx +17 -0
  21. package/app/robots.ts +14 -0
  22. package/app/rss/route.ts +82 -0
  23. package/app/search/page.tsx +46 -0
  24. package/app/sitemap.ts +38 -0
  25. package/app/suggestions/page.tsx +47 -0
  26. package/app/tag/[tag]/page.tsx +58 -0
  27. package/app/tags/page.tsx +18 -0
  28. package/app/template.tsx +6 -0
  29. package/components/article/ArchiveBanner.tsx +40 -0
  30. package/components/article/ArticleActionsBar.tsx +53 -0
  31. package/components/article/ArticleCarousel.tsx +237 -0
  32. package/components/article/ArticleCopyrightBar.tsx +128 -0
  33. package/components/article/ArticleEndBar.tsx +19 -0
  34. package/components/article/ArticleFeedSection.tsx +59 -0
  35. package/components/article/ArticleList.tsx +170 -0
  36. package/components/article/ArticleReadingChrome.tsx +104 -0
  37. package/components/article/ArticleReadingProgress.tsx +75 -0
  38. package/components/article/ArticleRecommend.tsx +362 -0
  39. package/components/article/ArticleTocPanel.tsx +19 -0
  40. package/components/article/ArticleViewClient.tsx +221 -0
  41. package/components/article/CategoryMenu.tsx +67 -0
  42. package/components/article/CmsPageClient.tsx +92 -0
  43. package/components/article/LoadMore.tsx +59 -0
  44. package/components/article/ReadingContent.tsx +51 -0
  45. package/components/auth/AuthPageClient.tsx +314 -0
  46. package/components/comment/CommentEditor.tsx +290 -0
  47. package/components/comment/CommentEmoji.tsx +110 -0
  48. package/components/comment/CommentIconButton.tsx +37 -0
  49. package/components/comment/CommentItem.tsx +135 -0
  50. package/components/comment/CommentSection.tsx +123 -0
  51. package/components/comment/emojis.ts +152 -0
  52. package/components/knowledge/KnowledgeList.tsx +70 -0
  53. package/components/layout/BackToTop.tsx +27 -0
  54. package/components/layout/ConditionalSiteFooter.tsx +29 -0
  55. package/components/layout/ContentStylesLoader.tsx +9 -0
  56. package/components/layout/DoubleColumnLayout.tsx +171 -0
  57. package/components/layout/HeaderSearch.tsx +23 -0
  58. package/components/layout/LocaleSwitcher.tsx +24 -0
  59. package/components/layout/PageContainer.tsx +11 -0
  60. package/components/layout/PageLoadProgress.tsx +169 -0
  61. package/components/layout/SiteFooter.tsx +75 -0
  62. package/components/layout/SiteHeader.tsx +312 -0
  63. package/components/layout/SystemNotification.tsx +176 -0
  64. package/components/layout/ThemeSwitch.tsx +19 -0
  65. package/components/layout/UserAuth.tsx +130 -0
  66. package/components/search/SearchHero.tsx +323 -0
  67. package/components/search/SearchQuickLinks.tsx +285 -0
  68. package/components/shared/DevChunkRecovery.tsx +8 -0
  69. package/components/shared/Link.tsx +30 -0
  70. package/components/shared/SectionHeading.tsx +38 -0
  71. package/components/shared/ThemeImage.tsx +13 -0
  72. package/components/shared/logo.svg +7 -0
  73. package/components/views/ArchivesClient.tsx +137 -0
  74. package/components/views/CategoryClient.tsx +87 -0
  75. package/components/views/HomeClient.tsx +79 -0
  76. package/components/views/KnowledgeBookClient.tsx +127 -0
  77. package/components/views/KnowledgeChapterClient.tsx +175 -0
  78. package/components/views/KnowledgeClient.tsx +69 -0
  79. package/components/views/LoginClient.tsx +80 -0
  80. package/components/views/NavDetailClient.tsx +177 -0
  81. package/components/views/SearchClient.tsx +100 -0
  82. package/components/views/TagArchiveClient.tsx +105 -0
  83. package/components/views/TagsClient.tsx +46 -0
  84. package/components/widgets/AboutUs.tsx +180 -0
  85. package/components/widgets/ArticleActionButton.tsx +95 -0
  86. package/components/widgets/BookmarkWidget.tsx +61 -0
  87. package/components/widgets/CategoriesWidget.tsx +47 -0
  88. package/components/widgets/HomeSidebar.tsx +40 -0
  89. package/components/widgets/LikesWidget.tsx +77 -0
  90. package/components/widgets/ShareWidget.tsx +120 -0
  91. package/components/widgets/TagCloud.tsx +94 -0
  92. package/components/widgets/TagsWidget.tsx +160 -0
  93. package/eslint.config.mjs +80 -0
  94. package/lib/auth/githubOAuth.ts +26 -0
  95. package/lib/auth/sessionUser.ts +41 -0
  96. package/lib/mock-api/auth.ts +129 -0
  97. package/lib/mock-api/config.ts +11 -0
  98. package/lib/mock-api/cors.ts +53 -0
  99. package/lib/mock-api/data.ts +998 -0
  100. package/lib/mock-api/match-route.ts +202 -0
  101. package/lib/mock-api/proxy.ts +28 -0
  102. package/lib/providers/client.ts +58 -0
  103. package/lib/providers/materializeProvider.ts +9 -0
  104. package/lib/providers/server.ts +8 -0
  105. package/lib/reactpress/appearance.ts +69 -0
  106. package/lib/reactpress/bootstrap.ts +48 -0
  107. package/lib/reactpress/coercePublishedPage.ts +12 -0
  108. package/lib/reactpress/contentSeo.ts +123 -0
  109. package/lib/reactpress/env.ts +31 -0
  110. package/lib/reactpress/feedFooterPlacement.ts +16 -0
  111. package/lib/reactpress/fetchSetting.ts +19 -0
  112. package/lib/reactpress/homeFooter.tsx +27 -0
  113. package/lib/reactpress/layout-shell.tsx +19 -0
  114. package/lib/reactpress/materializeBootstrapCatalog.ts +31 -0
  115. package/lib/reactpress/normalizeApiResponse.ts +25 -0
  116. package/lib/reactpress/providers.tsx +199 -0
  117. package/lib/reactpress/resolveSiteNav.ts +56 -0
  118. package/lib/reactpress/seoMetadata.ts +82 -0
  119. package/lib/reactpress/serverLocale.ts +43 -0
  120. package/lib/reactpress/siteMetadata.ts +151 -0
  121. package/lib/reactpress/staticParams.ts +46 -0
  122. package/lib/reactpress/suggestionsContent.ts +48 -0
  123. package/lib/reactpress/useFeedFooterPlacement.ts +22 -0
  124. package/lib/reactpress/useSiteNavConfig.ts +39 -0
  125. package/lib/utils/archiveBanner.ts +8 -0
  126. package/lib/utils/colors.ts +119 -0
  127. package/lib/utils/comment.ts +46 -0
  128. package/lib/utils/headerPosition.ts +55 -0
  129. package/lib/utils/icons.tsx +238 -0
  130. package/lib/utils/reading.ts +16 -0
  131. package/lib/utils/scroll.ts +8 -0
  132. package/lib/utils/tagCloud.ts +222 -0
  133. package/next.config.js +103 -0
  134. package/package.json +98 -0
  135. package/postcss.config.js +5 -0
  136. package/prettier.config.js +10 -0
  137. package/public/apple-touch-icon.png +0 -0
  138. package/public/cli.png +0 -0
  139. package/public/favicon-32.png +0 -0
  140. package/public/favicon.ico +0 -0
  141. package/public/home-dark.png +0 -0
  142. package/public/home-light.png +0 -0
  143. package/public/icon-192.png +0 -0
  144. package/public/icon-512.png +0 -0
  145. package/public/logo-400.png +0 -0
  146. package/public/logo.png +0 -0
  147. package/public/mobile.png +0 -0
  148. package/public/pad.png +0 -0
  149. package/public/site.webmanifest +19 -0
  150. package/scripts/build-mock.mjs +139 -0
  151. package/scripts/dev.mjs +221 -0
  152. package/scripts/ensure-env.mjs +100 -0
  153. package/scripts/remote-dev.mjs +143 -0
  154. package/scripts/resolve-api-env.mjs +114 -0
  155. package/scripts/set-publish-version.mjs +31 -0
  156. package/scripts/smoke-pages.mjs +113 -0
  157. package/scripts/start-mock.mjs +79 -0
  158. package/scripts/verify-toolkit.mjs +37 -0
  159. package/svg.d.ts +6 -0
  160. package/theme.json +123 -0
  161. package/theme.manifest.schema.json +265 -0
  162. package/tsconfig.json +41 -0
  163. package/vercel.json +12 -0
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Theme dev server with optional remote ReactPress API.
4
+ *
5
+ * Examples:
6
+ * pnpm dev
7
+ * pnpm dev -- --remote-origin api.yoursite.com
8
+ * pnpm dev -- --remote-origin https://api.example.com --port 3001
9
+ */
10
+ import { spawn } from 'node:child_process'
11
+ import path from 'node:path'
12
+ import { fileURLToPath } from 'node:url'
13
+ import {
14
+ applyDevApiOriginsToEnv,
15
+ normalizeRemoteOrigin,
16
+ readDevClientApiOrigin,
17
+ resolveDevApiOrigins,
18
+ resolveRemoteThemeApiBase,
19
+ } from './remote-dev.mjs'
20
+ import {
21
+ DEFAULT_LOCAL_API,
22
+ DEFAULT_THEME_DEV_PORT,
23
+ resolveThemeApiEnv as resolveProjectApiEnv,
24
+ } from './resolve-api-env.mjs'
25
+
26
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
27
+ const projectRoot = path.resolve(__dirname, '..')
28
+
29
+ const ERROR_MESSAGES = {
30
+ REMOTE_DEFAULT_REQUIRED:
31
+ 'remote mode requires a URL: pass --remote-origin or set REACTPRESS_DEV_REMOTE_ORIGIN',
32
+ INVALID_ORIGIN: 'Invalid API origin — use a hostname or full URL (e.g. api.yoursite.com)',
33
+ }
34
+
35
+ function parseArgs(argv) {
36
+ const result = {
37
+ port: DEFAULT_THEME_DEV_PORT,
38
+ remoteOrigin: undefined,
39
+ adminOrigin: undefined,
40
+ clientOrigin: undefined,
41
+ passthrough: [],
42
+ }
43
+
44
+ for (let i = 0; i < argv.length; i += 1) {
45
+ const arg = argv[i]
46
+
47
+ if (arg === '--remote-origin') {
48
+ result.remoteOrigin = argv[++i]
49
+ continue
50
+ }
51
+ if (arg.startsWith('--remote-origin=')) {
52
+ result.remoteOrigin = arg.slice('--remote-origin='.length)
53
+ continue
54
+ }
55
+ if (arg === '--admin-origin') {
56
+ result.adminOrigin = argv[++i]
57
+ continue
58
+ }
59
+ if (arg.startsWith('--admin-origin=')) {
60
+ result.adminOrigin = arg.slice('--admin-origin='.length)
61
+ continue
62
+ }
63
+ if (arg === '--client-origin') {
64
+ result.clientOrigin = argv[++i]
65
+ continue
66
+ }
67
+ if (arg.startsWith('--client-origin=')) {
68
+ result.clientOrigin = arg.slice('--client-origin='.length)
69
+ continue
70
+ }
71
+ if (arg === '-p' || arg === '--port') {
72
+ result.port = Number(argv[++i]) || DEFAULT_THEME_DEV_PORT
73
+ continue
74
+ }
75
+ if (arg.startsWith('--port=')) {
76
+ result.port = Number(arg.slice('--port='.length)) || DEFAULT_THEME_DEV_PORT
77
+ continue
78
+ }
79
+
80
+ result.passthrough.push(arg)
81
+ }
82
+
83
+ return result
84
+ }
85
+
86
+ function readLocalApiUrl() {
87
+ const fromEnv =
88
+ process.env.REACTPRESS_API_URL?.trim() ||
89
+ process.env.SERVER_API_URL?.trim() ||
90
+ DEFAULT_LOCAL_API
91
+ return fromEnv.replace(/\/$/, '')
92
+ }
93
+
94
+ function resolveThemeApiEnv(cli) {
95
+ const hasOriginFlag =
96
+ cli.remoteOrigin !== undefined ||
97
+ cli.adminOrigin !== undefined ||
98
+ cli.clientOrigin !== undefined
99
+
100
+ if (hasOriginFlag) {
101
+ const resolved = resolveDevApiOrigins(projectRoot, {
102
+ remoteOrigin: cli.remoteOrigin,
103
+ adminOrigin: cli.adminOrigin,
104
+ clientOrigin: cli.clientOrigin,
105
+ })
106
+
107
+ if (resolved.error) {
108
+ console.error(`[dev] ${ERROR_MESSAGES[resolved.error] || resolved.error}`)
109
+ process.exit(1)
110
+ }
111
+
112
+ if (
113
+ cli.remoteOrigin !== undefined &&
114
+ !resolved.remoteDefault &&
115
+ cli.adminOrigin === undefined &&
116
+ cli.clientOrigin === undefined
117
+ ) {
118
+ console.error('[dev] --remote-origin requires a hostname (e.g. api.yoursite.com)')
119
+ process.exit(1)
120
+ }
121
+
122
+ applyDevApiOriginsToEnv(resolved)
123
+
124
+ const remoteClient =
125
+ resolved.client ||
126
+ (cli.remoteOrigin !== undefined && resolved.remoteDefault ? resolved.remoteDefault : null)
127
+
128
+ if (remoteClient) {
129
+ const apiBase = resolveRemoteThemeApiBase(remoteClient)
130
+ return {
131
+ serverApiUrl: apiBase,
132
+ publicApiUrl: apiBase,
133
+ remoteOrigin: normalizeRemoteOrigin(remoteClient),
134
+ }
135
+ }
136
+ }
137
+
138
+ const envClient = readDevClientApiOrigin(projectRoot)
139
+ if (envClient) {
140
+ const apiBase = resolveRemoteThemeApiBase(envClient)
141
+ return {
142
+ serverApiUrl: apiBase,
143
+ publicApiUrl: apiBase,
144
+ remoteOrigin: normalizeRemoteOrigin(envClient),
145
+ }
146
+ }
147
+
148
+ const local = readLocalApiUrl()
149
+ const publicApi = process.env.NEXT_PUBLIC_REACTPRESS_API_URL?.trim()?.replace(/\/$/, '') || local
150
+
151
+ return {
152
+ serverApiUrl: local,
153
+ publicApiUrl: publicApi,
154
+ remoteOrigin: null,
155
+ }
156
+ }
157
+
158
+ function buildChildEnv(apiEnv, port) {
159
+ const projectEnv = resolveProjectApiEnv()
160
+ const mockEnabled = process.env.REACTPRESS_MOCK_API === '1'
161
+ const localMockApi = `http://localhost:${port}/api`
162
+ const serverApiUrl = mockEnabled ? localMockApi : apiEnv.serverApiUrl
163
+ const publicApiUrl = mockEnabled ? '/api' : apiEnv.publicApiUrl
164
+
165
+ return {
166
+ ...process.env,
167
+ INIT_CWD: projectRoot,
168
+ REACTPRESS_ORIGINAL_CWD: projectRoot,
169
+ SERVER_API_URL: serverApiUrl,
170
+ REACTPRESS_API_URL: serverApiUrl,
171
+ NEXT_PUBLIC_REACTPRESS_API_URL: publicApiUrl,
172
+ NEXT_PUBLIC_REACTPRESS_ADMIN_URL: projectEnv.NEXT_PUBLIC_REACTPRESS_ADMIN_URL,
173
+ CLIENT_SITE_URL: projectEnv.CLIENT_SITE_URL,
174
+ REACTPRESS_MOCK_API: mockEnabled ? '1' : '',
175
+ NEXT_TELEMETRY_DISABLED: '1',
176
+ }
177
+ }
178
+
179
+ function printBanner(apiEnv, port) {
180
+ const mockEnabled = process.env.REACTPRESS_MOCK_API === '1'
181
+ console.log('')
182
+ console.log(' ReactPress Theme Dev')
183
+ console.log(` Local: http://localhost:${port}`)
184
+ if (mockEnabled) {
185
+ console.log(` API: /api (mock routes, same-origin in browser)`)
186
+ } else if (apiEnv.remoteOrigin) {
187
+ console.log(` API: ${apiEnv.serverApiUrl} (remote: ${apiEnv.remoteOrigin})`)
188
+ } else {
189
+ console.log(` API: ${apiEnv.serverApiUrl} (local)`)
190
+ }
191
+ console.log('')
192
+ }
193
+
194
+ function main() {
195
+ const cli = parseArgs(process.argv.slice(2))
196
+ const apiEnv = resolveThemeApiEnv(cli)
197
+ const childEnv = buildChildEnv(apiEnv, cli.port)
198
+
199
+ printBanner(apiEnv, cli.port)
200
+
201
+ const nextBin = path.join(projectRoot, 'node_modules', 'next', 'dist', 'bin', 'next')
202
+ const child = spawn(
203
+ process.execPath,
204
+ [nextBin, 'dev', '-p', String(cli.port), ...cli.passthrough],
205
+ {
206
+ cwd: projectRoot,
207
+ env: childEnv,
208
+ stdio: 'inherit',
209
+ }
210
+ )
211
+
212
+ child.on('exit', (code, signal) => {
213
+ if (signal) {
214
+ process.kill(process.pid, signal)
215
+ return
216
+ }
217
+ process.exit(code ?? 0)
218
+ })
219
+ }
220
+
221
+ main()
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Ensure a `.env` file exists for @fecommunity/reactpress-toolkit/config (requires on-disk env).
4
+ * Copies from `.env.example` when missing; optionally merges overrides from CI / Vercel / mock build.
5
+ */
6
+ import fs from 'node:fs'
7
+ import path from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
11
+ const projectRoot = path.resolve(__dirname, '..')
12
+
13
+ const MERGE_KEYS = [
14
+ 'REACTPRESS_API_URL',
15
+ 'SERVER_API_URL',
16
+ 'NEXT_PUBLIC_REACTPRESS_API_URL',
17
+ 'CLIENT_SITE_URL',
18
+ 'BASE_PATH',
19
+ 'REACTPRESS_MOCK_API',
20
+ 'NEXT_PUBLIC_REACTPRESS_ADMIN_URL',
21
+ 'REACTPRESS_DEV_REMOTE_ORIGIN',
22
+ ]
23
+
24
+ const FALLBACK_ENV = `# Auto-generated for CI / Vercel mock build
25
+ REACTPRESS_API_URL=http://localhost:3002/api
26
+ NEXT_PUBLIC_REACTPRESS_API_URL=http://localhost:3002/api
27
+ CLIENT_SITE_URL=http://localhost:3001
28
+ `
29
+
30
+ function readBaseContent(envPath, examplePath) {
31
+ if (fs.existsSync(envPath)) {
32
+ return fs.readFileSync(envPath, 'utf8')
33
+ }
34
+ if (fs.existsSync(examplePath)) {
35
+ return fs.readFileSync(examplePath, 'utf8')
36
+ }
37
+ return FALLBACK_ENV
38
+ }
39
+
40
+ function upsertEnvLine(lines, key, value) {
41
+ const entry = `${key}=${value}`
42
+ const index = lines.findIndex((line) => {
43
+ const trimmed = line.trim()
44
+ return trimmed && !trimmed.startsWith('#') && trimmed.startsWith(`${key}=`)
45
+ })
46
+
47
+ if (index >= 0) {
48
+ lines[index] = entry
49
+ } else {
50
+ lines.push(entry)
51
+ }
52
+ }
53
+
54
+ /**
55
+ * @param {string} [root]
56
+ * @param {{ onlyIfMissing?: boolean, overrides?: Record<string, string | undefined> }} [options]
57
+ */
58
+ export function ensureEnvFile(root = projectRoot, options = {}) {
59
+ const { onlyIfMissing = false, overrides = {} } = options
60
+ const envPath = path.join(root, '.env')
61
+ const examplePath = path.join(root, '.env.example')
62
+
63
+ if (onlyIfMissing && fs.existsSync(envPath)) {
64
+ return envPath
65
+ }
66
+
67
+ const lines = readBaseContent(envPath, examplePath)
68
+ .split('\n')
69
+ .filter((line, index, all) => !(index === all.length - 1 && line === ''))
70
+
71
+ for (const key of MERGE_KEYS) {
72
+ const override = overrides[key]?.trim()
73
+ if (override) {
74
+ upsertEnvLine(lines, key, override)
75
+ continue
76
+ }
77
+
78
+ if (onlyIfMissing) continue
79
+
80
+ const fromProcess = process.env[key]?.trim()
81
+ if (fromProcess) {
82
+ upsertEnvLine(lines, key, fromProcess)
83
+ }
84
+ }
85
+
86
+ const nextContent = `${lines.join('\n').trimEnd()}\n`
87
+ if (!fs.existsSync(envPath) || fs.readFileSync(envPath, 'utf8') !== nextContent) {
88
+ fs.writeFileSync(envPath, nextContent)
89
+ console.log(`[ensure-env] wrote ${envPath}`)
90
+ }
91
+
92
+ return envPath
93
+ }
94
+
95
+ const isDirectRun =
96
+ process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
97
+
98
+ if (isDirectRun) {
99
+ ensureEnvFile(projectRoot, { onlyIfMissing: true })
100
+ }
@@ -0,0 +1,143 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ /** Normalize user input (e.g. api.yoursite.com) to an HTTPS origin without trailing slash. */
5
+ export function normalizeRemoteOrigin(input) {
6
+ const raw = typeof input === 'string' ? input.trim() : ''
7
+ if (!raw) return null
8
+
9
+ let origin = raw
10
+ if (!/^https?:\/\//i.test(origin)) {
11
+ origin = `https://${origin}`
12
+ }
13
+ return origin.replace(/\/$/, '')
14
+ }
15
+
16
+ function readEnvValue(projectRoot, key) {
17
+ const envPath = path.join(projectRoot, '.env')
18
+ try {
19
+ const content = fs.readFileSync(envPath, 'utf8')
20
+ const match = content.match(new RegExp(`^${key}=(.+)$`, 'm'))
21
+ if (match) {
22
+ return match[1].trim().replace(/^['"]|['"]$/g, '')
23
+ }
24
+ } catch {
25
+ // ignore
26
+ }
27
+ return null
28
+ }
29
+
30
+ function readOriginFromEnv(projectRoot, envKey) {
31
+ const fromShell = normalizeRemoteOrigin(process.env[envKey])
32
+ if (fromShell) return fromShell
33
+ return normalizeRemoteOrigin(readEnvValue(projectRoot, envKey))
34
+ }
35
+
36
+ /** Default remote API URL (--remote-origin or REACTPRESS_DEV_REMOTE_ORIGIN). */
37
+ export function readDevRemoteDefault(projectRoot) {
38
+ return readOriginFromEnv(projectRoot, 'REACTPRESS_DEV_REMOTE_ORIGIN')
39
+ }
40
+
41
+ /** Remote admin API origin; null = local Nest. */
42
+ export function readDevAdminApiOrigin(projectRoot) {
43
+ return readOriginFromEnv(projectRoot, 'REACTPRESS_DEV_ADMIN_API_ORIGIN')
44
+ }
45
+
46
+ /** Remote client/theme API origin (nginx /api); null = local Nest. */
47
+ export function readDevClientApiOrigin(projectRoot) {
48
+ return readOriginFromEnv(projectRoot, 'REACTPRESS_DEV_CLIENT_API_ORIGIN')
49
+ }
50
+
51
+ /**
52
+ * Parse one origin flag: local | remote | URL/host.
53
+ * @returns {{ url: string|null } | { error: string }}
54
+ */
55
+ export function parseOriginSpec(value, remoteDefault) {
56
+ const trimmed = typeof value === 'string' ? value.trim() : ''
57
+ if (!trimmed) return { url: null }
58
+
59
+ const lower = trimmed.toLowerCase()
60
+ if (lower === 'local') return { url: null }
61
+ if (lower === 'remote') {
62
+ if (!remoteDefault) return { error: 'REMOTE_DEFAULT_REQUIRED' }
63
+ return { url: remoteDefault }
64
+ }
65
+
66
+ const url = normalizeRemoteOrigin(trimmed)
67
+ if (!url) return { error: 'INVALID_ORIGIN' }
68
+ return { url }
69
+ }
70
+
71
+ /**
72
+ * Resolve admin/client API targets for this dev session.
73
+ * @returns {{ admin: string|null, client: string|null, remoteDefault: string|null, needsLocalApi: boolean, error?: string }}
74
+ */
75
+ export function resolveDevApiOrigins(projectRoot, cli = {}) {
76
+ const remoteDefault = normalizeRemoteOrigin(cli.remoteOrigin) || readDevRemoteDefault(projectRoot)
77
+
78
+ const onlyRemoteShorthand =
79
+ cli.remoteOrigin !== undefined &&
80
+ cli.adminOrigin === undefined &&
81
+ cli.clientOrigin === undefined
82
+
83
+ const resolveSide = (cliValue, envKey, useRemoteShorthand) => {
84
+ if (cliValue !== undefined) {
85
+ return parseOriginSpec(cliValue, remoteDefault)
86
+ }
87
+ const fromEnv = readOriginFromEnv(projectRoot, envKey)
88
+ if (fromEnv) return { url: fromEnv }
89
+ if (useRemoteShorthand && remoteDefault) return { url: remoteDefault }
90
+ return { url: null }
91
+ }
92
+
93
+ const adminParsed = resolveSide(
94
+ cli.adminOrigin,
95
+ 'REACTPRESS_DEV_ADMIN_API_ORIGIN',
96
+ onlyRemoteShorthand
97
+ )
98
+ if (adminParsed.error) return { error: adminParsed.error }
99
+
100
+ const clientParsed = resolveSide(
101
+ cli.clientOrigin,
102
+ 'REACTPRESS_DEV_CLIENT_API_ORIGIN',
103
+ onlyRemoteShorthand
104
+ )
105
+ if (clientParsed.error) return { error: clientParsed.error }
106
+
107
+ const admin = adminParsed.url
108
+ const client = clientParsed.url
109
+
110
+ return {
111
+ admin,
112
+ client,
113
+ remoteDefault,
114
+ needsLocalApi: !admin || !client,
115
+ }
116
+ }
117
+
118
+ export function applyDevApiOriginsToEnv(origins) {
119
+ if (origins.remoteDefault) {
120
+ process.env.REACTPRESS_DEV_REMOTE_ORIGIN = origins.remoteDefault
121
+ } else {
122
+ delete process.env.REACTPRESS_DEV_REMOTE_ORIGIN
123
+ }
124
+
125
+ if (origins.admin) {
126
+ process.env.REACTPRESS_DEV_ADMIN_API_ORIGIN = origins.admin
127
+ } else {
128
+ delete process.env.REACTPRESS_DEV_ADMIN_API_ORIGIN
129
+ }
130
+
131
+ if (origins.client) {
132
+ process.env.REACTPRESS_DEV_CLIENT_API_ORIGIN = origins.client
133
+ } else {
134
+ delete process.env.REACTPRESS_DEV_CLIENT_API_ORIGIN
135
+ }
136
+ }
137
+
138
+ /** Nest client base URL (includes /api when origin is host-only). */
139
+ export function resolveRemoteThemeApiBase(origin) {
140
+ const base = origin.replace(/\/$/, '')
141
+ if (/\/api$/i.test(base)) return base
142
+ return `${base}/api`
143
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Single source of truth for ReactPress theme URL resolution (build, dev, Vercel).
3
+ */
4
+ export const DEFAULT_LOCAL_API = 'http://localhost:3002/api'
5
+ export const DEFAULT_LOCAL_SITE = 'http://localhost:3001'
6
+ export const DEFAULT_LOCAL_ADMIN = 'http://localhost:3000'
7
+ export const DEFAULT_THEME_DEV_PORT = 3001
8
+ export const DEFAULT_VERCEL_DEMO_SITE = 'https://reactpress-theme-starter.vercel.app'
9
+
10
+ function trimUrl(value) {
11
+ if (typeof value !== 'string') return ''
12
+ return value.trim().replace(/\/$/, '')
13
+ }
14
+
15
+ function trimApiUrl(value) {
16
+ return trimUrl(value)
17
+ }
18
+
19
+ export function isLocalhostUrl(url) {
20
+ return /localhost|127\.0\.0\.1/i.test(url)
21
+ }
22
+
23
+ function toApiBase(origin) {
24
+ const base = trimApiUrl(origin)
25
+ if (!base) return ''
26
+ return /\/api$/i.test(base) ? base : `${base}/api`
27
+ }
28
+
29
+ function apiOriginFromBase(serverApi) {
30
+ return serverApi.replace(/\/api\/?$/, '')
31
+ }
32
+
33
+ /**
34
+ * @param {NodeJS.ProcessEnv} [processEnv]
35
+ */
36
+ export function resolveClientSiteUrl(processEnv = process.env) {
37
+ const explicit = trimUrl(processEnv.CLIENT_SITE_URL)
38
+ if (explicit) return explicit
39
+
40
+ const vercelUrl = trimUrl(processEnv.VERCEL_URL)
41
+ if (vercelUrl) {
42
+ return (vercelUrl.startsWith('http') ? vercelUrl : `https://${vercelUrl}`).replace(/\/$/, '')
43
+ }
44
+
45
+ if (processEnv.VERCEL === '1') {
46
+ return DEFAULT_VERCEL_DEMO_SITE
47
+ }
48
+
49
+ return DEFAULT_LOCAL_SITE
50
+ }
51
+
52
+ /**
53
+ * @param {NodeJS.ProcessEnv} [processEnv]
54
+ */
55
+ export function resolveThemeApiEnv(processEnv = process.env) {
56
+ const mockEnabled = processEnv.REACTPRESS_MOCK_API === '1'
57
+ const explicitServer =
58
+ trimApiUrl(processEnv.REACTPRESS_API_URL) || trimApiUrl(processEnv.SERVER_API_URL)
59
+ const explicitPublic = trimApiUrl(processEnv.NEXT_PUBLIC_REACTPRESS_API_URL)
60
+ const remoteApi = toApiBase(processEnv.REACTPRESS_DEV_REMOTE_ORIGIN)
61
+
62
+ let serverApi = explicitServer || explicitPublic || remoteApi || DEFAULT_LOCAL_API
63
+
64
+ // Vercel runtime: SSR should call this deployment's /api mock route handler.
65
+ if (mockEnabled && processEnv.VERCEL === '1' && !explicitServer) {
66
+ serverApi = `${resolveClientSiteUrl(processEnv)}/api`
67
+ }
68
+
69
+ let publicApi = explicitPublic
70
+ if (mockEnabled) {
71
+ // Browser must hit same-origin mock routes; ignore .env localhost:3002.
72
+ publicApi = '/api'
73
+ } else if (!publicApi) {
74
+ if (processEnv.NODE_ENV === 'production' && !isLocalhostUrl(serverApi)) {
75
+ publicApi = '/api'
76
+ } else {
77
+ publicApi = serverApi
78
+ }
79
+ }
80
+
81
+ const nginxEntry = trimUrl(processEnv.REACTPRESS_NGINX_ENTRY_URL || processEnv.NGINX_ENTRY_URL)
82
+ let adminUrl =
83
+ trimUrl(processEnv.NEXT_PUBLIC_REACTPRESS_ADMIN_URL) ||
84
+ (nginxEntry ? `${nginxEntry}/admin` : DEFAULT_LOCAL_ADMIN)
85
+
86
+ if (processEnv.VERCEL === '1' && isLocalhostUrl(adminUrl) && !isLocalhostUrl(serverApi)) {
87
+ adminUrl = `${apiOriginFromBase(serverApi)}/admin`
88
+ }
89
+
90
+ return {
91
+ SERVER_API_URL: serverApi,
92
+ REACTPRESS_API_URL: serverApi,
93
+ NEXT_PUBLIC_REACTPRESS_API_URL: publicApi,
94
+ NEXT_PUBLIC_REACTPRESS_ADMIN_URL: adminUrl,
95
+ CLIENT_SITE_URL: resolveClientSiteUrl(processEnv),
96
+ }
97
+ }
98
+
99
+ /**
100
+ * @param {ReturnType<typeof resolveThemeApiEnv>} apiEnv
101
+ */
102
+ export function resolveApiRewriteOrigin(apiEnv) {
103
+ return apiOriginFromBase(apiEnv.SERVER_API_URL)
104
+ }
105
+
106
+ /**
107
+ * @param {NodeJS.ProcessEnv} [processEnv]
108
+ */
109
+ export function resolveApiPreconnectOrigin(processEnv = process.env) {
110
+ const apiEnv = resolveThemeApiEnv(processEnv)
111
+ const origin = resolveApiRewriteOrigin(apiEnv)
112
+ if (!origin.startsWith('http') || isLocalhostUrl(origin)) return null
113
+ return origin
114
+ }
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Set package.json + theme.json version for CI npm publish (does not commit).
4
+ */
5
+ import fs from 'node:fs'
6
+ import path from 'node:path'
7
+ import { fileURLToPath } from 'node:url'
8
+
9
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
10
+ const version = process.argv[2]?.trim()
11
+
12
+ if (!version || !/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(version)) {
13
+ console.error('Usage: node scripts/set-publish-version.mjs <semver>')
14
+ process.exit(1)
15
+ }
16
+
17
+ const VERSION_RE = /("version"\s*:\s*")[^"]+(")/
18
+
19
+ function setFileVersion(filePath) {
20
+ const content = fs.readFileSync(filePath, 'utf8')
21
+ if (!VERSION_RE.test(content)) {
22
+ console.error(`Could not find version field in ${filePath}`)
23
+ process.exit(1)
24
+ }
25
+ fs.writeFileSync(filePath, content.replace(VERSION_RE, `$1${version}$2`))
26
+ }
27
+
28
+ setFileVersion(path.join(root, 'package.json'))
29
+ setFileVersion(path.join(root, 'theme.json'))
30
+
31
+ console.log(`[set-publish-version] ${version}`)