chore: guard frontend production builds
This commit is contained in:
parent
d29ece3a85
commit
e11ce1aaaa
@ -1,3 +1,3 @@
|
|||||||
# 小程序构建时的 API 域名(api.s-good.com 已挪作他用,勿再指向)
|
# 安全占位符:提审/体验版构建前必须用受控环境文件覆盖,并运行发布门禁。
|
||||||
# 提审/体验版前请改为宠小它专用域名,并在微信后台配置合法域名
|
VITE_API_ORIGIN=https://api.petstore.invalid
|
||||||
VITE_API_ORIGIN=http://localhost:8080
|
VITE_REPORT_PUBLIC_ORIGIN=https://report.petstore.invalid
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
# 宠小它生产 API 域名(api.s-good.com 已挪作他用,勿再指向)
|
# 安全占位符:正式构建必须通过 PETSTORE_FRONTEND_ENV_FILE 指向服务器/CI 的受控环境文件,
|
||||||
# 部署前请改为宠小它专用域名,例如 https://api.chongxiaota.example
|
# 并先执行 npm run preflight:release;.invalid 会被门禁拒绝。
|
||||||
VITE_API_ORIGIN=http://localhost:8080
|
VITE_API_ORIGIN=https://api.petstore.invalid
|
||||||
|
VITE_REPORT_PUBLIC_ORIGIN=https://report.petstore.invalid
|
||||||
|
|||||||
16
README.md
16
README.md
@ -76,9 +76,7 @@ npm run dev:h5
|
|||||||
|
|
||||||
开发阶段:在微信开发者工具中勾选「不校验合法域名」(设置 → 项目设置)
|
开发阶段:在微信开发者工具中勾选「不校验合法域名」(设置 → 项目设置)
|
||||||
|
|
||||||
生产阶段:在微信公众平台后台添加以下合法域名:
|
生产阶段:仅在微信公众平台后台添加本项目实际 HTTPS 后端域名;localhost 只用于开发,不能进入体验版/正式版。
|
||||||
- `http://localhost:8080`(开发用)
|
|
||||||
- 你的实际后端域名
|
|
||||||
|
|
||||||
## API 配置
|
## API 配置
|
||||||
|
|
||||||
@ -147,15 +145,23 @@ npm run dev:h5
|
|||||||
# 1. 安装依赖
|
# 1. 安装依赖
|
||||||
npm --prefix frontend install
|
npm --prefix frontend install
|
||||||
|
|
||||||
# 2. H5 构建
|
# 2. 用权限受控的真实生产环境文件做门禁(不会打印变量值)
|
||||||
|
PETSTORE_FRONTEND_ENV_FILE=/secure/path/petstore-h5.env npm --prefix frontend run preflight:release
|
||||||
|
|
||||||
|
# 3. 让 Vite 使用同一份已通过门禁的配置并构建 H5
|
||||||
|
cp /secure/path/petstore-h5.env frontend/.env.production.local
|
||||||
npm --prefix frontend run build:h5
|
npm --prefix frontend run build:h5
|
||||||
# 期望:DONE Build complete.
|
# 期望:DONE Build complete.
|
||||||
|
|
||||||
# 3. 微信小程序构建
|
# 4. 小程序配置单独门禁并构建
|
||||||
|
PETSTORE_FRONTEND_ENV_FILE=/secure/path/petstore-mp.env npm --prefix frontend run preflight:release
|
||||||
|
cp /secure/path/petstore-mp.env frontend/.env.mp-weixin.local
|
||||||
npm --prefix frontend run build:mp-weixin
|
npm --prefix frontend run build:mp-weixin
|
||||||
# 期望:DONE Build complete. + [verify-mp-app-json] OK
|
# 期望:DONE Build complete. + [verify-mp-app-json] OK
|
||||||
```
|
```
|
||||||
|
|
||||||
|
仓库内 `.env.production` / `.env.mp-weixin` 只含 `.invalid` 安全占位符,会被门禁主动拒绝。真实域名文件不得提交;发布结束后删除临时 `.env.*.local`。
|
||||||
|
|
||||||
### 前端 RC 检查项
|
### 前端 RC 检查项
|
||||||
|
|
||||||
- [ ] `VITE_API_ORIGIN` 已配置(宠小它专用后端 API 域名;**勿再用** `api.s-good.com`)
|
- [ ] `VITE_API_ORIGIN` 已配置(宠小它专用后端 API 域名;**勿再用** `api.s-good.com`)
|
||||||
|
|||||||
@ -4,9 +4,10 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev:mp-weixin": "uni -p mp-weixin",
|
"dev:mp-weixin": "uni -p mp-weixin",
|
||||||
"build:mp-weixin": "uni build -p mp-weixin && node scripts/verify-mp-app-json.cjs",
|
"build:mp-weixin": "uni build -p mp-weixin --mode mp-weixin && node scripts/verify-mp-app-json.cjs",
|
||||||
"dev:h5": "uni",
|
"dev:h5": "uni",
|
||||||
"build:h5": "uni build"
|
"build:h5": "uni build",
|
||||||
|
"preflight:release": "node scripts/release-preflight.cjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dcloudio/uni-app": "3.0.0-4060620250520001",
|
"@dcloudio/uni-app": "3.0.0-4060620250520001",
|
||||||
|
|||||||
59
scripts/release-preflight.cjs
Normal file
59
scripts/release-preflight.cjs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
const fs = require('node:fs')
|
||||||
|
const path = require('node:path')
|
||||||
|
|
||||||
|
const envFile = process.env.PETSTORE_FRONTEND_ENV_FILE || '.env.production'
|
||||||
|
const absoluteEnvFile = path.resolve(process.cwd(), envFile)
|
||||||
|
|
||||||
|
function fail(message) {
|
||||||
|
console.error(`[release-preflight] FAIL: ${message}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(absoluteEnvFile)) {
|
||||||
|
fail('environment file does not exist')
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = {}
|
||||||
|
for (const rawLine of fs.readFileSync(absoluteEnvFile, 'utf8').split(/\r?\n/)) {
|
||||||
|
const line = rawLine.trim()
|
||||||
|
if (!line || line.startsWith('#')) continue
|
||||||
|
const separator = line.indexOf('=')
|
||||||
|
if (separator < 1) continue
|
||||||
|
const key = line.slice(0, separator).trim().replace(/^export\s+/, '')
|
||||||
|
const value = line.slice(separator + 1).trim().replace(/^(['"])(.*)\1$/, '$2')
|
||||||
|
values[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireProductionOrigin(name, forbiddenHosts) {
|
||||||
|
const value = values[name]
|
||||||
|
if (!value) fail(`${name} is required`)
|
||||||
|
|
||||||
|
let url
|
||||||
|
try {
|
||||||
|
url = new URL(value)
|
||||||
|
} catch {
|
||||||
|
fail(`${name} must be a valid URL`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostname = url.hostname.toLowerCase()
|
||||||
|
const isLocal = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'
|
||||||
|
const isPlaceholder = hostname.endsWith('.invalid')
|
||||||
|
|| hostname.endsWith('.test')
|
||||||
|
|| hostname.endsWith('.example')
|
||||||
|
|| ['example.com', 'example.net', 'example.org'].some(
|
||||||
|
(reserved) => hostname === reserved || hostname.endsWith(`.${reserved}`),
|
||||||
|
)
|
||||||
|
const hasExtraParts = Boolean(url.username || url.password || url.search || url.hash)
|
||||||
|
|| (url.pathname !== '' && url.pathname !== '/')
|
||||||
|
if (url.protocol !== 'https:' || isLocal || isPlaceholder || hasExtraParts) {
|
||||||
|
fail(`${name} must be an explicit production HTTPS origin`)
|
||||||
|
}
|
||||||
|
if (forbiddenHosts.includes(hostname)) {
|
||||||
|
fail(`${name} uses a domain reserved by another product`)
|
||||||
|
}
|
||||||
|
console.log(`[release-preflight] ${name}: PASS`)
|
||||||
|
}
|
||||||
|
|
||||||
|
requireProductionOrigin('VITE_API_ORIGIN', ['api.s-good.com'])
|
||||||
|
requireProductionOrigin('VITE_REPORT_PUBLIC_ORIGIN', ['www.s-good.com'])
|
||||||
|
console.log('[release-preflight] frontend release configuration PASS')
|
||||||
Loading…
Reference in New Issue
Block a user