chore(desktop-client): add macOS code-signing and notarization scaffolding
Backend CI / Backend (push) Successful in 13m6s
Deployment Config CI / Deployment Config (push) Successful in 17s
Frontend CI / Frontend (push) Successful in 2m24s

- Move the build config out of package.json into electron-builder.yml so
  the mac block can carry hardenedRuntime, entitlements, universal arch,
  and usage-description Info.plist keys. Drop identity:null (which
  forces unsigned builds) and let CSC_NAME or the keychain decide.
- Add entitlements plist files and three helper scripts: sign-setup.sh
  (one-time keychain prep with notarytool store-credentials),
  sign-mac.sh (sign + notarize + staple, supports keychain or CI env
  vars), and ci-import-cert.sh (CI keychain bootstrap).
- Ignore .env.signing so local credentials don't sneak into the repo,
  and pin @emnapi/{core,runtime} as devDependencies so universal builds
  resolve them deterministically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 16:02:14 +08:00
parent 9c58fe2312
commit a9af6c634c
11 changed files with 336 additions and 86 deletions
+2
View File
@@ -5,6 +5,8 @@ coverage/
dist/ dist/
.env .env
.env.local .env.local
.env.signing
apps/*/.env.signing
apps/*/dist/ apps/*/dist/
apps/*/out/ apps/*/out/
apps/*/release/ apps/*/release/
+2
View File
@@ -365,3 +365,5 @@ The color system is almost entirely achromatic — dark backgrounds with white/g
5. Brand indigo (`#5e6ad2` / `#7170ff`) is the only chromatic color — everything else is grayscale 5. Brand indigo (`#5e6ad2` / `#7170ff`) is the only chromatic color — everything else is grayscale
6. Borders are always semi-transparent white, never solid dark colors on dark backgrounds 6. Borders are always semi-transparent white, never solid dark colors on dark backgrounds
7. Berkeley Mono for any code or technical content, Inter Variable for everything else 7. Berkeley Mono for any code or technical content, Inter Variable for everything else
rnzy-benw-ynai-ftbl
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key><true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key><true/>
<key>com.apple.security.cs.disable-library-validation</key><true/>
<key>com.apple.security.inherit</key><true/>
</dict>
</plist>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key><true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key><true/>
<key>com.apple.security.cs.disable-library-validation</key><true/>
<key>com.apple.security.device.audio-input</key><true/>
<key>com.apple.security.device.camera</key><true/>
<key>com.apple.security.network.client</key><true/>
<key>com.apple.security.network.server</key><true/>
<key>com.apple.security.files.user-selected.read-write</key><true/>
<key>com.apple.security.automation.apple-events</key><true/>
</dict>
</plist>
+13 -6
View File
@@ -35,18 +35,25 @@ files:
- "!**/node_modules/**/*.{c,cc,cpp,h,hpp,cmake,m4,am,sh}" - "!**/node_modules/**/*.{c,cc,cpp,h,hpp,cmake,m4,am,sh}"
mac: mac:
category: public.app-category.productivity category: public.app-category.productivity
identity: null # identity 不写死:让 electron-builder 自动从 CSC_NAME 环境变量或钥匙串里挑
notarize: false # 注意:写 `identity: null` 是"明确不签名",不要这么写
hardenedRuntime: false notarize: false # 我们用 notarytool 自己跑,比 electron-builder 内置更稳
hardenedRuntime: true
gatekeeperAssess: false gatekeeperAssess: false
entitlements: build/entitlements.mac.plist
entitlementsInherit: build/entitlements.mac.inherit.plist
electronLanguages: electronLanguages:
- en - en
- zh_CN - zh_CN
extendInfo:
NSCameraUsageDescription: 用于视频会议和录屏功能
NSMicrophoneUsageDescription: 用于音频采集
NSAppleEventsUsageDescription: 用于自动化控制其他应用
target: target:
- target: dmg - target: dmg
arch: [arm64] arch: [arm64, universal]
- target: zip # - target: zip
arch: [arm64] # arch: [arm64, universal]
dmg: dmg:
format: ULFO format: ULFO
contents: contents:
+2 -76
View File
@@ -14,6 +14,8 @@
"dev": "unset ELECTRON_RUN_AS_NODE && electron-vite dev --inspect=9229", "dev": "unset ELECTRON_RUN_AS_NODE && electron-vite dev --inspect=9229",
"build": "electron-vite build", "build": "electron-vite build",
"package:mac": "electron-builder --mac --arm64", "package:mac": "electron-builder --mac --arm64",
"sign:setup": "bash scripts/sign-setup.sh",
"sign:mac": "bash scripts/sign-mac.sh",
"test": "vitest run", "test": "vitest run",
"typecheck": "vue-tsc --noEmit" "typecheck": "vue-tsc --noEmit"
}, },
@@ -46,81 +48,5 @@
"vue": "^3.5.31", "vue": "^3.5.31",
"vue-router": "^4.5.1", "vue-router": "^4.5.1",
"vue-tsc": "^3.2.6" "vue-tsc": "^3.2.6"
},
"build": {
"appId": "com.geo.rankly.desktop",
"productName": "省心推",
"copyright": "Copyright © 2026 shengxintui.com. All Rights Reserved.",
"directories": {
"output": "release",
"buildResources": "build"
},
"compression": "maximum",
"removePackageScripts": true,
"removePackageKeywords": true,
"asar": true,
"asarUnpack": [
"**/node_modules/better-sqlite3/build/Release/**",
"**/node_modules/better-sqlite3/lib/**",
"**/node_modules/bindings/**",
"**/node_modules/file-uri-to-path/**",
"**/node_modules/playwright-core/lib/**",
"**/node_modules/playwright-core/bin/**"
],
"files": [
"out/**/*",
"package.json",
"!**/*.map",
"!**/*.ts",
"!**/*.tsx",
"!**/*.md",
"!**/*.markdown",
"!**/{test,tests,__tests__,__mocks__,example,examples,docs,doc,demo,samples,man}/**",
"!**/{LICENSE,LICENSE.*,license,CHANGELOG,CHANGELOG.*,README,README.*,HISTORY,HISTORY.*,AUTHORS,CONTRIBUTING}*",
"!**/.*",
"!**/node_modules/.bin/**",
"!**/node_modules/*/{Makefile,Gulpfile.js,Gruntfile.js,CMakeLists.txt,*.coffee,*.flow,*.c,*.cc,*.cpp,*.h,*.hpp,*.gyp,*.gypi,*.tgz,binding.gyp}",
"!**/node_modules/**/{*.d.ts,tsconfig*.json,jest.config*,rollup.config*,webpack.config*,vite.config*}",
"!**/node_modules/better-sqlite3/{deps,src,docs,benchmark}/**",
"!**/node_modules/better-sqlite3/{test_extension.c,download.sh,*.gyp,*.gypi}",
"!**/node_modules/playwright-core/{types,ThirdPartyNotices.txt}/**",
"!**/node_modules/**/*.{c,cc,cpp,h,hpp,cmake,m4,am,sh}"
],
"mac": {
"category": "public.app-category.productivity",
"identity": null,
"electronLanguages": ["en", "zh_CN"],
"target": [
{ "target": "dmg", "arch": ["arm64"] },
{ "target": "zip", "arch": ["arm64"] }
],
"notarize": false,
"hardenedRuntime": false,
"gatekeeperAssess": false
},
"dmg": {
"format": "ULFO",
"contents": [
{ "x": 130, "y": 220 },
{ "x": 410, "y": 220, "type": "link", "path": "/Applications" }
]
},
"win": {
"target": [
{ "target": "nsis", "arch": ["x64"] }
]
},
"nsis": {
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true
},
"linux": {
"target": [
{ "target": "AppImage", "arch": ["x64"] }
],
"category": "Office"
},
"publish": null
} }
} }
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# CI 专用:把 base64 编码的 .p12 证书导入到一个临时钥匙串
# 仅在 CI 环境调用,本地不要跑(会污染你的登录钥匙串体验)
#
# 必需环境变量:
# CSC_LINK - base64 编码的 .p12 文件内容
# CSC_KEY_PASSWORD - .p12 文件的密码
# KEYCHAIN_PASSWORD - 临时钥匙串自身的密码(任意字符串,例如 secrets.GITHUB_TOKEN 截断都行)
#
# 副作用:创建 ~/Library/Keychains/build.keychain-db,已加入搜索路径并解锁
# 销毁:脚本注册了 trap,进程退出时自动 delete-keychain
set -euo pipefail
: "${CSC_LINK:?CI 必须提供 CSC_LINK (base64 .p12)}"
: "${CSC_KEY_PASSWORD:?CI 必须提供 CSC_KEY_PASSWORD}"
: "${KEYCHAIN_PASSWORD:?CI 必须提供 KEYCHAIN_PASSWORD(用于临时钥匙串自身)}"
KEYCHAIN_NAME="build.keychain"
CERT_PATH="$RUNNER_TEMP/build_certificate.p12"
[ -z "${RUNNER_TEMP:-}" ] && CERT_PATH="/tmp/build_certificate.p12"
# 1. 解码证书到临时文件
echo "$CSC_LINK" | base64 --decode > "$CERT_PATH"
# 2. 创建临时钥匙串
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
security set-keychain-settings -lut 21600 "$KEYCHAIN_NAME" # 6 小时不锁
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
# 3. 导入 .p12(允许 codesign 和 productsign 不弹密码框)
security import "$CERT_PATH" \
-P "$CSC_KEY_PASSWORD" \
-A -t cert -f pkcs12 \
-k "$KEYCHAIN_NAME"
# 4. 关键:允许 codesign 不交互访问私钥
security set-key-partition-list \
-S apple-tool:,apple:,codesign: \
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME" >/dev/null
# 5. 把临时钥匙串加进搜索路径(放第一位,登录钥匙串保留作 fallback)
security list-keychain -d user -s "$KEYCHAIN_NAME" $(security list-keychain -d user | tr -d '"')
# 6. 立即清理证书文件
rm -f "$CERT_PATH"
# 7. 输出实际可用的 identity 名(让 sign-mac.sh 拿到精确的 CSC_NAME
IDENTITY=$(security find-identity -v -p codesigning "$KEYCHAIN_NAME" \
| grep "Developer ID Application" | head -n1 \
| sed -E 's/.*"(Developer ID Application: [^"]+)".*/\1/')
if [ -z "$IDENTITY" ]; then
echo "✗ 导入后未在临时钥匙串里找到 Developer ID Application" >&2
exit 1
fi
echo "✓ 已导入证书到临时钥匙串: $KEYCHAIN_NAME"
echo "✓ Identity: $IDENTITY"
# 在 GitHub Actions 里把 CSC_NAME 写入后续步骤的 env
if [ -n "${GITHUB_ENV:-}" ]; then
echo "CSC_NAME=$IDENTITY" >> "$GITHUB_ENV"
echo "✓ 已写入 \$GITHUB_ENVCSC_NAME"
fi
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env bash
# 一键:构建 → 签名 → 公证 → 装订 → 验证
# 用法: pnpm sign:mac
set -euo pipefail
cd "$(dirname "$0")/.."
R=$'\033[31m'; G=$'\033[32m'; Y=$'\033[33m'; B=$'\033[1m'; N=$'\033[0m'
ok() { echo " ${G}${N} $1"; }
fail() { echo " ${R}${N} $1"; }
step() { echo ""; echo "${B}── $1 ──${N}"; }
# ── 前置检查(本地/CI 双模式)─────────────────────────────────────
# 本地模式:读 .env.signing,公证走钥匙串 profile
# CI 模式:靠环境变量 CSC_NAME / APPLE_ID / APPLE_APP_SPECIFIC_PASSWORD / APPLE_TEAM_ID
NOTARY_ARGS=()
if [ -f .env.signing ] && [ -z "${CI:-}" ]; then
# shellcheck source=/dev/null
set -a; source .env.signing; set +a
: "${CSC_NAME:?.env.signing 缺少 CSC_NAME}"
: "${APPLE_TEAM_ID:?.env.signing 缺少 APPLE_TEAM_ID}"
: "${NOTARY_KEYCHAIN_PROFILE:?.env.signing 缺少 NOTARY_KEYCHAIN_PROFILE}"
NOTARY_ARGS=(--keychain-profile "$NOTARY_KEYCHAIN_PROFILE")
ok "模式: 本地(钥匙串 profile"
elif [ -n "${CSC_NAME:-}" ] && [ -n "${APPLE_ID:-}" ] && [ -n "${APPLE_APP_SPECIFIC_PASSWORD:-}" ] && [ -n "${APPLE_TEAM_ID:-}" ]; then
NOTARY_ARGS=(--apple-id "$APPLE_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --team-id "$APPLE_TEAM_ID")
ok "模式: CI(环境变量)"
else
fail "未找到签名配置"
echo " 本地: 运行 ${B}pnpm sign:setup${N}"
echo " CI: 设置 ${B}CSC_NAME / APPLE_ID / APPLE_APP_SPECIFIC_PASSWORD / APPLE_TEAM_ID${N} 环境变量"
exit 1
fi
ok "签名身份: $CSC_NAME"
ok "Team ID: $APPLE_TEAM_ID"
# ── 1. 构建 ──────────────────────────────────────────────────────
step "[1/5] 构建 Electron 产物"
pnpm build
ok "构建完成"
# ── 2. 打包 + 签名 ───────────────────────────────────────────────
step "[2/5] 打包 + 签名 (electron-builder)"
# 清理旧产物,避免 spctl 把上次未公证的 dmg 认成本次产物
rm -f release/*.dmg release/*-mac.zip release/*.blockmap 2>/dev/null || true
pnpm exec electron-builder --mac --arm64
ok "打包完成"
DMG=$(ls -t release/*.dmg 2>/dev/null | head -n1 || true)
ZIP=$(ls -t release/*-mac.zip 2>/dev/null | head -n1 || true)
APP=$(ls -td release/mac*/*.app 2>/dev/null | head -n1 || true)
[ -z "$DMG" ] && { fail "未找到 .dmg 产物"; exit 1; }
ok "DMG: $DMG"
[ -n "$ZIP" ] && ok "ZIP: $ZIP"
[ -n "$APP" ] && ok "APP: $APP"
# 验证签名是否真的盖上了
if [ -n "$APP" ]; then
if ! codesign --verify --deep --strict --verbose=2 "$APP" >/dev/null 2>&1; then
fail ".app 签名验证失败,停止公证"
codesign --verify --deep --strict --verbose=2 "$APP" || true
exit 1
fi
ok ".app 签名校验通过"
fi
# ── 3. 公证 ──────────────────────────────────────────────────────
step "[3/5] 提交公证 (notarytool, 通常 1-5 分钟)"
echo " 正在提交 DMG..."
xcrun notarytool submit "$DMG" \
"${NOTARY_ARGS[@]}" \
--wait
ok "DMG 公证通过"
if [ -n "$ZIP" ]; then
echo " 正在提交 ZIP(用于自动更新)..."
xcrun notarytool submit "$ZIP" \
--keychain-profile "$NOTARY_KEYCHAIN_PROFILE" \
--wait
ok "ZIP 公证通过"
fi
# ── 4. 装订 ticket ───────────────────────────────────────────────
step "[4/5] 装订 ticket (stapler)"
xcrun stapler staple "$DMG"
ok "DMG ticket 已装订(断网也能验证)"
# zip 不能 staple,但用户解压后里面的 .app 已经被 staple 过
# ── 5. 验证 ──────────────────────────────────────────────────────
step "[5/5] 最终验证 (Gatekeeper)"
if spctl -a -vv -t install "$DMG" 2>&1 | tee /tmp/spctl.log | grep -q "accepted"; then
ok "Gatekeeper 验证通过"
grep -E "source=|origin=" /tmp/spctl.log | sed 's/^/ /'
else
fail "Gatekeeper 拒绝了产物"
cat /tmp/spctl.log
exit 1
fi
# ── 完成 ────────────────────────────────────────────────────────
SIZE=$(du -h "$DMG" | awk '{print $1}')
echo ""
echo "${G}${B}━━━━━━━━━━ 全部完成 ━━━━━━━━━━${N}"
echo " 📦 $DMG (${SIZE})"
[ -n "$ZIP" ] && echo " 📦 $ZIP"
echo ""
echo " 这份 DMG 可以直接发给任何 macOS 用户,"
echo " 双击安装,首次打开${B}不会${N}弹\"无法验证开发者\"或被 Gatekeeper 拦截。"
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
# 一次性签名环境配置:检查证书 + 录入 Apple ID 凭证 + 写入 .env.signing
# 用法: pnpm sign:setup
set -euo pipefail
cd "$(dirname "$0")/.."
KEYCHAIN_PROFILE="GeoRanklySign"
ENV_FILE=".env.signing"
R=$'\033[31m'; G=$'\033[32m'; Y=$'\033[33m'; B=$'\033[1m'; N=$'\033[0m'
ok() { echo " ${G}${N} $1"; }
fail() { echo " ${R}${N} $1"; }
warn() { echo " ${Y}!${N} $1"; }
hr() { echo ""; echo "${B}── $1 ──${N}"; }
# ── 1. Xcode CLT ────────────────────────────────────────────────
hr "1/3 检查 Xcode Command Line Tools"
if xcode-select -p >/dev/null 2>&1; then
ok "已安装:$(xcode-select -p)"
else
fail "未安装"
echo " 请在终端运行 ${B}xcode-select --install${N},弹窗装好后再来"
exit 1
fi
# ── 2. Developer ID 证书 ────────────────────────────────────────
hr "2/3 检查 Developer ID Application 证书"
IDENTITIES=$(security find-identity -v -p codesigning | grep "Developer ID Application" || true)
if [ -z "$IDENTITIES" ]; then
fail "钥匙串里没找到 Developer ID Application 证书"
cat <<EOF
${B}申请步骤(一次性,约 10 分钟):${N}
${Y}1.${N} 浏览器登录 https://developer.apple.com/account/resources/certificates
${Y}2.${N} 点 ${B}+${N} 新建证书 → 选 ${B}Developer ID Application${N} → Continue
${Y}3.${N} 它要你上传 CSR,按下面方法生成:
打开 macOS ${B}钥匙串访问.app${N}
菜单栏 → 钥匙串访问 → 证书助理 → ${B}从证书颁发机构请求证书…${N}
邮箱填你的 Apple ID,常用名随意,选 ${B}存储到磁盘${N},保存为 .certSigningRequest
${Y}4.${N} 把这个 .certSigningRequest 上传,下载得到 ${B}developerID_application.cer${N}
${Y}5.${N} 双击 .cer,导入到 ${B}登录${N} 钥匙串
${Y}6.${N} 重新运行 ${B}pnpm sign:setup${N}
EOF
exit 1
fi
# 多张证书时取第一张
IDENTITY_LINE=$(echo "$IDENTITIES" | head -n1)
IDENTITY_NAME=$(echo "$IDENTITY_LINE" | sed -E 's/.*"(Developer ID Application: [^"]+)".*/\1/')
# CSC_NAME 必须去掉 "Developer ID Application: " 前缀,否则 electron-builder 会拒绝
CSC_NAME_VALUE=$(echo "$IDENTITY_NAME" | sed -E 's/^Developer ID Application: //')
TEAM_ID=$(echo "$IDENTITY_NAME" | sed -E 's/.*\(([A-Z0-9]+)\).*/\1/')
ok "找到证书:${B}$IDENTITY_NAME${N}"
ok "Team ID${B}$TEAM_ID${N}"
if [ "$(echo "$IDENTITIES" | wc -l | tr -d ' ')" -gt 1 ]; then
warn "钥匙串里有多张 Developer ID 证书,本脚本默认用上面这一张"
fi
# ── 3. Apple ID 凭证(公证用)─────────────────────────────────────
hr "3/3 录入 Apple ID 凭证(用于公证 notarize"
NEED_CREDENTIALS=1
if security find-generic-password -s "com.apple.gke.notary.tool.saved-creds" -a "$KEYCHAIN_PROFILE" >/dev/null 2>&1; then
ok "钥匙串里已有 profile$KEYCHAIN_PROFILE"
read -r -p " 是否重新录入?(y/N): " RECONFIG
[[ "${RECONFIG:-n}" =~ ^[Yy]$ ]] || NEED_CREDENTIALS=0
fi
if [ "$NEED_CREDENTIALS" = "1" ]; then
echo ""
echo " 需要 ${B}Apple ID 邮箱${N} + ${B}App 专用密码${N}(不是登录密码!)"
echo " 生成 App 专用密码: https://appleid.apple.com → 登录与安全 → App 专用密码 → +"
echo ""
read -r -p " Apple ID 邮箱: " APPLE_ID
read -r -s -p " App 专用密码 (xxxx-xxxx-xxxx-xxxx): " APPLE_PWD
echo ""
if ! xcrun notarytool store-credentials "$KEYCHAIN_PROFILE" \
--apple-id "$APPLE_ID" \
--team-id "$TEAM_ID" \
--password "$APPLE_PWD" >/dev/null; then
fail "凭证录入失败,常见原因:密码格式错(需带短横线),或 Team ID 不匹配"
exit 1
fi
ok "凭证已存入钥匙串(profile: ${KEYCHAIN_PROFILE}"
fi
# ── 写入 .env.signing ───────────────────────────────────────────
cat > "$ENV_FILE" <<EOF
# Auto-generated by scripts/sign-setup.sh — DO NOT COMMIT
# 让 electron-builder 自动选用钥匙串里的证书
# 注意:CSC_NAME 不能带 "Developer ID Application:" 前缀
CSC_NAME="$CSC_NAME_VALUE"
APPLE_TEAM_ID="$TEAM_ID"
NOTARY_KEYCHAIN_PROFILE="$KEYCHAIN_PROFILE"
EOF
ok "写入 ${B}apps/desktop-client/$ENV_FILE${N}"
echo ""
echo "${G}${B}━━━ 配置完成 ━━━${N}"
echo " 下一步:运行 ${B}pnpm sign:mac${N} 一键打包 + 签名 + 公证"
+4
View File
@@ -19,5 +19,9 @@
"electron", "electron",
"esbuild" "esbuild"
] ]
},
"devDependencies": {
"@emnapi/core": "1.9.1",
"@emnapi/runtime": "1.9.1"
} }
} }
+8 -4
View File
@@ -6,7 +6,14 @@ settings:
importers: importers:
.: {} .:
devDependencies:
'@emnapi/core':
specifier: 1.9.1
version: 1.9.1
'@emnapi/runtime':
specifier: 1.9.1
version: 1.9.1
apps/admin-web: apps/admin-web:
dependencies: dependencies:
@@ -5157,17 +5164,14 @@ snapshots:
dependencies: dependencies:
'@emnapi/wasi-threads': 1.2.0 '@emnapi/wasi-threads': 1.2.0
tslib: 2.8.1 tslib: 2.8.1
optional: true
'@emnapi/runtime@1.9.1': '@emnapi/runtime@1.9.1':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
optional: true
'@emnapi/wasi-threads@1.2.0': '@emnapi/wasi-threads@1.2.0':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
optional: true
'@emotion/hash@0.9.2': {} '@emotion/hash@0.9.2': {}