@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.
- package/.env.example +22 -0
- package/LICENSE +21 -0
- package/README.md +286 -0
- package/app/api/[[...path]]/route.ts +69 -0
- package/app/archives/page.tsx +28 -0
- package/app/article/[id]/page.tsx +92 -0
- package/app/category/[category]/page.tsx +60 -0
- package/app/globals.css +4969 -0
- package/app/knowledge/[pId]/[id]/page.tsx +54 -0
- package/app/knowledge/[pId]/page.tsx +47 -0
- package/app/knowledge/page.tsx +29 -0
- package/app/layout.tsx +76 -0
- package/app/llms.txt/route.ts +26 -0
- package/app/login/page.tsx +17 -0
- package/app/nav/[id]/page.tsx +44 -0
- package/app/nav/page.tsx +5 -0
- package/app/not-found.tsx +21 -0
- package/app/page/[id]/page.tsx +57 -0
- package/app/page.tsx +61 -0
- package/app/register/page.tsx +17 -0
- package/app/robots.ts +14 -0
- package/app/rss/route.ts +82 -0
- package/app/search/page.tsx +46 -0
- package/app/sitemap.ts +38 -0
- package/app/suggestions/page.tsx +47 -0
- package/app/tag/[tag]/page.tsx +58 -0
- package/app/tags/page.tsx +18 -0
- package/app/template.tsx +6 -0
- package/components/article/ArchiveBanner.tsx +40 -0
- package/components/article/ArticleActionsBar.tsx +53 -0
- package/components/article/ArticleCarousel.tsx +237 -0
- package/components/article/ArticleCopyrightBar.tsx +128 -0
- package/components/article/ArticleEndBar.tsx +19 -0
- package/components/article/ArticleFeedSection.tsx +59 -0
- package/components/article/ArticleList.tsx +170 -0
- package/components/article/ArticleReadingChrome.tsx +104 -0
- package/components/article/ArticleReadingProgress.tsx +75 -0
- package/components/article/ArticleRecommend.tsx +362 -0
- package/components/article/ArticleTocPanel.tsx +19 -0
- package/components/article/ArticleViewClient.tsx +221 -0
- package/components/article/CategoryMenu.tsx +67 -0
- package/components/article/CmsPageClient.tsx +92 -0
- package/components/article/LoadMore.tsx +59 -0
- package/components/article/ReadingContent.tsx +51 -0
- package/components/auth/AuthPageClient.tsx +314 -0
- package/components/comment/CommentEditor.tsx +290 -0
- package/components/comment/CommentEmoji.tsx +110 -0
- package/components/comment/CommentIconButton.tsx +37 -0
- package/components/comment/CommentItem.tsx +135 -0
- package/components/comment/CommentSection.tsx +123 -0
- package/components/comment/emojis.ts +152 -0
- package/components/knowledge/KnowledgeList.tsx +70 -0
- package/components/layout/BackToTop.tsx +27 -0
- package/components/layout/ConditionalSiteFooter.tsx +29 -0
- package/components/layout/ContentStylesLoader.tsx +9 -0
- package/components/layout/DoubleColumnLayout.tsx +171 -0
- package/components/layout/HeaderSearch.tsx +23 -0
- package/components/layout/LocaleSwitcher.tsx +24 -0
- package/components/layout/PageContainer.tsx +11 -0
- package/components/layout/PageLoadProgress.tsx +169 -0
- package/components/layout/SiteFooter.tsx +75 -0
- package/components/layout/SiteHeader.tsx +312 -0
- package/components/layout/SystemNotification.tsx +176 -0
- package/components/layout/ThemeSwitch.tsx +19 -0
- package/components/layout/UserAuth.tsx +130 -0
- package/components/search/SearchHero.tsx +323 -0
- package/components/search/SearchQuickLinks.tsx +285 -0
- package/components/shared/DevChunkRecovery.tsx +8 -0
- package/components/shared/Link.tsx +30 -0
- package/components/shared/SectionHeading.tsx +38 -0
- package/components/shared/ThemeImage.tsx +13 -0
- package/components/shared/logo.svg +7 -0
- package/components/views/ArchivesClient.tsx +137 -0
- package/components/views/CategoryClient.tsx +87 -0
- package/components/views/HomeClient.tsx +79 -0
- package/components/views/KnowledgeBookClient.tsx +127 -0
- package/components/views/KnowledgeChapterClient.tsx +175 -0
- package/components/views/KnowledgeClient.tsx +69 -0
- package/components/views/LoginClient.tsx +80 -0
- package/components/views/NavDetailClient.tsx +177 -0
- package/components/views/SearchClient.tsx +100 -0
- package/components/views/TagArchiveClient.tsx +105 -0
- package/components/views/TagsClient.tsx +46 -0
- package/components/widgets/AboutUs.tsx +180 -0
- package/components/widgets/ArticleActionButton.tsx +95 -0
- package/components/widgets/BookmarkWidget.tsx +61 -0
- package/components/widgets/CategoriesWidget.tsx +47 -0
- package/components/widgets/HomeSidebar.tsx +40 -0
- package/components/widgets/LikesWidget.tsx +77 -0
- package/components/widgets/ShareWidget.tsx +120 -0
- package/components/widgets/TagCloud.tsx +94 -0
- package/components/widgets/TagsWidget.tsx +160 -0
- package/eslint.config.mjs +80 -0
- package/lib/auth/githubOAuth.ts +26 -0
- package/lib/auth/sessionUser.ts +41 -0
- package/lib/mock-api/auth.ts +129 -0
- package/lib/mock-api/config.ts +11 -0
- package/lib/mock-api/cors.ts +53 -0
- package/lib/mock-api/data.ts +998 -0
- package/lib/mock-api/match-route.ts +202 -0
- package/lib/mock-api/proxy.ts +28 -0
- package/lib/providers/client.ts +58 -0
- package/lib/providers/materializeProvider.ts +9 -0
- package/lib/providers/server.ts +8 -0
- package/lib/reactpress/appearance.ts +69 -0
- package/lib/reactpress/bootstrap.ts +48 -0
- package/lib/reactpress/coercePublishedPage.ts +12 -0
- package/lib/reactpress/contentSeo.ts +123 -0
- package/lib/reactpress/env.ts +31 -0
- package/lib/reactpress/feedFooterPlacement.ts +16 -0
- package/lib/reactpress/fetchSetting.ts +19 -0
- package/lib/reactpress/homeFooter.tsx +27 -0
- package/lib/reactpress/layout-shell.tsx +19 -0
- package/lib/reactpress/materializeBootstrapCatalog.ts +31 -0
- package/lib/reactpress/normalizeApiResponse.ts +25 -0
- package/lib/reactpress/providers.tsx +199 -0
- package/lib/reactpress/resolveSiteNav.ts +56 -0
- package/lib/reactpress/seoMetadata.ts +82 -0
- package/lib/reactpress/serverLocale.ts +43 -0
- package/lib/reactpress/siteMetadata.ts +151 -0
- package/lib/reactpress/staticParams.ts +46 -0
- package/lib/reactpress/suggestionsContent.ts +48 -0
- package/lib/reactpress/useFeedFooterPlacement.ts +22 -0
- package/lib/reactpress/useSiteNavConfig.ts +39 -0
- package/lib/utils/archiveBanner.ts +8 -0
- package/lib/utils/colors.ts +119 -0
- package/lib/utils/comment.ts +46 -0
- package/lib/utils/headerPosition.ts +55 -0
- package/lib/utils/icons.tsx +238 -0
- package/lib/utils/reading.ts +16 -0
- package/lib/utils/scroll.ts +8 -0
- package/lib/utils/tagCloud.ts +222 -0
- package/next.config.js +103 -0
- package/package.json +98 -0
- package/postcss.config.js +5 -0
- package/prettier.config.js +10 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/cli.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/home-dark.png +0 -0
- package/public/home-light.png +0 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/logo-400.png +0 -0
- package/public/logo.png +0 -0
- package/public/mobile.png +0 -0
- package/public/pad.png +0 -0
- package/public/site.webmanifest +19 -0
- package/scripts/build-mock.mjs +139 -0
- package/scripts/dev.mjs +221 -0
- package/scripts/ensure-env.mjs +100 -0
- package/scripts/remote-dev.mjs +143 -0
- package/scripts/resolve-api-env.mjs +114 -0
- package/scripts/set-publish-version.mjs +31 -0
- package/scripts/smoke-pages.mjs +113 -0
- package/scripts/start-mock.mjs +79 -0
- package/scripts/verify-toolkit.mjs +37 -0
- package/svg.d.ts +6 -0
- package/theme.json +123 -0
- package/theme.manifest.schema.json +265 -0
- package/tsconfig.json +41 -0
- package/vercel.json +12 -0
package/scripts/dev.mjs
ADDED
|
@@ -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}`)
|