diff --git a/.docs/ci.md b/.docs/ci.md new file mode 100644 index 0000000..d50bc91 --- /dev/null +++ b/.docs/ci.md @@ -0,0 +1,134 @@ +## Native/JS 部署方案 + +本节结合我们当前的 GitHub Actions 工作流与 `scripts/ci` 中的 Nx 表面检测逻辑,给出一套同时覆盖原生(App Store / Google Play)与 JS 热更新的发布蓝图。目标是在不牺牲审核合规性的前提下,把「需要重新上架的二进制」与「仅资源更新」拆开,同时保留 main/staging/test/feat 分支的预览能力。 + +### 设计目标 + +- 原生改动(ios/android 目录、原生依赖、expo runtime bump)触发完整构建、使用现有 `deploy-native-*.yml` 流程提交至审核,待 App Store Connect / Play Console 审核完毕后由发布经理手动放量。 +- JS 改动(UI、业务)走热更新通道 +- 当两种改动同时存在时,以原生流程为主,但依然生成 JS update 产物供同版本 runtime 升级。 +- 原生与 JS Channel 都能绑定到具体分支, 分支预览: + - `main` 对应生产 + - `staging` 对应预生产 + - `test` 对应测试 + - `feat-*` 作为 ephemeral preview + +### 现有 GitHub Actions / Nx 基线 + +| 组件 | 作用 | +| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `.github/workflows/ci-native-ios.yml` | detect/build 两阶段,使用 `scripts/ci/detect-surfaces.ts` 检测 native surface,`scripts/ci/run-ci-stage.ts --stage native-ios` 生成 `dist/native/ios-{profile}.ipa` 并上传 artifact。 | +| `.github/workflows/deploy-native-ios.yml` | 监听 iOS Build workflow_run,拉取 `native-ios-{profile}` 压缩包后执行 `--stage deploy-native-ios`,由 Nx target 触发 EAS submit。 | +| `.github/workflows/deploy-native-android.yml` | 与 iOS 等同,调用 `--stage deploy-native-android`,上传到 Google Play。 | +| `.github/workflows/release.yaml` | 统一的 web/native 检测,负责 lint/typecheck/web build;未来的 JS update 工作流可以沿用此工作流的 `check-build` 逻辑输出。 | +| `scripts/ci/detect-surfaces.ts` + `surface-detector.ts` | 基于 Nx tags 的「surface」检测,输出 `surface-native-ios` / `surface-native-android` 等布尔值。 | +| `scripts/ci/stages.ts` | 定义 `native-android`, `native-ios`, `deploy-native-*` stage,与 `run-ci-stage.ts` 组合成复用的 Nx pipeline。 | + +### 指纹 + 变更分类流程 + +1. **Surface 检测**:沿用 `ci-native-ios.yml` 中的 `detect` job,输出 Nx 受影响项目列表。 +2. **指纹对比**:新增 `fingerprint` job(所有 native workflow 共用),执行: + ```bash + tsx scripts/ci/native-fingerprint.ts --platform ios --output fingerprint-ios.json + tsx scripts/ci/native-fingerprint.ts --platform android --output fingerprint-android.json + ``` + 自动根据 `NX_BASE/NX_HEAD`(或仓库 merge-base)创建 git worktree,给 worktree 注入 `node_modules/.pnpm` 与 `.expo` 的符号链接,然后调用 `@expo/fingerprint` 生成原生 runtime 哈希。便于在 CI 与本地调试时共享一份事实来源。 +3. **分类输出**:`classify` job 汇总 `surface-*` 与指纹 diff: + - `native_changed = surface-native-ios || surface-native-android || fingerprint.requiresStoreRelease`。 + - `js_changed = surface-web || surface-client || git diff` 中仅命中 JS/asset 目录。 + - 产出 `change_scope=js-only | native-only | mixed`,供后续 job 条件判断,并把指纹文件保存为 artifact,留待审核通过后溯源。 + +| `change_scope` | 触发条件 | 动作 | +| -------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------- | +| `native-only` | 指纹 diff 或 Nx surface 表明有原生改动,JS bundle 未变 | 运行 `ci-native-*` build,触发 `deploy-native-*` 提交审核;JS update 流程跳过。 | +| `js-only` | 指纹 diff 为 false,Nx native surface 也为 false | 跳过原生 build;启动新建的 `deploy-js-update.yml`,构建 JS 包并推送热更新通道。 | +| `mixed` | 原生与 JS 均有改动 | 执行原生流程,同时生成 JS update(用于原生审核通过后第一时间推送 runtime 相同的增量)。 | + +### 通道/分支映射 + +| Git 分支 | 原生 build profile | Store 渠道 | JS Update 分支/频道 | 说明 | +| --------- | ------------------ | --------------------------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `main` | `production` | App Store Production / Play Production | `updates/prod` | 通过 `deploy-native-*` 自动提交,发布经理在审核通过后手动 `promote release`;JS update 在审核通过信号后自动推送。 | +| `staging` | `preview` | TestFlight Beta / Play Internal Testing | `updates/staging` | 全量自动发布至内部测试,同时记录指纹供下一次发布比对。 | +| `test` | `preview` | 封闭测试轨道 | `updates/test` | 回归环境,通常与 staging 同步,但允许注入特定 QA 配置。 | +| `feat-*` | `preview` | 不自动提交,Artifact 供分支 QA 下载安装 | `updates/preview/` | 通过 workflow_dispatch 可选择是否推送到 TestFlight Internal track;JS update 使用分支专属 channel,便于回滚。 | + +### 流程细节 + +#### 1. 原生发布(Apple Store / Google Play) + +1. 开发者合入 `staging`/`main` 等受控分支。 +2. `ci-native-ios.yml` / `ci-native-android.yml`: + - `detect` job 确认 surface。 + - 新增 `fingerprint` job 读取上一次发布的指纹(可存入 `gh release` 或 S3)并与当前 diff。 + - `build` job 若 `needs.classify.outputs.native_changed == 'true' && github.secret_source != 'None'` 则执行 Nx stage,上传 artifact(包含 `ipa/aab` + 指纹 json)。 +3. `deploy-native-*` workflow_run 触发: + - 下载 artifact,运行 `tsx scripts/run-ci-stage.ts --stage deploy-native-ios|android`,实际调用 EAS Submit。 + - 记录 `submissionId` 到 GitHub deployment,用作审核状态查询。 +4. 审核等待:App Store Connect / Play Console 审核完成后,发布经理在评论中打 `@bot release ios@1.3.0`(未来可通过 `gh workflow run release-app-store.yml` 自动化)以执行 `eas submit --release` 或在控制台手动放量。 +5. 合并完成后将 `fingerprint-.json` 存档,供下一次比较。 + +#### 2. JS-only 热更新 + +1. 创建 `deploy-js-update.yml`,仅在 `change_scope` 为 `js-only` 且 branch 属于 `main/staging/test/feat-*` 时运行。 +2. Job 阶段: + - `setup`: 复用 `.github/actions/setup`。 + - `bundle`: 运行 `nx run-many --target=update-bundle --projects=` 生成 JS bundle + assets。 + - `publish`: 运行 `npx hot-updater deploy -p -c `(由 `deploy-js-update` 自动执行),channel 映射自上表。 + - `notify`: 将 Update ID 写入 GitHub Deployment 状态,未来热更新平台接入前可先上传到 S3/CDN(占位)。 +3. 预览分支:channel 命名为 `preview/`,在 PR 关闭时自动删除 channel。 + +#### 3. Mixed 变更 + +1. 走完整原生流程,将 `js update` 构建阶段放到 `ci-native-*` 的 `post-build` 步骤,确保 JS bundle 与对应 runtime 指纹一致。 +2. `deploy-native-*` 成功后,`deploy-js-update` job 在 Deployment `state=approved` 钩子上触发,把相同 commit 的更新推送到 `updates/prod`,缩短上架与热更新的时间差。 + +### 分支预览实现要点 + +- `ci-native-*` 保持对 `feat-*` 分支的自动触发,Build profile 永远为 `preview`,输出 `native-{platform}-preview` artifact;团队可通过 `actions/download-artifact` + fastlane 安装包进行验证。 +- 需要提交到 TestFlight/Play Internal 的特定分支,可通过 `workflow_dispatch` 覆盖 `profile=preview` 并把 branch 写入 `deploy-native-*` 的 allow 列表。 +- JS 预览频道使用 `` 前缀,自动写入 `NX_PREVIEW_CHANNEL` 环境变量以保证热更新与二进制匹配。 + +### 整体开发迭代流程 + +1. **需求评估与分支策略** + - 与 PM/Design 确认需求类型(JS-only / 原生 / mixed / hotfix)。 + - 选择起始分支: + - `main`:生产最新环境修复。 + - `staging` / `test` / `feat/`:预览环境 + - `version-x`:旧版本热修,尤其是生产环境补丁。 + +2. **编码与本地校验** + - JS-only 开发:常规 `pnpm lint`、`pnpm test`、`nx affected --target=build`。 + - 原生相关改动,建议每个大提交后执行: + ```bash + tsx scripts/ci/check-version-branch.ts + tsx scripts/ci/native-fingerprint.ts --platform ios + tsx scripts/ci/native-fingerprint.ts --platform android + tsx scripts/run-ci-stage.ts --stage native-ios --platform macos + tsx scripts/run-ci-stage.ts --stage native-android --platform linux + ``` + - Mixed 改动需同时跑以上指令,并保留 `fingerprint.json`(供 PR 附件或调试)。 + +3. **提交前 checklist** + - JS-only:确认 `native-fingerprint` 均返回 `false`,即可推送,等待 `⚡ JS Update` 自动部署。 + - Native / Mixed: + - 所有场景:`pnpm lint`、`nx affected --target=typecheck`、`nx affected --target=test` 必须通过。 + +4. **发起 PR** + - PR 模板建议包含: + - 变更类型、涉及版本分支、目标渠道。 + - `fingerprint.json` 结论(或 CLI 日志)。 + - 是否需要手动审核动作(如 App Store 审核备注)。 + - PR CI 会依次触发 `CI Quality`、`🍎 iOS Build`、`🤖 Android Build`、`⚡ JS Update`(条件满足时)。 + +5. **合入与发布** + - 合入 `main/staging/test` 后: + - `ci-native-*` 根据 classifier 判定是否构建 artifact,并上传供 `deploy-native-*` 使用。 + - `deploy-native-*` 将 IPA/AAB 提交到 Store,Deployment 面板显示审核状态。 + - `deploy-js-update` 在 JS-only 或 mixed 场景下将 bundle 推送到 BRANCH_CHANNEL 登记的 channel。 + - `feat-*` 分支默认仅生成 preview artifact,需要手动触发 `deploy-*` workflow 或 `js-update` 命令。 + +6. **Hotfix 模式** + - JS hotfix:切换至历史 `version-y`,cherry-pick 修复,运行 `tsx scripts/ci/js-update.ts --env production --branch version-y`。 + - Native hotfix:不允许在旧 runtime 修改,必须 bump 新 version 并走完整 native 发布流程。 diff --git a/.github/workflows/ci-desktop.yml b/.github/workflows/ci-desktop.yml new file mode 100644 index 0000000..2314e1c --- /dev/null +++ b/.github/workflows/ci-desktop.yml @@ -0,0 +1,90 @@ +name: 💻 Desktop Builds + +on: + workflow_dispatch: + pull_request: + branches: + - main + - test + - staging + - 'feat-*' + push: + branches: + - main + - test + - staging + - 'feat-*' + +permissions: + contents: read + +concurrency: + group: desktop-${{ github.ref }} + cancel-in-progress: true + +jobs: + detect: + name: Detect desktop impact + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + surface-desktop: ${{ steps.detect.outputs.surface-desktop }} + steps: + - name: ⬇️ Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: 🚧 Setup workspace + uses: ./.github/actions/setup + + - name: 🔍 Detect desktop surface + id: detect + run: tsx scripts/detect-ci-surfaces.ts --surface desktop --format github + + build: + name: Build desktop artifacts + needs: detect + if: needs.detect.outputs.surface-desktop == 'true' + runs-on: ubuntu-latest + steps: + - name: ⬇️ Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: 🚧 Setup workspace + uses: ./.github/actions/setup + + - name: 🏗️ Run desktop stage + run: tsx scripts/run-ci-stage.ts --stage desktop + + - name: 📦 Package desktop dist + id: package + shell: bash + run: | + set -euo pipefail + if [ ! -d "dist/desktop" ]; then + echo "created=false" >> "$GITHUB_OUTPUT" + echo "dist/desktop not found. Skipping artifact packaging." + exit 0 + fi + tar -czf desktop-dist.tar.gz dist/desktop + echo "created=true" >> "$GITHUB_OUTPUT" + + - name: ⬆️ Upload desktop artifact + if: steps.package.outputs.created == 'true' + uses: actions/upload-artifact@v5 + with: + name: desktop-dist + path: desktop-dist.tar.gz + if-no-files-found: error + retention-days: 7 + + skip: + name: Nothing to build + needs: detect + if: needs.detect.outputs.surface-desktop != 'true' + runs-on: ubuntu-latest + steps: + - run: echo "No desktop/electron projects affected. Skipping." diff --git a/.github/workflows/ci-native-android.yml b/.github/workflows/ci-native-android.yml new file mode 100644 index 0000000..01f46ea --- /dev/null +++ b/.github/workflows/ci-native-android.yml @@ -0,0 +1,179 @@ +name: 🤖 Android Build + +on: + workflow_dispatch: + inputs: + profile: + type: choice + default: preview + options: + - preview + - production + description: 'EAS build profile' + pull_request: + branches: + - main + - test + - staging + - 'feat-*' + push: + branches: + - main + - test + - staging + - 'feat-*' + +permissions: + contents: read + +concurrency: + group: native-android-${{ github.ref }} + cancel-in-progress: true + +jobs: + detect: + name: Detect Android impact + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + impacted: ${{ steps.detect.outputs.surface-native-android }} + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - uses: ./.github/actions/setup + - id: detect + run: tsx scripts/ci/detect-surfaces.ts --surface native-android --format github + + fingerprint: + name: Fingerprint diff (android) + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + requires_release: ${{ steps.fp.outputs.requires_release }} + head_hash: ${{ steps.fp.outputs.head_hash }} + base_hash: ${{ steps.fp.outputs.base_hash }} + fallback_reason: ${{ steps.fp.outputs.fallback_reason }} + head_sources: ${{ steps.fp.outputs.head_sources }} + base_sources: ${{ steps.fp.outputs.base_sources }} + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - uses: ./.github/actions/setup + - id: fp + shell: bash + run: | + set -euo pipefail + tsx scripts/ci/native-fingerprint.ts --platform android --output fingerprint-android.json | tee fingerprint-android.log + require_release=$(jq -r '.requiresStoreRelease' fingerprint-android.json) + head_hash=$(jq -r '.headHash' fingerprint-android.json) + base_hash=$(jq -r '.baseHash // ""' fingerprint-android.json) + fallback_reason=$(jq -r '.fallbackReason // ""' fingerprint-android.json) + head_sources=$(jq -r 'if .headFingerprint then (.headFingerprint.sources | length) else 0 end' fingerprint-android.json) + base_sources=$(jq -r 'if .baseFingerprint then (.baseFingerprint.sources | length) else 0 end' fingerprint-android.json) + { + echo "requires_release=${require_release}" + echo "head_hash=${head_hash}" + echo "base_hash=${base_hash}" + echo "fallback_reason=${fallback_reason}" + echo "head_sources=${head_sources}" + echo "base_sources=${base_sources}" + } >> "$GITHUB_OUTPUT" + - uses: actions/upload-artifact@v5 + with: + name: android-fingerprint + path: fingerprint-android.json + retention-days: 7 + + classify: + name: Determine native build need + runs-on: ubuntu-latest + needs: [detect, fingerprint] + outputs: + build_native: ${{ steps.compute.outputs.build_native }} + steps: + - id: compute + shell: bash + run: | + set -euo pipefail + build_native=false + if [[ "${{ needs.detect.outputs.impacted }}" == 'true' ]]; then + build_native=true + fi + if [[ "${{ needs.fingerprint.outputs.requires_release }}" == 'true' ]]; then + build_native=true + fi + if [[ "${{ github.event_name }}" == 'workflow_dispatch' ]]; then + build_native=true + fi + echo "build_native=${build_native}" >> "$GITHUB_OUTPUT" + printf 'build_native=%s\n' "$build_native" + + build: + name: Build Android artifact + runs-on: ubuntu-latest + needs: [classify] + if: needs.classify.outputs.build_native == 'true' && github.secret_source != 'None' + timeout-minutes: 90 + env: + EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - uses: ./.github/actions/setup + - id: profile + shell: bash + run: | + if [[ "${{ github.event_name }}" == 'workflow_dispatch' ]]; then + PROFILE="${{ github.event.inputs.profile }}" + elif [[ "${{ github.ref_name }}" == 'main' ]]; then + PROFILE="production" + else + PROFILE="preview" + fi + echo "profile=$PROFILE" >> "$GITHUB_OUTPUT" + echo "EAS_ANDROID_PROFILE=$PROFILE" >> "$GITHUB_ENV" + - uses: expo/expo-github-action@v8 + with: + eas-version: latest + token: ${{ secrets.EXPO_TOKEN }} + - name: 🏗️ Run Android stage + run: tsx scripts/run-ci-stage.ts --stage native-android --platform linux + - name: 📦 Package artifact + id: package + shell: bash + run: | + set -euo pipefail + mkdir -p artifacts + PROFILE="${{ steps.profile.outputs.profile }}" + CANDIDATES=("dist/native/android-${PROFILE}.aab" "dist/native/android-${PROFILE}.apk") + ARTIFACT_PATH="" + for file in "${CANDIDATES[@]}"; do + if [[ -f "$file" ]]; then + ARTIFACT_PATH="$file" + break + fi + done + if [[ -z "$ARTIFACT_PATH" ]]; then + echo "No Android artifact found under dist/native" >&2 + exit 1 + fi + tar -czf "artifacts/native-android-${PROFILE}.tar.gz" "$ARTIFACT_PATH" + echo "artifact-name=native-android-${PROFILE}" >> "$GITHUB_OUTPUT" + echo "artifact-path=artifacts/native-android-${PROFILE}.tar.gz" >> "$GITHUB_OUTPUT" + - uses: actions/upload-artifact@v5 + with: + name: ${{ steps.package.outputs.artifact-name }} + path: ${{ steps.package.outputs.artifact-path }} + if-no-files-found: error + retention-days: 7 + + skip: + name: Nothing to build + needs: classify + if: needs.classify.outputs.build_native != 'true' + runs-on: ubuntu-latest + steps: + - run: echo "No Android-related Nx projects changed. Skipping builds." diff --git a/.github/workflows/ci-native-ios.yml b/.github/workflows/ci-native-ios.yml new file mode 100644 index 0000000..d71797c --- /dev/null +++ b/.github/workflows/ci-native-ios.yml @@ -0,0 +1,195 @@ +name: 🍎 iOS Build + +on: + workflow_dispatch: + inputs: + profile: + type: choice + default: preview + options: + - preview + - production + description: 'EAS build profile' + pull_request: + branches: + - main + - test + - staging + - 'feat-*' + push: + branches: + - main + - test + - staging + - 'feat-*' + +permissions: + contents: read + +concurrency: + group: native-ios-${{ github.ref }} + cancel-in-progress: true + +jobs: + detect: + name: Detect iOS impact + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + impacted: ${{ steps.detect.outputs.surface-native-ios }} + steps: + - name: ⬇️ Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: 🚧 Setup workspace + uses: ./.github/actions/setup + + - name: 🔍 Detect surface + id: detect + run: tsx scripts/ci/detect-surfaces.ts --surface native-ios --format github + + fingerprint: + name: Fingerprint diff (ios) + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + requires_release: ${{ steps.fp.outputs.requires_release }} + head_hash: ${{ steps.fp.outputs.head_hash }} + base_hash: ${{ steps.fp.outputs.base_hash }} + fallback_reason: ${{ steps.fp.outputs.fallback_reason }} + head_sources: ${{ steps.fp.outputs.head_sources }} + base_sources: ${{ steps.fp.outputs.base_sources }} + steps: + - name: ⬇️ Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: 🚧 Setup workspace + uses: ./.github/actions/setup + + - name: 🔐 Native fingerprint + id: fp + shell: bash + run: | + set -euo pipefail + tsx scripts/ci/native-fingerprint.ts --platform ios --output fingerprint-ios.json | tee fingerprint-ios.log + require_release=$(jq -r '.requiresStoreRelease' fingerprint-ios.json) + head_hash=$(jq -r '.headHash' fingerprint-ios.json) + base_hash=$(jq -r '.baseHash // ""' fingerprint-ios.json) + fallback_reason=$(jq -r '.fallbackReason // ""' fingerprint-ios.json) + head_sources=$(jq -r 'if .headFingerprint then (.headFingerprint.sources | length) else 0 end' fingerprint-ios.json) + base_sources=$(jq -r 'if .baseFingerprint then (.baseFingerprint.sources | length) else 0 end' fingerprint-ios.json) + { + echo "requires_release=${require_release}" + echo "head_hash=${head_hash}" + echo "base_hash=${base_hash}" + echo "fallback_reason=${fallback_reason}" + echo "head_sources=${head_sources}" + echo "base_sources=${base_sources}" + } >> "$GITHUB_OUTPUT" + + - name: 📤 Upload fingerprint artifact + uses: actions/upload-artifact@v5 + with: + name: ios-fingerprint + path: fingerprint-ios.json + retention-days: 7 + + classify: + name: Determine native build need + runs-on: ubuntu-latest + needs: [detect, fingerprint] + outputs: + build_native: ${{ steps.compute.outputs.build_native }} + steps: + - name: Decide build + id: compute + shell: bash + run: | + set -euo pipefail + build_native=false + if [[ "${{ needs.detect.outputs.impacted }}" == 'true' ]]; then + build_native=true + fi + if [[ "${{ needs.fingerprint.outputs.requires_release }}" == 'true' ]]; then + build_native=true + fi + if [[ "${{ github.event_name }}" == 'workflow_dispatch' ]]; then + build_native=true + fi + echo "build_native=${build_native}" >> "$GITHUB_OUTPUT" + printf 'build_native=%s\n' "$build_native" + + build: + name: Build iOS artifact + needs: [classify] + if: needs.classify.outputs.build_native == 'true' && github.secret_source != 'None' + runs-on: macos-latest + timeout-minutes: 120 + env: + EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} + steps: + - name: ⬇️ Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: 🚧 Setup workspace + uses: ./.github/actions/setup + + - name: 📋 Determine build profile + id: profile + shell: bash + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + PROFILE="${{ github.event.inputs.profile }}" + elif [[ "${{ github.ref_name }}" == "main" ]]; then + PROFILE="production" + else + PROFILE="preview" + fi + echo "profile=$PROFILE" >> "$GITHUB_OUTPUT" + echo "EAS_IOS_PROFILE=$PROFILE" >> "$GITHUB_ENV" + + - name: 📱 Setup EAS CLI + uses: expo/expo-github-action@v8 + with: + eas-version: latest + token: ${{ secrets.EXPO_TOKEN }} + + - name: 🏗️ Run iOS stage + run: tsx scripts/run-ci-stage.ts --stage native-ios --platform macos + + - name: 📦 Package artifact + id: package + shell: bash + run: | + mkdir -p artifacts + PROFILE="${{ steps.profile.outputs.profile }}" + ARTIFACT_PATH="dist/native/ios-${PROFILE}.ipa" + if [[ ! -f "$ARTIFACT_PATH" ]]; then + echo "Artifact $ARTIFACT_PATH not found." >&2 + exit 1 + fi + tar -czf "artifacts/native-ios-${PROFILE}.tar.gz" "$ARTIFACT_PATH" + echo "artifact-name=native-ios-${PROFILE}" >> "$GITHUB_OUTPUT" + echo "artifact-path=artifacts/native-ios-${PROFILE}.tar.gz" >> "$GITHUB_OUTPUT" + + - name: ⬆️ Upload artifact + uses: actions/upload-artifact@v5 + with: + name: ${{ steps.package.outputs.artifact-name }} + path: ${{ steps.package.outputs.artifact-path }} + if-no-files-found: error + retention-days: 7 + + skip: + name: Nothing to build + needs: classify + if: needs.classify.outputs.build_native != 'true' + runs-on: ubuntu-latest + steps: + - run: echo "No iOS-related Nx projects changed. Skipping builds." diff --git a/.github/workflows/ci-quality.yml b/.github/workflows/ci-quality.yml new file mode 100644 index 0000000..9c3ce36 --- /dev/null +++ b/.github/workflows/ci-quality.yml @@ -0,0 +1,50 @@ +name: 🧪 Quality Checks + +on: + workflow_dispatch: + pull_request: + branches: + - main + - test + - staging + - 'feat-*' + push: + branches: + - main + - test + - staging + - 'feat-*' + +permissions: + contents: read + pull-requests: read + +concurrency: + group: quality-${{ github.ref }} + cancel-in-progress: true + +jobs: + quality: + name: Lint, Typecheck & Tests + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: ⬇️ Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: 🚧 Setup workspace + uses: ./.github/actions/setup + + - name: 🔍 Detect impacted lint surface + id: detect + run: tsx scripts/detect-ci-surfaces.ts --surface lint --format github + + - name: ✨ Run quality stage + if: steps.detect.outputs.surface-lint == 'true' + run: tsx scripts/run-ci-stage.ts --stage lint + + - name: ✅ Nothing to lint + if: steps.detect.outputs.surface-lint != 'true' + run: echo "No Nx projects affected for lint/typecheck/tests. Skipping the quality stage." diff --git a/.github/workflows/ci-web.yml b/.github/workflows/ci-web.yml new file mode 100644 index 0000000..f9c9c55 --- /dev/null +++ b/.github/workflows/ci-web.yml @@ -0,0 +1,67 @@ +name: 🌐 Web Build & Tests + +on: + workflow_dispatch: + pull_request: + branches: + - main + - test + - staging + - 'feat-*' + push: + branches: + - main + - test + - staging + - 'feat-*' + +permissions: + contents: read + +concurrency: + group: web-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build & Package Web Targets + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: ⬇️ Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: 🚧 Setup workspace + uses: ./.github/actions/setup + + - name: 🔍 Detect impacted web surface + id: detect + run: tsx scripts/detect-ci-surfaces.ts --surface web --format github + + - name: 💤 Skip web build + if: steps.detect.outputs.surface-web != 'true' + run: echo "No web/server style projects affected. Skipping build." + + - name: 🏗️ Run web build stage + if: steps.detect.outputs.surface-web == 'true' + run: tsx scripts/run-ci-stage.ts --stage web + + - name: 📦 Package dist + if: steps.detect.outputs.surface-web == 'true' + run: | + if [ ! -d "dist" ]; then + echo "dist folder not found. Nothing to package." + exit 0 + fi + tar -czf web-dist.tar.gz dist + + - name: ⬆️ Upload web artifact + if: steps.detect.outputs.surface-web == 'true' + uses: actions/upload-artifact@v5 + with: + name: web-dist + path: web-dist.tar.gz + if-no-files-found: ignore + retention-days: 3 diff --git a/.github/workflows/deploy-desktop.yml b/.github/workflows/deploy-desktop.yml new file mode 100644 index 0000000..853fb77 --- /dev/null +++ b/.github/workflows/deploy-desktop.yml @@ -0,0 +1,83 @@ +name: 🚀 Deploy Desktop + +on: + workflow_run: + workflows: + - '💻 Desktop Builds' + types: + - completed + +permissions: + contents: write + deployments: write + +concurrency: + group: deploy-desktop-${{ github.event.workflow_run.head_branch }} + cancel-in-progress: false + +jobs: + deploy: + name: Publish desktop artifacts + if: > + github.event.workflow_run.conclusion == 'success' && + (github.event.workflow_run.event == 'push' || github.event.workflow_run.event == 'workflow_dispatch') && + ( + github.event.workflow_run.head_branch == 'main' || + github.event.workflow_run.head_branch == 'staging' || + github.event.workflow_run.head_branch == 'test' + ) + runs-on: ubuntu-latest + environment: ${{ github.event.workflow_run.head_branch == 'main' && 'production' || github.event.workflow_run.head_branch == 'staging' && 'staging' || 'test' }} + timeout-minutes: 45 + env: + DESKTOP_DEPLOY_TARGET: ${{ vars.DESKTOP_DEPLOY_TARGET || 'r2' }} + DESKTOP_R2_PREFIX: ${{ vars.DESKTOP_R2_PREFIX }} + DESKTOP_RELEASE_TAG: ${{ vars.DESKTOP_RELEASE_TAG }} + DESKTOP_RELEASE_NAME: ${{ vars.DESKTOP_RELEASE_NAME }} + DESKTOP_RELEASE_BODY: ${{ vars.DESKTOP_RELEASE_BODY }} + DESKTOP_RELEASE_DRAFT: ${{ vars.DESKTOP_RELEASE_DRAFT }} + CLOUDFLARE_R2_BUCKET: ${{ secrets.CLOUDFLARE_R2_BUCKET }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + CLOUDFLARE_R2_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }} + CLOUDFLARE_R2_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: ⬇️ Checkout + uses: actions/checkout@v5 + with: + ref: ${{ github.event.workflow_run.head_sha }} + fetch-depth: 0 + + - name: 🚧 Setup workspace + uses: ./.github/actions/setup + + - name: ⬇️ Download desktop artifact + uses: actions/download-artifact@v5 + continue-on-error: true + with: + name: desktop-dist + run-id: ${{ github.event.workflow_run.id }} + + - name: 🔎 Check artifact + id: artifact + run: | + if [ -f "desktop-dist.tar.gz" ]; then + echo "found=true" >> "$GITHUB_OUTPUT" + else + echo "found=false" >> "$GITHUB_OUTPUT" + fi + + - name: 📦 Unpack desktop dist + if: steps.artifact.outputs.found == 'true' + run: | + mkdir -p dist + tar -xzf desktop-dist.tar.gz + ls -al dist + + - name: 🚀 Run desktop deploy stage + if: steps.artifact.outputs.found == 'true' + run: tsx scripts/run-ci-stage.ts --stage deploy-desktop + + - name: ℹ️ No desktop artifact found + if: steps.artifact.outputs.found != 'true' + run: echo "desktop-dist artifact missing for run ${{ github.event.workflow_run.id }}. Skipping deploy." diff --git a/.github/workflows/deploy-native-android.yml b/.github/workflows/deploy-native-android.yml new file mode 100644 index 0000000..373ff6d --- /dev/null +++ b/.github/workflows/deploy-native-android.yml @@ -0,0 +1,80 @@ +name: 🚀 Deploy Android + +on: + workflow_run: + workflows: + - '🤖 Android Build' + types: + - completed + +permissions: + contents: read + deployments: write + +concurrency: + group: deploy-android-${{ github.event.workflow_run.head_branch }} + cancel-in-progress: false + +jobs: + deploy: + name: Submit Android artifact + if: > + github.event.workflow_run.conclusion == 'success' && + (github.event.workflow_run.event == 'push' || github.event.workflow_run.event == 'workflow_dispatch') && + (github.event.workflow_run.head_branch == 'main' || + github.event.workflow_run.head_branch == 'staging' || + github.event.workflow_run.head_branch == 'test') + runs-on: ubuntu-latest + environment: ${{ github.event.workflow_run.head_branch == 'main' && 'production' || github.event.workflow_run.head_branch == 'staging' && 'staging' || 'test' }} + timeout-minutes: 45 + env: + EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} + steps: + - name: ⬇️ Checkout + uses: actions/checkout@v5 + with: + ref: ${{ github.event.workflow_run.head_sha }} + fetch-depth: 0 + + - name: 🚧 Setup workspace + uses: ./.github/actions/setup + + - name: ⬇️ Download preview artifact + continue-on-error: true + uses: actions/download-artifact@v5 + with: + name: native-android-preview + run-id: ${{ github.event.workflow_run.id }} + path: artifacts-preview + + - name: ⬇️ Download production artifact + continue-on-error: true + uses: actions/download-artifact@v5 + with: + name: native-android-production + run-id: ${{ github.event.workflow_run.id }} + path: artifacts-production + + - name: 📦 Extract artifacts + id: extract + shell: bash + run: | + set -euo pipefail + shopt -s nullglob + files=(artifacts-*/native-android-*.tar.gz) + if (( ${#files[@]} == 0 )); then + echo "found=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + for file in "${files[@]}"; do + tar -xzf "$file" + done + echo "found=true" >> "$GITHUB_OUTPUT" + + - name: 🚀 Submit to Google Play + if: steps.extract.outputs.found == 'true' + run: tsx scripts/run-ci-stage.ts --stage deploy-native-android + + - name: ℹ️ No artifacts to deploy + if: steps.extract.outputs.found != 'true' + run: echo "No Android artifacts were available for run ${{ github.event.workflow_run.id }}." diff --git a/.github/workflows/deploy-native-ios.yml b/.github/workflows/deploy-native-ios.yml new file mode 100644 index 0000000..c1bb3e5 --- /dev/null +++ b/.github/workflows/deploy-native-ios.yml @@ -0,0 +1,80 @@ +name: 🚀 Deploy iOS + +on: + workflow_run: + workflows: + - '🍎 iOS Build' + types: + - completed + +permissions: + contents: read + deployments: write + +concurrency: + group: deploy-ios-${{ github.event.workflow_run.head_branch }} + cancel-in-progress: false + +jobs: + deploy: + name: Submit iOS artifact + if: > + github.event.workflow_run.conclusion == 'success' && + (github.event.workflow_run.event == 'push' || github.event.workflow_run.event == 'workflow_dispatch') && + (github.event.workflow_run.head_branch == 'main' || + github.event.workflow_run.head_branch == 'staging' || + github.event.workflow_run.head_branch == 'test') + runs-on: macos-latest + environment: ${{ github.event.workflow_run.head_branch == 'main' && 'production' || github.event.workflow_run.head_branch == 'staging' && 'staging' || 'test' }} + timeout-minutes: 60 + env: + EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} + steps: + - name: ⬇️ Checkout + uses: actions/checkout@v5 + with: + ref: ${{ github.event.workflow_run.head_sha }} + fetch-depth: 0 + + - name: 🚧 Setup workspace + uses: ./.github/actions/setup + + - name: ⬇️ Download preview artifact + continue-on-error: true + uses: actions/download-artifact@v5 + with: + name: native-ios-preview + run-id: ${{ github.event.workflow_run.id }} + path: artifacts-preview + + - name: ⬇️ Download production artifact + continue-on-error: true + uses: actions/download-artifact@v5 + with: + name: native-ios-production + run-id: ${{ github.event.workflow_run.id }} + path: artifacts-production + + - name: 📦 Extract artifacts + id: extract + shell: bash + run: | + set -euo pipefail + shopt -s nullglob + files=(artifacts-*/native-ios-*.tar.gz) + if (( ${#files[@]} == 0 )); then + echo "found=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + for file in "${files[@]}"; do + tar -xzf "$file" + done + echo "found=true" >> "$GITHUB_OUTPUT" + + - name: 🚀 Submit to App Store + if: steps.extract.outputs.found == 'true' + run: tsx scripts/run-ci-stage.ts --stage deploy-native-ios --platform macos + + - name: ℹ️ No artifacts to deploy + if: steps.extract.outputs.found != 'true' + run: echo "No iOS artifacts were available for run ${{ github.event.workflow_run.id }}." diff --git a/.github/workflows/deploy-native-update.yml b/.github/workflows/deploy-native-update.yml new file mode 100644 index 0000000..c37190f --- /dev/null +++ b/.github/workflows/deploy-native-update.yml @@ -0,0 +1,128 @@ +name: ⚡ Deploy Native Update + +on: + push: + branches: + - main + - staging + - test + - 'feat-*' + workflow_dispatch: + inputs: + env: + description: 'Override update environment (production|staging|test|preview)' + required: false + branch: + description: 'Override version branch for publishing' + required: false + +permissions: + contents: read + +concurrency: + group: native-update-${{ github.ref }} + cancel-in-progress: true + +jobs: + classify: + name: Determine JS update eligibility + runs-on: ubuntu-latest + timeout-minutes: 15 + outputs: + should_publish: ${{ steps.decide.outputs.should_publish }} + update_env: ${{ steps.decide.outputs.update_env }} + version_branch: ${{ steps.decide.outputs.version_branch }} + git_branch: ${{ steps.decide.outputs.git_branch }} + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - uses: ./.github/actions/setup + - id: branch + run: tsx scripts/ci/check-version-branch.ts --github + - name: Detect surfaces + id: detect + run: tsx scripts/ci/detect-surfaces.ts --surfaces native-ios,native-android --format json > detection.json + - name: Fingerprint ios + id: ios + shell: bash + run: | + set -euo pipefail + tsx scripts/ci/native-fingerprint.ts --platform ios --output fingerprint-ios.json > /dev/null + echo "requires=$(jq -r '.requiresStoreRelease' fingerprint-ios.json)" >> "$GITHUB_OUTPUT" + - name: Fingerprint android + id: android + shell: bash + run: | + set -euo pipefail + tsx scripts/ci/native-fingerprint.ts --platform android --output fingerprint-android.json > /dev/null + echo "requires=$(jq -r '.requiresStoreRelease' fingerprint-android.json)" >> "$GITHUB_OUTPUT" + - name: Decide JS update + id: decide + shell: bash + run: | + set -euo pipefail + affected=$(jq -r '.affectedProjects[]?' detection.json) + template_changed=false + while read -r project; do + if [[ "$project" == 'template-native' ]]; then + template_changed=true + break + fi + done <<< "$affected" + ios_requires='${{ steps.ios.outputs.requires }}' + android_requires='${{ steps.android.outputs.requires }}' + should_publish=false + if [[ "$template_changed" == true && "$ios_requires" != 'true' && "$android_requires" != 'true' ]]; then + should_publish=true + fi + git_branch='${{ steps.branch.outputs['git-branch'] || github.ref_name }}' + version_branch='${{ steps.branch.outputs['version-branch'] }}' + if [[ -z "$version_branch" ]]; then + version_branch='${{ github.ref_name }}' + fi + env='preview' + case "$git_branch" in + main) env='production' ;; + staging) env='staging' ;; + test) env='test' ;; + feat-*) env='preview' ;; + version-*) env='staging' ;; + esac + if [[ -n "${{ github.event.inputs.env }}" ]]; then + env='${{ github.event.inputs.env }}' + fi + if [[ -n "${{ github.event.inputs.branch }}" ]]; then + version_branch='${{ github.event.inputs.branch }}' + fi + echo "should_publish=${should_publish}" >> "$GITHUB_OUTPUT" + echo "update_env=${env}" >> "$GITHUB_OUTPUT" + echo "version_branch=${version_branch}" >> "$GITHUB_OUTPUT" + echo "git_branch=${git_branch}" >> "$GITHUB_OUTPUT" + printf 'Resolved git=%s -> version=%s (%s env)\n' "$git_branch" "$version_branch" "$env" + - uses: actions/upload-artifact@v5 + if: always() + with: + name: js-update-diagnostics + path: | + detection.json + fingerprint-ios.json + fingerprint-android.json + retention-days: 3 + + publish: + name: Publish JS update + runs-on: ubuntu-latest + needs: classify + if: needs.classify.outputs.should_publish == 'true' && github.secret_source != 'None' + timeout-minutes: 30 + env: + JS_UPDATE_ENV: ${{ needs.classify.outputs.update_env }} + VERSION_BRANCH: ${{ needs.classify.outputs.version_branch || github.ref_name }} + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - uses: ./.github/actions/setup + - name: 🚀 Publish update + run: tsx scripts/run-ci-stage.ts --stage js-update --platform linux diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml new file mode 100644 index 0000000..e67886c --- /dev/null +++ b/.github/workflows/deploy-web.yml @@ -0,0 +1,72 @@ +name: 🚀 Deploy Web + +on: + workflow_run: + workflows: + - '🌐 Web Build & Tests' + types: + - completed + +permissions: + contents: read + deployments: write + +concurrency: + group: deploy-web-${{ github.event.workflow_run.head_branch }} + cancel-in-progress: false + +jobs: + deploy: + name: Deploy artifacts + if: > + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' && + (github.event.workflow_run.head_branch == 'main' || + github.event.workflow_run.head_branch == 'staging' || + github.event.workflow_run.head_branch == 'test' || + startsWith(github.event.workflow_run.head_branch, 'feat-')) + runs-on: ubuntu-latest + environment: ${{ github.event.workflow_run.head_branch == 'main' && 'production' || github.event.workflow_run.head_branch == 'staging' && 'staging' || 'test' }} + timeout-minutes: 30 + steps: + - name: ⬇️ Checkout + uses: actions/checkout@v5 + with: + ref: ${{ github.event.workflow_run.head_sha }} + fetch-depth: 0 + + - name: 🚧 Setup workspace + uses: ./.github/actions/setup + + - name: ⬇️ Download web artifact + id: download + uses: actions/download-artifact@v5 + continue-on-error: true + with: + name: web-dist + run-id: ${{ github.event.workflow_run.id }} + + - name: 🔎 Check artifact + id: artifact-check + run: | + if [ -f "web-dist.tar.gz" ]; then + echo "found=true" >> "$GITHUB_OUTPUT" + else + echo "found=false" >> "$GITHUB_OUTPUT" + fi + + - name: 📦 Unpack dist + if: steps.artifact-check.outputs.found == 'true' + run: tar -xzf web-dist.tar.gz + + - name: 🚀 Deploy web targets + if: steps.artifact-check.outputs.found == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + run: tsx scripts/run-ci-stage.ts --stage deploy-web + + - name: ℹ️ No artifacts to deploy + if: steps.artifact-check.outputs.found != 'true' + run: echo "No web-dist artifact available for run ${{ github.event.workflow_run.id }} (likely skipped build). Nothing to deploy." diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 995f343..f33d0f1 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -78,25 +78,25 @@ jobs: run: pnpm lint - name: ♻️ Run Circular Dependency Check - run: pnpm nx affected --target=madge + run: nx affected --target=madge - name: 🔍 Run Typecheck - run: pnpm nx affected --target=typecheck + run: nx affected --target=typecheck - name: 🔍 Run Unit Tests run: echo 'TODO unit tests' - name: 🏗️ Build Web Libraries if: steps.detect-affected.outputs.affected-web == 'true' - run: pnpm nx affected --target=build --parallel=1 + run: nx affected --target=build --parallel=1 - name: ⚡ Build Web Apps if: steps.detect-affected.outputs.affected-web == 'true' - run: pnpm nx affected --target=app-build --parallel=1 + run: nx affected --target=app-build --parallel=1 - name: 🔍 Run Web apps E2E Tests if: steps.detect-affected.outputs.affected-web == 'true' - run: pnpm nx affected --target=e2e --parallel=1 + run: nx affected --target=e2e --parallel=1 - name: 📦 Zip Web Artifacts run: | @@ -149,14 +149,14 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - run: pnpm nx affected --target=deploy --parallel=1 + run: nx affected --target=deploy --parallel=1 - name: 🚀 Deploy Web App env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - run: pnpm nx affected --target=app-deploy --parallel=1 + run: nx affected --target=app-deploy --parallel=1 build-native: name: Build Native Apps @@ -184,12 +184,12 @@ jobs: uses: ./.github/actions/setup - name: 🏗️ Build Native Libraries - run: pnpm nx affected --target=build --parallel=1 + run: nx affected --target=build --parallel=1 - name: ⚡ Build Native Apps env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: pnpm nx affected --target=native-build --parallel=1 + run: nx affected --target=native-build --parallel=1 - name: 🔍 Run Native apps E2E Tests run: echo 'TODO native apps e2e tests' diff --git a/.github/workflows/template-sync.yml b/.github/workflows/template-sync.yml index 497ce01..c156917 100644 --- a/.github/workflows/template-sync.yml +++ b/.github/workflows/template-sync.yml @@ -33,6 +33,7 @@ jobs: upstream_branch: main is_pr_cleanup: true pr_labels: 🔁 template-sync + pr_branch_name_prefix: misc/template-sync - name: 📊 Report Sync Status if: always() run: | diff --git a/.gitmodules b/.gitmodules index 73f1e4d..a1ee014 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,9 @@ [submodule ".repo/effect"] path = .repo/effect url = https://github.com/Effect-TS/effect +[submodule ".repo/hot-updater"] + path = .repo/hot-updater + url = https://github.com/gronxb/hot-updater +[submodule ".repo/expo-github-action"] + path = .repo/expo-github-action + url = https://github.com/expo/expo-github-action diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000..f5d3edb --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,26 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 120, + "singleQuote": true, + "semi": false, + "ignorePatterns": [ + "**/.git", + "**/.svn", + "**/.hg", + "**/node_modules", + ".nx", + ".direnv", + ".repo", + "**/coverage", + "**/.wrangler", + "**/build", + "**/dist", + "**/vendor", + "**/.expo", + "**/ios/Pods", + "**/traceDir", + "pnpm-lock.yaml", + "**/models", + "packages/user-kit/src/random.ts" + ] +} diff --git a/.zed/settings.json b/.zed/settings.json index 80dce39..3d650f0 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -42,7 +42,10 @@ "autoUseWorkspaceTsdk": true, "settings": { "typescript": { - "tsserver": { "maxTsServerMemory": 8092, "enableRegionDiagnostics": true }, + "tsserver": { + "maxTsServerMemory": 8092, + "enableRegionDiagnostics": true + }, "workspaceSymbols": { "scope": "currentProject" }, "preferGoToSourceDefinition": true, "preferences": { @@ -85,7 +88,6 @@ // Environment and config files ".direnv", ".envrc", - ".env", "**/.env", "**/.env.*", ".node-version", @@ -105,6 +107,7 @@ "*turbo.json", "*.turbo", ".expo", + ".hot-updater", ".venv", "**/node_modules", "**/pnpm-lock.yaml", @@ -126,10 +129,9 @@ "**/*.tsbuildinfo", // Configuration files - ".prettierignore", - ".prettierrc", "cspell.json", "**/.oxlintrc.json", + "**/.oxfmtrc.json", "flake.*", "lefthook.yml", "nx.json", diff --git a/.zed/tasks.json b/.zed/tasks.json index c432af3..8253b64 100644 --- a/.zed/tasks.json +++ b/.zed/tasks.json @@ -5,7 +5,7 @@ "args": ["run", "lint"], "use_new_terminal": false, "allow_concurrent_runs": false, - "reveal": "always", + "reveal": "no_focus", "tags": ["lint"] }, { @@ -14,7 +14,7 @@ "args": ["run", "lint-fix"], "use_new_terminal": false, "allow_concurrent_runs": false, - "reveal": "always", + "reveal": "no_focus", "show_summary": false, "show_command": false, "tags": ["lint"] @@ -25,7 +25,7 @@ "args": ["run", "format"], "use_new_terminal": false, "allow_concurrent_runs": false, - "reveal": "always", + "reveal": "no_focus", "hide": "on_success", "show_summary": false, "show_command": false, @@ -37,7 +37,7 @@ "args": ["run", "format-fix"], "use_new_terminal": false, "allow_concurrent_runs": false, - "reveal": "always", + "reveal": "no_focus", "hide": "on_success", "show_summary": false, "show_command": false, @@ -46,10 +46,10 @@ { "label": "Madge", "command": "bash", - "args": ["scripts/run-nx-affected.sh", "pnpm nx affected -t madge"], + "args": ["scripts/nx-run-affected.sh", "nx affected -t madge"], "use_new_terminal": false, "allow_concurrent_runs": false, - "reveal": "always", + "reveal": "no_focus", "hide": "on_success", "show_summary": false, "show_command": false, @@ -61,7 +61,7 @@ "args": ["run-many", "-t", "typecheck"], "use_new_terminal": false, "allow_concurrent_runs": false, - "reveal": "always", + "reveal": "no_focus", "hide": "on_success", "show_summary": false, "show_command": false, @@ -70,10 +70,10 @@ { "label": "Affected Typecheck", "command": "bash", - "args": ["scripts/run-nx-affected.sh", "pnpm nx affected -t typecheck"], + "args": ["scripts/nx-run-affected.sh", "nx affected -t typecheck"], "use_new_terminal": false, "allow_concurrent_runs": false, - "reveal": "always", + "reveal": "no_focus", "hide": "on_success", "show_summary": false, "show_command": false, @@ -85,7 +85,7 @@ "args": ["run-many", "--all", "-t", "test", "-c", "all"], "use_new_terminal": false, "allow_concurrent_runs": false, - "reveal": "always", + "reveal": "no_focus", "hide": "on_success", "show_summary": false, "show_command": false, @@ -94,10 +94,10 @@ { "label": "Affected Test", "command": "bash", - "args": ["scripts/run-nx-affected.sh", "pnpm nx affected -t test -c all"], + "args": ["scripts/nx-run-affected.sh", "nx affected -t test -c all"], "use_new_terminal": false, "allow_concurrent_runs": false, - "reveal": "always", + "reveal": "no_focus", "hide": "on_success", "show_summary": false, "show_command": false, @@ -109,7 +109,7 @@ "args": ["run-many", "-t", "build"], "use_new_terminal": false, "allow_concurrent_runs": false, - "reveal": "always", + "reveal": "no_focus", "hide": "on_success", "show_summary": false, "show_command": false, @@ -121,7 +121,7 @@ "args": ["run-many", "-t", "app-build"], "use_new_terminal": false, "allow_concurrent_runs": false, - "reveal": "always", + "reveal": "no_focus", "hide": "on_success", "show_summary": false, "show_command": false, @@ -130,10 +130,10 @@ { "label": "Affected App Build", "command": "bash", - "args": ["scripts/run-nx-affected.sh", "pnpm nx affected -t app-build"], + "args": ["scripts/nx-run-affected.sh", "nx affected -t app-build"], "use_new_terminal": false, "allow_concurrent_runs": false, - "reveal": "always", + "reveal": "no_focus", "hide": "on_success", "show_summary": false, "show_command": false, @@ -145,7 +145,7 @@ "args": ["run-many", "-t", "app-deploy"], "use_new_terminal": false, "allow_concurrent_runs": false, - "reveal": "always", + "reveal": "no_focus", "hide": "never", "show_summary": false, "show_command": false, @@ -154,10 +154,10 @@ { "label": "Affected App Deploy", "command": "bash", - "args": ["scripts/run-nx-affected.sh", "pnpm nx affected -t app-deploy"], + "args": ["scripts/nx-run-affected.sh", "nx affected -t app-deploy"], "use_new_terminal": false, "allow_concurrent_runs": false, - "reveal": "always", + "reveal": "no_focus", "hide": "never", "show_summary": false, "show_command": false, diff --git a/flake.nix b/flake.nix index 9ebd170..37bb9da 100644 --- a/flake.nix +++ b/flake.nix @@ -49,7 +49,8 @@ watchman ] ++ lib.optionals stdenv.isDarwin [ cocoapods - ruby_3_4 + ruby_3_3 + fastlane xcbeautify libimobiledevice ccache @@ -85,7 +86,7 @@ export PATH="$(pwd)/node_modules/.bin:$PATH" - export NODE_OPTIONS="--max-old-space-size=8192" + export NODE_OPTIONS="''${NODE_OPTIONS:+$NODE_OPTIONS }--max-old-space-size=8192" export GRADLE_OPTS="-Xmx4g -XX:+UseG1GC" export UV_CACHE_DIR="''${XDG_CACHE_HOME:-$HOME/.cache}/uv" diff --git a/lefthook.yml b/lefthook.yml index 413f382..e16ba24 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -6,18 +6,18 @@ pre-push: commands: madge: - run: CI=true ./scripts/run-nx-affected.sh 'pnpm nx affected --target=madge' + run: CI=true ./scripts/run-nx-affected.sh 'nx affected --target=madge' typecheck: - run: CI=true ./scripts/run-nx-affected.sh 'pnpm nx affected --target=typecheck' + run: CI=true ./scripts/run-nx-affected.sh 'nx affected --target=typecheck' pre-commit: commands: env-check: glob: '*.env*' - run: pnpm dotenvx ext precommit + run: dotenvx ext precommit lint: glob: '*.{js,ts,mjs,jsx,tsx,json,jsonc}' - run: pnpm oxlint + run: oxlint format: glob: '*.{js,ts,mjs,jsx,tsx,json,jsonc}' - run: pnpm prettier --check --log-level warn {staged_files} && git update-index --again + run: oxfmt --check && git update-index --again diff --git a/nx.json b/nx.json index 8b81a27..29a385c 100644 --- a/nx.json +++ b/nx.json @@ -89,26 +89,8 @@ "args": "--all" } } - }, - "e2e": { - "inputs": ["default", "^production"], - "cache": true } }, - "plugins": [ - { - "plugin": "@nx/expo/plugin", - "options": { - "startTargetName": "start", - "runIosTargetName": "rn:ios", - "runAndroidTargetName": "rn:android", - "prebuildTargetName": "rn:prebuild", - "installTargetName": "rn:install", - "buildTargetName": "rn:build", - "submitTargetName": "rn:submit" - } - } - ], "parallel": 6, "nxCloudAccessToken": "YjM0NGViNDAtNWRlZS00YjY0LWIzNTQtNTk5ZmU4ZTI3OGNjfHJlYWQtd3JpdGU=" } diff --git a/package.json b/package.json index 58519ba..eecbed9 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,12 @@ "version": "0.0.0", "private": true, "sideEffects": [], - "packageManager": "pnpm@10.20.0+sha512.cf9998222162dd85864d0a8102e7892e7ba4ceadebbf5a31f9c2fce48dfce317a9c53b9f6464d1ef9042cba2e02ae02a9f7c143a2b438cd93c91840f0192b9dd", + "packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a", "scripts": { "xdev": "./scripts/thing/bin.ts", "up": "pnpm up -iL", - "format": "prettier --check --log-level warn .", - "format-fix": "prettier --write --log-level warn .", + "format": "oxfmt --check", + "format-fix": "oxfmt .", "lint": "oxlint", "lint-fix": "oxlint --fix" }, @@ -30,32 +30,32 @@ "@craftzdog/react-native-buffer": "^6.1.1", "@discordjs/opus": "^0.10.0", "@ebay/nice-modal-react": "^1.2.13", - "@effect-atom/atom": "^0.4.3", + "@effect-atom/atom": "^0.4.8", "@effect-x/wa-sqlite": "^0.1.5", "@effect/ai": "^0.32.1", - "@effect/ai-openai": "^0.35.0", - "@effect/cluster": "^0.53.6", - "@effect/experimental": "^0.57.5", - "@effect/opentelemetry": "^0.59.1", - "@effect/platform": "^0.93.4", + "@effect/ai-openai": "^0.36.0", + "@effect/cluster": "^0.55.0", + "@effect/experimental": "^0.57.11", + "@effect/opentelemetry": "^0.59.2", + "@effect/platform": "^0.93.6", "@effect/platform-browser": "^0.73.0", - "@effect/platform-bun": "^0.84.0", - "@effect/platform-node": "^0.101.2", + "@effect/platform-node": "^0.103.0", "@effect/rpc": "^0.72.2", - "@effect/sql": "^0.48.1", + "@effect/sql": "^0.48.6", "@effect/sql-d1": "^0.46.0", "@effect/sql-sqlite-do": "^0.26.0", "@effect/sql-sqlite-node": "^0.49.1", "@expo/metro-runtime": "^6.1.2", - "@expo/ui": "^0.2.0-beta.7", + "@expo/ui": "^0.2.0-beta.9", "@glideapps/glide-data-grid": "^6.0.4-alpha8", + "@hot-updater/expo": "^0.24.1", "@noble/hashes": "^2.0.1", "@noble/secp256k1": "^3.0.0", - "@op-engineering/op-sqlite": "^15.1.1", + "@op-engineering/op-sqlite": "^15.1.6", "@opentelemetry/semantic-conventions": "^1.38.0", - "@opentui/core": "^0.1.50", - "@opentui/react": "^0.1.50", - "@paddle/paddle-js": "^1.5.1", + "@opentui/core": "^0.1.59", + "@opentui/react": "^0.1.59", + "@paddle/paddle-js": "^1.6.1", "@paddle/paddle-node-sdk": "^3.4.0", "@portabletext/react": "^5.0.0", "@radix-ui/colors": "^3.0.0", @@ -89,63 +89,63 @@ "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.2.8", - "@react-navigation/drawer": "^7.7.5", - "@react-navigation/elements": "^2.8.4", - "@react-navigation/native": "^7.1.22", - "@react-navigation/stack": "^7.6.8", + "@react-navigation/drawer": "^7.7.9", + "@react-navigation/elements": "^2.9.2", + "@react-navigation/native": "^7.1.25", + "@react-navigation/stack": "^7.6.12", "@recappi/sdk": "^1.0.0", "@sanity/client": "^7.13.1", - "@sanity/image-url": "^1.2.0", + "@sanity/image-url": "^2.0.2", "@sanity/react-loader": "^2.0.0", "@scure/bip39": "^2.0.1", "@standard-schema/utils": "^0.3.0", "@tanstack/react-table": "^8.21.3", + "@xstack/expo-bip39": "workspace:*", "accept-language-parser": "^1.5.0", "ahooks": "^3.9.6", "arctic": "^3.7.0", - "better-sqlite3": "^12.4.6", - "chroma-js": "^3.1.2", + "better-sqlite3": "^12.5.0", + "chroma-js": "^3.2.0", "class-variance-authority": "^0.7.1", "cloudflare": "^5.2.0", - "clsx": "^2.1.1", + "clsx": "2.1.1", "cmdk": "^1.1.1", - "cookie": "^1.1.1", + "cookie": "1.1.1", "culori": "^4.0.2", - "effect": "^3.19.6", + "effect": "3.19.9", "embla-carousel-react": "^8.6.0", - "expo": "^54.0.25", - "expo-asset": "^12.0.10", - "expo-audio": "^1.0.15", - "@xstack/expo-bip39": "workspace:*", - "expo-clipboard": "^8.0.7", - "expo-constants": "^18.0.10", - "expo-crypto": "^15.0.7", - "expo-dev-client": "^6.0.18", - "expo-device": "^8.0.9", - "expo-document-picker": "^14.0.7", - "expo-file-system": "^19.0.19", - "expo-font": "^14.0.9", - "expo-haptics": "^15.0.7", - "expo-image": "^3.0.10", - "expo-image-manipulator": "^14.0.7", - "expo-image-picker": "^17.0.8", - "expo-keep-awake": "^15.0.7", - "expo-linear-gradient": "^15.0.7", - "expo-linking": "^8.0.9", - "expo-local-authentication": "^17.0.7", - "expo-localization": "^17.0.7", - "expo-modules-core": "^3.0.26", - "expo-network": "^8.0.7", - "expo-router": "^6.0.15", - "expo-screen-orientation": "^9.0.7", - "expo-secure-store": "^15.0.7", - "expo-sharing": "^14.0.7", - "expo-speech": "^14.0.7", - "expo-splash-screen": "^31.0.11", - "expo-status-bar": "^3.0.8", - "expo-system-ui": "^6.0.8", - "groq": "^4.19.0", - "i18next": "^25.6.3", + "expo": "^54.0.27", + "expo-asset": "^12.0.11", + "expo-audio": "^1.0.16", + "expo-clipboard": "^8.0.8", + "expo-constants": "^18.0.11", + "expo-crypto": "^15.0.8", + "expo-dev-client": "^6.0.20", + "expo-device": "^8.0.10", + "expo-document-picker": "^14.0.8", + "expo-file-system": "^19.0.20", + "expo-font": "^14.0.10", + "expo-haptics": "^15.0.8", + "expo-image": "^3.0.11", + "expo-image-manipulator": "^14.0.8", + "expo-image-picker": "^17.0.9", + "expo-keep-awake": "^15.0.8", + "expo-linear-gradient": "^15.0.8", + "expo-linking": "^8.0.10", + "expo-local-authentication": "^17.0.8", + "expo-localization": "^17.0.8", + "expo-modules-core": "^3.0.28", + "expo-network": "^8.0.8", + "expo-router": "^6.0.17", + "expo-screen-orientation": "^9.0.8", + "expo-secure-store": "^15.0.8", + "expo-sharing": "^14.0.8", + "expo-speech": "^14.0.8", + "expo-splash-screen": "^31.0.12", + "expo-status-bar": "^3.0.9", + "expo-system-ui": "^6.0.9", + "groq": "^4.20.3", + "i18next": "^25.7.2", "ieee754": "^1.2.1", "input-otp": "^1.4.2", "intl-parse-accept-language": "^1.0.0", @@ -154,47 +154,48 @@ "leva": "^0.10.1", "lucia": "^3.2.2", "metro-runtime": "0.83.3", - "motion": "^12.23.24", + "motion": "^12.23.25", "msgpackr": "^1.11.5", "nanoid": "^5.1.6", "nativewind": "^4.2.1", - "react": "^19.2.0", - "react-day-picker": "^9.11.2", - "react-dom": "^19.2.0", + "react": "19.2.1", + "react-day-picker": "^9.12.0", + "react-dom": "19.2.1", "react-error-boundary": "^6.0.0", "react-fast-marquee": "^1.6.5", - "react-hook-form": "^7.66.1", + "react-freeze": "^1.0.4", + "react-hook-form": "^7.68.0", "react-hotkeys-hook": "^5.2.1", - "react-i18next": "^16.3.5", + "react-i18next": "^16.4.0", "react-image-previewer": "^1.1.6", - "react-is": "^19.2.0", + "react-is": "19.2.1", "react-native": "0.83.0-rc.3", "react-native-edge-to-edge": "^1.7.0", "react-native-gesture-handler": "^2.29.1", - "react-native-keyboard-controller": "^1.19.6", + "react-native-keyboard-controller": "^1.20.1", "react-native-masked-view": "^0.2.0", "react-native-nitro-modules": "^0.31.10", "react-native-quick-base64": "^2.2.2", - "react-native-quick-crypto": "^0.7.17", - "react-native-reanimated": "^4.1.5", + "react-native-quick-crypto": "^1.0.3", + "react-native-reanimated": "^4.2.0", "react-native-safe-area-context": "^5.6.2", "react-native-screens": "^4.18.0", - "react-native-svg": "^15.15.0", - "react-native-worklets": "^0.6.1", + "react-native-svg": "^15.15.1", + "react-native-worklets": "^0.7.1", "react-resizable-panels": "^3.0.6", - "react-router": "^7.9.6", + "react-router": "^7.10.1", "react-virtualized": "^9.22.6", "readable-stream": "^4.7.0", "recharts": "^2.15.4", "remeda": "^2.32.0", "scheduler": "0.27.0", - "sherpa-onnx-node": "^1.12.17", + "sherpa-onnx-node": "^1.12.19", "simple-hue-picker": "^0.2.0", "sonner": "^2.0.7", "stripe": "^20.0.0", "tailwind-merge": "^3.4.0", "temporal-polyfill": "^0.3.0", - "type-fest": "^5.2.0", + "type-fest": "^5.3.1", "ua-parser-js": "^2.0.6", "uuid": "^13.0.0", "vaul": "^1.1.2" @@ -207,43 +208,47 @@ "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@cloudflare/types": "^7.0.0", - "@cloudflare/workers-types": "^4.20251127.0", - "@cloudflare/workers-utils": "^0.3.0", + "@cloudflare/workers-types": "^4.20251209.0", + "@cloudflare/workers-utils": "^0.4.0", "@dotenvx/dotenvx": "^1.51.1", "@effect/cli": "^0.72.1", - "@effect/language-service": "^0.57.0", + "@effect/language-service": "^0.60.0", "@effect/printer": "^0.47.0", "@effect/printer-ansi": "^0.47.0", "@effect/typeclass": "^0.38.0", "@effect/vitest": "^0.27.0", "@egoist/tailwindcss-icons": "^1.9.0", - "@expo/cli": "^54.0.16", - "@expo/metro-config": "^54.0.9", + "@expo/build-tools": "^1.0.252", + "@expo/cli": "^54.0.18", + "@expo/fingerprint": "^0.15.4", + "@expo/metro-config": "^54.0.10", "@faker-js/faker": "^10.1.0", "@githubnext/vitale": "^0.0.19", + "@hot-updater/cloudflare": "^0.24.1", + "@hot-updater/react-native": "^0.24.1", "@iconify-json/logos": "^1.2.10", - "@iconify-json/lucide": "^1.2.75", - "@nx/devkit": "^22.1.2", - "@nx/expo": "^22.1.2", - "@nx/js": "^22.1.2", - "@nx/vite": "^22.1.2", - "@nx/workspace": "^22.1.2", + "@iconify-json/lucide": "^1.2.79", + "@nx/devkit": "^22.2.0", + "@nx/expo": "^22.2.0", + "@nx/js": "^22.2.0", + "@nx/vite": "^22.2.0", + "@nx/workspace": "^22.2.0", "@octokit/types": "^16.0.0", "@portabletext/types": "^3.0.0", - "@prettier/plugin-oxc": "^0.0.5", - "@prisma/generator-helper": "^7.0.1", - "@prisma/internals": "^7.0.1", - "@prisma/migrate": "^7.0.1", + "@prettier/plugin-oxc": "^0.1.3", + "@prisma/generator-helper": "^7.1.0", + "@prisma/internals": "^7.1.0", + "@prisma/migrate": "^7.1.0", "@react-native/metro-config": "^0.82.1", - "@react-router/dev": "^7.9.6", - "@react-router/node": "^7.9.6", + "@react-router/dev": "^7.10.1", + "@react-router/node": "^7.10.1", "@rnx-kit/metro-config": "^2.2.1", "@rnx-kit/metro-resolver-symlinks": "^0.2.7", "@rnx-kit/tools-node": "^3.0.2", "@sanity/document-internationalization": "^4.1.0", - "@sanity/types": "^4.19.0", + "@sanity/types": "^4.20.3", "@sanity/ui": "^3.1.11", - "@sanity/vision": "^4.19.0", + "@sanity/vision": "^4.20.3", "@sanity/visual-editing": "^4.0.2", "@sindresorhus/slugify": "^3.0.0", "@standard-schema/spec": "^1.0.0", @@ -255,54 +260,58 @@ "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.0", "@types/babel__core": "^7.20.5", - "@types/node": "^24.10.1", + "@types/node": "^24.10.2", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@types/react-virtualized": "^9.22.3", "@types/ua-parser-js": "^0.7.39", "@types/ws": "^8.18.1", - "@typescript/native-preview": "^7.0.0-dev.20251126.1", + "@typescript/native-preview": "7.0.0-dev.20251208.1", "@vite-pwa/assets-generator": "^1.0.2", - "@vitest/browser": "^4.0.14", - "@vitest/browser-playwright": "^4.0.14", - "@vitest/coverage-v8": "^4.0.14", - "@vitest/ui": "^4.0.14", + "@vitest/browser": "^4.0.15", + "@vitest/browser-playwright": "^4.0.15", + "@vitest/coverage-v8": "^4.0.15", + "@vitest/ui": "^4.0.15", "address": "^2.0.3", "autoprefixer": "^10.4.22", "babel-plugin-react-compiler": "^1.0.0", - "browserslist": "^4.28.0", - "esbuild": "0.27.0", + "browserslist": "^4.28.1", + "eas-cli": "^16.28.0", + "eas-cli-local-build-plugin": "^1.0.252", + "esbuild": "0.27.1", "expo-atlas": "^0.4.3", - "expo-build-properties": "^1.0.9", - "expo-module-scripts": "^5.0.7", - "fast-check": "^4.3.0", + "expo-build-properties": "^1.0.10", + "expo-module-scripts": "^5.0.8", + "fast-check": "^4.4.0", "fast-xml-parser": "^5.3.2", + "hot-updater": "^0.24.1", "js-toml": "^1.0.2", - "jsx-email": "^2.8.1", - "lefthook": "^2.0.4", + "jsx-email": "^2.8.3", + "lefthook": "^2.0.9", "libsodium-wrappers": "^0.7.15", "lodash-es": "^4.17.21", "madge": "^8.0.0", "metro": "^0.83.3", "metro-config": "^0.83.3", - "miniflare": "^4.20251125.0", - "nx": "^22.1.2", - "oxc-transform": "^0.99.0", - "oxlint": "^1.30.0", + "miniflare": "^4.20251202.1", + "nx": "^22.2.0", + "oxc-transform": "^0.102.0", + "oxfmt": "^0.17.0", + "oxlint": "^1.32.0", "playwright": "^1.57.0", "postcss": "^8.5.6", "postcss-flexbugs-fixes": "^5.0.2", - "postcss-preset-env": "^10.4.0", - "prettier": "^3.6.2", - "prisma": "^7.0.1", + "postcss-preset-env": "^10.5.0", + "prettier": "^3.7.4", + "prisma": "^7.1.0", "pure-rand": "^7.0.1", "react-dev-inspector": "beta", "react-native-svg-transformer": "^1.5.2", "react-scan": "^0.4.3", - "rolldown": "^1.0.0-beta.52", + "rolldown": "1.0.0-beta.53", "rollup-plugin-visualizer": "^6.0.5", "rxjs": "^7.8.2", - "sanity": "^4.19.0", + "sanity": "^4.20.3", "shellac": "^0.8.0", "styled-components": "^6.1.19", "svgo": "^4.0.0", @@ -310,25 +319,25 @@ "tailwindcss-animate": "^1.0.7", "tinyglobby": "^0.2.15", "tsconfck": "^3.1.6", - "tstyche": "^5.0.1", - "tsx": "^4.20.6", + "tstyche": "^5.0.2", + "tsx": "^4.21.0", "turbo-stream": "2.4.1", "typescript": "^5.9.3", "unplugin-preprocessor-directives": "^1.2.0", - "vite": "^7.2.4", - "vite-plugin-checker": "^0.11.0", + "vite": "npm:rolldown-vite@latest", + "vite-plugin-checker": "^0.12.0", "vite-plugin-mkcert": "^1.17.9", - "vite-plugin-pwa": "^1.1.0", - "vitest": "^4.0.14", + "vite-plugin-pwa": "^1.2.0", + "vitest": "^4.0.15", "vitest-browser-react": "^2.0.2", "workbox-build": "^7.4.0", "workbox-core": "^7.4.0", - "workbox-expiration": "^7.3.0", - "workbox-precaching": "^7.3.0", - "workbox-routing": "^7.3.0", - "workbox-strategies": "^7.3.0", - "workbox-window": "^7.3.0", - "wrangler": "^4.51.0", + "workbox-expiration": "^7.4.0", + "workbox-precaching": "^7.4.0", + "workbox-routing": "^7.4.0", + "workbox-strategies": "^7.4.0", + "workbox-window": "^7.4.0", + "wrangler": "^4.53.0", "ws": "^8.18.3" } } diff --git a/packages/app-kit/project.json b/packages/app-kit/project.json index 8aa7a4a..474441c 100644 --- a/packages/app-kit/project.json +++ b/packages/app-kit/project.json @@ -8,7 +8,7 @@ "executor": "nx:run-commands", "outputs": ["{workspaceRoot}/node_modules/.vitest/packages/app-kit", "{workspaceRoot}/coverage/packages/app-kit"], "options": { - "command": "pnpm xdev test --project pkgs-app-kit" + "command": "xdev test --project pkgs-app-kit" } }, "madge": { diff --git a/packages/app-kit/src/http.ts b/packages/app-kit/src/http.ts index f887ee5..2334b88 100644 --- a/packages/app-kit/src/http.ts +++ b/packages/app-kit/src/http.ts @@ -222,7 +222,11 @@ export const HttpTransactionLive = HttpApiBuilder.group(PurchaseHttpApi, 'transa const { transactionId } = path - yield* Effect.annotateLogsScoped({ namespace, customerId: customerEmail, transactionId }) + yield* Effect.annotateLogsScoped({ + namespace, + customerId: customerEmail, + transactionId, + }) const pdfUrl = yield* client.transactions .invoiceGeneratePDF({ diff --git a/packages/app-kit/tsconfig.check.json b/packages/app-kit/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/app-kit/tsconfig.check.json +++ b/packages/app-kit/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/app-kit/tsconfig.json b/packages/app-kit/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/app-kit/tsconfig.json +++ b/packages/app-kit/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/app-kit/tsconfig.lib.json b/packages/app-kit/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/app-kit/tsconfig.lib.json +++ b/packages/app-kit/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/app-kit/tsconfig.test.json b/packages/app-kit/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/app-kit/tsconfig.test.json +++ b/packages/app-kit/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/app/project.json b/packages/app/project.json index d987d60..0fbbef1 100644 --- a/packages/app/project.json +++ b/packages/app/project.json @@ -8,7 +8,7 @@ "executor": "nx:run-commands", "outputs": ["{workspaceRoot}/node_modules/.vitest/packages/app", "{workspaceRoot}/coverage/packages/app"], "options": { - "command": "pnpm xdev test --project pkgs-app" + "command": "xdev test --project pkgs-app" } }, "madge": { diff --git a/packages/app/src/components/boot.tsx b/packages/app/src/components/boot.tsx index 14c5672..ad1a6f8 100644 --- a/packages/app/src/components/boot.tsx +++ b/packages/app/src/components/boot.tsx @@ -4,9 +4,9 @@ import * as Cause from 'effect/Cause' import * as Effect from 'effect/Effect' import type * as Layer from 'effect/Layer' import type { ReactNode } from 'react' -// import { ErrorBoundary } from 'react-error-boundary' -// import { Button } from '@/components/ui/button' -// import { ErrorFullPageFallback } from '@xstack/errors/react/error-boundary' +import { ErrorBoundary } from 'react-error-boundary' +import { Button } from '@/components/ui/button' +import { ErrorFullPageFallback } from '@xstack/errors/react/error-boundary' export const Boot = ({ layer, @@ -22,23 +22,23 @@ export const Boot = ({ children: ReactNode }) => { return ( - // { - // // @ts-ignore - // globalThis.hideLoading() + { + // @ts-ignore + globalThis.hideLoading() - // onError?.(error) - // }} - // fallbackRender={({ error, resetErrorBoundary }) => ( - // - // - // - // )} - // > + onError?.(error) + }} + fallbackRender={({ error, resetErrorBoundary }) => ( + + + + )} + > {children} - // + ) } diff --git a/packages/app/src/metric/utils.ts b/packages/app/src/metric/utils.ts index 3cdf7a6..aace753 100644 --- a/packages/app/src/metric/utils.ts +++ b/packages/app/src/metric/utils.ts @@ -429,7 +429,10 @@ export const effectMetricsFromSnapshot = createMetricsProcessor({ { pattern: 'fiber_started', style: { color: COLORS.blue, label: 'Started Fibers' } }, { pattern: 'fiber_successes', style: { color: COLORS.green, label: 'Successful Fibers' } }, { pattern: 'fiber_failures', style: { color: COLORS.red, label: 'Failed Fibers' } }, - { pattern: 'fiber_lifetimes', style: { color: COLORS.gray, label: 'Fiber Lifetimes', unit: 'ms' } }, + { + pattern: 'fiber_lifetimes', + style: { color: COLORS.gray, label: 'Fiber Lifetimes', unit: 'ms' }, + }, ], }) @@ -439,7 +442,10 @@ export const sqlMetricsFromSnapshot = createMetricsProcessor({ { pattern: 'query.count', style: { color: COLORS.blue, label: 'Queries' } }, { pattern: 'query.latency', style: { label: 'Query Latency', unit: 'ms' } }, { pattern: 'query.types', style: { label: 'Query Types' } }, - { pattern: 'query.last.latency', style: { color: COLORS.yellow, label: 'Last Query', unit: 'ms' } }, + { + pattern: 'query.last.latency', + style: { color: COLORS.yellow, label: 'Last Query', unit: 'ms' }, + }, ...DEFAULT_METRIC_PATTERNS, ], }) diff --git a/packages/app/src/onboarding/features/feature-cards.tsx b/packages/app/src/onboarding/features/feature-cards.tsx index 9de9111..c4a4f46 100644 --- a/packages/app/src/onboarding/features/feature-cards.tsx +++ b/packages/app/src/onboarding/features/feature-cards.tsx @@ -51,7 +51,9 @@ export const FeatureCards = ({ features }: FeatureCardsProps) => {
{feature.title} diff --git a/packages/app/src/onboarding/features/feature-list.tsx b/packages/app/src/onboarding/features/feature-list.tsx index 04ab0ee..2f93df1 100644 --- a/packages/app/src/onboarding/features/feature-list.tsx +++ b/packages/app/src/onboarding/features/feature-list.tsx @@ -183,7 +183,9 @@ export const FeatureList = ({ features }: FeatureListProps) => { 'rounded-lg bg-muted/30 overflow-hidden', expandedCharts[`${index}-${detailIndex}`] ? 'h-48' : 'h-24', )} - animate={{ height: expandedCharts[`${index}-${detailIndex}`] ? 192 : 96 }} + animate={{ + height: expandedCharts[`${index}-${detailIndex}`] ? 192 : 96, + }} >
{detail.chart.data.map((value, i) => ( diff --git a/packages/app/src/pwa/errors.ts b/packages/app/src/pwa/errors.ts index 4f0d5f3..8d405ea 100644 --- a/packages/app/src/pwa/errors.ts +++ b/packages/app/src/pwa/errors.ts @@ -257,7 +257,9 @@ export class PwaErrorFactory { * Create fallback data usage info (treated as warning, not error) */ static createFallbackUsed(context: string, reason: string): PwaError { - return this.createError(PwaErrorCategory.DATA, PwaErrorCode.FALLBACK_DATA_USED, context, { reason }) + return this.createError(PwaErrorCategory.DATA, PwaErrorCode.FALLBACK_DATA_USED, context, { + reason, + }) } /** diff --git a/packages/app/tsconfig.check.json b/packages/app/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/app/tsconfig.check.json +++ b/packages/app/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/app/tsconfig.json b/packages/app/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/app/tsconfig.json +++ b/packages/app/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/app/tsconfig.lib.json b/packages/app/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/app/tsconfig.lib.json +++ b/packages/app/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/app/tsconfig.test.json b/packages/app/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/app/tsconfig.test.json +++ b/packages/app/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/atom-react/project.json b/packages/atom-react/project.json index beb862f..4151c4e 100644 --- a/packages/atom-react/project.json +++ b/packages/atom-react/project.json @@ -11,7 +11,7 @@ "{workspaceRoot}/coverage/packages/atom-react" ], "options": { - "command": "pnpm xdev test --project pkgs-atom-react" + "command": "xdev test --project pkgs-atom-react" } }, "madge": { diff --git a/packages/atom-react/src/react.ts b/packages/atom-react/src/react.ts index aedbeaf..4d24411 100644 --- a/packages/atom-react/src/react.ts +++ b/packages/atom-react/src/react.ts @@ -107,7 +107,7 @@ export const useAtomInitialValues = (initialValues: Iterable(atom: Atom.Atom): A + (atom: Atom.Atom): A; (atom: Atom.Atom, f: (_: A) => B): B } = (atom: Atom.Atom, f?: (_: A) => A): A => { const registry = React.use(RegistryContext) @@ -138,7 +138,9 @@ function setAtom (value: W) => { registry.set(atom, value) const promise = Effect.runPromiseExit( - Registry.getResult(registry, atom as Atom.Atom>, { suspendOnWaiting: true }), + Registry.getResult(registry, atom as Atom.Atom>, { + suspendOnWaiting: true, + }), ) return options!.mode === 'promise' ? promise.then(flattenExit) : promise }, @@ -377,9 +379,9 @@ export const atomHooks: { type?: 'value', option?: never, ): { - (): A + (): A; (f: (_: A) => B): B - } + }; ( atom: Atom.Atom, type: 'suspense', diff --git a/packages/atom-react/src/rx.ts b/packages/atom-react/src/rx.ts index 2f06f4c..105bcf6 100644 --- a/packages/atom-react/src/rx.ts +++ b/packages/atom-react/src/rx.ts @@ -234,7 +234,11 @@ const createAtomWrapper: >(key: string, atom: T) => any } // Readable, refreshable - const proto = { ...getAtomTypeId(atom), ...AtomBaseProto, refresh: AtomWritableProto.refresh.bind(self) } + const proto = { + ...getAtomTypeId(atom), + ...AtomBaseProto, + refresh: AtomWritableProto.refresh.bind(self), + } return Object.create(proto, { atom: { value: atom, writable: false, enumerable: false, configurable: false }, @@ -352,7 +356,11 @@ export const UseUseServices = const typeId = typeIds[prop as keyof typeof typeIds] if (typeId) { - Object.defineProperty(result, xTypeId, { value: typeId, enumerable: true, configurable: false }) + Object.defineProperty(result, xTypeId, { + value: typeId, + enumerable: true, + configurable: false, + }) } return result } diff --git a/packages/atom-react/tsconfig.check.json b/packages/atom-react/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/atom-react/tsconfig.check.json +++ b/packages/atom-react/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/atom-react/tsconfig.json b/packages/atom-react/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/atom-react/tsconfig.json +++ b/packages/atom-react/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/atom-react/tsconfig.lib.json b/packages/atom-react/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/atom-react/tsconfig.lib.json +++ b/packages/atom-react/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/atom-react/tsconfig.test.json b/packages/atom-react/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/atom-react/tsconfig.test.json +++ b/packages/atom-react/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/cloudflare/project.json b/packages/cloudflare/project.json index 4c150fa..b812aa8 100644 --- a/packages/cloudflare/project.json +++ b/packages/cloudflare/project.json @@ -11,7 +11,7 @@ "{workspaceRoot}/coverage/packages/cloudflare" ], "options": { - "command": "pnpm xdev test --project pkgs-cloudflare" + "command": "xdev test --project pkgs-cloudflare" } }, "madge": { diff --git a/packages/cloudflare/src/entry/fetch.ts b/packages/cloudflare/src/entry/fetch.ts index fc026fd..e49c55a 100644 --- a/packages/cloudflare/src/entry/fetch.ts +++ b/packages/cloudflare/src/entry/fetch.ts @@ -172,7 +172,9 @@ export class CloudflareFetchHandle extends Context.Tag('@cloudflare:fetch-handle } } - const [app, rt] = yield* Effect.all([HttpApiBuilder.httpApp, runtime.runtimeEffect], { concurrency: 'unbounded' }) + const [app, rt] = yield* Effect.all([HttpApiBuilder.httpApp, runtime.runtimeEffect], { + concurrency: 'unbounded', + }) const webHandle = HttpApp.toWebHandlerRuntime(rt)( pipe( diff --git a/packages/cloudflare/src/entry/queue.ts b/packages/cloudflare/src/entry/queue.ts index 148872c..fa56a61 100644 --- a/packages/cloudflare/src/entry/queue.ts +++ b/packages/cloudflare/src/entry/queue.ts @@ -50,7 +50,7 @@ export class CloudflareQueueHandle extends Context.Tag('@cloudflare:queue-handle const process: { ( effect: (message: QueueEventMessage) => Effect.Effect, - ): Effect.Effect[], never> + ): Effect.Effect[], never>; ( schema: Schema.Schema, effect: (message: QueueEventMessage) => Effect.Effect, diff --git a/packages/cloudflare/src/queue.ts b/packages/cloudflare/src/queue.ts index 1cd8988..7d0b44a 100644 --- a/packages/cloudflare/src/queue.ts +++ b/packages/cloudflare/src/queue.ts @@ -62,7 +62,7 @@ export interface QueueEvent { process: { ( effect: (message: QueueEventMessage) => Effect.Effect, - ): Effect.Effect[], never> + ): Effect.Effect[], never>; ( schema: Schema.Schema, effect: (message: QueueEventMessage) => Effect.Effect, diff --git a/packages/cloudflare/src/runtime.ts b/packages/cloudflare/src/runtime.ts index eba3adf..c18cbc7 100644 --- a/packages/cloudflare/src/runtime.ts +++ b/packages/cloudflare/src/runtime.ts @@ -93,7 +93,10 @@ export const parseConfig = Effect.fn('wrangler.parse-config')(function* ( Effect.fnUntraced(function* (acc, configPath) { if (Option.isSome(acc)) return acc - const result: Option.Option<{ config: Unstable_Config; path: string }> = yield* pipe( + const result: Option.Option<{ + config: Unstable_Config + path: string + }> = yield* pipe( Effect.try(() => unstable_readConfig({ config: configPath, env, remote: false }, { hideWarnings: false })), Effect.tapErrorCause(Effect.logError), Effect.map((config) => Option.some({ config, path: configPath })), @@ -190,7 +193,11 @@ const layer = (options: { export const runMain = ( effect: Effect.Effect, - options: { configPath: string; run?: 'local' | 'remote' | undefined; environment?: string | undefined }, + options: { + configPath: string + run?: 'local' | 'remote' | undefined + environment?: string | undefined + }, ) => { NodeRuntime.runMain( Effect.provide( diff --git a/packages/cloudflare/src/workflows.ts b/packages/cloudflare/src/workflows.ts index 05e56fe..1f15798 100644 --- a/packages/cloudflare/src/workflows.ts +++ b/packages/cloudflare/src/workflows.ts @@ -704,7 +704,9 @@ export const runEffectWorkflow = ( }), ), catchAll({ decode: decodeExit }), - Effect.withSpan('Workflows.doSchema', { attributes: { label: SchemaClass.identifier } }), + Effect.withSpan('Workflows.doSchema', { + attributes: { label: SchemaClass.identifier }, + }), ) return run as any diff --git a/packages/cloudflare/tsconfig.check.json b/packages/cloudflare/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/cloudflare/tsconfig.check.json +++ b/packages/cloudflare/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/cloudflare/tsconfig.json b/packages/cloudflare/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/cloudflare/tsconfig.json +++ b/packages/cloudflare/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/cloudflare/tsconfig.lib.json b/packages/cloudflare/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/cloudflare/tsconfig.lib.json +++ b/packages/cloudflare/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/cloudflare/tsconfig.test.json b/packages/cloudflare/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/cloudflare/tsconfig.test.json +++ b/packages/cloudflare/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/cms/project.json b/packages/cms/project.json index 63fed5c..4f23ba2 100644 --- a/packages/cms/project.json +++ b/packages/cms/project.json @@ -8,7 +8,7 @@ "executor": "nx:run-commands", "outputs": ["{workspaceRoot}/node_modules/.vitest/packages/cms", "{workspaceRoot}/coverage/packages/cms"], "options": { - "command": "pnpm xdev test --project pkgs-cms" + "command": "xdev test --project pkgs-cms" } }, "madge": { diff --git a/packages/cms/src/image.ts b/packages/cms/src/image.ts index 72629e5..65f60fa 100644 --- a/packages/cms/src/image.ts +++ b/packages/cms/src/image.ts @@ -1,9 +1,9 @@ -import imageUrlBuilder from '@sanity/image-url' +import { createImageUrlBuilder } from '@sanity/image-url' import type { Image } from '@xstack/cms/sanity' import { dataset, projectId } from '@xstack/cms/env' -const builder = imageUrlBuilder({ projectId, dataset }) +const builder = createImageUrlBuilder({ projectId, dataset }) export function urlFor(source: Image) { return builder.image(source) diff --git a/packages/cms/tsconfig.check.json b/packages/cms/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/cms/tsconfig.check.json +++ b/packages/cms/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/cms/tsconfig.json b/packages/cms/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/cms/tsconfig.json +++ b/packages/cms/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/cms/tsconfig.lib.json b/packages/cms/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/cms/tsconfig.lib.json +++ b/packages/cms/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/cms/tsconfig.test.json b/packages/cms/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/cms/tsconfig.test.json +++ b/packages/cms/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/db/project.json b/packages/db/project.json index e4269a5..5be8975 100644 --- a/packages/db/project.json +++ b/packages/db/project.json @@ -8,7 +8,7 @@ "executor": "nx:run-commands", "outputs": ["{workspaceRoot}/node_modules/.vitest/packages/db", "{workspaceRoot}/coverage/packages/db"], "options": { - "command": "pnpm xdev test --project pkgs-db" + "command": "xdev test --project pkgs-db" } }, "madge": { diff --git a/packages/db/src/kysely.ts b/packages/db/src/kysely.ts index d682e7c..a16d896 100644 --- a/packages/db/src/kysely.ts +++ b/packages/db/src/kysely.ts @@ -35,7 +35,7 @@ type Selectable = { encodeSchema: Schema.Schema, statement: (data: NI) => Effect.Effect, E, R>, input: NA, - ): SelectableEffect, E | ParseError | SqlError, R> + ): SelectableEffect, E | ParseError | SqlError, R>; ( encodeSchema: Schema.Schema, @@ -49,7 +49,7 @@ type Selectable = { decodeSchema: Schema.Schema, statement: (data: NI) => Effect.Effect, E, R>, input: NI, - ): SelectableEffect, E | ParseError | SqlError, R> + ): SelectableEffect, E | ParseError | SqlError, R>; ( encodeSchema: Schema.Schema, @@ -80,7 +80,7 @@ type Insertable = { , E, R = never>( statement: (data: Array) => Effect.Effect, E, R>, input: X, - ): InsertableEffect ? ReadonlyArray : A, E | ParseError | SqlError, R> + ): InsertableEffect ? ReadonlyArray : A, E | ParseError | SqlError, R>; ( statement: (data: Array) => Effect.Effect, E, R>, @@ -92,7 +92,7 @@ type Insertable = { , E, X1 = any, R = never>( statement: (data: Array) => Effect.Effect, input: X, - ): Effect.Effect + ): Effect.Effect; ( statement: (data: Array) => Effect.Effect, @@ -104,7 +104,7 @@ type Insertable = { decode: Schema.Schema, statement: (data: Array) => Effect.Effect, E, R>, input: X, - ): InsertableEffect ? ReadonlyArray : DA, E | ParseError | SqlError, R> + ): InsertableEffect ? ReadonlyArray : DA, E | ParseError | SqlError, R>; ( decode: Schema.Schema, @@ -139,7 +139,7 @@ type Updateable = { ( statement: (data: UI) => Effect.Effect, E, R>, input: UA, - ): UpdateableEffect + ): UpdateableEffect; ( statement: (data: UI) => Effect.Effect, E, R>, @@ -149,7 +149,7 @@ type Updateable = { ( statement: (data: UI) => Effect.Effect, input: UA, - ): Effect.Effect + ): Effect.Effect; ( statement: (input: UI) => Effect.Effect, @@ -161,7 +161,7 @@ type Updateable = { encodeSchema: Schema.Schema, statement: (data: NI) => Effect.Effect, E, R>, input: NA, - ): UpdateableEffect + ): UpdateableEffect; ( encodeSchema: Schema.Schema, @@ -173,7 +173,7 @@ type Updateable = { encodeSchema: Schema.Schema, statement: (data: NI) => Effect.Effect, input: NA, - ): Effect.Effect + ): Effect.Effect; ( encodeSchema: Schema.Schema, @@ -187,7 +187,7 @@ type Updateable = { decode: Schema.Schema, statement: (data: UI) => Effect.Effect, E, R>, input: UA, - ): UpdateableEffect + ): UpdateableEffect; ( decode: Schema.Schema, @@ -542,7 +542,7 @@ export const encode: { schema: Schema.Schema, statement: (input: I) => Effect.Effect, input: A, - ): Effect.Effect + ): Effect.Effect; ( schema: Schema.Schema, statement: (input: I) => Effect.Effect, @@ -566,7 +566,7 @@ export const decode: { ( schema: Schema.Schema, statement: Effect.Effect, - ): Effect.Effect, E | ParseError | SqlError, R> + ): Effect.Effect, E | ParseError | SqlError, R>; ( schema: Schema.Schema, ): (statement: Effect.Effect) => Effect.Effect, E | ParseError | SqlError, R> @@ -585,7 +585,7 @@ export const codec: { decode: Schema.Schema, statement: (input: I) => Effect.Effect, input: A, - ): Effect.Effect + ): Effect.Effect; ( encode: Schema.Schema, decode: Schema.Schema, diff --git a/packages/db/src/migrator.ts b/packages/db/src/migrator.ts index 3739fce..6377abd 100644 --- a/packages/db/src/migrator.ts +++ b/packages/db/src/migrator.ts @@ -193,7 +193,11 @@ const make = Effect.gen(function* () { `.withoutTransform } - const latestMigration = sql<{ name: string; created_at: string; finished_at: string }>` + const latestMigration = sql<{ + name: string + created_at: string + finished_at: string + }>` SELECT * FROM ${sql(migrationsTable)} ORDER BY created_at DESC LIMIT 1 `.withoutTransform.pipe( Effect.map((_) => diff --git a/packages/db/src/prisma.ts b/packages/db/src/prisma.ts index 47d22d6..878a8d8 100644 --- a/packages/db/src/prisma.ts +++ b/packages/db/src/prisma.ts @@ -78,7 +78,6 @@ const DatabaseTypeMap: Record> = { export interface PrismaGenerateOptions { provider: 'sqlite' | 'mysql' | 'postgresql' - url: string format?: { modelCase?: CaseFormat // How to format model names fieldCase?: CaseFormat // How to format field names @@ -132,7 +131,6 @@ const formatCase = { function normalizeOptions(options?: Partial): PrismaGenerateOptions { const defaultOptions: PrismaGenerateOptions = { provider: 'postgresql', - url: "env('DATABASE_URL')", format: { modelCase: 'snake', fieldCase: 'snake', @@ -158,7 +156,6 @@ function normalizeOptions(options?: Partial): PrismaGener return { provider: options.provider ?? defaultOptions.provider, - url: options.url ?? defaultOptions.url, format: { ...defaultOptions.format, ...options.format }, database: { ...defaultOptions.database, ...options.database }, relations: { ...defaultOptions.relations, ...options.relations }, @@ -486,7 +483,11 @@ const generateFields = (key: string, table: Table, options: PrismaGenerateOption break } case 'Refinement': { - annotations = { ...field.annotations, ...field.type.annotations, ...field.type.from.annotations } + annotations = { + ...field.annotations, + ...field.type.annotations, + ...field.type.from.annotations, + } const ast = AST.annotations(field.type.from, annotations) columnType = getEffectiveType(ast) columnConfig.description = AST.getDescriptionAnnotation(ast).pipe(Option.getOrElse(() => '')) @@ -883,7 +884,6 @@ export function generate(options: PrismaGenerateOptions, tables: Tables): string datasource db { provider = "${normalizedOptions.provider}" - url = ${normalizedOptions.url} } ${generatorBlock} diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index b304074..3af0baf 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -199,8 +199,10 @@ export const toBool = (value: unknown) => { * @since 1.0.0 * @category date & time */ -export interface DateTime - extends Schema.transform {} +export interface DateTime extends Schema.transform< + typeof Schema.ValidDateFromSelf, + typeof Schema.DateTimeUtcFromSelf +> {} /** * @since 1.0.0 @@ -365,13 +367,12 @@ export const UuidV7 = Schema.String.pipe( * @since 1.0.0 * @category uuid */ -export interface UuidInsert - extends VariantSchema.Field<{ - readonly select: Schema.brand - readonly insert: VariantSchema.Overrideable, string> - readonly update: Schema.brand - readonly json: Schema.brand - }> {} +export interface UuidInsert extends VariantSchema.Field<{ + readonly select: Schema.brand + readonly insert: VariantSchema.Overrideable, string> + readonly update: Schema.brand + readonly json: Schema.brand +}> {} /** * @since 1.0.0 @@ -420,13 +421,12 @@ export const uuidInsert = Schema.String.pipe( * @since 1.0.0 * @category uuid */ -export interface UuidV7Insert - extends VariantSchema.Field<{ - readonly select: Schema.brand - readonly insert: VariantSchema.Overrideable, string> - readonly update: Schema.brand - readonly json: Schema.brand - }> {} +export interface UuidV7Insert extends VariantSchema.Field<{ + readonly select: Schema.brand + readonly insert: VariantSchema.Overrideable, string> + readonly update: Schema.brand + readonly json: Schema.brand +}> {} /** * @since 1.0.0 @@ -475,13 +475,12 @@ export const uuidV7Insert = Schema.String.pipe( * @since 1.0.0 * @category uuid */ -interface NanoIdInsert - extends VariantSchema.Field<{ - readonly select: Schema.brand - readonly insert: VariantSchema.Overrideable, string> - readonly update: Schema.brand - readonly json: Schema.brand - }> {} +interface NanoIdInsert extends VariantSchema.Field<{ + readonly select: Schema.brand + readonly insert: VariantSchema.Overrideable, string> + readonly update: Schema.brand + readonly json: Schema.brand +}> {} /** * @since 1.0.0 diff --git a/packages/db/tests/prisma.test.ts b/packages/db/tests/prisma.test.ts index 2110044..61df2c7 100644 --- a/packages/db/tests/prisma.test.ts +++ b/packages/db/tests/prisma.test.ts @@ -3,7 +3,7 @@ import * as Database from '@xstack/db' import * as Schema from 'effect/Schema' import * as Prisma from '../src/prisma' -const g = (tables: Prisma.Tables) => Prisma.generate({ provider: 'sqlite', url: 'file:./dev.dv' }, tables) +const g = (tables: Prisma.Tables) => Prisma.generate({ provider: 'sqlite' }, tables) describe('Prisma schema generate', () => { it.skip('should generate prisma schema', () => { @@ -36,7 +36,10 @@ describe('Prisma schema generate', () => { Schema.propertySignature, Schema.withConstructorDefault(() => 'active' as const), ), - settings: Schema.Record({ key: Schema.String, value: Schema.Unknown }).pipe( + settings: Schema.Record({ + key: Schema.String, + value: Schema.Unknown, + }).pipe( Database.ColumnConfig({ description: 'Organization configuration settings', }), @@ -110,7 +113,10 @@ describe('Prisma schema generate', () => { description: "Reference to user's organization", }), ), - metadata: Schema.Record({ key: Schema.String, value: Schema.Unknown }).pipe( + metadata: Schema.Record({ + key: Schema.String, + value: Schema.Unknown, + }).pipe( Database.ColumnConfig({ description: 'Additional user metadata', }), @@ -390,7 +396,10 @@ describe('Prisma schema generate', () => { description: 'Reference to owning organization', }), ), - metadata: Schema.Record({ key: Schema.String, value: Schema.Unknown }).pipe( + metadata: Schema.Record({ + key: Schema.String, + value: Schema.Unknown, + }).pipe( Database.ColumnConfig({ description: 'Additional product metadata', }), diff --git a/packages/db/tsconfig.check.json b/packages/db/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/db/tsconfig.check.json +++ b/packages/db/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/db/tsconfig.json +++ b/packages/db/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/db/tsconfig.lib.json b/packages/db/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/db/tsconfig.lib.json +++ b/packages/db/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/db/tsconfig.test.json b/packages/db/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/db/tsconfig.test.json +++ b/packages/db/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/emails/tsconfig.check.json b/packages/emails/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/emails/tsconfig.check.json +++ b/packages/emails/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/emails/tsconfig.json b/packages/emails/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/emails/tsconfig.json +++ b/packages/emails/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/emails/tsconfig.lib.json b/packages/emails/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/emails/tsconfig.lib.json +++ b/packages/emails/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/emails/tsconfig.test.json b/packages/emails/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/emails/tsconfig.test.json +++ b/packages/emails/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/errors/project.json b/packages/errors/project.json index f6bdaaa..72e4c9c 100644 --- a/packages/errors/project.json +++ b/packages/errors/project.json @@ -8,7 +8,7 @@ "executor": "nx:run-commands", "outputs": ["{workspaceRoot}/node_modules/.vitest/packages/errors", "{workspaceRoot}/coverage/packages/errors"], "options": { - "command": "pnpm xdev test --project pkgs-errors" + "command": "xdev test --project pkgs-errors" } }, "madge": { diff --git a/packages/errors/tsconfig.check.json b/packages/errors/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/errors/tsconfig.check.json +++ b/packages/errors/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/errors/tsconfig.json b/packages/errors/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/errors/tsconfig.json +++ b/packages/errors/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/errors/tsconfig.lib.json b/packages/errors/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/errors/tsconfig.lib.json +++ b/packages/errors/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/errors/tsconfig.test.json b/packages/errors/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/errors/tsconfig.test.json +++ b/packages/errors/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/event-log-server/e2e/fixtures/test-events.ts b/packages/event-log-server/e2e/fixtures/test-events.ts index 9900b27..2f2e4df 100644 --- a/packages/event-log-server/e2e/fixtures/test-events.ts +++ b/packages/event-log-server/e2e/fixtures/test-events.ts @@ -59,7 +59,10 @@ export class AgentEvents extends Events.make(AgentEventGroup, { return Effect.gen(function* () { yield* Effect.log('set name from client', payload.name) // 将结果通过新的 Client 事件发送回去 - yield* client('Hi', { id: Math.round(Math.random() * 1000), message: `Hi ${payload.name}` }) + yield* client('Hi', { + id: Math.round(Math.random() * 1000), + message: `Hi ${payload.name}`, + }) }).pipe(Effect.orDie) }) .handle('Hi', ({ payload }) => diff --git a/packages/event-log-server/e2e/fixtures/test-sync-agent-client.ts b/packages/event-log-server/e2e/fixtures/test-sync-agent-client.ts index 5f54553..0e86990 100644 --- a/packages/event-log-server/e2e/fixtures/test-sync-agent-client.ts +++ b/packages/event-log-server/e2e/fixtures/test-sync-agent-client.ts @@ -1,7 +1,6 @@ import * as Reactivity from '@effect/experimental/Reactivity' import * as FetchHttpClient from '@effect/platform/FetchHttpClient' import * as Socket from '@effect/platform/Socket' -import { NodeRuntime } from '@effect/platform-node' import * as NodeSqliteClient from '@effect/sql-sqlite-node/SqliteClient' import { CryptoLive } from '@xstack/event-log/CryptoWeb' import * as EventJournal from '@xstack/event-log/EventJournal' diff --git a/packages/event-log-server/project.json b/packages/event-log-server/project.json index b74410f..d5ecbb3 100644 --- a/packages/event-log-server/project.json +++ b/packages/event-log-server/project.json @@ -11,7 +11,7 @@ "{workspaceRoot}/coverage/packages/event-log-server" ], "options": { - "command": "pnpm xdev test --project pkgs-event-log-server" + "command": "xdev test --project pkgs-event-log-server" } }, "madge": { diff --git a/packages/event-log-server/src/cloudflare/EventLogSyncServer.ts b/packages/event-log-server/src/cloudflare/EventLogSyncServer.ts index 4b0f026..3276499 100644 --- a/packages/event-log-server/src/cloudflare/EventLogSyncServer.ts +++ b/packages/event-log-server/src/cloudflare/EventLogSyncServer.ts @@ -380,7 +380,9 @@ export abstract class SyncDurableServer extends EventLogDurableObject { return Effect.gen(this, function* () { const client = yield* SyncAgentClient - yield* Effect.forEach(changes, (chunk) => client.write(this.remoteId, chunk), { discard: true }) + yield* Effect.forEach(changes, (chunk) => client.write(this.remoteId, chunk), { + discard: true, + }) // only dev, broadcast changes to nodejs // yield* Effect.promise(() => fetch("http://localhost:9995/sync", { method: "post", body: changes[0] })).pipe( diff --git a/packages/event-log-server/src/cloudflare/Rpc/SyncAgentClient.ts b/packages/event-log-server/src/cloudflare/Rpc/SyncAgentClient.ts index bc3719b..da2a265 100644 --- a/packages/event-log-server/src/cloudflare/Rpc/SyncAgentClient.ts +++ b/packages/event-log-server/src/cloudflare/Rpc/SyncAgentClient.ts @@ -57,7 +57,9 @@ export class SyncAgentClient extends Context.Tag('@xstack/event-log-server/RpcCl ), ) - const rpcClientRef = yield* RpcClient_.makeFetchFromWebsocket(binding.fetch, Client, { rpcPath: config.rpcPath }) + const rpcClientRef = yield* RpcClient_.makeFetchFromWebsocket(binding.fetch, Client, { + rpcPath: config.rpcPath, + }) const write = Effect.fn(function* ( remoteId: typeof EventJournal.RemoteId.Type, diff --git a/packages/event-log-server/src/cloudflare/Rpc/SyncServer.ts b/packages/event-log-server/src/cloudflare/Rpc/SyncServer.ts index 64996b8..361e4b4 100644 --- a/packages/event-log-server/src/cloudflare/Rpc/SyncServer.ts +++ b/packages/event-log-server/src/cloudflare/Rpc/SyncServer.ts @@ -55,10 +55,11 @@ export const SyncServerConfig = Context.GenericTag( export class SyncServerClient extends Context.Tag('@xstack/event-log-server/RpcClient/SyncServerClient')< SyncServerClient, { - write: ( - data: Uint8Array, - ) => Effect.Effect< - { readonly response: Uint8Array; readonly changes: readonly Uint8Array[] }, + write: (data: Uint8Array) => Effect.Effect< + { + readonly response: Uint8Array + readonly changes: readonly Uint8Array[] + }, SyncServerError > requestChanges: ( @@ -79,7 +80,9 @@ export class SyncServerClient extends Context.Tag('@xstack/event-log-server/RpcC ), ) - const rpcClientRef = yield* RpcClient_.makeFetchFromWebsocket(binding.fetch, Client, { rpcPath: config.rpcPath }) + const rpcClientRef = yield* RpcClient_.makeFetchFromWebsocket(binding.fetch, Client, { + rpcPath: config.rpcPath, + }) const write = Effect.fnUntraced(function* (data: Uint8Array) { const client = yield* rpcClientRef.get diff --git a/packages/event-log-server/src/cloudflare/Rpc/SyncStorageProxy.ts b/packages/event-log-server/src/cloudflare/Rpc/SyncStorageProxy.ts index dfcffd8..25427d0 100644 --- a/packages/event-log-server/src/cloudflare/Rpc/SyncStorageProxy.ts +++ b/packages/event-log-server/src/cloudflare/Rpc/SyncStorageProxy.ts @@ -122,7 +122,9 @@ export class SyncStorageProxyClient extends Context.Tag('@xstack/event-log-serve ), ) - const rpcClientRef = yield* RpcClient_.makeFetchFromWebsocket(binding.fetch, Client, { rpcPath: config.rpcPath }) + const rpcClientRef = yield* RpcClient_.makeFetchFromWebsocket(binding.fetch, Client, { + rpcPath: config.rpcPath, + }) const exec = Effect.fn( function* ({ sql, bindings }: { sql: string; bindings: SqlStorageValue[] }) { diff --git a/packages/event-log-server/src/cloudflare/SyncStorageProxy.ts b/packages/event-log-server/src/cloudflare/SyncStorageProxy.ts index 342caf3..d37c8a9 100644 --- a/packages/event-log-server/src/cloudflare/SyncStorageProxy.ts +++ b/packages/event-log-server/src/cloudflare/SyncStorageProxy.ts @@ -293,7 +293,10 @@ export const makeWorker = (options: { const upgradeHeader = headers.get('Upgrade') if (!upgradeHeader || upgradeHeader !== 'websocket') { - return new Response(null, { status: 426, statusText: 'Durable Object expected Upgrade: websocket' }) + return new Response(null, { + status: 426, + statusText: 'Durable Object expected Upgrade: websocket', + }) } const identityEither = DurableObjectUtils.DurableObjectIdentitySchema.fromHeaders(headers) diff --git a/packages/event-log-server/src/server/vault.ts b/packages/event-log-server/src/server/vault.ts index dca4084..bec9969 100644 --- a/packages/event-log-server/src/server/vault.ts +++ b/packages/event-log-server/src/server/vault.ts @@ -312,7 +312,11 @@ export class Vault extends Effect.Service()('Vault', { { concurrency: 2 }, ) - const syncInfo = yield* storage.getSyncInfo({ namespace, userId, publicKey: item.publicKey }) + const syncInfo = yield* storage.getSyncInfo({ + namespace, + userId, + publicKey: item.publicKey, + }) return Option.some( SyncPublicKeyItem.make({ diff --git a/packages/event-log-server/tsconfig.check.json b/packages/event-log-server/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/event-log-server/tsconfig.check.json +++ b/packages/event-log-server/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/event-log-server/tsconfig.json b/packages/event-log-server/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/event-log-server/tsconfig.json +++ b/packages/event-log-server/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/event-log-server/tsconfig.lib.json b/packages/event-log-server/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/event-log-server/tsconfig.lib.json +++ b/packages/event-log-server/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/event-log-server/tsconfig.test.json b/packages/event-log-server/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/event-log-server/tsconfig.test.json +++ b/packages/event-log-server/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/event-log/project.json b/packages/event-log/project.json index 3b57a57..f99a33c 100644 --- a/packages/event-log/project.json +++ b/packages/event-log/project.json @@ -11,7 +11,7 @@ "{workspaceRoot}/coverage/packages/event-log" ], "options": { - "command": "pnpm xdev test --project pkgs-event-log" + "command": "xdev test --project pkgs-event-log" } }, "madge": { diff --git a/packages/event-log/src/CryptoNative.ts b/packages/event-log/src/CryptoNative.ts index 79d621a..08705fa 100644 --- a/packages/event-log/src/CryptoNative.ts +++ b/packages/event-log/src/CryptoNative.ts @@ -141,7 +141,7 @@ export const CryptoLive = Layer.effect( const encryptionKey = hmac.digest() const decipher = QuickCrypto.createDecipheriv('aes-256-gcm', encryptionKey, iv) - decipher.setAuthTag(tag) // Set the authentication tag + decipher.setAuthTag(Buffer.from(tag)) // Set the authentication tag const decryptedPart1 = decipher.update(encrypted) const decryptedPart2 = decipher.final() // Throws error if tag verification fails diff --git a/packages/event-log/src/EventLogDaemon.ts b/packages/event-log/src/EventLogDaemon.ts index d3efbed..04cee88 100644 --- a/packages/event-log/src/EventLogDaemon.ts +++ b/packages/event-log/src/EventLogDaemon.ts @@ -104,7 +104,9 @@ export class EventLogDaemon extends Effect.Service()('EventLogDa Stream.debounce('100 millis'), Stream.filter((socketOnline) => !!socketOnline), Stream.tap(() => Effect.logTrace('Socket online, starting periodic remote stats fetch')), - Stream.flatMap(() => Stream.fromSchedule(Schedule.spaced(syncRemoteStatsInterval)), { switch: true }), + Stream.flatMap(() => Stream.fromSchedule(Schedule.spaced(syncRemoteStatsInterval)), { + switch: true, + }), Stream.filterEffect(() => isOnlinePredicate()), Stream.mapEffect(() => updateRemoteSyncStats), Stream.runDrain, diff --git a/packages/event-log/src/EventLogPlatformEffectsNative.ts b/packages/event-log/src/EventLogPlatformEffectsNative.ts index e35edb2..7d78d23 100644 --- a/packages/event-log/src/EventLogPlatformEffectsNative.ts +++ b/packages/event-log/src/EventLogPlatformEffectsNative.ts @@ -28,9 +28,9 @@ export const Live = Layer.effect( // Get internal SQL database size const internalDbPath = yield* sql.extra.getDbPath() const internalDbSize = yield* Effect.sync(() => { - const file = new ExpoFileSystem.File(internalDbPath); - const info = file.info(); - return info.exists ? info.size??0 : 0; + const file = new ExpoFileSystem.File(internalDbPath) + const info = file.info() + return info.exists ? (info.size ?? 0) : 0 }) // Get external database size diff --git a/packages/event-log/src/EventLogWorker.ts b/packages/event-log/src/EventLogWorker.ts index 156da27..f781049 100644 --- a/packages/event-log/src/EventLogWorker.ts +++ b/packages/event-log/src/EventLogWorker.ts @@ -13,7 +13,12 @@ export const EventLogWorker = Layer.effect( return EventLog.EventLog.of({ write: (options) => workerPool - .executeEffect(new EventLogSchema.EventLogWriteRequest({ event: options.event, payload: options.payload })) + .executeEffect( + new EventLogSchema.EventLogWriteRequest({ + event: options.event, + payload: options.payload, + }), + ) .pipe(Effect.orDie), destroy: Effect.void, diff --git a/packages/event-log/src/IdentityStorageNative.ts b/packages/event-log/src/IdentityStorageNative.ts index 9c9530a..243df9c 100644 --- a/packages/event-log/src/IdentityStorageNative.ts +++ b/packages/event-log/src/IdentityStorageNative.ts @@ -36,9 +36,9 @@ const makeNativeStorage = Effect.gen(function* () { const dbPath = db.getDbPath() return Effect.sync(() => { - const file = new ExpoFileSystem.File(dbPath); - const info = file.info(); - return info.exists ? info.size??0 : 0; + const file = new ExpoFileSystem.File(dbPath) + const info = file.info() + return info.exists ? (info.size ?? 0) : 0 }) }), Effect.scoped, diff --git a/packages/event-log/src/Metrics.ts b/packages/event-log/src/Metrics.ts index 39af1cf..ca7f8d2 100644 --- a/packages/event-log/src/Metrics.ts +++ b/packages/event-log/src/Metrics.ts @@ -8,7 +8,9 @@ export const eventQueryCount = Metric.counter('event.query.count', { incremental: true, }) -export const eventQueryLatency = Metric.gauge('event.query.latency.ms', { description: 'Event query latency' }) +export const eventQueryLatency = Metric.gauge('event.query.latency.ms', { + description: 'Event query latency', +}) export const eventWriteCount = Metric.counter('event.write.count', { description: 'Events written count', @@ -21,7 +23,9 @@ export const eventWriteMessageSize = Metric.histogram( 'Write Msg size', ) -export const eventWriteLatency = Metric.gauge('event.write.latency.ms', { description: 'Event write latency' }) +export const eventWriteLatency = Metric.gauge('event.write.latency.ms', { + description: 'Event write latency', +}) export const eventWriteErrorCount = Metric.counter('event.write.error.count', { description: 'Event write errors', @@ -74,14 +78,18 @@ export const syncLatency = Metric.gauge('event.sync.latency.ms', { description: // Encrypt/Decrypt -export const encryptionLatency = Metric.gauge('event.encryption.latency.ms', { description: 'Encryption latency' }) +export const encryptionLatency = Metric.gauge('event.encryption.latency.ms', { + description: 'Encryption latency', +}) export const encryptionCount = Metric.counter('event.encryption.count', { description: 'Encryption count', incremental: true, }) -export const decryptionLatency = Metric.gauge('event.decryption.latency.ms', { description: 'Decryption latency' }) +export const decryptionLatency = Metric.gauge('event.decryption.latency.ms', { + description: 'Decryption latency', +}) export const decryptionCount = Metric.counter('event.decryption.count', { description: 'Decryption count', diff --git a/packages/event-log/src/MsgPack.ts b/packages/event-log/src/MsgPack.ts index 76e5d69..e3d92e7 100644 --- a/packages/event-log/src/MsgPack.ts +++ b/packages/event-log/src/MsgPack.ts @@ -196,7 +196,7 @@ export const duplexSchema: { OutDone, InDone, IR | OR | R - > + >; ( self: Channel.Channel< Chunk.Chunk, diff --git a/packages/event-log/src/SqlEventJournal.ts b/packages/event-log/src/SqlEventJournal.ts index 9ca4831..6b35457 100644 --- a/packages/event-log/src/SqlEventJournal.ts +++ b/packages/event-log/src/SqlEventJournal.ts @@ -258,10 +258,9 @@ export const make = (options?: { for (let i = 0; i < entryIds.length; i += selectInClauseBatchSize) { const chunkOfIds = entryIds.slice(i, i + selectInClauseBatchSize) if (chunkOfIds.length > 0) { - yield* sql<{ id: Uint8Array }>`SELECT id FROM ${sql(entryTable)} WHERE ${sql.in( - 'id', - chunkOfIds, - )}`.withoutTransform.pipe( + yield* sql<{ + id: Uint8Array + }>`SELECT id FROM ${sql(entryTable)} WHERE ${sql.in('id', chunkOfIds)}`.withoutTransform.pipe( Effect.withSpan('SqlEventJournal.checkExistingEntriesChunk'), Effect.annotateSpans({ 'event.journal.operation': 'check_existing_entries_chunk', diff --git a/packages/event-log/tsconfig.check.json b/packages/event-log/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/event-log/tsconfig.check.json +++ b/packages/event-log/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/event-log/tsconfig.json b/packages/event-log/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/event-log/tsconfig.json +++ b/packages/event-log/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/event-log/tsconfig.lib.json b/packages/event-log/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/event-log/tsconfig.lib.json +++ b/packages/event-log/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/event-log/tsconfig.test.json b/packages/event-log/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/event-log/tsconfig.test.json +++ b/packages/event-log/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/expo-bip39/ios/ExpoBip39Module.swift b/packages/expo-bip39/ios/ExpoBip39Module.swift index d9c3541..fe5bc37 100644 --- a/packages/expo-bip39/ios/ExpoBip39Module.swift +++ b/packages/expo-bip39/ios/ExpoBip39Module.swift @@ -15,17 +15,17 @@ public class ExpoBip39Module: Module { return try self.deriveSeed(mnemonic: mnemonic, password: password) } } - + private func deriveSeed(mnemonic: String, password: String) throws -> [String] { let salt = "mnemonic" + password guard let passData = mnemonic.data(using: .utf8), let saltData = salt.data(using: .utf8) else { throw Exception(name: "ENCODING_ERROR", description: "Invalid input encoding") } - - // 准备输出缓冲区 + + // 准备输出缓冲区 111 var derivedKey = [UInt8](repeating: 0, count: 64) - + // 调用 CommonCrypto PBKDF2-HMAC-SHA512 let status = passData.withUnsafeBytes { passBytes in saltData.withUnsafeBytes { saltBytes in @@ -39,11 +39,11 @@ public class ExpoBip39Module: Module { ) } } - + guard status == kCCSuccess else { throw Exception(name: "PBKDF2_ERROR", description: "Key derivation failed") } - + // 转换为字符串数组 return derivedKey.map { String($0) } } diff --git a/packages/expo-bip39/project.json b/packages/expo-bip39/project.json index 30b7fbd..30eab53 100644 --- a/packages/expo-bip39/project.json +++ b/packages/expo-bip39/project.json @@ -11,7 +11,7 @@ "{workspaceRoot}/coverage/packages/expo-bip39" ], "options": { - "command": "pnpm xdev test --project pkgs-expo-bip39" + "command": "xdev test --project pkgs-expo-bip39" } }, "build": { diff --git a/packages/expo-bip39/tsconfig.build.json b/packages/expo-bip39/tsconfig.build.json index fa40425..92da9ad 100644 --- a/packages/expo-bip39/tsconfig.build.json +++ b/packages/expo-bip39/tsconfig.build.json @@ -5,6 +5,6 @@ "noEmit": false, "declarationMap": true, "declaration": true, - "allowImportingTsExtensions": false - } + "allowImportingTsExtensions": false, + }, } diff --git a/packages/expo-bip39/tsconfig.check.json b/packages/expo-bip39/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/expo-bip39/tsconfig.check.json +++ b/packages/expo-bip39/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/expo-bip39/tsconfig.json b/packages/expo-bip39/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/expo-bip39/tsconfig.json +++ b/packages/expo-bip39/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/expo-bip39/tsconfig.lib.json b/packages/expo-bip39/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/expo-bip39/tsconfig.lib.json +++ b/packages/expo-bip39/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/expo-bip39/tsconfig.test.json b/packages/expo-bip39/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/expo-bip39/tsconfig.test.json +++ b/packages/expo-bip39/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/form/project.json b/packages/form/project.json index 3bae0de..6e1ab30 100644 --- a/packages/form/project.json +++ b/packages/form/project.json @@ -8,7 +8,7 @@ "executor": "nx:run-commands", "outputs": ["{workspaceRoot}/node_modules/.vitest/packages/form", "{workspaceRoot}/coverage/packages/form"], "options": { - "command": "pnpm xdev test --project pkgs-form" + "command": "xdev test --project pkgs-form" } }, "madge": { diff --git a/packages/form/src/schema-form.tsx b/packages/form/src/schema-form.tsx index 6f347ee..b8ad95a 100644 --- a/packages/form/src/schema-form.tsx +++ b/packages/form/src/schema-form.tsx @@ -132,7 +132,15 @@ export const makeSchemaForm = ( const { schemaJSON } = useMemo(() => FG.toJson(schema, control._defaultValues as any), [schema]) const fields = useMemo( - () => render({ schemaJSON, groups, register, components, control: control, skipFirstGroup: rest.skipFirstGroup }), + () => + render({ + schemaJSON, + groups, + register, + components, + control: control, + skipFirstGroup: rest.skipFirstGroup, + }), [schemaJSON, groups, rest.form.register, components, control], ) diff --git a/packages/form/src/use-schema-form.ts b/packages/form/src/use-schema-form.ts index f07a43d..36827ae 100644 --- a/packages/form/src/use-schema-form.ts +++ b/packages/form/src/use-schema-form.ts @@ -57,14 +57,14 @@ const makeWriteableAtom: { ( schema: Schema.Schema, readOrWriteFn: ReadOrWriteEffect, - ): Atom.Writable, typeof Atom.Reset | Option.Option>> + ): Atom.Writable, typeof Atom.Reset | Option.Option>>; /** * rx writable */ ( schema: Schema.Schema, rx: Atom.AtomResultFn, - ): Atom.Writable, typeof Atom.Reset | Option.Option>> + ): Atom.Writable, typeof Atom.Reset | Option.Option>>; /** * promise writable */ @@ -157,14 +157,14 @@ export const useSchemaForm: { ( schema: Schema.Schema, readOrWrite: ReadOrWriteEffect, - ): Simplify> + ): Simplify>; /** * form schema (rx) */ ( schema: Schema.Schema, readOrWrite: ReadOrWriteAtomFn, - ): Simplify> + ): Simplify>; /** * form schema (promise) */ diff --git a/packages/form/tsconfig.check.json b/packages/form/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/form/tsconfig.check.json +++ b/packages/form/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/form/tsconfig.json b/packages/form/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/form/tsconfig.json +++ b/packages/form/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/form/tsconfig.lib.json b/packages/form/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/form/tsconfig.lib.json +++ b/packages/form/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/form/tsconfig.test.json b/packages/form/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/form/tsconfig.test.json +++ b/packages/form/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/fx/project.json b/packages/fx/project.json index 6823882..771f183 100644 --- a/packages/fx/project.json +++ b/packages/fx/project.json @@ -8,7 +8,7 @@ "executor": "nx:run-commands", "outputs": ["{workspaceRoot}/node_modules/.vitest/packages/fx", "{workspaceRoot}/coverage/packages/fx"], "options": { - "command": "pnpm xdev test --project pkgs-fx" + "command": "xdev test --project pkgs-fx" } }, "madge": { diff --git a/packages/fx/src/worker/scheduler/handle.ts b/packages/fx/src/worker/scheduler/handle.ts index c92ed45..4616463 100644 --- a/packages/fx/src/worker/scheduler/handle.ts +++ b/packages/fx/src/worker/scheduler/handle.ts @@ -53,9 +53,10 @@ abstract class RemoteResource { } } -export class ResourcePlan, B extends Effect.Effect> - implements Plan -{ +export class ResourcePlan< + A extends Effect.Effect, + B extends Effect.Effect, +> implements Plan { readonly name: string readonly commands: CommandRequestHandle[] readonly events: EventRequestHandle[] diff --git a/packages/fx/src/worker/scheduler/runner.ts b/packages/fx/src/worker/scheduler/runner.ts index 6bd8a83..65aaf04 100644 --- a/packages/fx/src/worker/scheduler/runner.ts +++ b/packages/fx/src/worker/scheduler/runner.ts @@ -15,7 +15,9 @@ export const workerHandles = ( Effect.gen(function* () { const manager = yield* SchedulerManager - yield* Effect.forEach(plans, (plan) => manager.register(plan), { concurrency: 'unbounded' }) + yield* Effect.forEach(plans, (plan) => manager.register(plan), { + concurrency: 'unbounded', + }) yield* manager.run }).pipe(Effect.provideService(Scope.Scope, scope)) as Effect.Effect, diff --git a/packages/fx/tsconfig.check.json b/packages/fx/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/fx/tsconfig.check.json +++ b/packages/fx/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/fx/tsconfig.json b/packages/fx/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/fx/tsconfig.json +++ b/packages/fx/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/fx/tsconfig.lib.json b/packages/fx/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/fx/tsconfig.lib.json +++ b/packages/fx/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/fx/tsconfig.test.json b/packages/fx/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/fx/tsconfig.test.json +++ b/packages/fx/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/i18n/project.json b/packages/i18n/project.json index a766032..35d6e68 100644 --- a/packages/i18n/project.json +++ b/packages/i18n/project.json @@ -8,7 +8,7 @@ "executor": "nx:run-commands", "outputs": ["{workspaceRoot}/node_modules/.vitest/packages/i18n", "{workspaceRoot}/coverage/packages/i18n"], "options": { - "command": "pnpm xdev test --project pkgs-i18n" + "command": "xdev test --project pkgs-i18n" } }, "madge": { diff --git a/packages/i18n/src/client.ts b/packages/i18n/src/client.ts index b951ad0..fabffdd 100644 --- a/packages/i18n/src/client.ts +++ b/packages/i18n/src/client.ts @@ -109,8 +109,12 @@ export class LanguageDetector { private fromSupported(language: string | null) { return ( - pick(this.options.supportedLanguages, language ?? this.options.fallbackLanguage, { loose: false }) || - pick(this.options.supportedLanguages, language ?? this.options.fallbackLanguage, { loose: true }) + pick(this.options.supportedLanguages, language ?? this.options.fallbackLanguage, { + loose: false, + }) || + pick(this.options.supportedLanguages, language ?? this.options.fallbackLanguage, { + loose: true, + }) ) } } diff --git a/packages/i18n/src/server.ts b/packages/i18n/src/server.ts index f19f979..88bc133 100644 --- a/packages/i18n/src/server.ts +++ b/packages/i18n/src/server.ts @@ -188,8 +188,12 @@ export class LanguageDetector { private fromSupported(language: string | null) { return ( - pick(this.options.supportedLanguages, language ?? this.options.fallbackLanguage, { loose: false }) || - pick(this.options.supportedLanguages, language ?? this.options.fallbackLanguage, { loose: true }) + pick(this.options.supportedLanguages, language ?? this.options.fallbackLanguage, { + loose: false, + }) || + pick(this.options.supportedLanguages, language ?? this.options.fallbackLanguage, { + loose: true, + }) ) } } diff --git a/packages/i18n/tsconfig.check.json b/packages/i18n/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/i18n/tsconfig.check.json +++ b/packages/i18n/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/i18n/tsconfig.json b/packages/i18n/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/i18n/tsconfig.json +++ b/packages/i18n/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/i18n/tsconfig.lib.json b/packages/i18n/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/i18n/tsconfig.lib.json +++ b/packages/i18n/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/i18n/tsconfig.test.json b/packages/i18n/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/i18n/tsconfig.test.json +++ b/packages/i18n/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/internal-kit/project.json b/packages/internal-kit/project.json index 8dc439f..6222ce1 100644 --- a/packages/internal-kit/project.json +++ b/packages/internal-kit/project.json @@ -11,7 +11,7 @@ "{workspaceRoot}/coverage/packages/internal-kit" ], "options": { - "command": "pnpm xdev test --project pkgs-internal-kit" + "command": "xdev test --project pkgs-internal-kit" } }, "madge": { diff --git a/packages/internal-kit/tsconfig.check.json b/packages/internal-kit/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/internal-kit/tsconfig.check.json +++ b/packages/internal-kit/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/internal-kit/tsconfig.json b/packages/internal-kit/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/internal-kit/tsconfig.json +++ b/packages/internal-kit/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/internal-kit/tsconfig.lib.json b/packages/internal-kit/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/internal-kit/tsconfig.lib.json +++ b/packages/internal-kit/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/internal-kit/tsconfig.test.json b/packages/internal-kit/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/internal-kit/tsconfig.test.json +++ b/packages/internal-kit/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/lib/project.json b/packages/lib/project.json index 0d58106..a7716a4 100644 --- a/packages/lib/project.json +++ b/packages/lib/project.json @@ -8,7 +8,7 @@ "executor": "nx:run-commands", "outputs": ["{workspaceRoot}/node_modules/.vitest/packages/lib", "{workspaceRoot}/coverage/packages/lib"], "options": { - "command": "pnpm xdev test --project pkgs-lib" + "command": "xdev test --project pkgs-lib" } }, "madge": { diff --git a/packages/lib/src/ui/alert-dialog.tsx b/packages/lib/src/ui/alert-dialog.tsx index 1c7ac32..ea6ba46 100644 --- a/packages/lib/src/ui/alert-dialog.tsx +++ b/packages/lib/src/ui/alert-dialog.tsx @@ -29,7 +29,9 @@ const AlertDialogContent = ({ containerClassName, children, ...props -}: React.ComponentPropsWithRef & { containerClassName?: string | undefined }) => { +}: React.ComponentPropsWithRef & { + containerClassName?: string | undefined +}) => { return ( diff --git a/packages/lib/src/ui/button.tsx b/packages/lib/src/ui/button.tsx index 3d1f9a4..6219d0a 100644 --- a/packages/lib/src/ui/button.tsx +++ b/packages/lib/src/ui/button.tsx @@ -55,8 +55,7 @@ const buttonGroupVariants = cva( }, ) export interface ButtonGroupProps - extends React.ComponentPropsWithRef<'div'>, - VariantProps {} + extends React.ComponentPropsWithRef<'div'>, VariantProps {} const ButtonGroup = ({ className, direction, ...props }: ButtonGroupProps) => { return
diff --git a/packages/lib/tsconfig.check.json b/packages/lib/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/lib/tsconfig.check.json +++ b/packages/lib/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/lib/tsconfig.json b/packages/lib/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/lib/tsconfig.json +++ b/packages/lib/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/lib/tsconfig.lib.json b/packages/lib/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/lib/tsconfig.lib.json +++ b/packages/lib/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/lib/tsconfig.test.json b/packages/lib/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/lib/tsconfig.test.json +++ b/packages/lib/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/local-first/project.json b/packages/local-first/project.json index 3ff4bc1..9379b0e 100644 --- a/packages/local-first/project.json +++ b/packages/local-first/project.json @@ -11,7 +11,7 @@ "{workspaceRoot}/coverage/packages/local-first" ], "options": { - "command": "pnpm xdev test --project pkgs-local-first" + "command": "xdev test --project pkgs-local-first" } }, "madge": { diff --git a/packages/local-first/src/services/storage.ts b/packages/local-first/src/services/storage.ts index 0ec6a90..3b52b20 100644 --- a/packages/local-first/src/services/storage.ts +++ b/packages/local-first/src/services/storage.ts @@ -36,32 +36,31 @@ export class LocalFirstStorageService extends Effect.Service { - const export_ = runtime.fn((options?: { filename?: string }) => - Effect.gen(function* () { - const blob = yield* LocalFirstStorageService.export() +const useStorageService = UseUseServices({ LocalFirstStorageService })( + ({ runtime, services: { LocalFirstStorageService } }) => { + const export_ = runtime.fn((options?: { filename?: string }) => + Effect.gen(function* () { + const blob = yield* LocalFirstStorageService.export() - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = options?.filename || 'db.sqlite' - a.click() - }), - ) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = options?.filename || 'db.sqlite' + a.click() + }), + ) - const import_ = runtime.fn((_: File) => - Effect.gen(function* () { - const arrayBuffer = yield* Effect.promise(() => _.arrayBuffer()) - const uint8Array = new Uint8Array(arrayBuffer) - yield* LocalFirstStorageService.import(uint8Array) - }), - ) + const import_ = runtime.fn((_: File) => + Effect.gen(function* () { + const arrayBuffer = yield* Effect.promise(() => _.arrayBuffer()) + const uint8Array = new Uint8Array(arrayBuffer) + yield* LocalFirstStorageService.import(uint8Array) + }), + ) - return { - import: import_, - export: export_, - } -}) + return { + import: import_, + export: export_, + } + }, +) diff --git a/packages/local-first/src/services/sync.ts b/packages/local-first/src/services/sync.ts index 0fee379..720cab2 100644 --- a/packages/local-first/src/services/sync.ts +++ b/packages/local-first/src/services/sync.ts @@ -74,7 +74,9 @@ const useSyncService = UseUseServices( const syncing = runtime.atom(Stream.unwrap(SyncService.syncing), { initialValue: false }) - const socketStatus = runtime.atom(Stream.unwrap(SyncService.socketStatusStream), { initialValue: Option.none() }) + const socketStatus = runtime.atom(Stream.unwrap(SyncService.socketStatusStream), { + initialValue: Option.none(), + }) const remoteSyncStats = runtime.atom(Stream.unwrap(SyncService.remoteSyncStatsStream), { initialValue: Option.none(), diff --git a/packages/local-first/tsconfig.check.json b/packages/local-first/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/local-first/tsconfig.check.json +++ b/packages/local-first/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/local-first/tsconfig.json b/packages/local-first/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/local-first/tsconfig.json +++ b/packages/local-first/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/local-first/tsconfig.lib.json b/packages/local-first/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/local-first/tsconfig.lib.json +++ b/packages/local-first/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/local-first/tsconfig.test.json b/packages/local-first/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/local-first/tsconfig.test.json +++ b/packages/local-first/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/otel/project.json b/packages/otel/project.json index 1a19309..bc52490 100644 --- a/packages/otel/project.json +++ b/packages/otel/project.json @@ -8,7 +8,7 @@ "executor": "nx:run-commands", "outputs": ["{workspaceRoot}/node_modules/.vitest/packages/otel", "{workspaceRoot}/coverage/packages/otel"], "options": { - "command": "pnpm xdev test --project pkgs-otel" + "command": "xdev test --project pkgs-otel" } }, "madge": { diff --git a/packages/otel/src/session/session.ts b/packages/otel/src/session/session.ts index 3e4dedb..9679694 100644 --- a/packages/otel/src/session/session.ts +++ b/packages/otel/src/session/session.ts @@ -56,9 +56,13 @@ export function updateSessionStatus({ if (Globals.recentActivity || forceActivity) { sessionState.expiresAt = Date.now() + SESSION_INACTIVITY_TIMEOUT_MS if (useLocalStorage) { - setSessionStateToLocalStorage(sessionState, { forceStoreWrite: shouldForceWrite || forceStore }) + setSessionStateToLocalStorage(sessionState, { + forceStoreWrite: shouldForceWrite || forceStore, + }) } else { - renewCookieTimeout(sessionState, cookieDomain, { forceStoreWrite: shouldForceWrite || forceStore }) + renewCookieTimeout(sessionState, cookieDomain, { + forceStoreWrite: shouldForceWrite || forceStore, + }) } } @@ -73,7 +77,10 @@ export function initSessionTracking(domain?: string, useLocalStorage = true) { } ACTIVITY_EVENTS.forEach((type) => - document.addEventListener(type, () => Globals.setRecentActivity(true), { capture: true, passive: true }), + document.addEventListener(type, () => Globals.setRecentActivity(true), { + capture: true, + passive: true, + }), ) updateSessionStatus({ useLocalStorage, forceStore: true }) diff --git a/packages/otel/tsconfig.check.json b/packages/otel/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/otel/tsconfig.check.json +++ b/packages/otel/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/otel/tsconfig.json b/packages/otel/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/otel/tsconfig.json +++ b/packages/otel/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/otel/tsconfig.lib.json b/packages/otel/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/otel/tsconfig.lib.json +++ b/packages/otel/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/otel/tsconfig.test.json b/packages/otel/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/otel/tsconfig.test.json +++ b/packages/otel/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/preset-cloudflare/project.json b/packages/preset-cloudflare/project.json index 39b22db..2073737 100644 --- a/packages/preset-cloudflare/project.json +++ b/packages/preset-cloudflare/project.json @@ -11,7 +11,7 @@ "{workspaceRoot}/coverage/packages/preset-cloudflare" ], "options": { - "command": "pnpm xdev test --project pkgs-preset-cloudflare" + "command": "xdev test --project pkgs-preset-cloudflare" } }, "madge": { diff --git a/packages/preset-cloudflare/src/miniflare.ts b/packages/preset-cloudflare/src/miniflare.ts index 53a4309..19f9cbb 100644 --- a/packages/preset-cloudflare/src/miniflare.ts +++ b/packages/preset-cloudflare/src/miniflare.ts @@ -139,7 +139,11 @@ class WorkflowIntrospectorInstance { mockStepResult(step: Workflows.StepSelector, stepResult: any) { return Effect.promise(async () => { const ins = await this.getIns() - const exit = Schema.ExitFromSelf({ success: Schema.Any, defect: Schema.Defect, failure: Schema.Defect }) + const exit = Schema.ExitFromSelf({ + success: Schema.Any, + defect: Schema.Defect, + failure: Schema.Defect, + }) const encode = Schema.encodeUnknownSync(exit) const result = encode(Exit.succeed(stepResult)) @@ -607,7 +611,9 @@ const make = ( const workflowBinding = bindings[binding] as Workflow | undefined if (!workflowBinding) { - return Response.json({ error: `Binding ${binding} not found` }) + return Response.json({ + error: `Binding ${binding} not found`, + }) } try { @@ -637,10 +643,14 @@ const make = ( } } catch (error) { console.error('Workflow proxy error:', error) - return Response.json({ error: error instanceof Error ? error.message : 'Unknown error' }) + return Response.json({ + error: error instanceof Error ? error.message : 'Unknown error', + }) } - return Response.json({ error: `Unknown method: ${method}` }) + return Response.json({ + error: `Unknown method: ${method}`, + }) } if (url.pathname === '/instance') { @@ -649,12 +659,16 @@ const make = ( const workflowBinding = bindings[binding] as Workflow | undefined if (!workflowBinding) { - return Response.json({ error: `Binding ${binding} not found` }) + return Response.json({ + error: `Binding ${binding} not found`, + }) } const instance = await workflowBinding.get(instanceId) if (!instance) { - return Response.json({ error: `Instance ${instanceId} not found` }) + return Response.json({ + error: `Instance ${instanceId} not found`, + }) } let result @@ -698,7 +712,9 @@ const make = ( } break default: - return Response.json({ error: `Unknown instance method: ${method}` }) + return Response.json({ + error: `Unknown instance method: ${method}`, + }) } return Response.json(result ?? {}) diff --git a/packages/preset-cloudflare/tsconfig.check.json b/packages/preset-cloudflare/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/preset-cloudflare/tsconfig.check.json +++ b/packages/preset-cloudflare/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/preset-cloudflare/tsconfig.json b/packages/preset-cloudflare/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/preset-cloudflare/tsconfig.json +++ b/packages/preset-cloudflare/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/preset-cloudflare/tsconfig.lib.json b/packages/preset-cloudflare/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/preset-cloudflare/tsconfig.lib.json +++ b/packages/preset-cloudflare/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/preset-cloudflare/tsconfig.test.json b/packages/preset-cloudflare/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/preset-cloudflare/tsconfig.test.json +++ b/packages/preset-cloudflare/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/preset-react-native/project.json b/packages/preset-react-native/project.json index b529a00..675374d 100644 --- a/packages/preset-react-native/project.json +++ b/packages/preset-react-native/project.json @@ -11,7 +11,7 @@ "{workspaceRoot}/coverage/packages/preset-react-native" ], "options": { - "command": "pnpm xdev test --project pkgs-preset-react-native" + "command": "xdev test --project pkgs-preset-react-native" } }, "madge": { diff --git a/packages/preset-react-native/src/context.ts b/packages/preset-react-native/src/context.ts index 142e6a0..26dc7dd 100644 --- a/packages/preset-react-native/src/context.ts +++ b/packages/preset-react-native/src/context.ts @@ -169,7 +169,7 @@ export const make = ( ) // @ts-ignore - globalThis.patchEnv = options.envPatch ?? (() => { }) + globalThis.patchEnv = options.envPatch ?? (() => {}) }), ) @@ -196,12 +196,12 @@ export const make = ( // Convert RequestInit to FetchRequestInit const fetchInit = init ? { - body: init.body, - credentials: init.credentials, - headers: init.headers, - method: init.method, - signal: init.signal, - } + body: init.body, + credentials: init.credentials, + headers: init.headers, + method: init.method, + signal: init.signal, + } : undefined return ExpoFetch(url, fetchInit as any) as unknown as Promise @@ -318,9 +318,10 @@ const Test = Layer.scopedDiscard( Option.some(Redacted.make('7w3jnwsw3j24xsvvxfvssk6pxuhcuwiut5u5hudc')), ) - yield* identity.importFromMnemonic( - Redacted.make('motor royal future decade cousin modify phone roast empty village treat modify'), - ).pipe( - Effect.forkScoped) + yield* identity + .importFromMnemonic( + Redacted.make('motor royal future decade cousin modify phone roast empty village treat modify'), + ) + .pipe(Effect.forkScoped) }), ) diff --git a/packages/preset-react-native/tsconfig.check.json b/packages/preset-react-native/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/preset-react-native/tsconfig.check.json +++ b/packages/preset-react-native/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/preset-react-native/tsconfig.json b/packages/preset-react-native/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/preset-react-native/tsconfig.json +++ b/packages/preset-react-native/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/preset-react-native/tsconfig.lib.json b/packages/preset-react-native/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/preset-react-native/tsconfig.lib.json +++ b/packages/preset-react-native/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/preset-react-native/tsconfig.test.json b/packages/preset-react-native/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/preset-react-native/tsconfig.test.json +++ b/packages/preset-react-native/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/preset-server/tsconfig.check.json b/packages/preset-server/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/preset-server/tsconfig.check.json +++ b/packages/preset-server/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/preset-server/tsconfig.json b/packages/preset-server/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/preset-server/tsconfig.json +++ b/packages/preset-server/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/preset-server/tsconfig.lib.json b/packages/preset-server/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/preset-server/tsconfig.lib.json +++ b/packages/preset-server/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/preset-server/tsconfig.test.json b/packages/preset-server/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/preset-server/tsconfig.test.json +++ b/packages/preset-server/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/preset-web/project.json b/packages/preset-web/project.json index d13a1c7..468d4a4 100644 --- a/packages/preset-web/project.json +++ b/packages/preset-web/project.json @@ -11,7 +11,7 @@ "{workspaceRoot}/coverage/packages/preset-web" ], "options": { - "command": "pnpm xdev test --project pkgs-preset-web" + "command": "xdev test --project pkgs-preset-web" } }, "madge": { diff --git a/packages/preset-web/tsconfig.check.json b/packages/preset-web/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/preset-web/tsconfig.check.json +++ b/packages/preset-web/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/preset-web/tsconfig.json b/packages/preset-web/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/preset-web/tsconfig.json +++ b/packages/preset-web/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/preset-web/tsconfig.lib.json b/packages/preset-web/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/preset-web/tsconfig.lib.json +++ b/packages/preset-web/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/preset-web/tsconfig.test.json b/packages/preset-web/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/preset-web/tsconfig.test.json +++ b/packages/preset-web/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/purchase/project.json b/packages/purchase/project.json index 6e5f969..2ef6914 100644 --- a/packages/purchase/project.json +++ b/packages/purchase/project.json @@ -11,7 +11,7 @@ "{workspaceRoot}/coverage/packages/purchase" ], "options": { - "command": "pnpm xdev test --project pkgs-purchase" + "command": "xdev test --project pkgs-purchase" } }, "madge": { diff --git a/packages/purchase/src/schema.ts b/packages/purchase/src/schema.ts index e585df0..91421b8 100644 --- a/packages/purchase/src/schema.ts +++ b/packages/purchase/src/schema.ts @@ -284,7 +284,11 @@ export class Customer extends Schema.Class('Customer')({ export class Price extends Schema.Class('Price')({ id: PriceId, - name: Schema.optionalWith(PriceName, { exact: true, nullable: true, default: () => PriceName.make('') }), + name: Schema.optionalWith(PriceName, { + exact: true, + nullable: true, + default: () => PriceName.make(''), + }), productId: ProductId, unitPrice: UnitPrice, unitPriceOverride: Schema.Array( @@ -305,7 +309,11 @@ export class Price extends Schema.Class('Price')({ export class Product extends Schema.Class('Product')({ id: ProductId, name: ProductName, - description: Schema.optionalWith(Schema.String, { exact: true, nullable: true, default: () => '' }), + description: Schema.optionalWith(Schema.String, { + exact: true, + nullable: true, + default: () => '', + }), active: Schema.Boolean, metadata: Metadata, prices: Schema.Array(Price), @@ -387,7 +395,10 @@ export class Subscription extends Schema.Class('Subscription')({ /** * 计划变更 */ - scheduledChange: Schema.optionalWith(SubscriptionScheduledChange, { exact: true, nullable: true }), + scheduledChange: Schema.optionalWith(SubscriptionScheduledChange, { + exact: true, + nullable: true, + }), /** * 管理 URL */ @@ -402,7 +413,10 @@ export class Subscription extends Schema.Class('Subscription')({ /** * 下一个交易 */ - nextTransaction: Schema.optionalWith(NextSubscriptionTransaction, { exact: true, nullable: true }), + nextTransaction: Schema.optionalWith(NextSubscriptionTransaction, { + exact: true, + nullable: true, + }), /** * 元数据 */ diff --git a/packages/purchase/tests/payment/internal/paddle-sdk.test.ts b/packages/purchase/tests/payment/internal/paddle-sdk.test.ts index 5b3a655..ae694c3 100644 --- a/packages/purchase/tests/payment/internal/paddle-sdk.test.ts +++ b/packages/purchase/tests/payment/internal/paddle-sdk.test.ts @@ -92,7 +92,10 @@ it.layer(TestPaddle)('Paddle SDK', ({ effect }) => { const email = 'test2@test.com' - const res = yield* sdk.customers.update({ customerId: 'ctm_01jp7k30m8xfvsv10y4mq9gxnm', email }) + const res = yield* sdk.customers.update({ + customerId: 'ctm_01jp7k30m8xfvsv10y4mq9gxnm', + email, + }) console.log(res) }), ) @@ -112,7 +115,9 @@ it.layer(TestPaddle)('Paddle SDK', ({ effect }) => { Effect.gen(function* () { const sdk = yield* Paddle.PaddleSdk - const res = yield* sdk.transactions.get({ transactionId: 'txn_01jgkdz3wztdj93nq84xaq73fy1' }) + const res = yield* sdk.transactions.get({ + transactionId: 'txn_01jgkdz3wztdj93nq84xaq73fy1', + }) console.log(res) }), ) @@ -132,7 +137,9 @@ it.layer(TestPaddle)('Paddle SDK', ({ effect }) => { Effect.gen(function* () { const sdk = yield* Paddle.PaddleSdk - const res = yield* sdk.subscriptions.get({ subscriptionId: 'sub_01jc66wby1xh91gz2azjebwa4a' }) + const res = yield* sdk.subscriptions.get({ + subscriptionId: 'sub_01jc66wby1xh91gz2azjebwa4a', + }) console.log(res) }), ) @@ -141,7 +148,9 @@ it.layer(TestPaddle)('Paddle SDK', ({ effect }) => { Effect.gen(function* () { const sdk = yield* Paddle.PaddleSdk - const res = yield* sdk.subscriptions.cancel({ subscriptionId: 'sub_01jc66wby1xh91gz2azjebwa4a' }) + const res = yield* sdk.subscriptions.cancel({ + subscriptionId: 'sub_01jc66wby1xh91gz2azjebwa4a', + }) console.log(res) }), ) diff --git a/packages/purchase/tsconfig.check.json b/packages/purchase/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/purchase/tsconfig.check.json +++ b/packages/purchase/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/purchase/tsconfig.json b/packages/purchase/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/purchase/tsconfig.json +++ b/packages/purchase/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/purchase/tsconfig.lib.json b/packages/purchase/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/purchase/tsconfig.lib.json +++ b/packages/purchase/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/purchase/tsconfig.test.json b/packages/purchase/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/purchase/tsconfig.test.json +++ b/packages/purchase/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/react-native/project.json b/packages/react-native/project.json index e7557de..96b65ac 100644 --- a/packages/react-native/project.json +++ b/packages/react-native/project.json @@ -11,7 +11,7 @@ "{workspaceRoot}/coverage/packages/react-native" ], "options": { - "command": "pnpm xdev test --project pkgs-react-native" + "command": "xdev test --project pkgs-react-native" } }, "madge": { diff --git a/packages/react-native/tsconfig.check.json b/packages/react-native/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/react-native/tsconfig.check.json +++ b/packages/react-native/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/react-native/tsconfig.json b/packages/react-native/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/react-native/tsconfig.json +++ b/packages/react-native/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/react-native/tsconfig.lib.json b/packages/react-native/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/react-native/tsconfig.lib.json +++ b/packages/react-native/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/react-native/tsconfig.test.json b/packages/react-native/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/react-native/tsconfig.test.json +++ b/packages/react-native/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/react-router/project.json b/packages/react-router/project.json index 86f4bd9..e1240e9 100644 --- a/packages/react-router/project.json +++ b/packages/react-router/project.json @@ -11,7 +11,7 @@ "{workspaceRoot}/coverage/packages/react-router" ], "options": { - "command": "pnpm xdev test --project pkgs-react-router" + "command": "xdev test --project pkgs-react-router" } }, "madge": { diff --git a/packages/react-router/src/entry/client.tsx b/packages/react-router/src/entry/client.tsx index ae657ab..48f651c 100644 --- a/packages/react-router/src/entry/client.tsx +++ b/packages/react-router/src/entry/client.tsx @@ -50,8 +50,8 @@ export function init() { document, // , - // , ) + // , }) } else { let data: any @@ -70,8 +70,8 @@ export function init() { root.render( // , - // , ) + // , } } diff --git a/packages/react-router/src/request.ts b/packages/react-router/src/request.ts index 54d65cc..3c74a00 100644 --- a/packages/react-router/src/request.ts +++ b/packages/react-router/src/request.ts @@ -103,7 +103,9 @@ export const getFormData = ( ): Effect.Effect => getFormDataEntries.pipe( Effect.flatMap((entries) => Schema.decodeUnknown(schema)(entries)), - Effect.catchTags({ ParseError: (error) => new ReactRouterFormDataParseError({ cause: error }) }), + Effect.catchTags({ + ParseError: (error) => new ReactRouterFormDataParseError({ cause: error }), + }), Effect.withSpan('ReactRouter.decodeFormData'), ) @@ -146,7 +148,9 @@ export const getSearchParams = ( }), }).pipe( Effect.flatMap((_) => Schema.decodeUnknown(schema)(_)), - Effect.catchTags({ ParseError: (error) => new ReactRouterSearchParamsParseError({ cause: error }) }), + Effect.catchTags({ + ParseError: (error) => new ReactRouterSearchParamsParseError({ cause: error }), + }), Effect.withSpan('ReactRouter.decodeSearchParams'), ), ) diff --git a/packages/react-router/tsconfig.check.json b/packages/react-router/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/react-router/tsconfig.check.json +++ b/packages/react-router/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/react-router/tsconfig.json b/packages/react-router/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/react-router/tsconfig.json +++ b/packages/react-router/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/react-router/tsconfig.lib.json b/packages/react-router/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/react-router/tsconfig.lib.json +++ b/packages/react-router/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/react-router/tsconfig.test.json b/packages/react-router/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/react-router/tsconfig.test.json +++ b/packages/react-router/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/router/project.json b/packages/router/project.json index bc89224..e23d764 100644 --- a/packages/router/project.json +++ b/packages/router/project.json @@ -8,7 +8,7 @@ "executor": "nx:run-commands", "outputs": ["{workspaceRoot}/node_modules/.vitest/packages/router", "{workspaceRoot}/coverage/packages/router"], "options": { - "command": "pnpm xdev test --project pkgs-router" + "command": "xdev test --project pkgs-router" } }, "madge": { diff --git a/packages/router/tsconfig.check.json b/packages/router/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/router/tsconfig.check.json +++ b/packages/router/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/router/tsconfig.json b/packages/router/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/router/tsconfig.json +++ b/packages/router/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/router/tsconfig.lib.json b/packages/router/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/router/tsconfig.lib.json +++ b/packages/router/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/router/tsconfig.test.json b/packages/router/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/router/tsconfig.test.json +++ b/packages/router/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/server-testing/project.json b/packages/server-testing/project.json index e3b1295..5b53bd9 100644 --- a/packages/server-testing/project.json +++ b/packages/server-testing/project.json @@ -11,7 +11,7 @@ "{workspaceRoot}/coverage/packages/server-testing" ], "options": { - "command": "pnpm xdev test --project pkgs-server-testing" + "command": "xdev test --project pkgs-server-testing" } }, "madge": { diff --git a/packages/server-testing/tsconfig.check.json b/packages/server-testing/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/server-testing/tsconfig.check.json +++ b/packages/server-testing/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/server-testing/tsconfig.json b/packages/server-testing/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/server-testing/tsconfig.json +++ b/packages/server-testing/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/server-testing/tsconfig.lib.json b/packages/server-testing/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/server-testing/tsconfig.lib.json +++ b/packages/server-testing/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/server-testing/tsconfig.test.json b/packages/server-testing/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/server-testing/tsconfig.test.json +++ b/packages/server-testing/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/server/project.json b/packages/server/project.json index 12c8cba..5898067 100644 --- a/packages/server/project.json +++ b/packages/server/project.json @@ -8,7 +8,7 @@ "executor": "nx:run-commands", "outputs": ["{workspaceRoot}/node_modules/.vitest/packages/server", "{workspaceRoot}/coverage/packages/server"], "options": { - "command": "pnpm xdev test --project pkgs-server" + "command": "xdev test --project pkgs-server" } }, "madge": { diff --git a/packages/server/tsconfig.check.json b/packages/server/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/server/tsconfig.check.json +++ b/packages/server/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/server/tsconfig.json +++ b/packages/server/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/server/tsconfig.lib.json b/packages/server/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/server/tsconfig.lib.json +++ b/packages/server/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/server/tsconfig.test.json b/packages/server/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/server/tsconfig.test.json +++ b/packages/server/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/sql-do-proxy/project.json b/packages/sql-do-proxy/project.json index 761c605..d30b32a 100644 --- a/packages/sql-do-proxy/project.json +++ b/packages/sql-do-proxy/project.json @@ -11,7 +11,7 @@ "{workspaceRoot}/coverage/packages/sql-do-proxy" ], "options": { - "command": "pnpm xdev test --project pkgs-sql-do-proxy" + "command": "xdev test --project pkgs-sql-do-proxy" } }, "madge": { diff --git a/packages/sql-do-proxy/src/SqlClient.ts b/packages/sql-do-proxy/src/SqlClient.ts index ff4ae69..6f77276 100644 --- a/packages/sql-do-proxy/src/SqlClient.ts +++ b/packages/sql-do-proxy/src/SqlClient.ts @@ -222,13 +222,15 @@ export const effect = ( } }, Effect.provide(ctx)), databaseSize: Effect.fnUntraced(function* () { - const databaseSize = yield* fn - .databaseSize() - .pipe( - Effect.catchAllDefect( - (e) => new SqlError.SqlError({ message: 'Proxy sqlite get database size failure', cause: e }), - ), - ) + const databaseSize = yield* fn.databaseSize().pipe( + Effect.catchAllDefect( + (e) => + new SqlError.SqlError({ + message: 'Proxy sqlite get database size failure', + cause: e, + }), + ), + ) return databaseSize }, Effect.provide(ctx)), diff --git a/packages/sql-do-proxy/tsconfig.check.json b/packages/sql-do-proxy/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/sql-do-proxy/tsconfig.check.json +++ b/packages/sql-do-proxy/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/sql-do-proxy/tsconfig.json b/packages/sql-do-proxy/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/sql-do-proxy/tsconfig.json +++ b/packages/sql-do-proxy/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/sql-do-proxy/tsconfig.lib.json b/packages/sql-do-proxy/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/sql-do-proxy/tsconfig.lib.json +++ b/packages/sql-do-proxy/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/sql-do-proxy/tsconfig.test.json b/packages/sql-do-proxy/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/sql-do-proxy/tsconfig.test.json +++ b/packages/sql-do-proxy/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/sql-kysely/project.json b/packages/sql-kysely/project.json index a63bd2c..895026a 100644 --- a/packages/sql-kysely/project.json +++ b/packages/sql-kysely/project.json @@ -11,7 +11,7 @@ "{workspaceRoot}/coverage/packages/sql-kysely" ], "options": { - "command": "pnpm xdev test --project pkgs-sql-kysely" + "command": "xdev test --project pkgs-sql-kysely" } }, "madge": { diff --git a/packages/sql-kysely/tsconfig.check.json b/packages/sql-kysely/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/sql-kysely/tsconfig.check.json +++ b/packages/sql-kysely/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/sql-kysely/tsconfig.json b/packages/sql-kysely/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/sql-kysely/tsconfig.json +++ b/packages/sql-kysely/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/sql-kysely/tsconfig.lib.json b/packages/sql-kysely/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/sql-kysely/tsconfig.lib.json +++ b/packages/sql-kysely/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/sql-kysely/tsconfig.test.json b/packages/sql-kysely/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/sql-kysely/tsconfig.test.json +++ b/packages/sql-kysely/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/sql-op-sqlite/project.json b/packages/sql-op-sqlite/project.json index a0bbfcc..40b834b 100644 --- a/packages/sql-op-sqlite/project.json +++ b/packages/sql-op-sqlite/project.json @@ -11,7 +11,7 @@ "{workspaceRoot}/coverage/packages/sql-op-sqlite" ], "options": { - "command": "pnpm xdev test --project pkgs-sql-op-sqlite" + "command": "xdev test --project pkgs-sql-op-sqlite" } }, "madge": { diff --git a/packages/sql-op-sqlite/tsconfig.check.json b/packages/sql-op-sqlite/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/sql-op-sqlite/tsconfig.check.json +++ b/packages/sql-op-sqlite/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/sql-op-sqlite/tsconfig.json b/packages/sql-op-sqlite/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/sql-op-sqlite/tsconfig.json +++ b/packages/sql-op-sqlite/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/sql-op-sqlite/tsconfig.lib.json b/packages/sql-op-sqlite/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/sql-op-sqlite/tsconfig.lib.json +++ b/packages/sql-op-sqlite/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/sql-op-sqlite/tsconfig.test.json b/packages/sql-op-sqlite/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/sql-op-sqlite/tsconfig.test.json +++ b/packages/sql-op-sqlite/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/sqlite/project.json b/packages/sqlite/project.json index 2d56e92..2c995b0 100644 --- a/packages/sqlite/project.json +++ b/packages/sqlite/project.json @@ -8,7 +8,7 @@ "executor": "nx:run-commands", "outputs": ["{workspaceRoot}/node_modules/.vitest/packages/sqlite", "{workspaceRoot}/coverage/packages/sqlite"], "options": { - "command": "pnpm xdev test --project pkgs-sqlite" + "command": "xdev test --project pkgs-sqlite" } }, "madge": { diff --git a/packages/sqlite/src/hooks/use-live-query.ts b/packages/sqlite/src/hooks/use-live-query.ts index 8049ed0..f7096ef 100644 --- a/packages/sqlite/src/hooks/use-live-query.ts +++ b/packages/sqlite/src/hooks/use-live-query.ts @@ -41,6 +41,6 @@ export const useLiveQuery: { * ```tsx * // Basic usage * const users = useLiveQuery(() => sql`SELECT * FROM users`)) - */ + */; (effect: Effect.Effect, options?: LiveEffectQueryOptions): CastArray } = createQueryHooks(null as any) diff --git a/packages/sqlite/tsconfig.check.json b/packages/sqlite/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/sqlite/tsconfig.check.json +++ b/packages/sqlite/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/sqlite/tsconfig.json b/packages/sqlite/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/sqlite/tsconfig.json +++ b/packages/sqlite/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/sqlite/tsconfig.lib.json b/packages/sqlite/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/sqlite/tsconfig.lib.json +++ b/packages/sqlite/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/sqlite/tsconfig.test.json b/packages/sqlite/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/sqlite/tsconfig.test.json +++ b/packages/sqlite/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/tailwind/project.json b/packages/tailwind/project.json index 0fc5d51..f0a856a 100644 --- a/packages/tailwind/project.json +++ b/packages/tailwind/project.json @@ -11,7 +11,7 @@ "{workspaceRoot}/coverage/packages/tailwind" ], "options": { - "command": "pnpm xdev test --project pkgs-tailwind" + "command": "xdev test --project pkgs-tailwind" } }, "madge": { diff --git a/packages/tailwind/src/postcss.mjs b/packages/tailwind/src/postcss.mjs index 02f269e..2e77b0a 100644 --- a/packages/tailwind/src/postcss.mjs +++ b/packages/tailwind/src/postcss.mjs @@ -4,7 +4,7 @@ import { workspaceRoot } from '@nx/devkit' function projectConfig(dir) { const projectPath = join(workspaceRoot, dir) - return { + const config = { plugins: { 'postcss-flexbugs-fixes': {}, 'postcss-preset-env': { @@ -21,6 +21,8 @@ function projectConfig(dir) { }, }, } + + return config } export { projectConfig } diff --git a/packages/tailwind/src/tailwind.mjs b/packages/tailwind/src/tailwind.mjs index 7caee79..e89b3ce 100644 --- a/packages/tailwind/src/tailwind.mjs +++ b/packages/tailwind/src/tailwind.mjs @@ -1,4 +1,5 @@ import { join, resolve } from 'node:path' +import { readdirSync, statSync } from 'node:fs' import { workspaceRoot } from '@nx/devkit' import defaultPreset from './tailwind-presets/default' @@ -7,29 +8,28 @@ import defaultPreset from './tailwind-presets/default' * @param {import('tailwindcss').Config} options */ function projectConfig(dir, options = {}) { - const project = resolve(workspaceRoot, dir, '../') + const currentProject = resolve(workspaceRoot, dir) const packagesDir = join(workspaceRoot, 'packages') + const parentDir = resolve(currentProject, '../') const pkgs = [ - `${packagesDir}/lib/src`, - `${packagesDir}/errors/src`, - `${packagesDir}/form/src`, - `${packagesDir}/emails/src`, - `${packagesDir}/app/src`, - `${packagesDir}/app-kit/src`, - `${packagesDir}/user-kit/src`, - `${packagesDir}/local-first/src`, - ] - - const projectPkgs = [ - `${project}/website`, - `${project}/studio`, - `${project}/web`, - `${project}/desktop`, - `${project}/client`, - `${project}/shared`, - `${project}/apps`, - ] + 'lib/src', + 'errors/src', + 'form/src', + 'emails/src', + 'app/src', + 'app-kit/src', + 'user-kit/src', + 'local-first/src', + ].map((item) => join(packagesDir, item)) + + const filterList = ['native'] + const projectPkgs = readdirSync(parentDir) + .filter((item) => { + const fullPath = join(parentDir, item) + return statSync(fullPath).isDirectory() && !filterList.includes(item) + }) + .map((item) => join(parentDir, item)) const content = pkgs.concat(projectPkgs).map((item) => `${item}/**/*.tsx`) diff --git a/packages/tailwind/tsconfig.check.json b/packages/tailwind/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/tailwind/tsconfig.check.json +++ b/packages/tailwind/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/tailwind/tsconfig.json b/packages/tailwind/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/tailwind/tsconfig.json +++ b/packages/tailwind/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/tailwind/tsconfig.lib.json b/packages/tailwind/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/tailwind/tsconfig.lib.json +++ b/packages/tailwind/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/tailwind/tsconfig.test.json b/packages/tailwind/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/tailwind/tsconfig.test.json +++ b/packages/tailwind/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/testing/project.json b/packages/testing/project.json index f546a49..dcb6361 100644 --- a/packages/testing/project.json +++ b/packages/testing/project.json @@ -8,7 +8,7 @@ "executor": "nx:run-commands", "outputs": ["{workspaceRoot}/node_modules/.vitest/packages/testing", "{workspaceRoot}/coverage/packages/testing"], "options": { - "command": "pnpm xdev test --project pkgs-testing" + "command": "xdev test --project pkgs-testing" } }, "madge": { diff --git a/packages/testing/tsconfig.check.json b/packages/testing/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/testing/tsconfig.check.json +++ b/packages/testing/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/testing/tsconfig.json b/packages/testing/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/testing/tsconfig.json +++ b/packages/testing/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/testing/tsconfig.lib.json b/packages/testing/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/testing/tsconfig.lib.json +++ b/packages/testing/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/testing/tsconfig.test.json b/packages/testing/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/testing/tsconfig.test.json +++ b/packages/testing/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/toaster/project.json b/packages/toaster/project.json index b2fc536..c1ed244 100644 --- a/packages/toaster/project.json +++ b/packages/toaster/project.json @@ -8,7 +8,7 @@ "executor": "nx:run-commands", "outputs": ["{workspaceRoot}/node_modules/.vitest/packages/toaster", "{workspaceRoot}/coverage/packages/toaster"], "options": { - "command": "pnpm xdev test --project pkgs-toaster" + "command": "xdev test --project pkgs-toaster" } }, "madge": { diff --git a/packages/toaster/src/noop.tsx b/packages/toaster/src/noop.tsx index c478be7..ae8f7de 100644 --- a/packages/toaster/src/noop.tsx +++ b/packages/toaster/src/noop.tsx @@ -4,7 +4,10 @@ import type { ToasterMethods } from './toaster' function createNoOpToaster(): ToasterMethods { const noOp = (message?: any, data?: any) => { - console.warn('Toaster method called but no valid toaster implementation is available', { message, data }) + console.warn('Toaster method called but no valid toaster implementation is available', { + message, + data, + }) return 0 } diff --git a/packages/toaster/src/provider.native.tsx b/packages/toaster/src/provider.native.tsx index 50b7e04..f29e7fc 100644 --- a/packages/toaster/src/provider.native.tsx +++ b/packages/toaster/src/provider.native.tsx @@ -34,7 +34,10 @@ export function useToasterContext(): ToasterMethods { function createNoOpToaster(): ToasterMethods { const noOp = (message?: any, data?: any) => { - console.warn('Toaster method called but no valid toaster implementation is available', { message, data }) + console.warn('Toaster method called but no valid toaster implementation is available', { + message, + data, + }) return 0 } diff --git a/packages/toaster/tsconfig.check.json b/packages/toaster/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/toaster/tsconfig.check.json +++ b/packages/toaster/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/toaster/tsconfig.json b/packages/toaster/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/toaster/tsconfig.json +++ b/packages/toaster/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/toaster/tsconfig.lib.json b/packages/toaster/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/toaster/tsconfig.lib.json +++ b/packages/toaster/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/toaster/tsconfig.test.json b/packages/toaster/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/toaster/tsconfig.test.json +++ b/packages/toaster/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/user-kit/project.json b/packages/user-kit/project.json index b8b6b78..6029729 100644 --- a/packages/user-kit/project.json +++ b/packages/user-kit/project.json @@ -11,7 +11,7 @@ "{workspaceRoot}/coverage/packages/user-kit" ], "options": { - "command": "pnpm xdev test --project pkgs-user-kit" + "command": "xdev test --project pkgs-user-kit" } }, "madge": { diff --git a/packages/user-kit/src/authentication.ts b/packages/user-kit/src/authentication.ts index 78fcc7e..438b901 100644 --- a/packages/user-kit/src/authentication.ts +++ b/packages/user-kit/src/authentication.ts @@ -68,7 +68,10 @@ const make = Effect.gen(function* () { }) const action: EmailVerificationAction = Option.isNone(user) ? 'create-user' : 'login' - const { code, token } = yield* emailVerificationCodeRepo.generate({ email: userEmail, action: action }) + const { code, token } = yield* emailVerificationCodeRepo.generate({ + email: userEmail, + action: action, + }) yield* Effect.annotateCurrentSpan({ email }) yield* Effect.annotateLogsScoped({ diff --git a/packages/user-kit/src/oauth/github.ts b/packages/user-kit/src/oauth/github.ts index 36b6e45..2df436b 100644 --- a/packages/user-kit/src/oauth/github.ts +++ b/packages/user-kit/src/oauth/github.ts @@ -61,7 +61,11 @@ export class GithubOAuthProvider extends Context.Tag('@userkit:oauth:github-prov Effect.flatMap(({ github, state }) => { const url = Effect.try({ try: () => github.createAuthorizationURL(state, ['user:email']), - catch: (error) => new OAuthInternalError({ message: 'OAuth get authorization url error', cause: error }), + catch: (error) => + new OAuthInternalError({ + message: 'OAuth get authorization url error', + cause: error, + }), }).pipe(Effect.map((url) => url.toString())) return Effect.all({ @@ -80,7 +84,11 @@ export class GithubOAuthProvider extends Context.Tag('@userkit:oauth:github-prov Effect.tryPromise({ try: () => github.validateAuthorizationCode(code), catch: (error) => - new OAuthAppError({ message: 'OAuth callback error', cause: error, reason: 'invalid-code' }), + new OAuthAppError({ + message: 'OAuth callback error', + cause: error, + reason: 'invalid-code', + }), }).pipe(Effect.withSpan('oauth.github.validateAuthorizationCode')), ), Effect.bind('response', ({ tokens }) => diff --git a/packages/user-kit/src/oauth/google.ts b/packages/user-kit/src/oauth/google.ts index 628b30d..22832fc 100644 --- a/packages/user-kit/src/oauth/google.ts +++ b/packages/user-kit/src/oauth/google.ts @@ -62,7 +62,11 @@ export class GoogleOAuthProvider extends Context.Tag('@userkit:oauth:google-prov Effect.flatMap(({ google, codeVerifier, state }) => { const url = Effect.try({ try: () => google.createAuthorizationURL(state, codeVerifier, ['profile', 'email']), - catch: (error) => new OAuthInternalError({ message: 'OAuth get authorization url error', cause: error }), + catch: (error) => + new OAuthInternalError({ + message: 'OAuth get authorization url error', + cause: error, + }), }).pipe(Effect.map((url) => url.toString())) return Effect.all({ @@ -93,7 +97,11 @@ export class GoogleOAuthProvider extends Context.Tag('@userkit:oauth:google-prov return Effect.tryPromise({ try: () => google.validateAuthorizationCode(code, codeVerifier), catch: (error) => - new OAuthAppError({ message: 'OAuth callback error', cause: error, reason: 'invalid-code' }), + new OAuthAppError({ + message: 'OAuth callback error', + cause: error, + reason: 'invalid-code', + }), }) }, }), diff --git a/packages/user-kit/src/oauth/http.ts b/packages/user-kit/src/oauth/http.ts index 5f3a11f..4a7a46b 100644 --- a/packages/user-kit/src/oauth/http.ts +++ b/packages/user-kit/src/oauth/http.ts @@ -77,7 +77,10 @@ export const OAuthHttpLayer = HttpApiBuilder.group(MyHttpApi, 'oauth', (handles) return handles .handleRaw('oauthLogin', ({ path, urlParams }) => pipe( - OAuth.getAuthorizationUrl({ isPortal: urlParams.isPortal, redirectUri: urlParams.redirectUri }), + OAuth.getAuthorizationUrl({ + isPortal: urlParams.isPortal, + redirectUri: urlParams.redirectUri, + }), Effect.map(({ url, state, codeVerifier }) => { const cookies = pipe( Array.empty(), diff --git a/packages/user-kit/tsconfig.check.json b/packages/user-kit/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/user-kit/tsconfig.check.json +++ b/packages/user-kit/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/user-kit/tsconfig.json b/packages/user-kit/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/user-kit/tsconfig.json +++ b/packages/user-kit/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/user-kit/tsconfig.lib.json b/packages/user-kit/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/user-kit/tsconfig.lib.json +++ b/packages/user-kit/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/user-kit/tsconfig.test.json b/packages/user-kit/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/user-kit/tsconfig.test.json +++ b/packages/user-kit/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/vite/project.json b/packages/vite/project.json index 1b16b87..a39efb9 100644 --- a/packages/vite/project.json +++ b/packages/vite/project.json @@ -8,7 +8,7 @@ "executor": "nx:run-commands", "outputs": ["{workspaceRoot}/node_modules/.vitest/packages/vite", "{workspaceRoot}/coverage/packages/vite"], "options": { - "command": "pnpm xdev test --project pkgs-vite" + "command": "xdev test --project pkgs-vite" } }, "madge": { diff --git a/packages/vite/src/vite-config.ts b/packages/vite/src/vite-config.ts index bf2d080..8dd1780 100644 --- a/packages/vite/src/vite-config.ts +++ b/packages/vite/src/vite-config.ts @@ -73,13 +73,6 @@ function workerChunkPlugin() { } export async function reactRouter(projectPath: string, options?: ViteConfig) { - if (!process.env.VITE_RUNN) { - process.env.VITE_RUNN = '0' - } - let runnerCount = parseInt(process.env.VITE_RUNN, 10) - runnerCount++ - process.env.VITE_RUNN = runnerCount.toString() - const isChildCompiler = runnerCount > 1 const isProduction = process.env.NODE_ENV === 'production' const isDev = process.env.NODE_ENV === 'development' const isAnalyze = process.env.ANALYZE === 'true' @@ -279,6 +272,7 @@ export async function reactRouter(projectPath: string, options?: ViteConfig) { root: projectPath, cacheDir: join(workspaceRoot, `node_modules/.vite/${projectLocation}`), envPrefix: ['VITE_'], + envDir: false, logLevel: 'warn', server: { cors: false, diff --git a/packages/vite/tsconfig.check.json b/packages/vite/tsconfig.check.json index a5c94ea..bdde452 100644 --- a/packages/vite/tsconfig.check.json +++ b/packages/vite/tsconfig.check.json @@ -1,8 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client"] + "types": ["vite/client"], }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/packages/vite/tsconfig.json b/packages/vite/tsconfig.json index 943e0e4..be2ad0d 100644 --- a/packages/vite/tsconfig.json +++ b/packages/vite/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.base.json", "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.test.json" - } + "path": "./tsconfig.test.json", + }, ], "include": [], - "files": [] + "files": [], } diff --git a/packages/vite/tsconfig.lib.json b/packages/vite/tsconfig.lib.json index c32fd87..cb4b54b 100644 --- a/packages/vite/tsconfig.lib.json +++ b/packages/vite/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"] + "exclude": ["node_modules", "dist", "build", "e2e/**/*", "tests/**/*"], } diff --git a/packages/vite/tsconfig.test.json b/packages/vite/tsconfig.test.json index 7ada865..e333ba0 100644 --- a/packages/vite/tsconfig.test.json +++ b/packages/vite/tsconfig.test.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["vite/client"], - "outDir": "../../dist/out-tsc" + "outDir": "../../dist/out-tsc", }, "include": ["**/*.d.ts", "vite.config.ts", "e2e/**/*.ts", "e2e/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], - "exclude": ["node_modules", "dist", "build"] + "exclude": ["node_modules", "dist", "build"], } diff --git a/patches/@expo__build-tools.patch b/patches/@expo__build-tools.patch new file mode 100644 index 0000000..5dfa2b5 --- /dev/null +++ b/patches/@expo__build-tools.patch @@ -0,0 +1,32 @@ +diff --git a/dist/common/setup.js b/dist/common/setup.js +index c356a72931a29cad6dbb34daf27b9a02176e6300..3e3be5b72a4fe0a711aed2e3d473b16e6b18f551 100644 +--- a/dist/common/setup.js ++++ b/dist/common/setup.js +@@ -32,9 +32,11 @@ async function setupAsync(ctx) { + var _a; + await ctx.runBuildPhase(eas_build_job_1.BuildPhase.PREPARE_PROJECT, async () => { + await (0, retry_1.retryAsync)(async () => { +- await fs_extra_1.default.rm(ctx.buildDirectory, { recursive: true, force: true }); +- await fs_extra_1.default.mkdir(ctx.buildDirectory, { recursive: true }); +- await (0, projectSources_1.prepareProjectSourcesAsync)(ctx, ctx.buildDirectory); ++ if (!process.env.EXPO_FIXED_BUILD_WORKDIR) { ++ await fs_extra_1.default.rm(ctx.buildDirectory, { recursive: true, force: true }); ++ await fs_extra_1.default.mkdir(ctx.buildDirectory, { recursive: true }); ++ await (0, projectSources_1.prepareProjectSourcesAsync)(ctx, ctx.buildDirectory); ++ } + }, { + retryOptions: { + retries: 3, +diff --git a/dist/context.js b/dist/context.js +index d0e718f04dc9f7b4154454e46f5fc7fba80ab794..e5d7d1358c614cc482d9aa365d61aa277ce1d278 100644 +--- a/dist/context.js ++++ b/dist/context.js +@@ -65,7 +65,7 @@ class BuildContext { + return this._env; + } + get buildDirectory() { +- return path_1.default.join(this.workingdir, 'build'); ++ return process.env.EXPO_FIXED_BUILD_WORKDIR ?? path_1.default.join(this.workingdir, 'build'); + } + get buildLogsDirectory() { + return path_1.default.join(this.workingdir, 'logs'); diff --git a/patches/@expo__cli.patch b/patches/@expo__cli.patch new file mode 100644 index 0000000..668ef2f --- /dev/null +++ b/patches/@expo__cli.patch @@ -0,0 +1,15 @@ +diff --git a/build/src/run/ios/XcodeBuild.js b/build/src/run/ios/XcodeBuild.js +index 6169d0174a87249d8ee59a5b105a1158ef6aae1b..f10ce74de85a3cce0ca156a89d6d5b124fb3f69a 100644 +--- a/build/src/run/ios/XcodeBuild.js ++++ b/build/src/run/ios/XcodeBuild.js +@@ -229,7 +229,9 @@ async function getXcodeBuildArgsAsync(props) { + '-scheme', + props.scheme, + '-destination', +- `id=${props.device.udid}` ++ `id=${props.device.udid}`, ++ '-derivedDataPath', ++ './ios/build', + ]; + if (!props.isSimulator || (0, _simulatorCodeSigning.simulatorBuildRequiresCodeSigning)(props.projectRoot)) { + const developmentTeamId = await (0, _configureCodeSigning.ensureDeviceIsCodeSignedForDeploymentAsync)(props.projectRoot); diff --git a/patches/@expo__metro-config.patch b/patches/@expo__metro-config.patch new file mode 100644 index 0000000..1c3f06b --- /dev/null +++ b/patches/@expo__metro-config.patch @@ -0,0 +1,13 @@ +diff --git a/build/serializer/exportHermes.js b/build/serializer/exportHermes.js +index 27fe57f0e8677a5f7a663d9dddf20a9556d82273..3a680b0394cfcc58f078356654038dbcd26771d3 100644 +--- a/build/serializer/exportHermes.js ++++ b/build/serializer/exportHermes.js +@@ -19,7 +19,7 @@ function importHermesCommandFromProject(projectRoot) { + const hermescPaths = [ + // Override hermesc dir by environment variables + process_1.default.env['REACT_NATIVE_OVERRIDE_HERMES_DIR'] +- ? `${process_1.default.env['REACT_NATIVE_OVERRIDE_HERMES_DIR']}/build/bin/hermesc` ++ ? `${process_1.default.env['REACT_NATIVE_OVERRIDE_HERMES_DIR']}/${platformExecutable}` + : '', + // Building hermes from source + `${reactNativeRoot}/ReactAndroid/hermes-engine/build/hermes/bin/hermesc`, diff --git a/patches/eas-cli-local-build-plugin.patch b/patches/eas-cli-local-build-plugin.patch new file mode 100644 index 0000000..d77f4d0 --- /dev/null +++ b/patches/eas-cli-local-build-plugin.patch @@ -0,0 +1,13 @@ +diff --git a/dist/build.js b/dist/build.js +index 0ce914946459052f49bfbc9a2ea708992bcf3c1c..f83afddac1eb5f6f65280dfd580d71094f14ebbc 100644 +--- a/dist/build.js ++++ b/dist/build.js +@@ -35,7 +35,7 @@ async function buildAsync(job, metadata) { + EAS_BUILD: '1', + EAS_BUILD_RUNNER: 'local-build-plugin', + EAS_BUILD_PLATFORM: job.platform, +- EAS_BUILD_WORKINGDIR: path_1.default.join(workingdir, 'build'), ++ EAS_BUILD_WORKINGDIR: process.env.EXPO_FIXED_BUILD_WORKDIR ?? path_1.default.join(workingdir, 'build'), + EAS_BUILD_PROFILE: metadata.buildProfile, + EAS_BUILD_GIT_COMMIT_HASH: metadata.gitCommitHash, + EAS_BUILD_USERNAME: username, diff --git a/patches/kysely.patch b/patches/kysely.patch new file mode 100644 index 0000000..7e524d3 --- /dev/null +++ b/patches/kysely.patch @@ -0,0 +1,15 @@ +diff --git a/dist/esm/migration/file-migration-provider.js b/dist/esm/migration/file-migration-provider.js +index f410a83b59570edf52ac97e9aef6d9c351659109..2ab9322e7e4f7af13cb31dc0b2aa9907cb136883 100644 +--- a/dist/esm/migration/file-migration-provider.js ++++ b/dist/esm/migration/file-migration-provider.js +@@ -29,8 +29,8 @@ export class FileMigrationProvider { + (fileName.endsWith('.ts') && !fileName.endsWith('.d.ts')) || + fileName.endsWith('.mjs') || + (fileName.endsWith('.mts') && !fileName.endsWith('.d.mts'))) { +- const migration = await import( +- /* webpackIgnore: true */ this.#props.path.join(this.#props.migrationFolder, fileName)); ++ // fix metro build issue ++ const migration = {}; + const migrationKey = fileName.substring(0, fileName.lastIndexOf('.')); + // Handle esModuleInterop export's `default` prop... + if (isMigration(migration?.default)) { diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b628813..d6d7e1b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -43,10 +43,10 @@ overrides: clsx: 2.1.1 cookie: 1.1.1 deep-equal: npm:@nolyfill/deep-equal@^1 - effect: 3.19.6 + effect: 3.19.10 es-iterator-helpers: npm:@nolyfill/es-iterator-helpers@^1 es-set-tostringtag: npm:@nolyfill/es-set-tostringtag@^1 - esbuild: 0.27.0 + esbuild: 0.27.1 function-bind: npm:@nolyfill/function-bind@^1 harmony-reflect: npm:@nolyfill/harmony-reflect@^1 has: npm:@nolyfill/has@^1 @@ -62,9 +62,9 @@ overrides: object.groupby: npm:@nolyfill/object.groupby@ object.hasown: npm:@nolyfill/object.hasown@^1 object.values: npm:@nolyfill/object.values@^1 - react: 19.2.0 - react-dom: 19.2.0 - react-is: 19.2.0 + react: 19.2.1 + react-dom: 19.2.1 + react-is: 19.2.1 react-native: 0.83.0-rc.3 safe-buffer: npm:@nolyfill/safe-buffer@^1 safer-buffer: npm:@nolyfill/safer-buffer@^1 @@ -82,9 +82,14 @@ overrides: patchedDependencies: '@babel/plugin-transform-react-jsx': patches/@babel__plugin-transform-react-jsx.patch + '@expo/build-tools': patches/@expo__build-tools.patch + '@expo/cli': patches/@expo__cli.patch + '@expo/metro-config': patches/@expo__metro-config.patch '@nx/expo': patches/@nx__expo.patch '@paddle/paddle-js': patches/@paddle__paddle-js.patch + eas-cli-local-build-plugin: patches/eas-cli-local-build-plugin.patch expo-modules-core: patches/expo-modules-core.patch + kysely: patches/kysely.patch react-dev-inspector: patches/react-dev-inspector.patch react-dom: patches/react-dom.patch react-image-previewer@1.1.6: patches/react-image-previewer@1.1.6.patch @@ -109,15 +114,16 @@ peerDependencyRules: - '@glideapps/glide-data-grid' publicHoistPattern: + - babel-* - '@babel/*' - - 'expo' - - 'expo-*' + - expo + - expo-* - '@expo/*' + - hermes-compiler - react-native* - '@react-native*' - '@react-navigation*' - '@rnx-kit/*' - - '@babel/*' - '@iconify-json/*' - '@electron/*' - '@electron-*' diff --git a/scripts/ci-detect.ts b/scripts/ci-detect.ts new file mode 100755 index 0000000..6f089fd --- /dev/null +++ b/scripts/ci-detect.ts @@ -0,0 +1,2 @@ +#!/usr/bin/env tsx +import './ci/detect-surfaces' diff --git a/scripts/ci-run.ts b/scripts/ci-run.ts new file mode 100755 index 0000000..63e508b --- /dev/null +++ b/scripts/ci-run.ts @@ -0,0 +1,2 @@ +#!/usr/bin/env tsx +import './ci/run-ci-stage' diff --git a/scripts/ci/detect-surfaces.ts b/scripts/ci/detect-surfaces.ts new file mode 100644 index 0000000..c379ee5 --- /dev/null +++ b/scripts/ci/detect-surfaces.ts @@ -0,0 +1,151 @@ +#!/usr/bin/env tsx + +import { appendFileSync } from 'node:fs' + +import { detectSurfaces } from './surface-detector' +import type { SurfaceDetectionPayload } from './surface-detector' + +type OutputFormat = 'text' | 'json' | 'github' + +interface CliOptions { + surfaces: string[] + format: OutputFormat + base?: string + head?: string + help?: boolean +} + +function parseArgs(argv: string[]): CliOptions { + const options: CliOptions = { + surfaces: [], + format: 'text', + } + for (let i = 0; i < argv.length; i++) { + const arg = argv[i] + switch (arg) { + case '-h': + case '--help': + options.help = true + break + case '--surface': + options.surfaces.push(expectValue('--surface', argv[++i])) + break + case '--surfaces': + options.surfaces.push( + ...expectValue('--surfaces', argv[++i]) + .split(',') + .map((value) => value.trim()) + .filter(Boolean), + ) + break + case '--format': + options.format = expectValue('--format', argv[++i]) as OutputFormat + break + case '--base': + options.base = expectValue('--base', argv[++i]) + break + case '--head': + options.head = expectValue('--head', argv[++i]) + break + default: + if (arg.startsWith('--surface=')) { + options.surfaces.push(arg.split('=')[1]) + } else if (arg.startsWith('--surfaces=')) { + options.surfaces.push( + ...arg + .split('=')[1] + .split(',') + .map((value) => value.trim()) + .filter(Boolean), + ) + } else if (arg.startsWith('--format=')) { + options.format = arg.split('=')[1] as OutputFormat + } else if (arg.startsWith('--base=')) { + options.base = arg.split('=')[1] + } else if (arg.startsWith('--head=')) { + options.head = arg.split('=')[1] + } else { + console.error(`Unknown argument: ${arg}`) + options.help = true + } + break + } + } + return options +} + +function expectValue(flag: string, value: string | undefined): string { + if (!value) { + throw new Error(`Missing value for ${flag}`) + } + return value +} + +function printHelp() { + console.log(`Usage: scripts/ci/detect-surfaces.ts [options] + +Options: + --surface Check a single CI surface (repeatable) + --surfaces Comma separated surfaces + --format Output style (default: text) + --base Override git base commit (defaults to merge-base of main) + --head Override git head commit (defaults to HEAD) + -h, --help Show this message`) +} + +function writeGithubOutput(key: string, value: string) { + const outputFile = process.env.GITHUB_OUTPUT + if (!outputFile) return + appendFileSync(outputFile, `${key}=${value}\n`) +} + +function formatText(payload: SurfaceDetectionPayload) { + const { affectedProjects, results, base, head, changedFiles } = payload + + console.log( + `Detected ${affectedProjects.length} affected Nx project(s) between ${base.slice(0, 7)}...${head.slice(0, 7)}`, + ) + + for (const result of results) { + const status = result.impacted ? 'impacted' : 'no-impact' + const matched = result.matches.length > 0 ? ` (${result.matches.join(', ')})` : '' + console.log(`- ${result.name}: ${status}${matched}`) + if (result.description) { + console.log(` ${result.description}`) + } + writeGithubOutput(`surface-${result.name}`, String(result.impacted)) + writeGithubOutput(`surface-${result.name}-projects`, result.matches.join(',')) + } + writeGithubOutput('affected-projects', affectedProjects.join(',')) + writeGithubOutput('changed-files', changedFiles.join(',')) + console.log(`Changed files: ${changedFiles.length}`) +} + +function main() { + const options = parseArgs(process.argv.slice(2)) + if (options.help) { + printHelp() + return + } + const payload = detectSurfaces({ + surfaces: options.surfaces, + base: options.base, + head: options.head, + }) + if (options.format === 'json') { + console.log(JSON.stringify(payload, null, 2)) + return + } + if (options.format === 'github') { + for (const result of payload.results) { + writeGithubOutput(`surface-${result.name}`, String(result.impacted)) + writeGithubOutput(`surface-${result.name}-projects`, result.matches.join(',')) + } + writeGithubOutput('affected-projects', payload.affectedProjects.join(',')) + writeGithubOutput('changed-files', payload.changedFiles.join(',')) + return + } + formatText(payload) +} + +main() diff --git a/scripts/ci/run-ci-stage.ts b/scripts/ci/run-ci-stage.ts new file mode 100644 index 0000000..04e8a93 --- /dev/null +++ b/scripts/ci/run-ci-stage.ts @@ -0,0 +1,215 @@ +import { spawnSync } from 'node:child_process' +import path from 'node:path' +import { STAGES } from './stages' +import type { Platform, StageContext } from './types' +import { detectSurfaces } from './surface-detector' +import { ensureEnvVars } from './utils' +import { workspaceRoot } from '@nx/devkit' + +interface StageOptions { + stage?: string | undefined + platform?: Platform | undefined + base?: string | undefined + head?: string | undefined + dryRun?: boolean | undefined + list?: boolean | undefined + help?: boolean | undefined +} + +function parseArgs(argv: string[]): StageOptions { + const options: StageOptions = {} + for (let i = 0; i < argv.length; i++) { + const arg = argv[i] + switch (arg) { + case '--stage': + options.stage = expectValue('--stage', argv[++i]) + break + case '--platform': + options.platform = normalizePlatform(expectValue('--platform', argv[++i])) + break + case '--base': + options.base = expectValue('--base', argv[++i]) + break + case '--head': + options.head = expectValue('--head', argv[++i]) + break + case '--dry-run': + options.dryRun = true + break + case '--list': + options.list = true + break + case '-h': + case '--help': + options.help = true + break + default: + if (arg.startsWith('--stage=')) { + options.stage = arg.split('=')[1] + } else if (arg.startsWith('--platform=')) { + options.platform = normalizePlatform(arg.split('=')[1]) + } else { + console.error(`Unknown argument: ${arg}`) + options.help = true + } + break + } + } + return options +} + +function expectValue(flag: string, value: string | undefined) { + if (!value) { + throw new Error(`Missing value for ${flag}`) + } + return value +} + +function normalizePlatform(value: string | undefined): Platform { + switch ((value ?? '').toLowerCase()) { + case 'linux': + case 'ubuntu': + return 'linux' + case 'mac': + case 'macos': + case 'darwin': + return 'macos' + case 'win': + case 'windows': + return 'windows' + default: + return detectHostPlatform() + } +} + +function detectHostPlatform(): Platform { + switch (process.platform) { + case 'darwin': + return 'macos' + case 'win32': + return 'windows' + default: + return 'linux' + } +} + +function printHelp() { + console.log(`Usage: scripts/run-ci-stage.ts --stage [options] + +Options: + --stage Stage to run (${Object.keys(STAGES).join(', ')}) + --platform linux | macos | windows (defaults to host platform) + --base Override git base commit (defaults to merge-base of main) + --head Override git head commit (defaults to HEAD) + --dry-run Print the commands without executing them + --list List available stages + -h, --help Show this message`) +} + +function runStage(options: { + stage: string + platform: Platform + base?: string | undefined + head?: string | undefined + dryRun: boolean +}) { + const { platform, stage: stageName, dryRun } = options + const stage = STAGES[stageName] + if (!stage) { + throw new Error(`Unknown stage "${stageName}". Use --list to inspect options.`) + } + console.log(`Running stage "${stageName}" (${stage.description}) on ${platform}`) + if (stage.steps.length === 0) { + console.log('No commands configured for this stage. Nothing to do.') + return + } + if (stage.supportedPlatforms && !stage.supportedPlatforms.includes(platform)) { + throw new Error(`Stage "${stageName}" only runs on ${stage.supportedPlatforms.join(', ')}.`) + } + if (stage.requiredEnv) { + ensureEnvVars(stageName, stage.requiredEnv) + } + const detection = detectSurfaces({ + surfaces: stage.surface ? [stageName] : undefined, + base: options.base, + head: options.head, + }) + if (detection.base === detection.head) { + console.log(`Detected ${detection.affectedProjects.length} affected projects with local changes`) + } else { + console.log( + `Detected ${detection.affectedProjects.length} affected projects between ${detection.base.slice(0, 7)}...${detection.head.slice(0, 7)}`, + ) + } + const context: StageContext = { + ci: { + base: detection.base, + head: detection.head, + changedFiles: detection.changedFiles, + affectedProjects: detection.affectedProjects, + projectMeta: detection.projectMeta, + surfaces: detection.results, + }, + } + stage.prepare?.(context) + for (const step of stage.steps) { + if (step.platforms && !step.platforms.includes(platform)) { + console.log(`Skipping step "${step.name}" on ${platform}`) + continue + } + const args = typeof step.args === 'function' ? step.args(context) : (step.args ?? []) + const cwdOption = typeof step.cwd === 'function' ? step.cwd(context) : step.cwd + const envOption = typeof step.env === 'function' ? step.env(context) : step.env + console.log(`→ ${step.name}`) + if (dryRun) { + console.log(` ${step.command} ${args.join(' ')}`) + continue + } + const result = spawnSync(step.command, args, { + cwd: cwdOption ? path.resolve(workspaceRoot, cwdOption) : workspaceRoot, + stdio: 'inherit', + env: { ...process.env, ...envOption }, + }) + if (result.status !== 0) { + throw new Error(`Step "${step.name}" failed with code ${result.status ?? 1}`) + } + } +} + +function listStages() { + console.log('Available stages:') + for (const [name, stage] of Object.entries(STAGES)) { + console.log(`- ${name}: ${stage.description}`) + } +} + +function main() { + try { + const options = parseArgs(process.argv.slice(2)) + if (options.help) { + printHelp() + return + } + if (options.list) { + listStages() + return + } + if (!options.stage) { + printHelp() + process.exit(1) + } + const platform = options.platform ?? detectHostPlatform() + runStage({ + stage: options.stage, + platform, + base: options.base, + head: options.head, + dryRun: Boolean(options.dryRun), + }) + } catch (error) { + console.error(error instanceof Error ? error.message : error) + process.exit(1) + } +} + +main() diff --git a/scripts/ci/stages.ts b/scripts/ci/stages.ts new file mode 100644 index 0000000..bdde0fd --- /dev/null +++ b/scripts/ci/stages.ts @@ -0,0 +1,338 @@ +import path from 'node:path' +import type { StageDefinition } from './types' +import { workspaceRoot } from '@nx/devkit' +import { + discoverNativeArtifact, + ensureEnvVars, + fileExistsRelative, + getAndroidProfile, + getAndroidSubmitProfile, + getIosProfile, + getIosSubmitProfile, + getNativeProjectsFromContext, + listFilesRecursive, + resolveNativeProjects, + storeNativeProjects, +} from './utils' + +export const STAGES: Record = { + lint: { + description: 'Lint, circular dependency detection, typecheck, and unit tests', + surface: { + defaultToAffected: true, + }, + steps: [ + { + name: 'Lint (oxlint)', + command: 'pnpm', + args: ['lint'], + }, + { + name: 'Circular dependency check', + command: 'pnpm', + args: ['nx', 'affected', '--target=madge', '--parallel=1'], + }, + { + name: 'Typecheck affected projects', + command: 'pnpm', + args: ['nx', 'affected', '--target=typecheck', '--parallel=3'], + }, + { + name: 'Unit tests', + command: 'pnpm', + args: ['nx', 'affected', '--target=test', '--parallel=2'], + }, + ], + }, + web: { + description: 'Build Nx libs/apps that target web, SSR, and workers', + surface: { + selectors: [{ tagsAny: ['web'] }], + }, + steps: [ + { + name: 'Build', + command: 'pnpm', + args: ['nx', 'affected', '--target=build', '--parallel=4'], + }, + ], + }, + 'deploy-web': { + description: 'Deploy web/worker apps', + steps: [ + { + name: 'Deploy apps', + command: 'pnpm', + args: ['nx', 'affected', '--target=deploy', '--parallel=4'], + }, + ], + }, + 'native-android': { + description: 'Typecheck RN project and build Android artifacts via Nx/EAS', + surface: { + selectors: [{ tagsAny: ['native'] }], + }, + requiredEnv: [], + prepare: (context) => { + const projects = resolveNativeProjects(context, 'android') + storeNativeProjects(context, projects) + }, + steps: [ + { + name: 'Android builds', + command: 'pnpm', + args: (context) => { + const projects = getNativeProjectsFromContext(context) + return [ + 'nx', + 'run-many', + `--target=build:android`, + `--projects=${projects.join(',')}`, + '--parallel=1', + '--profile', + getAndroidProfile(), + ] + }, + }, + ], + }, + 'deploy-native-android': { + description: 'Submit Android artifacts to Google Play via EAS submit', + requiredEnv: [], + prepare: (context) => { + const projects = resolveNativeProjects(context, 'android') + storeNativeProjects(context, projects) + const overridePath = process.env.ANDROID_ARTIFACT_PATH + const artifactPath = + overridePath && overridePath.length > 0 ? overridePath : discoverNativeArtifact('android', ['aab', 'apk']) + if (!artifactPath || !fileExistsRelative(artifactPath)) { + throw new Error(`Android artifact not found at ${artifactPath}. Ensure artifacts are extracted to dist/native.`) + } + context.androidArtifactPath = artifactPath + }, + steps: [ + { + name: 'Android submit', + command: 'pnpm', + args: (context) => { + const projects = getNativeProjectsFromContext(context) + return [ + 'nx', + 'run-many', + `--target=deploy:android`, + `--projects=${projects.join(',')}`, + '--parallel=1', + '--profile', + getAndroidSubmitProfile(), + ] + }, + }, + ], + }, + 'native-ios': { + description: 'Build iOS artifacts via Nx', + surface: { + selectors: [{ tagsAny: ['native'] }], + }, + requiredEnv: [], + supportedPlatforms: ['macos'], + prepare: (context) => { + const projects = resolveNativeProjects(context, 'ios') + storeNativeProjects(context, projects) + }, + steps: [ + { + name: 'iOS builds', + command: 'pnpm', + args: (context) => { + const projects = getNativeProjectsFromContext(context) + return [ + 'nx', + 'run-many', + `--target=build:ios`, + `--projects=${projects.join(',')}`, + '--parallel=1', + '--profile', + getIosProfile(), + ] + }, + }, + ], + }, + 'deploy-native-ios': { + description: 'Deploy iOS artifacts to App Store', + requiredEnv: [], + prepare: (context) => { + const projects = resolveNativeProjects(context, 'ios') + storeNativeProjects(context, projects) + const overridePath = process.env.IOS_ARTIFACT_PATH + const artifactPath = + overridePath && overridePath.length > 0 ? overridePath : discoverNativeArtifact('ios', ['ipa']) + if (!artifactPath || !fileExistsRelative(artifactPath)) { + throw new Error(`iOS artifact not found at ${artifactPath}. Ensure artifacts are extracted to dist/native.`) + } + context.iosArtifactPath = artifactPath + }, + steps: [ + { + name: 'iOS submit', + command: 'pnpm', + args: (context) => { + const projects = getNativeProjectsFromContext(context) + return [ + 'nx', + 'run-many', + `--target=deploy:ios`, + `--projects=${projects.join(',')}`, + '--parallel=1', + '--profile', + getIosSubmitProfile(), + ] + }, + }, + ], + }, + 'js-update': { + description: 'Bundle and publish JS updates via hot-updater CLI', + steps: [ + { + name: 'Publish JS update', + command: 'pnpm', + args: () => { + const env = process.env.JS_UPDATE_ENV ?? process.env.UPDATE_ENV ?? 'staging' + const branchOverride = process.env.VERSION_BRANCH ?? '' + const args = ['tsx', 'scripts/ci/js-update.ts', '--env', env] + if (branchOverride) { + args.push('--branch', branchOverride) + } + const platform = process.env.JS_UPDATE_PLATFORM + if (platform) { + args.push('--platform', platform) + } + const channelOverride = process.env.JS_UPDATE_CHANNEL + if (channelOverride) { + args.push('--channel', channelOverride) + } + return args + }, + }, + ], + }, + desktop: { + description: 'Electron / desktop builds (placeholder until desktop project lands)', + surface: { + selectors: [{ tagsAny: ['desktop'] }], + }, + steps: [], + }, + 'deploy-desktop': { + description: 'Upload packaged desktop artifacts to Cloudflare R2 or GitHub Releases', + prepare: (context) => { + const artifactDir = desktopDistRoot() + const files = listFilesRecursive(artifactDir) + if (files.length === 0) { + throw new Error( + `No desktop artifacts found under ${artifactDir}. Set DESKTOP_ARTIFACT_DIR or ensure dist/desktop exists.`, + ) + } + const target = (process.env.DESKTOP_DEPLOY_TARGET ?? 'r2').toLowerCase() + if (!['r2', 'github-release'].includes(target)) { + throw new Error(`Unsupported DESKTOP_DEPLOY_TARGET "${target}". Use "r2" or "github-release".`) + } + if (target === 'r2') { + ensureEnvVars('deploy-desktop', [ + 'CLOUDFLARE_R2_BUCKET', + 'CLOUDFLARE_ACCOUNT_ID', + 'CLOUDFLARE_R2_ACCESS_KEY_ID', + 'CLOUDFLARE_R2_SECRET_ACCESS_KEY', + ]) + } + if (target === 'github-release') { + ensureEnvVars('deploy-desktop', ['GITHUB_TOKEN']) + } + context.desktopArtifactDir = artifactDir + context.desktopFiles = files + context.desktopTarget = target + }, + steps: [ + { + name: 'List desktop artifacts', + command: 'bash', + args: () => ['-c', 'set -euo pipefail\nls -al "$DESKTOP_ARTIFACT_DIR"\nfind "$DESKTOP_ARTIFACT_DIR" -type f'], + env: (context) => ({ + DESKTOP_ARTIFACT_DIR: path.resolve(workspaceRoot, String(context.desktopArtifactDir)), + }), + }, + { + name: 'Upload artifacts to Cloudflare R2', + command: 'bash', + args: () => [ + '-c', + [ + 'set -euo pipefail', + 'if [ "${DESKTOP_DEPLOY_TARGET}" != "r2" ]; then', + ' echo "Skipping R2 upload";', + ' exit 0;', + 'fi', + 'PREFIX=""', + 'if [ -n "${DESKTOP_R2_PREFIX}" ]; then', + ' PREFIX="/${DESKTOP_R2_PREFIX%/}"', + 'fi', + 'DEST="s3://${CLOUDFLARE_R2_BUCKET}${PREFIX}"', + 'aws s3 sync "$DESKTOP_ARTIFACT_DIR" "$DEST" --endpoint-url "https://${CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com" --delete --acl private', + ].join('\n'), + ], + env: (context) => ({ + DESKTOP_DEPLOY_TARGET: String(context.desktopTarget), + DESKTOP_ARTIFACT_DIR: path.resolve(workspaceRoot, String(context.desktopArtifactDir)), + DESKTOP_R2_PREFIX: process.env.DESKTOP_R2_PREFIX ?? '', + CLOUDFLARE_R2_BUCKET: process.env.CLOUDFLARE_R2_BUCKET ?? '', + CLOUDFLARE_ACCOUNT_ID: process.env.CLOUDFLARE_ACCOUNT_ID ?? '', + AWS_ACCESS_KEY_ID: process.env.CLOUDFLARE_R2_ACCESS_KEY_ID ?? '', + AWS_SECRET_ACCESS_KEY: process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY ?? '', + }), + }, + { + name: 'Upload artifacts to GitHub Release', + command: 'bash', + args: () => [ + '-c', + [ + 'set -euo pipefail', + 'if [ "${DESKTOP_DEPLOY_TARGET}" != "github-release" ]; then', + ' echo "Skipping GitHub Release upload";', + ' exit 0;', + 'fi', + 'TAG=${DESKTOP_RELEASE_TAG:-desktop/dev-$(date +%Y%m%d%H%M%S)}', + 'NAME=${DESKTOP_RELEASE_NAME:-$TAG}', + 'BODY=${DESKTOP_RELEASE_BODY:-Automated desktop deploy}', + 'DRAFT_FLAG=', + 'if [ "${DESKTOP_RELEASE_DRAFT}" = "true" ]; then', + ' DRAFT_FLAG="--draft"', + 'fi', + 'if gh release view "$TAG" >/dev/null 2>&1; then', + ' echo "Reusing release $TAG"', + 'else', + ' gh release create "$TAG" --title "$NAME" --notes "$BODY" $DRAFT_FLAG', + 'fi', + 'mapfile -t files < <(find "$DESKTOP_ARTIFACT_DIR" -type f)', + 'if [ "${#files[@]}" -eq 0 ]; then', + ' echo "No files found in $DESKTOP_ARTIFACT_DIR";', + ' exit 1;', + 'fi', + 'gh release upload "$TAG" "${files[@]}" --clobber', + ].join('\n'), + ], + env: (context) => ({ + DESKTOP_DEPLOY_TARGET: String(context.desktopTarget), + DESKTOP_ARTIFACT_DIR: path.resolve(workspaceRoot, String(context.desktopArtifactDir)), + DESKTOP_RELEASE_TAG: process.env.DESKTOP_RELEASE_TAG ?? '', + DESKTOP_RELEASE_NAME: process.env.DESKTOP_RELEASE_NAME ?? '', + DESKTOP_RELEASE_BODY: process.env.DESKTOP_RELEASE_BODY ?? '', + DESKTOP_RELEASE_DRAFT: process.env.DESKTOP_RELEASE_DRAFT ?? 'false', + GITHUB_TOKEN: process.env.GITHUB_TOKEN ?? '', + }), + }, + ], + }, +} diff --git a/scripts/ci/surface-detector.ts b/scripts/ci/surface-detector.ts new file mode 100644 index 0000000..22c5243 --- /dev/null +++ b/scripts/ci/surface-detector.ts @@ -0,0 +1,259 @@ +import { STAGES } from './stages' +import type { StageSurfaceConfig, SurfaceSelector } from './types' +import { git, nx } from './utils' + +type SurfaceName = string + +interface SurfaceResult { + name: SurfaceName + impacted: boolean + matches: string[] + description?: string | undefined +} + +interface SurfaceDetectionOptions { + surfaces?: SurfaceName[] | undefined + base?: string | undefined + head?: string | undefined +} + +interface ProjectMeta { + name: string + root: string + tags: string[] + projectType?: string | undefined +} + +export interface SurfaceDetectionPayload { + base: string + head: string + changedFiles: string[] + affectedProjects: string[] + results: SurfaceResult[] + projectMeta: Record +} + +type SurfaceConfig = StageSurfaceConfig & { description?: string | undefined } + +const SURFACE_DEFINITIONS: Record = Object.fromEntries( + Object.entries(STAGES) + .filter(([, stage]) => stage.surface) + .map(([name, stage]) => [ + name, + { + description: stage.description, + selectors: stage.surface?.selectors ?? [], + defaultToAffected: stage.surface?.defaultToAffected, + }, + ]), +) + +function getSurfaceNames() { + return Object.keys(SURFACE_DEFINITIONS) +} + +export function detectSurfaces(options: SurfaceDetectionOptions = {}): SurfaceDetectionPayload { + const available = getSurfaceNames() + const surfacesToCheck = (options.surfaces?.length ? options.surfaces : available).map((name) => { + if (!SURFACE_DEFINITIONS[name]) { + throw new Error(`Unknown surface "${name}". Available surfaces: ${available.join(', ')}`) + } + return name + }) + const head = resolveHead(options.head) + const base = process.env.CI ? resolveBase(head, options.base) : resolveLocalBase(head, options.base) + const changedFiles = collectChangedFiles(base, head) + const affectedProjects = getAffectedProjects({ base, head, files: changedFiles }) + const projectMeta = loadProjectMeta(affectedProjects) + const results = surfacesToCheck.map((name) => + evaluateSurface(name, SURFACE_DEFINITIONS[name], affectedProjects, projectMeta, affectedProjects.length), + ) + return { + base, + head, + changedFiles, + affectedProjects, + results, + projectMeta: Object.fromEntries(projectMeta.entries()), + } +} + +export function resolveHead(headOverride?: string | undefined) { + if (headOverride) return headOverride + if (process.env.NX_HEAD) return process.env.NX_HEAD + return git(['rev-parse', 'HEAD']) +} + +export function resolveBase(head: string, baseOverride?: string | undefined) { + if (baseOverride) return baseOverride + if (process.env.NX_BASE) return process.env.NX_BASE + const mainBranch = process.env.NX_MAIN_BRANCH ?? 'main' + const candidates = [ + () => git(['merge-base', `origin/${mainBranch}`, head]), + () => git(['merge-base', mainBranch, head]), + () => git(['rev-parse', `${head}^`]), + ] + for (const candidate of candidates) { + try { + const base = candidate() + if (base) return base + } catch { + // ignore + } + } + return git(['rev-parse', head]) +} + +function resolveLocalBase(head: string, baseOverride?: string | undefined) { + const override = baseOverride ?? process.env.NX_BASE + if (override) { + return override + } + const branch = getCurrentBranchName() + if (branch && branch !== 'HEAD') { + const remoteHead = getRemoteBranchSha(branch) + if (remoteHead) { + return remoteHead + } + } + return resolveBase(head) +} + +function getCurrentBranchName() { + try { + return git(['rev-parse', '--abbrev-ref', 'HEAD']) + } catch { + return 'HEAD' + } +} + +function getRemoteBranchSha(branch: string) { + try { + const output = git(['ls-remote', '--exit-code', '--heads', 'origin', branch]) + const lines = commitmentToArray(output) + if (lines.length === 0) { + return undefined + } + const [sha] = lines[0].split('\t') + return sha?.trim() ?? undefined + } catch { + return undefined + } +} + +export function collectChangedFiles(base: string, head: string) { + const sets: Array> = [] + const diffRange = process.env.CI ? 'triple-dot' : 'double-dot' + const args = diffRange === 'double-dot' ? [base, head] : [`${base}...${head}`] + const committed = git(['diff', '--name-only', ...args]) + sets.push(new Set(commitmentToArray(committed))) + try { + const staged = git(['diff', '--name-only', '--cached']) + sets.push(new Set(commitmentToArray(staged))) + } catch { + // ignore + } + try { + const working = git(['diff', '--name-only']) + sets.push(new Set(commitmentToArray(working))) + } catch { + // ignore + } + try { + const untracked = git(['ls-files', '--others', '--exclude-standard']) + sets.push(new Set(commitmentToArray(untracked))) + } catch { + // ignore + } + const merged = new Set() + for (const set of sets) { + for (const file of set) { + if (file) merged.add(file) + } + } + return Array.from(merged).sort() +} + +function commitmentToArray(value: string) { + return value + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) +} + +function getAffectedProjects(options: { base: string; head: string; files: string[] }): string[] { + if (process.env.CI) { + const raw = nx(['show', 'projects', '--affected', `--base=${options.base}`, `--head=${options.head}`, '--json']) + return JSON.parse(raw) as string[] + } + if (options.files.length === 0) { + return [] + } + const filesArg = options.files.join(',') + const raw = nx(['show', 'projects', '--affected', '--files', filesArg, '--json']) + return JSON.parse(raw) as string[] +} + +function loadProjectMeta(projectNames: string[]): Map { + const map = new Map() + for (const project of projectNames) { + const raw = nx(['show', 'project', project, '--json']) + const parsed = JSON.parse(raw) + map.set(project, { + name: parsed.name, + root: parsed.root ?? parsed.sourceRoot ?? '', + tags: Array.isArray(parsed.tags) ? parsed.tags : [], + projectType: parsed.projectType, + }) + } + return map +} + +function evaluateSurface( + name: SurfaceName, + config: SurfaceConfig | undefined, + affectedProjects: string[], + projectMeta: Map, + totalAffected: number, +): SurfaceResult { + if (!config) { + throw new Error(`Unknown surface "${name}". Configure it in STAGES.surface`) + } + const matches = new Set() + const selectors = config.selectors ?? [] + if (selectors.length > 0) { + for (const projectName of affectedProjects) { + const meta = projectMeta.get(projectName) + if (!meta) continue + if (selectors.some((selector) => matchesSelector(meta, selector))) { + matches.add(projectName) + } + } + } + let impacted = matches.size > 0 + if (config.defaultToAffected && totalAffected > 0) { + impacted = true + } + return { + name, + impacted, + matches: Array.from(matches).sort(), + description: config.description, + } +} + +function matchesSelector(meta: ProjectMeta, selector: SurfaceSelector): boolean { + if (selector.projects && !selector.projects.includes(meta.name)) { + return false + } + if (selector.projectTypes?.length && (!meta.projectType || !selector.projectTypes.includes(meta.projectType))) { + return false + } + if (selector.tagsAll?.length && !selector.tagsAll.every((tag) => meta.tags.includes(tag))) { + return false + } + if (selector.tagsAny?.length && !selector.tagsAny.some((tag) => meta.tags.includes(tag))) { + return false + } + return true +} diff --git a/scripts/ci/types.ts b/scripts/ci/types.ts new file mode 100644 index 0000000..4ffed24 --- /dev/null +++ b/scripts/ci/types.ts @@ -0,0 +1,52 @@ +export type Platform = 'linux' | 'macos' | 'windows' + +export interface CiProjectMetaSummary { + name?: string | undefined + root?: string | undefined + tags?: string[] | undefined + projectType?: string | undefined +} + +export interface CiDetectionContext { + base?: string | undefined + head?: string | undefined + changedFiles: string[] + affectedProjects: string[] + projectMeta: Record + surfaces?: unknown | undefined +} + +export interface StageContext { + ci: CiDetectionContext + [key: string]: unknown +} + +export interface SurfaceSelector { + projects?: string[] | undefined + tagsAll?: string[] | undefined + tagsAny?: string[] | undefined + projectTypes?: string[] | undefined +} + +export interface StageSurfaceConfig { + selectors?: SurfaceSelector[] | undefined + defaultToAffected?: boolean | undefined +} + +export interface StageStep { + name: string + command: string + args?: string[] | ((context: StageContext) => string[]) | undefined + cwd?: string | ((context: StageContext) => string | undefined) | undefined + env?: Record | ((context: StageContext) => Record) | undefined + platforms?: Platform[] | undefined +} + +export interface StageDefinition { + description: string + steps: StageStep[] + prepare?: ((context: StageContext) => void) | undefined + requiredEnv?: string[] | undefined + supportedPlatforms?: Platform[] | undefined + surface?: StageSurfaceConfig | undefined +} diff --git a/scripts/ci/utils.ts b/scripts/ci/utils.ts new file mode 100644 index 0000000..37d3692 --- /dev/null +++ b/scripts/ci/utils.ts @@ -0,0 +1,118 @@ +import fs from 'node:fs' +import path from 'node:path' +import { spawnSync } from 'node:child_process' +import { workspaceRoot } from '@nx/devkit' +import { type StageContext } from './types' + +export const getAndroidProfile = () => process.env.EAS_ANDROID_PROFILE ?? 'preview' +export const getAndroidArtifactExt = () => + process.env.ANDROID_ARTIFACT_EXT ?? (getAndroidProfile() === 'production' ? 'aab' : 'apk') + +export const getIosProfile = () => process.env.EAS_IOS_PROFILE ?? 'preview' + +export const getAndroidSubmitProfile = () => process.env.EAS_ANDROID_SUBMIT_PROFILE ?? getAndroidProfile() +export const getIosSubmitProfile = () => process.env.EAS_IOS_SUBMIT_PROFILE ?? getIosProfile() + +export function fileExistsRelative(relPath: string) { + return fs.existsSync(path.resolve(workspaceRoot, relPath)) +} + +export function discoverNativeArtifact(kind: 'android' | 'ios', extensions: string[]): string | undefined { + const root = path.resolve(workspaceRoot, nativeDistRoot) + if (!fs.existsSync(root)) { + return undefined + } + const entries = fs.readdirSync(root) + const matches = entries + .filter((file) => file.startsWith(`${kind}-`) && extensions.some((ext) => file.endsWith(`.${ext}`))) + .sort() + if (matches.length === 0) { + return undefined + } + return path.join(nativeDistRoot, matches[matches.length - 1]) +} + +export function listFilesRecursive(relDir: string): string[] { + const absolute = path.resolve(workspaceRoot, relDir) + if (!fs.existsSync(absolute)) { + return [] + } + const results: string[] = [] + const walk = (currentRel: string) => { + const currentAbs = path.resolve(workspaceRoot, currentRel) + const entries = fs.readdirSync(currentAbs, { withFileTypes: true }) + for (const entry of entries) { + const entryRel = path.join(currentRel, entry.name) + if (entry.isDirectory()) { + walk(entryRel) + } else if (entry.isFile()) { + results.push(entryRel) + } + } + } + walk(relDir) + return results +} + +export function ensureEnvVars(stageName: string, variables: string[]) { + const missing = variables.filter((name) => !process.env[name] || process.env[name] === '') + if (missing.length > 0) { + throw new Error(`Stage "${stageName}" requires the following env vars: ${missing.join(', ')}`) + } +} + +export function runCommand(command: string, args: string[], opts?: { cwd?: string }) { + const result = spawnSync(command, args, { + cwd: opts?.cwd ?? workspaceRoot, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }) + if (result.status !== 0) { + throw new Error(`Failed to run ${command} ${args.join(' ')}: ${result.stderr || result.stdout}`) + } + return result.stdout.trim() +} + +export function git(args: string[]) { + return runCommand('git', args) +} + +export function nx(args: string[]) { + return runCommand('pnpm', ['exec', 'nx', ...args]) +} + +export const getCiSharedContext = (context: StageContext) => context.ci + +const parseProjectOverride = (value: string | undefined) => + (value ?? '') + .split(/[, ]+/) + .map((entry) => entry.trim()) + .filter((entry) => entry.length > 0) + +export const getNativeProjectsFromContext = (context: StageContext): string[] => { + const value = context['nativeProjects'] + return Array.isArray(value) ? (value as string[]) : [] +} + +export const storeNativeProjects = (context: StageContext, projects: string[]) => { + context['nativeProjects'] = projects +} + +export const resolveNativeProjects = (context: StageContext, label: string) => { + const override = parseProjectOverride(process.env.NX_NATIVE_PROJECTS) + if (override.length > 0) { + console.log(`[${label}] Using NX_NATIVE_PROJECTS override: ${override.join(', ')}`) + return override + } + const ci = getCiSharedContext(context) + const native = ci.affectedProjects.filter((project) => { + const tags = ci.projectMeta?.[project]?.tags ?? [] + return tags.includes('native') + }) + if (native.length === 0) { + console.log(`[${label}] No affected native projects detected from surface analysis.`) + } else { + console.log(`[${label}] Native projects from surface analysis: ${native.join(', ')}`) + } + return native +} diff --git a/scripts/generate-tsconfig/config.ts b/scripts/generate-tsconfig/config.ts index 1e1c808..7970dc2 100644 --- a/scripts/generate-tsconfig/config.ts +++ b/scripts/generate-tsconfig/config.ts @@ -201,7 +201,7 @@ function buildGlobalAliasPaths({ function addSiblingAliases(items: ProjectItemDeclaration[], paths: TsPathAliasMap): TsPathAliasMap { const siblingPaths: TsPathAliasMap = { ...paths } - const rejectList = new Set(['web', 'rn']) + const rejectList = new Set(['web', 'native']) const frontendAppPaths: string[] = [] for (const item of items) { diff --git a/scripts/nx-run-affected.sh b/scripts/nx-run-affected.sh new file mode 100755 index 0000000..57610cb --- /dev/null +++ b/scripts/nx-run-affected.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# 用法: ./scripts/run-nx-affected.sh 'nx affected -t madge' +# 脚本会自动计算改动的文件,并用 --files 参数传给 nx affected + +set -e + +COMMAND="$@" + +if [ -z "$COMMAND" ]; then + echo "Error: No command provided" >&2 + echo "Usage: $0 'nx affected -t '" >&2 + exit 1 +fi + +# 获取当前分支名称 +CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) +MAIN_BRANCH=${NX_MAIN_BRANCH:-main} + +echo "Current branch: $CURRENT_BRANCH" >&2 + +# 检查远端是否存在对应分支 +if git ls-remote --exit-code --heads origin "$CURRENT_BRANCH" > /dev/null 2>&1; then + echo "Remote branch found: origin/$CURRENT_BRANCH" >&2 + # 远端分支存在,对比远端 vs 本地 + BASE=$(git rev-parse origin/$CURRENT_BRANCH) + HEAD_REF=$(git rev-parse HEAD) + echo "Mode: comparing local changes against remote branch" >&2 +else + echo "Remote branch not found, using merge-base with $MAIN_BRANCH" >&2 + # 远端分支不存在,对比分叉点 vs 本地 + if git show-ref --verify --quiet refs/remotes/origin/$MAIN_BRANCH; then + BASE=$(git merge-base origin/$MAIN_BRANCH HEAD) + else + BASE=$(git merge-base $MAIN_BRANCH HEAD 2>/dev/null || git rev-parse HEAD~1) + fi + HEAD_REF=$(git rev-parse HEAD) + echo "Mode: comparing new branch changes" >&2 +fi + +echo "BASE=$BASE" >&2 +echo "HEAD=$HEAD_REF" >&2 + +# 获取改动的文件列表 +# 包括已提交的改动、未暂存的改动、未跟踪的文件 +CHANGED_FILES=$(git diff --name-only $BASE $HEAD_REF; git diff --name-only; git ls-files --others --exclude-standard) + +# 去重并用逗号分隔 +FILES_LIST=$(echo "$CHANGED_FILES" | sort -u | tr '\n' ',' | sed 's/,$//') + +echo "Changed files: $FILES_LIST" >&2 +echo "" >&2 + +# 执行命令并附加 --files 参数 +if [ -z "$FILES_LIST" ]; then + echo "No files changed" >&2 + exit 0 +fi + +eval "$COMMAND --files $FILES_LIST" diff --git a/scripts/sync-github-secrets.ts b/scripts/sync-github-secrets.ts index a557f69..5fdd873 100644 --- a/scripts/sync-github-secrets.ts +++ b/scripts/sync-github-secrets.ts @@ -19,9 +19,9 @@ const SECRETS_FILE = '.env.github' async function main(): Promise { const env = dotenv.config({ - overload: true, quiet: true, ignore: ['MISSING_ENV_FILE'], + processEnv: {}, }) await sodium.ready @@ -45,7 +45,12 @@ async function main(): Promise { ]) await upsertSecrets({ octokit, repo, publicKey, secrets }) - await pruneSecrets({ octokit, repo, desired: new Set(Object.keys(secrets)), remote: remoteSecretNames }) + await pruneSecrets({ + octokit, + repo, + desired: new Set(Object.keys(secrets)), + remote: remoteSecretNames, + }) } async function inferRepoFromGit(): Promise { @@ -130,9 +135,8 @@ function parseRemotePath(pathname: string): RepoCoordinates { function loadSecretsFromEnvFile(filePath: string): Record { const result = dotenv.config({ path: filePath, - processEnv: {}, - override: true, quiet: true, + processEnv: {}, }) if (result.error) { @@ -155,7 +159,10 @@ function loadSecretsFromEnvFile(filePath: string): Record { } async function fetchPublicKey(octokit: ReturnType, repo: RepoCoordinates) { - const response = await octokit.rest.actions.getRepoPublicKey({ owner: repo.owner, repo: repo.repo }) + const response = await octokit.rest.actions.getRepoPublicKey({ + owner: repo.owner, + repo: repo.repo, + }) return response.data } diff --git a/scripts/sync-tsconfig.ts b/scripts/sync-tsconfig.ts new file mode 100755 index 0000000..ddeb557 --- /dev/null +++ b/scripts/sync-tsconfig.ts @@ -0,0 +1,232 @@ +#!/usr/bin/env tsx + +import { workspaceRoot } from '@nx/devkit' +import { exec } from 'node:child_process' +import * as fs from 'node:fs/promises' +import * as path from 'node:path' +import { ALIAS_DEFINITIONS, PROJECT_DEFINITIONS } from '../project-manifest' +import baseConfig from './generate-tsconfig/base-config.json' +import { + applyAliasPathsToBaseConfig, + buildProjectBaseTsconfig, + DefaultRootConfig, + generateAppConfigs, + generateAppLibraryConfigs, + generatePackageLibraryConfigs, +} from './generate-tsconfig/config' +import type { ProjectItemDeclaration } from './generate-tsconfig/types' + +async function generateTsConfigFile({ + filePath, + config, + dryRun = true, +}: { + filePath: string + config: any + dryRun?: boolean +}) { + const content = JSON.stringify(config, null, 2) + + if (dryRun) { + console.log(`\n📁 ${filePath}`) + console.log('─'.repeat(60)) + console.log(content) + } else { + await fs.mkdir(path.dirname(filePath), { recursive: true }) + await fs.writeFile(filePath, content) + console.log(`✅ Created: ${filePath}`) + } +} + +async function writeLibraryConfigs({ + projectRoot, + item, + dryRun, +}: { + projectRoot: string + item: ProjectItemDeclaration + dryRun: boolean +}) { + const itemPath = path.join(projectRoot, item.name) + const isPackage = item.options?.isPackage ?? false + + if (isPackage) { + const configs = generatePackageLibraryConfigs({ item }) + + await generateTsConfigFile({ + filePath: path.join(itemPath, 'tsconfig.json'), + config: configs.main, + dryRun, + }) + + await generateTsConfigFile({ + filePath: path.join(itemPath, 'tsconfig.lib.json'), + config: configs.lib, + dryRun, + }) + + await generateTsConfigFile({ + filePath: path.join(itemPath, 'tsconfig.test.json'), + config: configs.test, + dryRun, + }) + + await generateTsConfigFile({ + filePath: path.join(itemPath, 'tsconfig.check.json'), + config: configs.check, + dryRun, + }) + + return + } + + const configs = generateAppLibraryConfigs({ item }) + + await generateTsConfigFile({ + filePath: path.join(itemPath, 'tsconfig.json'), + config: configs.main, + dryRun, + }) + + await generateTsConfigFile({ + filePath: path.join(itemPath, 'tsconfig.app.json'), + config: configs.app, + dryRun, + }) + + await generateTsConfigFile({ + filePath: path.join(itemPath, 'tsconfig.test.json'), + config: configs.test, + dryRun, + }) +} + +async function writeApplicationConfigs({ + projectRoot, + item, + dryRun, + allItems, +}: { + projectRoot: string + item: ProjectItemDeclaration + dryRun: boolean + allItems: ProjectItemDeclaration[] +}) { + const itemPath = path.join(projectRoot, item.name) + const configs = generateAppConfigs({ item, allItems }) + + await generateTsConfigFile({ + filePath: path.join(itemPath, 'tsconfig.json'), + config: configs.main, + dryRun, + }) + + await generateTsConfigFile({ + filePath: path.join(itemPath, 'tsconfig.app.json'), + config: configs.app, + dryRun, + }) + + await generateTsConfigFile({ + filePath: path.join(itemPath, 'tsconfig.test.json'), + config: configs.test, + dryRun, + }) + + await generateTsConfigFile({ + filePath: path.join(itemPath, 'tsconfig.check.json'), + config: configs.check, + dryRun, + }) +} + +// 主函数 +async function main() { + try { + const shouldWrite = process.argv.includes('--write') || process.argv.includes('-w') + const dryRun = !shouldWrite + + console.log('🚀 Starting TSConfig synchronization...') + console.log(`📂 Workspace root: ${workspaceRoot}`) + console.log(`🔧 Mode: ${dryRun ? 'DRY RUN (preview only)' : 'WRITE MODE (will create files)'}`) + + if (dryRun) { + console.log('💡 Use --write or -w flag to actually write files') + } + + // 更新 base config 以包含生成的所有 paths + const updatedBaseConfig = applyAliasPathsToBaseConfig(baseConfig, ALIAS_DEFINITIONS) + + // 首先生成根目录的 tsconfig.base.json + await generateTsConfigFile({ + dryRun, + filePath: path.join(workspaceRoot, 'tsconfig.base.json'), + config: updatedBaseConfig, + }) + + await generateTsConfigFile({ + dryRun, + filePath: path.join(workspaceRoot, 'tsconfig.json'), + config: DefaultRootConfig, + }) + + for (const definition of PROJECT_DEFINITIONS) { + // 生成项目级别的 base config + console.log(`\n🏗️ Generating configs for project: ${definition.projectName}`) + console.log('='.repeat(80)) + + const projectRoot = path.join(workspaceRoot, definition.projectName) + + for (const item of definition.items) { + if (item.kind === 'library') { + await writeLibraryConfigs({ + projectRoot, + item, + dryRun, + }) + } else { + await writeApplicationConfigs({ + projectRoot, + item, + dryRun, + allItems: definition.items, + }) + } + } + + for (const target of definition.baseTargets ?? []) { + const targetItems = definition.items + const projectName = definition.projectName + '/' + target.name + await generateTsConfigFile({ + dryRun, + filePath: path.join(workspaceRoot, projectName, 'tsconfig.base.json'), + config: buildProjectBaseTsconfig({ + workspaceRoot, + projectName, + baseConfig: updatedBaseConfig, + items: targetItems, + aliasOptions: target.alias, + }), + }) + } + } + + console.log('\n✨ Configuration generation completed!') + + // 展示生成的 Vite 别名配置 + if (dryRun) { + console.log('\n💡 This was a dry run. Use --write flag to actually create the files.') + + return + } + + exec('./node_modules/.bin/oxfmt') + + console.log('✅ All configuration files have been written successfully!') + } catch (error) { + console.error('❌ Error:', error) + process.exit(1) + } +} + +main() diff --git a/scripts/thing/app/command.ts b/scripts/thing/app/command.ts new file mode 100644 index 0000000..c233852 --- /dev/null +++ b/scripts/thing/app/command.ts @@ -0,0 +1,207 @@ +import { Command } from '@effect/cli' +import { FileSystem, Path } from '@effect/platform' +import { Effect } from 'effect' +import { + BuildSubcommand, + BuildWorkersTarget, + DeploySubcommand, + PreviewSubcommand, + ServeSubcommand, + type BuildTarget, + type NodeEnv, + type Stage, +} from '../domain' +import { + appAnalyzeOption, + appCwdOption, + appMinifyOption, + appNodeEnvOption, + appStageOption, + appVerboseOption, +} from './options' +import { reactRouterDesktopOption, reactRouterSpaModeOption } from '../react-router/options' +import { + createReactRouterTarget, + runBuild as runReactRouterBuild, + runDeploy as runReactRouterDeploy, + runPreview as runReactRouterPreview, + runServe as runReactRouterServe, +} from '../react-router/command' +import * as Workers from '../workers/subcommand' +import type { Workspace as WorkspaceModel } from '../workspace' +import * as Workspace from '../workspace' + +interface TargetDetectionOptions { + readonly isSpaMode: boolean + readonly isDesktop: boolean +} + +const withWorkspace = (cwd: string, f: (workspace: WorkspaceModel) => Effect.Effect) => + Effect.gen(function* () { + const workspace = yield* Workspace.make(cwd) + return yield* f(workspace) + }) + +const detectBuildTarget = (workspace: WorkspaceModel, stage: Stage, options: TargetDetectionOptions) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const configCandidates = [ + 'react-router.config.ts', + 'react-router.config.mts', + 'react-router.config.cts', + 'react-router.config.js', + 'react-router.config.mjs', + 'react-router.config.cjs', + ] + + for (const candidate of configCandidates) { + const fullPath = path.join(workspace.projectPath, candidate) + const exists = yield* fs.exists(fullPath) + if (exists) { + return createReactRouterTarget(stage, options) + } + } + + return BuildWorkersTarget({ + runtime: 'cloudflare-workers', + options: {}, + stage, + }) + }) + +const loadBuildTarget = (workspace: WorkspaceModel) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const buildFile = path.join(workspace.projectOutput.dist, 'build.json') + const exists = yield* fs.exists(buildFile) + + if (!exists) { + return yield* Effect.fail(new Error('Missing build output. Run xdev build before deploy/preview.')) + } + + const content = yield* fs.readFileString(buildFile) + const parsed = yield* Effect.try({ + try: () => JSON.parse(content) as BuildTarget, + catch: (error) => error as Error, + }) + + return parsed + }) + +const dispatchServe = (workspace: WorkspaceModel, target: BuildTarget, subcommand: ServeSubcommand) => + target._tag === 'BuildReactRouter' ? runReactRouterServe(workspace, subcommand) : Workers.serve(workspace, subcommand) + +const dispatchBuild = (workspace: WorkspaceModel, target: BuildTarget, subcommand: BuildSubcommand) => + target._tag === 'BuildReactRouter' ? runReactRouterBuild(workspace, subcommand) : Workers.build(workspace, subcommand) + +const dispatchDeploy = (workspace: WorkspaceModel, target: BuildTarget, subcommand: DeploySubcommand) => + target._tag === 'BuildReactRouter' + ? runReactRouterDeploy(workspace, subcommand, target) + : Workers.deploy(workspace, subcommand, target) + +const dispatchPreview = (workspace: WorkspaceModel, target: BuildTarget, subcommand: PreviewSubcommand) => + target._tag === 'BuildReactRouter' + ? runReactRouterPreview(workspace, subcommand, target) + : Workers.preview(workspace, subcommand, target) + +const serveCommand = Command.make( + 'serve', + { + cwd: appCwdOption, + stage: appStageOption, + nodeEnv: appNodeEnvOption, + verbose: appVerboseOption, + spa: reactRouterSpaModeOption, + desktop: reactRouterDesktopOption, + }, + (config) => + withWorkspace(config.cwd, (workspace) => { + const stage = (config.stage ?? 'test') as Stage + const nodeEnv = (config.nodeEnv ?? 'development') as NodeEnv + return Effect.flatMap( + detectBuildTarget(workspace, stage, { isSpaMode: config.spa, isDesktop: config.desktop }), + (target) => + dispatchServe( + workspace, + target, + ServeSubcommand({ + cwd: config.cwd, + target, + verbose: config.verbose, + }), + ), + ) + }), +) + +const buildCommand = Command.make( + 'build', + { + cwd: appCwdOption, + stage: appStageOption, + nodeEnv: appNodeEnvOption, + minify: appMinifyOption, + analyze: appAnalyzeOption, + verbose: appVerboseOption, + spa: reactRouterSpaModeOption, + desktop: reactRouterDesktopOption, + }, + (config) => + withWorkspace(config.cwd, (workspace) => { + const stage = (config.stage ?? 'staging') as Stage + const nodeEnv = (config.nodeEnv ?? 'production') as NodeEnv + return Effect.flatMap( + detectBuildTarget(workspace, stage, { isSpaMode: config.spa, isDesktop: config.desktop }), + (target) => + dispatchBuild( + workspace, + target, + BuildSubcommand({ + cwd: config.cwd, + target, + stage, + nodeEnv, + minify: config.minify, + analyze: config.analyze, + verbose: config.verbose, + }), + ), + ) + }), +) + +const deployCommand = Command.make('deploy', { cwd: appCwdOption, verbose: appVerboseOption }, (config) => + withWorkspace(config.cwd, (workspace) => + Effect.flatMap(loadBuildTarget(workspace), (target) => + dispatchDeploy( + workspace, + target, + DeploySubcommand({ + cwd: config.cwd, + verbose: config.verbose, + }), + ), + ), + ), +) + +const previewCommand = Command.make('preview', { cwd: appCwdOption, verbose: appVerboseOption }, (config) => + withWorkspace(config.cwd, (workspace) => + Effect.flatMap(loadBuildTarget(workspace), (target) => + dispatchPreview( + workspace, + target, + PreviewSubcommand({ + cwd: config.cwd, + verbose: config.verbose, + }), + ), + ), + ), +) + +const appCommands = [serveCommand, buildCommand, deployCommand, previewCommand] as const + +export { appCommands, buildCommand, deployCommand, previewCommand, serveCommand } diff --git a/scripts/thing/app/options.ts b/scripts/thing/app/options.ts new file mode 100644 index 0000000..30c8fdd --- /dev/null +++ b/scripts/thing/app/options.ts @@ -0,0 +1,30 @@ +import { Options } from '@effect/cli' + +const appCwdOption = Options.text('cwd').pipe(Options.withDescription('Path to the app to operate on')) + +const appStageOption = Options.choice('stage', ['production', 'staging', 'test'] as const).pipe( + Options.withDescription('Deployment stage for the current command'), + Options.withDefault(undefined), +) + +const appNodeEnvOption = Options.choice('node-env', ['development', 'production'] as const).pipe( + Options.withDescription('NODE_ENV used for build pipelines'), + Options.withDefault(undefined), +) + +const appVerboseOption = Options.boolean('verbose').pipe( + Options.withDescription('Enable verbose logging'), + Options.withDefault(false), +) + +const appAnalyzeOption = Options.boolean('analyze').pipe( + Options.withDescription('Emit bundle analyzer output'), + Options.withDefault(false), +) + +const appMinifyOption = Options.boolean('minify', { negationNames: ['no-minify'] }).pipe( + Options.withDescription('Minify production bundles (use --no-minify to disable)'), + Options.withDefault(true), +) + +export { appAnalyzeOption, appCwdOption, appMinifyOption, appNodeEnvOption, appStageOption, appVerboseOption } diff --git a/scripts/thing/bin.ts b/scripts/thing/bin.ts index d3af20b..5c463a2 100755 --- a/scripts/thing/bin.ts +++ b/scripts/thing/bin.ts @@ -5,7 +5,7 @@ import * as Otlp from '@effect/opentelemetry/Otlp' import { NodeContext, NodeHttpClient } from '@effect/platform-node' import { defaultTeardown } from '@effect/platform/Runtime' import { workspaceRoot } from '@nx/devkit' -import { Effect, Exit, Fiber, Layer, Logger, LogLevel, pipe, Scope } from 'effect' +import { Effect, Exit, Fiber, FiberRef, Layer, Logger, LogLevel, pipe, Scope } from 'effect' import * as path from 'node:path' import { cli } from './cli' import { DiscordNotification } from './discord' @@ -30,11 +30,13 @@ declare global { const TracerLive = pipe( Otlp.layer({ baseUrl: 'http://localhost:4318', + shutdownTimeout: 500, resource: { serviceName: 'thing-cli', serviceVersion: '0.0.1', }, }), + Layer.locally(FiberRef.currentMinimumLogLevel, LogLevel.Error), Layer.provide(NodeHttpClient.layer), ) @@ -79,23 +81,59 @@ const fiber = pipe( Effect.runFork, ) -// ctrl + c 中断 -async function onSigint() { - await Effect.runPromise(Scope.close(scope, Exit.void)) - await Effect.runPromise(Fiber.interruptFork(fiber)) +let shuttingDown = false +async function shutdown(code: number) { + if (shuttingDown) return + shuttingDown = true + + let timeoutHandle: NodeJS.Timeout | undefined + const timeout = new Promise<'timeout'>((resolve) => { + timeoutHandle = setTimeout(() => { + console.warn('Graceful shutdown timeout, forcing exit') + resolve('timeout') + }, 50_000) + }) + + const cleanup = Promise.all([ + Effect.runPromise(Fiber.interrupt(fiber)), + Effect.runPromise(Scope.close(scope, Exit.void)), + ]).then(() => 'cleanup') + + const result = await Promise.race([cleanup, timeout]) + + if (result === 'cleanup') { + if (timeoutHandle) { + clearTimeout(timeoutHandle) + } + } else { + cleanup.catch((error) => { + if (error) { + console.error('Cleanup rejected after timeout', error) + } + }) + } + + process.exit(code) } -process.once('SIGINT', onSigint) -process.once('SIGTERM', onSigint) +process.once('SIGINT', () => { + shutdown(0) +}) + +process.once('SIGTERM', () => { + void shutdown(0) +}) const keepAlive = setInterval(() => {}, 2 ** 31 - 1) -// program 执行结束 fiber.addObserver((exit) => { clearInterval(keepAlive) - defaultTeardown(exit, async (code) => { - process.exit(code) - }) + + if (shuttingDown) { + return + } + + defaultTeardown(exit, (code) => shutdown(code)) }) process.on('unhandledRejection', (reason, p) => { diff --git a/scripts/thing/cli.ts b/scripts/thing/cli.ts index 14f9b7b..78390f7 100644 --- a/scripts/thing/cli.ts +++ b/scripts/thing/cli.ts @@ -1,652 +1,27 @@ import { Command, HelpDoc, Options } from '@effect/cli' -import { FileSystem, Path } from '@effect/platform' -import { Effect, Schema } from 'effect' -import * as Commands from './domain' -import { detectStage } from './git' -import * as Workspace from './workspace' - -const cwdOption = Options.text('cwd').pipe( - Options.withDescription('The current working directory'), - Options.withDefault(process.cwd()), -) - -const nodeEnvOption = Options.choice('node-env', ['production', 'development']).pipe( - Options.withDescription('The node environment (env.NODE_ENV)'), - Options.withDefault('production'), -) - -const stageOption = Options.choice('stage', ['production', 'staging', 'test']).pipe( - Options.withDescription('The stage'), - Options.optional, -) - -const minifyOption = Options.boolean('minify', { ifPresent: false }).pipe( - Options.withDescription('Minify the build'), - Options.withDefault(true), -) - -const analyzeOption = Options.boolean('analyze', { ifPresent: false }).pipe( - Options.withDescription('analyze the build'), - Options.withDefault(false), -) - -const RuntimeOption = Options.choice('runtime', ['cloudflare-workers']).pipe( - Options.withDescription('The runtime to deploy to'), - Options.withDefault('cloudflare-workers'), -) - -const verboseOption = Options.boolean('verbose', { aliases: ['v'], ifPresent: false }).pipe( - Options.withDescription('Show verbose logs'), - Options.withDefault(true), -) - -/** - * - React Router (SSR/SPA) mode - * - Worker - */ - -const getReactRouterOptions = Effect.fn('react-router.get-options')(function* (cwd: string) { - const path = yield* Path.Path - const fs = yield* FileSystem.FileSystem - - // exist functions dir - const isReactRouter = yield* fs.exists(path.join(cwd, 'root.tsx')) - - if (isReactRouter) { - // check src-tauri directory exists - const isDesktop = yield* fs.exists(path.join(cwd, 'src-tauri/tauri.conf.json')) - // TODO: more logic to check if it's a spa mode - const isSpaMode = isDesktop - - return { isSpaMode, isDesktop } - } - - return { isSpaMode: false, isDesktop: false } -}) - -const checkProjectType = Effect.fn('checkProjectType')(function* (cwd: string) { - const path = yield* Path.Path - const fs = yield* FileSystem.FileSystem - - const isReactRouter = yield* fs.exists(path.join(cwd, 'root.tsx')) - - if (isReactRouter) { - return { - type: 'react-router', - command: yield* Effect.promise(() => import('./react-router/subcommand')).pipe( - Effect.withSpan('subcommand.react-router-serve.import'), - ), - } as const - } - - return { - type: 'workers', - command: yield* Effect.promise(() => import('./workers/subcommand')).pipe( - Effect.withSpan('subcommand.workers-serve.import'), - ), - } as const -}) - -const loadDistProject = Effect.fn('loadDistProject')(function* (cwd: string) { - const fs = yield* FileSystem.FileSystem - const path = yield* Path.Path - - const workspace = yield* Workspace.make(cwd) - const [distExist, buildConfig] = yield* Effect.all([ - fs.exists(workspace.projectOutput.dist), - fs.exists(path.join(workspace.projectOutput.dist, 'build.json')), - ]) - - if (!distExist) { - return yield* Effect.dieMessage('dist folder not found') - } - - if (!buildConfig) { - return yield* Effect.dieMessage('build config not found') - } - - const buildJson = yield* fs.readFileString(path.join(workspace.projectOutput.dist, 'build.json')).pipe( - Effect.map(JSON.parse), - Effect.mapError((e: any) => new Error(`Failed to read build.json: ${e.message}`)), - ) - - const parse = Schema.decodeUnknown(Commands.TargetSchema) - - const buildTarget = yield* parse(buildJson) - - process.env.NODE_ENV = 'production' - process.env.STAGE = buildTarget.stage - - return { - buildTarget, - workspace, - } -}) - -const thingServe = Command.make( - 'serve', - { - cwd: cwdOption, - verbose: verboseOption, - }, - Effect.fn('command.serve')(function* (options) { - yield* Effect.annotateCurrentSpan({ - cwd: options.cwd, - }) - - process.env.STAGE = 'test' - process.env.NODE_ENV = 'development' - - const { type, command } = yield* checkProjectType(options.cwd) - - let target: Commands.BuildTarget - - if (type === 'react-router') { - const reactRouterOptions = yield* getReactRouterOptions(options.cwd) - - if (reactRouterOptions.isDesktop) { - process.env.DESKTOP = 'true' - } - - target = Commands.BuildReactRouterTarget({ - runtime: 'cloudflare-workers', - options: reactRouterOptions, - stage: process.env.STAGE, - }) - } else { - target = Commands.BuildWorkersTarget({ - runtime: 'cloudflare-workers', - options: {}, - stage: process.env.STAGE, - }) - } - - const subcommand = Commands.ServeSubcommand({ - ...options, - target, - }) - - const workspace = yield* Workspace.make(subcommand.cwd) - - return yield* command.serve(workspace, subcommand) - }), -).pipe(Command.withDescription('Start development server with hot reload')) - -const thingBuild = Command.make( - 'build', - { - cwd: cwdOption, - nodeEnv: nodeEnvOption, - stage: stageOption, - minify: minifyOption, - analyze: analyzeOption, - runtime: RuntimeOption, - verbose: verboseOption, - }, - Effect.fn('command.build')(function* (options) { - yield* detectStage(options.stage) - - process.env.NODE_ENV = options.nodeEnv - - const { type, command } = yield* checkProjectType(options.cwd) - - yield* Effect.annotateCurrentSpan({ - type, - cwd: options.cwd, - runtime: options.runtime, - stage: options.stage, - minify: options.minify, - analyze: options.analyze, - }) - - const workspace = yield* Workspace.make(options.cwd) - - if (type === 'react-router') { - const reactRouterOptions = yield* getReactRouterOptions(options.cwd) - - if (reactRouterOptions.isDesktop) { - process.env.DESKTOP = 'true' - } - - const target = Commands.BuildReactRouterTarget({ - runtime: options.runtime, - options: reactRouterOptions, - stage: process.env.STAGE, - }) - - const subcommand = Commands.BuildSubcommand({ - ...options, - stage: process.env.STAGE, - target, - }) - - yield* command.build(workspace, subcommand) - } else { - const target = Commands.BuildWorkersTarget({ - runtime: options.runtime, - options: {}, - stage: process.env.STAGE, - }) - - const subcommand = Commands.BuildSubcommand({ - ...options, - target, - stage: process.env.STAGE, - analyze: false, - }) - - yield* command.build(workspace, subcommand) - } - }), -).pipe(Command.withDescription('Build project for production deployment (outputs to dist/ with build.json config)')) - -const thingDeploy = Command.make( - 'deploy', - { - cwd: cwdOption, - verbose: verboseOption, - }, - Effect.fn('command.deploy')(function* (options) { - yield* Effect.annotateCurrentSpan({ - cwd: options.cwd, - verbose: options.verbose, - }) - - const { buildTarget, workspace } = yield* loadDistProject(options.cwd) - - const subcommand = Commands.DeploySubcommand({ - ...options, - }) - - const { command } = yield* checkProjectType(options.cwd) - - yield* command.deploy(workspace, subcommand, buildTarget) - }), -).pipe(Command.withDescription('Deploy built project to production (requires existing dist/ folder from build)')) - -const thingPreview = Command.make( - 'preview', - { - cwd: cwdOption, - verbose: verboseOption, - }, - Effect.fn('command.preview')(function* (options) { - yield* Effect.annotateCurrentSpan({ - cwd: options.cwd, - verbose: options.verbose, - }) - - const { buildTarget, workspace } = yield* loadDistProject(options.cwd) - - const subcommand = Commands.PreviewSubcommand({ - ...options, - }) - - const { command } = yield* checkProjectType(options.cwd) - - return yield* command.preview(workspace, subcommand, buildTarget) - }), -).pipe(Command.withDescription('Preview built project locally before deployment (requires existing dist/ folder)')) - -// Database - -const databaseOption = Options.text('database').pipe( - Options.withAlias('db'), - Options.withDescription('Database name (optional, uses default if not specified)'), - Options.withDefault(''), -) - -const dbSeed = Command.make( - 'seed', - { - cwd: cwdOption, - database: databaseOption, - file: Options.file('file').pipe( - Options.withDescription('Specific seed file to execute'), - Options.withDefault(undefined), - ), - }, - Effect.fn('command.db-seed')(function* (options) { - yield* Effect.annotateCurrentSpan({ - database: options.database, - hasFile: !!options.file, - }) - - yield* detectStage() - - const subcommand = Commands.DatabaseSeedSubcommand({ - ...options, - }) - - const workspace = yield* Workspace.make(subcommand.cwd) - - const command = yield* Effect.promise(() => import('./database/subcommand')) - - yield* command.seed(workspace, subcommand) - }), -).pipe(Command.withDescription('Populate database with seed data')) - -const dbPush = Command.make( - 'push', - { - cwd: cwdOption, - database: databaseOption, - skipSeed: Options.boolean('skip-seed').pipe( - Options.withDescription('Skip seeding the database'), - Options.withDefault(false), - ), - skipDump: Options.boolean('skip-dump').pipe( - Options.withDescription('Skip dumping the database'), - Options.withDefault(false), - ), - }, - Effect.fn('command.db-push')(function* (options) { - yield* Effect.annotateCurrentSpan({ - database: options.database, - skipSeed: options.skipSeed, - skipDump: options.skipDump, - }) - - yield* detectStage() - - const subcommand = Commands.DatabasePushSubcommand({ - ...options, - }) - - const workspace = yield* Workspace.make(subcommand.cwd) - - const command = yield* Effect.promise(() => import('./database/subcommand')) - - yield* command.push(workspace, subcommand) - }), -).pipe(Command.withDescription('Push schema changes to database (dev migration)')) - -const dbExecute = Command.make( - 'execute', - { - cwd: cwdOption, - database: databaseOption, - sql: Options.text('sql').pipe(Options.withDescription('The SQL to execute')), - file: Options.file('file').pipe(Options.withDescription('The file to execute')), - }, - Effect.fn('command.db-execute')(function* (options) { - yield* Effect.annotateCurrentSpan({ - database: options.database, - hasFile: !!options.file, - hasSql: !!options.sql, - }) - - yield* detectStage() - - const subcommand = Commands.DatabaseExecuteSubcommand({ - ...options, - }) - - const workspace = yield* Workspace.make(subcommand.cwd) - const command = yield* Effect.promise(() => import('./database/subcommand')) - - yield* command.execute(workspace, subcommand) - }), -).pipe(Command.withDescription('Execute SQL query or file against database')) - -const dbDump = Command.make( - 'dump', - { - cwd: cwdOption, - database: databaseOption, - }, - Effect.fn('command.db-dump')(function* (options) { - yield* Effect.annotateCurrentSpan({ - database: options.database, - }) - - yield* detectStage() - - const subcommand = Commands.DatabaseDumpSubcommand({ - ...options, - }) - - const workspace = yield* Workspace.make(subcommand.cwd) - const command = yield* Effect.promise(() => import('./database/subcommand')) - - yield* command.dump(workspace, subcommand) - }), -).pipe(Command.withDescription('Export database schema and data to files')) - -const dbMigrateDev = Command.make( - 'dev', - { - cwd: cwdOption, - database: databaseOption, - migrationName: Options.text('migration-name').pipe( - Options.withDescription('The migration name'), - Options.withAlias('name'), - ), - skipSeed: Options.boolean('skip-seed').pipe( - Options.withDescription('Skip seeding the database'), - Options.withDefault(false), - ), - skipDump: Options.boolean('skip-dump').pipe( - Options.withDescription('Skip dumping the database'), - Options.withDefault(false), - ), - }, - Effect.fn('command.db-migrate')(function* (options) { - yield* Effect.annotateCurrentSpan({ - database: options.database, - migrationName: options.migrationName, - skipSeed: options.skipSeed, - skipDump: options.skipDump, - }) - - yield* detectStage() - - const subcommand = Commands.DatabaseMigrateDevSubcommand({ - ...options, - }) - - const workspace = yield* Workspace.make(subcommand.cwd) - const command = yield* Effect.promise(() => import('./database/subcommand')) - - yield* command.dev(workspace, subcommand) - }), -).pipe(Command.withDescription('Create and apply new migration for development')) - -const dbReset = Command.make( - 'reset', - { - cwd: cwdOption, - database: databaseOption, - skipSeed: Options.boolean('skip-seed').pipe( - Options.withDescription('Skip seeding the database'), - Options.withDefault(false), - ), - }, - Effect.fn('command.db-reset')(function* (options) { - yield* Effect.annotateCurrentSpan({ - database: options.database, - skipSeed: options.skipSeed, - }) - - yield* detectStage() - - const subcommand = Commands.DatabaseMigrateResetSubcommand({ - ...options, - }) - - const workspace = yield* Workspace.make(subcommand.cwd) - - const command = yield* Effect.promise(() => import('./database/subcommand')) - - yield* command.reset(workspace, subcommand) - }), -).pipe(Command.withDescription('Reset database to initial state and re-run migrations')) -const dbDeploy = Command.make( - 'deploy', - { - cwd: cwdOption, - database: databaseOption, - }, - Effect.fn('command.db-deploy')(function* (options) { - yield* Effect.annotateCurrentSpan({ - database: options.database, - }) - - yield* detectStage() - - const { workspace } = yield* loadDistProject(options.cwd) - - const subcommand = Commands.DatabaseMigrateDeploySubcommand({ - ...options, - }) - - const command = yield* Effect.promise(() => import('./database/subcommand')) - - return yield* command.deploy(workspace, subcommand) - }), -).pipe(Command.withDescription('Deploy migrations to production database')) - -const dbResolve = Command.make( - 'resolve', - { - cwd: cwdOption, - database: databaseOption, - }, - Effect.fn('command.db-resolve')(function* (options) { - yield* Effect.annotateCurrentSpan({ - database: options.database, - }) - - yield* detectStage() - - const subcommand = Commands.DatabaseMigrateResolveSubcommand({ - ...options, - }) - - const workspace = yield* Workspace.make(subcommand.cwd) - const command = yield* Effect.promise(() => import('./database/subcommand')) - - return yield* command.resolve(workspace, subcommand) - }), -).pipe(Command.withDescription('Resolve migration conflicts and mark as applied')) - -const dbMigrate = Command.make('migrate').pipe( - Command.withDescription('Database migration management'), - Command.withSubcommands([dbMigrateDev, dbReset, dbDeploy, dbResolve]), -) - -const thingDB = Command.make('db').pipe( - Command.withDescription('Database commands'), - Command.withSubcommands([dbSeed, dbDump, dbPush, dbExecute, dbMigrate]), -) - -const thingAnalyze = Command.make('analyze').pipe(Command.withDescription('Analyze project structure and dependencies')) - -// build local jsx-email -const emailBuild = Command.make( - 'build', - { - cwd: cwdOption, - }, - Effect.fn('command.email-build')(function* (options) { - const subcommand = Commands.EmailBuildSubcommand({ - ...options, - }) - - const workspace = yield* Workspace.make(subcommand.cwd) - const command = yield* Effect.promise(() => import('./email')).pipe( - Effect.withSpan('subcommand.email-build.import'), - ) - - yield* command.build(workspace, subcommand) - }), -).pipe(Command.withDescription('Build JSX email templates for deployment')) - -const emailDeploy = Command.make( - 'deploy', - { - cwd: cwdOption, - stage: stageOption, - }, - Effect.fn('command.email-deploy')(function* (options) { - yield* detectStage(options.stage) - - const subcommand = Commands.EmailDeploySubcommand({ - ...options, - stage: process.env.STAGE, - }) - - const workspace = yield* Workspace.make(subcommand.cwd) - const command = yield* Effect.promise(() => import('./email')).pipe( - Effect.withSpan('subcommand.email-deploy.import'), - ) - - yield* command.deploy(workspace, subcommand) - }), -).pipe(Command.withDescription('Deploy JSX email templates to specified stage')) - -const thingEmail = Command.make('email').pipe( - Command.withDescription('JSX email template management'), - Command.withSubcommands([emailBuild, emailDeploy]), -) - -const projectOption = Options.text('project').pipe( - Options.withDescription('The project name (e.g., infra-emailer, pkgs-atom-react)'), -) - -const projectAllOption = Options.boolean('all').pipe( - Options.withDescription('Run all tests for project'), - Options.withDefault(false), -) - -const modeOption = Options.choice('mode', ['unit', 'e2e', 'browser'] as const).pipe( - Options.withDescription('The test mode to run'), - Options.withDefault('unit'), -) - -const watchOption = Options.boolean('watch').pipe(Options.withDefault(false)) - -const headlessOption = Options.boolean('headless', { ifPresent: false }).pipe( - Options.withDescription('Run browser tests in headless mode'), - Options.withDefault(true), -) - -const browserOption = Options.choice('browser', ['chromium', 'firefox', 'webkit', 'all']).pipe( - Options.withDescription('Browser to use for tests'), - Options.withDefault('chromium'), -) - -const thingTest = Command.make('test', { - project: projectOption, - all: projectAllOption, - mode: modeOption, - watch: watchOption, - headless: headlessOption, - browser: browserOption, -}).pipe( - Command.withHandler((options) => - Effect.gen(function* () { - const subcommand = Commands.TestSubcommand({ ...options }) - const command = yield* Effect.promise(() => import('./vitest/subcommand')).pipe( - Effect.withSpan('subcommand.vitest.import'), - ) - - yield* command.runTest(subcommand) - }), +import { buildCommand, deployCommand, previewCommand, serveCommand } from './app/command' +import { databaseCommand } from './database/command' +import { emailCommand } from './emails/command' +import { nativeCommand } from './react-native/command' +import { testCommand } from './vitest/command' + +const rootOptions = { + tsconfig: Options.text('tsconfig').pipe( + Options.withDescription('Optional tsconfig path forwarded to tooling (for compatibility)'), + Options.withDefault(undefined), ), - Command.withDescription('Run tests for a specific project and mode'), -) +} -export const xdevCommand = Command.make('xdev').pipe( +export const xdevCommand = Command.make('xdev', rootOptions).pipe( Command.withSubcommands([ - thingServe, - thingBuild, - thingPreview, - thingDeploy, - thingDB, - thingAnalyze, - thingEmail, - thingTest, + serveCommand, + buildCommand, + deployCommand, + previewCommand, + databaseCommand, + emailCommand, + testCommand, + nativeCommand, ]), ) diff --git a/scripts/thing/cloudflare/api.ts b/scripts/thing/cloudflare/api.ts index f2ad6fd..9ce063b 100644 --- a/scripts/thing/cloudflare/api.ts +++ b/scripts/thing/cloudflare/api.ts @@ -47,7 +47,10 @@ const make = Effect.gen(function* () { return CloudflareError.fromApiError(e as APIError) } - return new CloudflareError({ status: 500, errors: [{ code: 500, message: JSON.stringify(e) }] }) + return new CloudflareError({ + status: 500, + errors: [{ code: 500, message: JSON.stringify(e) }], + }) }, }) @@ -168,7 +171,7 @@ const make = Effect.gen(function* () { }), ).pipe(Effect.withSpan('cloudflare.createWorkersTmpVersion')) - yield* Effect.logDebug('Update worker environment variables success') + yield* Effect.logInfo('Update worker environment variables success') return { version: newVersion, worker } }) @@ -213,7 +216,7 @@ const make = Effect.gen(function* () { .sort((a, b) => (b.number ?? 0) - (a.number ?? 0)) if (apiVersions.length === 0 && sortedNonApiVersions.length <= keepLatest) { - yield* Effect.logDebug('cloudflare.cleanup-worker-versions skipped').pipe( + yield* Effect.logInfo('cloudflare.cleanup-worker-versions skipped').pipe( Effect.annotateLogs({ workerId, keepLatest, @@ -233,11 +236,11 @@ const make = Effect.gen(function* () { const staleVersions = Array.from(staleVersionMap.values()) if (staleVersions.length === 0) { - yield* Effect.logDebug('cloudflare.cleanup-worker-versions skipped') + yield* Effect.logInfo('cloudflare.cleanup-worker-versions skipped') return } - yield* Effect.logDebug('cloudflare.cleanup-worker-versions start').pipe( + yield* Effect.logInfo('cloudflare.cleanup-worker-versions start').pipe( Effect.annotateLogs({ workerId, keepLatest, @@ -252,7 +255,7 @@ const make = Effect.gen(function* () { (version) => deleteWorkerVersion(workerId, version.id).pipe( Effect.zipRight( - Effect.logDebug('Deleted worker version').pipe(Effect.annotateLogs({ workerId, versionId: version.id })), + Effect.logInfo('Deleted worker version').pipe(Effect.annotateLogs({ workerId, versionId: version.id })), ), Effect.catchAllCause((cause) => Effect.logWarning('Failed to delete worker version', cause)), ), diff --git a/scripts/thing/cloudflare/workers.ts b/scripts/thing/cloudflare/workers.ts index 21ff16e..8ed0049 100644 --- a/scripts/thing/cloudflare/workers.ts +++ b/scripts/thing/cloudflare/workers.ts @@ -2,7 +2,7 @@ import { FileSystem, Path } from '@effect/platform' import { Effect, pipe } from 'effect' import type { Unstable_Config } from 'wrangler' import type { NodeEnv, Stage } from '../domain' -import { shellInPath } from '../utils' +import { shellInPath } from '../utils/shell' import { CF } from './api' const workersUploadBlackList = ['build.json', '.env'] @@ -102,7 +102,7 @@ export const runWorkersDeploy = Effect.fn('cloudflare.workers-deploy')(function* $ ${deployCommand} `.pipe(Effect.withSpan('cloudflare.deploy')) - yield* Effect.logDebug('Executing Cloudflare Worker deploy').pipe(Effect.annotateLogs('deployArgs', deployArgs)) + yield* Effect.logInfo('Executing Cloudflare Worker deploy').pipe(Effect.annotateLogs('deployArgs', deployArgs)) yield* pipe( backup, @@ -131,5 +131,5 @@ export const runWorkersDeploy = Effect.fn('cloudflare.workers-deploy')(function* ), ) - yield* Effect.logDebug('Cloudflare Workers Deployment created') + yield* Effect.logInfo('Cloudflare Workers Deployment created') }) diff --git a/scripts/thing/cloudflare/wrangler.ts b/scripts/thing/cloudflare/wrangler.ts index f47fbb0..87eb7ec 100644 --- a/scripts/thing/cloudflare/wrangler.ts +++ b/scripts/thing/cloudflare/wrangler.ts @@ -209,6 +209,8 @@ export const getPlatformProxy = Effect.fn('wrangler.get-platform-proxy')(functio export const unstableDev = Effect.fn('wrangler.unstable-dev')(function* (script: string, options: Unstable_DevOptions) { const stop = Effect.fn('wrangler.stop-dev-worker')(function* (_: Unstable_DevWorker) { yield* Effect.promise(() => _.stop()).pipe(Effect.timeout(500), Effect.ignore) + + yield* Effect.log('Dev Worker stopped 🛏️') }) yield* Effect.acquireRelease( diff --git a/scripts/thing/core/env.ts b/scripts/thing/core/env.ts new file mode 100644 index 0000000..554a561 --- /dev/null +++ b/scripts/thing/core/env.ts @@ -0,0 +1,7 @@ +import { Schema } from 'effect' + +export const Stage = Schema.Literal('production', 'staging', 'test') +export type Stage = typeof Stage.Type + +export const NodeEnv = Schema.Literal('development', 'production') +export type NodeEnv = typeof NodeEnv.Type diff --git a/scripts/thing/database/command.ts b/scripts/thing/database/command.ts new file mode 100644 index 0000000..c802b5b --- /dev/null +++ b/scripts/thing/database/command.ts @@ -0,0 +1,177 @@ +import { Command } from '@effect/cli' +import { Effect } from 'effect' +import { + dbCwdOption, + dbDatabaseOption, + dbFileOption, + dbMigrationNameOption, + dbSkipDumpOption, + dbSkipSeedOption, + dbSqlOption, +} from './options' +import { + DatabaseDumpSubcommand, + DatabaseExecuteSubcommand, + DatabaseMigrateDeploySubcommand, + DatabaseMigrateDevSubcommand, + DatabaseMigrateResetSubcommand, + DatabaseMigrateResolveSubcommand, + DatabasePushSubcommand, + DatabaseSeedSubcommand, +} from './domain' +import * as Database from './subcommand' +import * as Workspace from '../workspace' +import type { Workspace as WorkspaceModel } from '../workspace' + +const withWorkspace = (cwd: string, f: (workspace: WorkspaceModel) => Effect.Effect) => + Effect.gen(function* () { + const workspace = yield* Workspace.make(cwd) + return yield* f(workspace) + }) + +const seedCommand = Command.make( + 'seed', + { cwd: dbCwdOption, database: dbDatabaseOption, file: dbFileOption }, + (config) => + withWorkspace(config.cwd, (workspace) => + Database.seed( + workspace, + DatabaseSeedSubcommand({ + cwd: config.cwd, + database: config.database, + file: config.file, + }), + ), + ), +) + +const pushCommand = Command.make( + 'push', + { + cwd: dbCwdOption, + database: dbDatabaseOption, + skipSeed: dbSkipSeedOption, + skipDump: dbSkipDumpOption, + }, + (config) => + withWorkspace(config.cwd, (workspace) => + Database.push( + workspace, + DatabasePushSubcommand({ + cwd: config.cwd, + database: config.database, + skipSeed: config.skipSeed, + skipDump: config.skipDump, + }), + ), + ), +) + +const dumpCommand = Command.make('dump', { cwd: dbCwdOption, database: dbDatabaseOption }, (config) => + withWorkspace(config.cwd, (workspace) => + Database.dump( + workspace, + DatabaseDumpSubcommand({ + cwd: config.cwd, + database: config.database, + }), + ), + ), +) + +const executeCommand = Command.make( + 'execute', + { cwd: dbCwdOption, database: dbDatabaseOption, sql: dbSqlOption, file: dbFileOption }, + (config) => + Effect.flatMap( + config.sql || config.file + ? Effect.succeed(undefined) + : Effect.dieMessage('Provide either --sql or --file for db execute'), + () => + withWorkspace(config.cwd, (workspace) => + Database.execute( + workspace, + DatabaseExecuteSubcommand({ + cwd: config.cwd, + database: config.database, + sql: config.sql, + file: config.file, + }), + ), + ), + ), +) + +const migrateDevCommand = Command.make( + 'dev', + { + cwd: dbCwdOption, + database: dbDatabaseOption, + skipSeed: dbSkipSeedOption, + skipDump: dbSkipDumpOption, + migrationName: dbMigrationNameOption, + }, + (config) => + withWorkspace(config.cwd, (workspace) => + Database.dev( + workspace, + DatabaseMigrateDevSubcommand({ + cwd: config.cwd, + database: config.database, + skipSeed: config.skipSeed, + skipDump: config.skipDump, + migrationName: config.migrationName, + }), + ), + ), +) + +const migrateResetCommand = Command.make( + 'reset', + { cwd: dbCwdOption, database: dbDatabaseOption, skipSeed: dbSkipSeedOption }, + (config) => + withWorkspace(config.cwd, (workspace) => + Database.reset( + workspace, + DatabaseMigrateResetSubcommand({ + cwd: config.cwd, + database: config.database, + skipSeed: config.skipSeed, + }), + ), + ), +) + +const migrateDeployCommand = Command.make('deploy', { cwd: dbCwdOption, database: dbDatabaseOption }, (config) => + withWorkspace(config.cwd, (workspace) => + Database.deploy( + workspace, + DatabaseMigrateDeploySubcommand({ + cwd: config.cwd, + database: config.database, + }), + ), + ), +) + +const migrateResolveCommand = Command.make('resolve', { cwd: dbCwdOption, database: dbDatabaseOption }, (config) => + withWorkspace(config.cwd, (workspace) => + Database.resolve( + workspace, + DatabaseMigrateResolveSubcommand({ + cwd: config.cwd, + database: config.database, + }), + ), + ), +) + +const migrateCommand = Command.make('migrate').pipe( + Command.withSubcommands([migrateDevCommand, migrateResetCommand, migrateDeployCommand, migrateResolveCommand]), +) + +const databaseCommand = Command.make('db').pipe( + Command.withSubcommands([seedCommand, pushCommand, dumpCommand, executeCommand, migrateCommand]), +) + +export { databaseCommand } diff --git a/scripts/thing/database/domain.ts b/scripts/thing/database/domain.ts new file mode 100644 index 0000000..597a1a9 --- /dev/null +++ b/scripts/thing/database/domain.ts @@ -0,0 +1,73 @@ +import { Data } from 'effect' + +export interface DatabaseSeedSubcommand { + readonly _tag: 'DatabaseSeedSubcommand' + readonly cwd: string + readonly database: string | undefined + readonly file: string | undefined +} +export const DatabaseSeedSubcommand = Data.tagged('DatabaseSeedSubcommand') + +export interface DatabasePushSubcommand { + readonly _tag: 'DatabasePushSubcommand' + readonly cwd: string + readonly database: string | undefined + readonly skipSeed: boolean + readonly skipDump: boolean +} +export const DatabasePushSubcommand = Data.tagged('DatabasePushSubcommand') + +export interface DatabaseDumpSubcommand { + readonly _tag: 'DatabaseDumpSubcommand' + readonly cwd: string + readonly database: string | undefined +} +export const DatabaseDumpSubcommand = Data.tagged('DatabaseDumpSubcommand') + +export interface DatabaseExecuteSubcommand { + readonly _tag: 'DatabaseExecuteSubcommand' + readonly cwd: string + readonly sql: string | undefined + readonly file: string | undefined + readonly database: string | undefined +} +export const DatabaseExecuteSubcommand = Data.tagged('DatabaseExecuteSubcommand') + +// Migrate +export interface DatabaseMigrateDevSubcommand { + readonly _tag: 'DatabaseMigrateDevSubcommand' + readonly cwd: string + readonly database: string | undefined + readonly migrationName: string + readonly skipSeed: boolean + readonly skipDump: boolean +} +export const DatabaseMigrateDevSubcommand = Data.tagged('DatabaseMigrateDevSubcommand') + +export interface DatabaseMigrateResetSubcommand { + readonly _tag: 'DatabaseMigrateResetSubcommand' + readonly cwd: string + readonly database: string | undefined + readonly skipSeed: boolean +} +export const DatabaseMigrateResetSubcommand = Data.tagged( + 'DatabaseMigrateResetSubcommand', +) + +export interface DatabaseMigrateDeploySubcommand { + readonly _tag: 'DatabaseMigrateDeploySubcommand' + readonly cwd: string + readonly database: string | undefined +} +export const DatabaseMigrateDeploySubcommand = Data.tagged( + 'DatabaseMigrateDeploySubcommand', +) + +export interface DatabaseMigrateResolveSubcommand { + readonly _tag: 'DatabaseMigrateResolveSubcommand' + readonly cwd: string + readonly database: string | undefined +} +export const DatabaseMigrateResolveSubcommand = Data.tagged( + 'DatabaseMigrateResolveSubcommand', +) diff --git a/scripts/thing/database/options.ts b/scripts/thing/database/options.ts new file mode 100644 index 0000000..f1afbfe --- /dev/null +++ b/scripts/thing/database/options.ts @@ -0,0 +1,45 @@ +import { Options } from '@effect/cli' + +const dbCwdOption = Options.text('cwd').pipe( + Options.withDescription('Workspace-relative path to the project with db/ folder'), +) + +const dbDatabaseOption = Options.text('database').pipe( + Options.withDescription('Optional database name from wrangler.jsonc'), + Options.withDefault(undefined), +) + +const dbFileOption = Options.text('file').pipe( + Options.withDescription('Relative path to a file used by the command (seed or SQL script)'), + Options.withDefault(undefined), +) + +const dbSqlOption = Options.text('sql').pipe( + Options.withDescription('Inline SQL command to execute'), + Options.withDefault(undefined), +) + +const dbSkipSeedOption = Options.boolean('skip-seed').pipe( + Options.withDescription('Skip running seeds after push operations'), + Options.withDefault(false), +) + +const dbSkipDumpOption = Options.boolean('skip-dump').pipe( + Options.withDescription('Skip writing Prisma schema dumps after push operations'), + Options.withDefault(false), +) + +const dbMigrationNameOption = Options.text('migration-name').pipe( + Options.withDescription('Name to use for newly generated migrations'), + Options.withDefault('migration'), +) + +export { + dbCwdOption, + dbDatabaseOption, + dbMigrationNameOption, + dbFileOption, + dbSkipDumpOption, + dbSkipSeedOption, + dbSqlOption, +} diff --git a/scripts/thing/database/prisma.ts b/scripts/thing/database/prisma.ts new file mode 100644 index 0000000..faad10b --- /dev/null +++ b/scripts/thing/database/prisma.ts @@ -0,0 +1,11 @@ +import * as PrismaInternal from '@prisma/internals' +export type { EngineArgs } from '@prisma/migrate' +import * as PrismaMigrate from '@prisma/migrate' + +const { formatSchema, loadSchemaContext } = ((PrismaInternal as any).default as typeof PrismaInternal) ?? PrismaInternal + +export { formatSchema, loadSchemaContext } + +const { SchemaEngineCLI } = ((PrismaMigrate as any).default as typeof PrismaMigrate) ?? PrismaMigrate + +export { SchemaEngineCLI } diff --git a/scripts/thing/database/subcommand.ts b/scripts/thing/database/subcommand.ts index 72242f5..6152cea 100644 --- a/scripts/thing/database/subcommand.ts +++ b/scripts/thing/database/subcommand.ts @@ -1,10 +1,7 @@ import { FileSystem, Path } from '@effect/platform' import type { SqlError } from '@effect/sql' import * as SqlD1 from '@effect/sql-d1/D1Client' -import { formatSchema, loadSchemaContext } from '@prisma/internals' -import type { EngineArgs } from '@prisma/migrate' -import { Migrate } from '@prisma/migrate' -import { Effect, Exit, Layer, Schedule, String } from 'effect' +import { Effect, Exit, Layer, pipe, Schedule, Scope, String } from 'effect' import { tsImport } from 'tsx/esm/api' import type { Unstable_Config } from 'wrangler' import { generate, type PrismaGenerateOptions } from '../../../packages/db/src/prisma' @@ -20,26 +17,87 @@ import type { DatabaseMigrateResolveSubcommand, DatabasePushSubcommand, DatabaseSeedSubcommand, -} from '../domain' -import { shell, shellInPath } from '../utils' +} from './domain' +import { shell, shellInPath } from '../utils/shell' import type * as Workspace from '../workspace' -import { CaptureStdout } from './capture-stdout' +import { CaptureStdout } from '../utils/capture-stdout' import { formatMigrationName } from './utils' +import * as PrismaMigrate from './prisma' -interface DatabaseConfig { - provider: PrismaGenerateOptions['provider'] - url?: PrismaGenerateOptions['url'] | undefined - runtime: 'd1' | 'browser' | 'server' -} +type DatabaseConfig = + | { + provider: PrismaGenerateOptions['provider'] + runtime: 'd1' + } + | { + provider: PrismaGenerateOptions['provider'] + runtime: 'browser' + } + | { + provider: PrismaGenerateOptions['provider'] + runtime: 'server' + url: string + } type SeedEntry = { start: Effect.Effect } +const getWranglerConfig = Effect.fn('wrangler.get-config')(function* ( + workspace: Workspace.Workspace, + { database }: { database?: string | undefined } = {}, +): Effect.fn.Return< + { + persistRoot: string + persistTo: string + wranglerConfigPath: string + databaseName: string + databaseNameId: string + databaseFile: string + databaseId: string | undefined + previewDatabaseId: string | undefined + }, + never, + Path.Path | Scope.Scope +> { + const path = yield* Path.Path + const wranglerConfigPath = path.join(workspace.projectPath, 'wrangler.jsonc') + + const { config: wranglerConfig, path: foundWranglerConfigPath } = yield* parseConfig( + wranglerConfigPath, + process.env.NODE_ENV, + process.env.STAGE, + ).pipe(Effect.orDie) + + const selectedDatabaseName = database || wranglerConfig.d1_databases[0].database_name + const selectedDatabase = wranglerConfig.d1_databases.find((_) => _.database_name === selectedDatabaseName) + + if (!selectedDatabaseName) { + return yield* Effect.dieMessage('No database name provided') + } + + const databaseNameId = yield* databaseNameToId(wranglerConfig, selectedDatabaseName) + const previewDatabaseId = selectedDatabase?.preview_database_id + const databaseId = selectedDatabase?.database_id + + const persistRoot = path.join(workspace.root, '.wrangler/state') + const persistTo = path.join(persistRoot, 'v3') + const dbFile = path.join(persistTo, 'd1/miniflare-D1DatabaseObject', `${databaseNameId}.sqlite`) + + return { + persistRoot, + persistTo, + wranglerConfigPath: foundWranglerConfigPath, + databaseName: selectedDatabaseName, + databaseNameId, + databaseFile: dbFile, + databaseId, + previewDatabaseId, + } +}) + const devDB = 'dev.db' -// https://vscode.dev/github/prisma/prisma-engines/blob/main/schema-engine/connectors/schema-connector/src/migrations_directory.rs#L30 -// "%Y%m%d%H%M%S" const getMigrationDate = () => { const date = new Date() const year = date.getFullYear() @@ -54,7 +112,9 @@ const getMigrationDate = () => { return `${year}${pad(month)}${pad(day)}${pad(hours)}${pad(minutes)}${pad(seconds)}` } -export const existDatabase = Effect.fn('db.exist-database')(function* (workspace: Workspace.Workspace) { +export const existDatabase = Effect.fn('db.exist-database')(function* ( + workspace: Workspace.Workspace, +): Effect.fn.Return { yield* Effect.annotateCurrentSpan({ projectName: workspace.projectName, }) @@ -66,13 +126,23 @@ export const existDatabase = Effect.fn('db.exist-database')(function* (workspace const migrationsDir = path.join(dbDir, 'migrations') - return yield* fs.exists(migrationsDir) + return yield* fs.exists(migrationsDir).pipe(Effect.orElseSucceed(() => false)) }, Effect.orDie) const detectDatabase = Effect.fn('db.detect-database')(function* ( workspace: Workspace.Workspace, - { databaseName }: { databaseName?: string } = {}, -) { + { databaseName }: { databaseName?: string | undefined } = {}, +): Effect.fn.Return< + { + dbDir: string + migrationsDir: string + tables: Record + config: DatabaseConfig + tsconfigPath: string + }, + never, + Path.Path | FileSystem.FileSystem +> { yield* Effect.annotateCurrentSpan({ projectName: workspace.projectName, databaseName, @@ -84,21 +154,24 @@ const detectDatabase = Effect.fn('db.detect-database')(function* ( const dbDir = path.join(workspace.projectPath, 'db') const migrationsDir = path.join(dbDir, 'migrations') - if (!(yield* fs.exists(migrationsDir))) { - yield* fs.makeDirectory(migrationsDir) - } + yield* pipe( + fs.exists(migrationsDir), + Effect.tap((exists) => (!exists ? fs.makeDirectory(migrationsDir) : Effect.void)), + Effect.orDie, + ) - const tsconfig = path.join(workspace.projectPath, 'tsconfig.app.json') + const tsconfigPath = path.join(workspace.projectPath, 'tsconfig.app.json') const tablesPath = path.join(dbDir, 'tables.ts') const { tables, config } = yield* Effect.promise(() => - tsImport(tablesPath, { parentURL: import.meta.url, tsconfig }), + tsImport(tablesPath, { + parentURL: import.meta.url, + tsconfig: tsconfigPath, + }), ).pipe( Effect.map((_) => { const config = _.config as DatabaseConfig - config.url = config.url || `"file:./${devDB}"` - return { tables: _.tables as TablesRecord, config, @@ -111,57 +184,20 @@ const detectDatabase = Effect.fn('db.detect-database')(function* ( migrationsDir, tables, config, - tsconfig, + tsconfigPath, } }) -const wranglerConfig = Effect.fn('wrangler.get-config')(function* ( - workspace: Workspace.Workspace, - { database }: { database?: string | undefined } = {}, -) { - const path = yield* Path.Path - const wranglerConfigPath = path.join(workspace.projectPath, 'wrangler.jsonc') - const fallbackConfigPath = path.join(workspace.projectRoot, '/web/wrangler.jsonc') - - const { config: wranglerConfig, path: foundWranglerConfigPath } = yield* parseConfig( - [wranglerConfigPath, fallbackConfigPath], - process.env.NODE_ENV, - process.env.STAGE, - ) - - const selectedDatabaseName = database || wranglerConfig.d1_databases[0].database_name - const selectedDatabase = wranglerConfig.d1_databases.find((_) => _.database_name === selectedDatabaseName) - - if (!selectedDatabaseName) { - return yield* Effect.dieMessage('No database name provided') - } - - const databaseNameId = yield* databaseNameToId(wranglerConfig, selectedDatabaseName) - const previewDatabaseId = selectedDatabase?.preview_database_id - const databaseId = selectedDatabase?.database_id - - const persistRoot = path.join(workspace.root, '.wrangler/state') - const persistTo = path.join(workspace.root, '.wrangler/state/v3') - const dbFile = path.join(persistTo, 'd1/miniflare-D1DatabaseObject', `${databaseNameId}.sqlite`) - - return { - persistRoot, - persistTo, - wranglerConfigPath: foundWranglerConfigPath, - databaseName: selectedDatabaseName, - databaseNameId, - databaseFile: dbFile, - databaseId, - previewDatabaseId, - } -}) +type PrismaMigration = { + filepath: string + content: string +} -const getMigrations = Effect.fn('db.get-migrations')(function* (dir: string) { +const getMigrations = Effect.fn('db.get-migrations')(function* ( + dir: string, +): Effect.fn.Return, never, FileSystem.FileSystem | Path.Path> { const fs = yield* FileSystem.FileSystem - - if (!(yield* fs.exists(dir))) { - yield* fs.makeDirectory(dir) - } + const path = yield* Path.Path const migrations = yield* fs.readDirectory(dir).pipe( Effect.map((files) => @@ -191,6 +227,21 @@ const getMigrations = Effect.fn('db.get-migrations')(function* (dir: string) { return 0 }), ), + Effect.flatMap((files) => + Effect.forEach( + files, + Effect.fnUntraced(function* (filename) { + const filepath = path.join(dir, filename) + const content = yield* fs.readFileString(filepath) + + return { + filepath, + content, + } + }), + ), + ), + Effect.orDie, ) return migrations @@ -201,56 +252,50 @@ const syncPrismaSchema = Effect.fn('prisma.sync-schema')(function* ( { dbDir }: { dbDir: string }, config: DatabaseConfig, tables: TablesRecord, -) { +): Effect.fn.Return< + { + prismaPath: string + prisma: string + }, + never, + Path.Path | FileSystem.FileSystem +> { const path = yield* Path.Path const fs = yield* FileSystem.FileSystem - const url = config.url - if (config.runtime === 'server' && !config.url) { return yield* Effect.dieMessage('Missing database url') } - const generated = yield* Effect.try({ - try: () => - generate( - { - provider: config.provider, - - url: url!, - generator: { - markdown: { - title: 'Database Schema', - output: './database-schema.md', - root: path.relative(dbDir, workspace.root), - }, + const generated = yield* Effect.try(() => + generate( + { + provider: config.provider, + generator: { + markdown: { + title: 'Database Schema', + output: './database-schema.md', + root: path.relative(dbDir, workspace.root), }, }, - tables, - ), - catch: (error) => new Error('Failed to generate prisma schema', { cause: error }), - }).pipe( - // format prisma schema + }, + tables, + ), + ).pipe( Effect.andThen((content) => - formatSchema( - { - schemas: [['schema.prisma', content]], - }, - { - insertSpaces: true, - tabSize: 2, - }, - ), + PrismaMigrate.formatSchema({ schemas: [['schema.prisma', content]] }, { insertSpaces: true, tabSize: 2 }), ), Effect.map((result) => result[0][1]), + Effect.orDie, ) + const prismaPath = path.join(dbDir, 'schema.prisma') - yield* fs.writeFileString(prismaPath, generated) + yield* fs.writeFileString(prismaPath, generated).pipe(Effect.orDie) const prismaBin = path.join(workspace.root, 'node_modules/.bin/prisma') yield* shellInPath(dbDir)` - $ ${prismaBin} generate + $ ${prismaBin} generate --schema=./schema.prisma ` yield* Effect.logInfo('Prisma schema generated') @@ -269,18 +314,19 @@ const databaseNameToId = (config: Unstable_Config, name: string) => }) /** - * Only Dev Mode - * Push changes to D1 database + * Push changes to local d1 database */ -const d1Push = Effect.fn('d1-push')(function* ( +const pushD1 = Effect.fn('push-d1')(function* ( workspace: Workspace.Workspace, { sql, database }: { sql: string; database?: string | undefined }, -) { - const { persistRoot, wranglerConfigPath, databaseName } = yield* wranglerConfig(workspace, { database }) +): Effect.fn.Return { + const { persistRoot, wranglerConfigPath, databaseName } = yield* getWranglerConfig(workspace, { + database, + }) const output = yield* shell` - $ wrangler d1 execute ${databaseName} --local --persist-to=${persistRoot} --config=${wranglerConfigPath} --json --command="${sql}" - `.pipe( + $ wrangler d1 execute ${databaseName} --local --persist-to=${persistRoot} --config=${wranglerConfigPath} --json --command="${sql}" + `.pipe( Effect.withSpan('db.d1-execute', { attributes: { projectName: workspace.projectName, @@ -294,12 +340,8 @@ const d1Push = Effect.fn('d1-push')(function* ( if (output.stderr) { yield* Effect.logError('D1 push failed', output.stderr) } else { - yield* Effect.try({ - try: () => JSON.parse(output.stdout), - catch(error) { - return error as Error - }, - }).pipe( + yield* Effect.try(() => JSON.parse(output.stdout)).pipe( + Effect.orDieWith(() => new Error('Failed to parse JSON')), Effect.andThen((result: any[]) => { const allSuccess = result.every((item) => item.success) @@ -307,84 +349,26 @@ const d1Push = Effect.fn('d1-push')(function* ( return Effect.logInfo('D1 push success').pipe(Effect.annotateLogs({ ...result })) } - return Effect.logError('D1 push failed', result) + return Effect.logError('D1 push failed').pipe(Effect.annotateLogs({ ...result })) }), ) } }) -const d1ApplyMigrations = Effect.fn('d1-apply-migrations')(function* ( +/** + * Reset local d1 database + */ +const resetD1 = Effect.fn('reset-d1')(function* ( workspace: Workspace.Workspace, - { deploy = false, database }: { deploy?: boolean; database?: string | undefined } = { deploy: false }, -) { - const isPreview = deploy && process.env.STAGE !== 'production' - const { persistRoot, wranglerConfigPath, databaseName, databaseId, previewDatabaseId } = yield* wranglerConfig( + subcommand: { database: string | undefined }, +): Effect.fn.Return { + const path = yield* Path.Path + const { persistRoot, wranglerConfigPath, databaseName, databaseId, databaseFile } = yield* getWranglerConfig( workspace, - { database }, - ) - - let API_TOKEN = '' - let ACCOUNT_ID = '' - let deployArgs = '' - - if (!deploy) { - deployArgs += ' --local' - deployArgs += ` --persist-to=${persistRoot}` - } else { - const config = yield* CloudflareConfig.pipe(Effect.orDie) - API_TOKEN = config.API_TOKEN - ACCOUNT_ID = config.ACCOUNT_ID - - if (!isPreview) { - deployArgs += ' --remote' - } else { - if (previewDatabaseId) { - deployArgs += ' --preview' - } - - deployArgs += ' --remote' - } - } - - yield* Effect.logInfo('D1 Apply migrations').pipe( - Effect.annotateLogs({ - deployArgs, - databaseName, - databaseId, - previewDatabaseId, - }), - ) - - yield* shell` - $ export CLOUDFLARE_API_TOKEN="${API_TOKEN}" - $ export CLOUDFLARE_ACCOUNT_ID="${ACCOUNT_ID}" - $$ wrangler d1 migrations apply ${databaseName} --config=${wranglerConfigPath} ${deployArgs} - `.pipe( - Effect.retry({ times: 2, schedule: Schedule.spaced('5 seconds') }), - Effect.tap( - Effect.fnUntraced(function* (output) { - if (output.stderr) { - yield* Effect.logError('D1 apply migrations failed', output.stderr) - } else { - if (output.stdout.indexOf('No migrations to apply') > -1) { - yield* Effect.logInfo('D1 apply migrations done') - } else { - yield* Effect.logInfo('D1 apply migrations done', output.stdout) - } - } - }), - ), - Effect.withSpan('d1-migrate-apply'), + { + database: subcommand.database, + }, ) -}) - -const d1Reset = Effect.fn('d1-reset')(function* ( - workspace: Workspace.Workspace, - subcommand: { database: string | undefined }, -) { - const { persistRoot, wranglerConfigPath, databaseName, databaseId, databaseFile } = yield* wranglerConfig(workspace, { - database: subcommand.database, - }) yield* Effect.logInfo('Reset database').pipe( Effect.annotateLogs({ @@ -396,26 +380,37 @@ const d1Reset = Effect.fn('d1-reset')(function* ( const d1MigrationsInit = ` CREATE TABLE IF NOT EXISTS d1_migrations( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT UNIQUE, - applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL - );` + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE, + applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL + ); + ` - const output = yield* shell` + // Ensure directory exists before creating files + const databaseDir = path.dirname(databaseFile) + yield* shell` + $ mkdir -p ${databaseDir} + ` + + // Remove database files if they exist (won't error if they don't) + yield* shell` $ rm -f ${databaseFile} ${databaseFile}-wal ${databaseFile}-shm + `.pipe(Effect.ignore) + + // Create the database file + yield* shell` $ touch ${databaseFile} + ` + + const output = yield* shell` $ wrangler d1 execute ${databaseName} --local --persist-to=${persistRoot} --config=${wranglerConfigPath} --json --command="${d1MigrationsInit}" ` if (output.stderr) { yield* Effect.logError('D1 reset failed', output.stderr) } else { - yield* Effect.try({ - try: () => JSON.parse(output.stdout), - catch(error) { - return error as Error - }, - }).pipe( + yield* Effect.try(() => JSON.parse(output.stdout)).pipe( + Effect.orDieWith(() => new Error('Failed to parse d1 output result')), Effect.andThen((result: any[]) => { const allSuccess = result.every((item) => item.success) @@ -429,29 +424,38 @@ const d1Reset = Effect.fn('d1-reset')(function* ( } }) -const d1Dump = Effect.fn('d1-dump')(function* (workspace: Workspace.Workspace, subcommand: DatabaseDumpSubcommand) { +/** + * Dump D1 database + */ +const dumpD1 = Effect.fn('dump-d1')(function* ( + workspace: Workspace.Workspace, + subcommand: DatabaseDumpSubcommand, +): Effect.fn.Return { const isProd = process.env.NODE_ENV === 'production' const path = yield* Path.Path const fs = yield* FileSystem.FileSystem - const { wranglerConfigPath, databaseName, databaseFile } = yield* wranglerConfig(workspace, { + const { wranglerConfigPath, databaseName, databaseFile } = yield* getWranglerConfig(workspace, { database: subcommand.database, }) - const dbDir = path.join(workspace.projectPath, 'db') + const { dbDir } = yield* detectDatabase(workspace, { + databaseName: subcommand.database, + }) const schemaOutput = path.join(dbDir, 'schema.sql') - const formatSchema = Effect.gen(function* () { - const content = yield* fs.readFileString(schemaOutput) - - yield* fs.writeFileString( - schemaOutput, - content - .replace(/create table sqlite_sequence\(name,seq\);/i, '') - .replace(/create table _cf_KV[\s\S]*?\);/im, '') - .replace(/create table _cf_METADATA[\s\S]*?\);/im, '') - .replace(/\n{2,}/gm, '\n') - .trim(), - ) - }) + const formatSchema = fs.readFileString(schemaOutput).pipe( + Effect.flatMap((content) => + fs.writeFileString( + schemaOutput, + content + .replace(/create table sqlite_sequence\(name,seq\);/i, '') + .replace(/create table _cf_KV[\s\S]*?\);/im, '') + .replace(/create table _cf_METADATA[\s\S]*?\);/im, '') + .replace(/\n{2,}/gm, '\n') + .trim(), + ), + ), + Effect.orDie, + ) if (isProd) { let args = `--config=${wranglerConfigPath} --no-data --remote` @@ -482,29 +486,144 @@ const d1Dump = Effect.fn('d1-dump')(function* (workspace: Workspace.Workspace, s } }) -const prismaApplyMigrations = Effect.fn('prisma.apply-migrations')(function* ( +const applyD1Migrations = Effect.fn('apply-d1-migrations')(function* ( + workspace: Workspace.Workspace, + { + deploy = false, + reset = false, + database, + }: { + deploy?: boolean | undefined + reset?: boolean | undefined + database?: string | undefined + } = { deploy: false }, +): Effect.fn.Return { + const isPreview = deploy && process.env.STAGE !== 'production' + const { persistRoot, wranglerConfigPath, databaseName, databaseId, previewDatabaseId } = yield* getWranglerConfig( + workspace, + { database }, + ) + + let API_TOKEN = '' + let ACCOUNT_ID = '' + let deployArgs = '' + + if (!deploy) { + deployArgs += ' --local' + deployArgs += ` --persist-to=${persistRoot}` + } else { + const config = yield* CloudflareConfig.pipe(Effect.orDie) + API_TOKEN = config.API_TOKEN + ACCOUNT_ID = config.ACCOUNT_ID + + if (!isPreview) { + deployArgs += ' --remote' + } else { + if (previewDatabaseId) { + deployArgs += ' --preview' + } + + deployArgs += ' --remote' + } + } + + yield* Effect.logInfo('D1 Apply migrations').pipe( + Effect.annotateLogs({ + deployArgs, + databaseName, + databaseId, + previewDatabaseId, + }), + ) + + if (!deploy && reset) { + yield* resetD1(workspace, { database }) + } + + yield* shell` + $ export CLOUDFLARE_API_TOKEN="${API_TOKEN}" + $ export CLOUDFLARE_ACCOUNT_ID="${ACCOUNT_ID}" + $$ wrangler d1 migrations apply ${databaseName} --config=${wranglerConfigPath} ${deployArgs} + `.pipe( + Effect.retry({ times: 2, schedule: Schedule.spaced('5 seconds') }), + Effect.tap( + Effect.fnUntraced(function* (output) { + if (output.stderr) { + yield* Effect.logError('D1 apply migrations failed', output.stderr) + } else { + if (output.stdout.indexOf('No migrations to apply') > -1) { + yield* Effect.logInfo('D1 apply migrations done') + } else { + yield* Effect.logInfo('D1 apply migrations done', output.stdout) + } + } + }), + ), + Effect.withSpan('d1-migrate-apply'), + ) +}) + +const applyPrismaMigrations = Effect.fn('apply-prisma-migrations')(function* ( _workspace: Workspace.Workspace, - { dbDir }: { dbDir: string }, - { reset = false }: { reset?: boolean } = { reset: false }, + { + dbDir, + datasource, + migrations, + reset = false, + }: { + dbDir: string + datasource: { url: string } + migrations: Array + reset?: boolean | undefined + }, ) { const path = yield* Path.Path - const prismaSchemaPath = path.join(dbDir, 'schema.prisma') - + const fs = yield* FileSystem.FileSystem + const lockFileContent = yield* fs.readFileString(path.join(dbDir, 'migration_lock.toml'), 'utf8') const schemaContext = yield* Effect.promise(() => - loadSchemaContext({ - schemaPathFromConfig: prismaSchemaPath, + PrismaMigrate.loadSchemaContext({ + schemaPath: { baseDir: dbDir }, + cwd: dbDir, }), ) + const prismaFilter = { externalEnums: [], externalTables: [] } + yield* Effect.acquireUseRelease( - Effect.promise(() => Migrate.setup({ schemaContext, configDir: dbDir, schemaEngineConfig: {} })), + Effect.promise(() => + PrismaMigrate.SchemaEngineCLI.setup({ + schemaContext, + baseDir: dbDir, + datasource: { url: datasource.url }, + }), + ), (migrate) => - Effect.suspend(() => (reset ? Effect.promise(() => migrate.reset()) : Effect.void)).pipe( + pipe( + Effect.suspend(() => (reset ? Effect.promise(() => migrate.reset({ filter: prismaFilter })) : Effect.void)), Effect.andThen( Effect.promise(() => { const captureStdout = new CaptureStdout() captureStdout.startCapture() - const output = migrate.applyMigrations() as Promise<{ + const output = migrate.applyMigrations({ + filters: prismaFilter, + migrationsList: { + baseDir: dbDir, + lockfile: { + content: lockFileContent, + path: 'migration_lock.toml', + }, + migrationDirectories: migrations.map((_) => { + return { + path: _.filepath, + migrationFile: { + path: 'migration.sql', + content: { tag: 'ok', value: _.content }, + }, + } + }), + shadowDbInitScript: '', + }, + }) as Promise<{ appliedMigrationNames: string[] }> @@ -529,25 +648,28 @@ const prismaApplyMigrations = Effect.fn('prisma.apply-migrations')(function* ( export const seed = Effect.fn('db.seed')(function* ( workspace: Workspace.Workspace, subcommand: DatabaseSeedSubcommand, -) { +): Effect.fn.Return { const path = yield* Path.Path const fs = yield* FileSystem.FileSystem - const { dbDir, tsconfig, config } = yield* detectDatabase(workspace) + const { dbDir, tsconfigPath, config } = yield* detectDatabase(workspace, { + databaseName: subcommand.database, + }) - // default seed file const defaultSeedFile = path.join(dbDir, 'seed.ts') - if (!(yield* fs.exists(defaultSeedFile))) { - yield* fs.writeFileString(defaultSeedFile, 'export const start = () => {}') - } + yield* fs.exists(defaultSeedFile).pipe( + Effect.tap((exists) => + exists ? fs.writeFileString(defaultSeedFile, 'export const start = () => {}') : Effect.void, + ), + Effect.orDie, + ) const seedPath = subcommand.file ? path.resolve(dbDir, subcommand.file) : defaultSeedFile - const seedExists = yield* fs.exists(seedPath) - if (!seedExists) { - yield* Effect.logInfo('No seed file found').pipe(Effect.annotateLogs('file', seedPath)) - return - } + yield* fs.exists(seedPath).pipe( + Effect.tap((exists) => (!exists ? Effect.dieMessage(`No seed file found: ${seedPath}`) : Effect.void)), + Effect.orDie, + ) if (config.runtime === 'browser') { yield* Effect.logInfo('Skip seed in browser') @@ -555,7 +677,7 @@ export const seed = Effect.fn('db.seed')(function* ( } const seed: SeedEntry = yield* Effect.promise(() => - tsImport(seedPath, { parentURL: import.meta.url, tsconfig }), + tsImport(seedPath, { parentURL: import.meta.url, tsconfig: tsconfigPath }), ).pipe( Effect.withSpan('db.load-seed-file', { attributes: { @@ -567,61 +689,77 @@ export const seed = Effect.fn('db.seed')(function* ( ) if (config.runtime === 'd1') { - const { wranglerConfigPath, persistTo, databaseId } = yield* wranglerConfig(workspace, { + const { wranglerConfigPath, persistTo, databaseId } = yield* getWranglerConfig(workspace, { database: subcommand.database, }) const { Miniflare } = yield* Effect.promise(() => import('miniflare')) const { config } = yield* parseConfig(wranglerConfigPath) - const dev = new Miniflare({ - script: '', - modules: true, - compatibilityDate: '2025-09-25', - defaultPersistRoot: persistTo, - cachePersist: path.join(persistTo, 'cache'), - workflowsPersist: path.join(persistTo, 'workflows'), - d1Persist: path.join(persistTo, 'd1'), - d1Databases: Object.fromEntries( - config.d1_databases.map((_) => { - return [_.binding, _.database_id || ''] - }), - ), - }) - - yield* Effect.promise(() => dev.ready) - const dbBinding = config.d1_databases.find((_) => _.database_id === databaseId)?.binding || 'DB' - const DB = yield* Effect.promise(() => dev.getD1Database(dbBinding)) + yield* Effect.acquireUseRelease( + Effect.gen(function* () { + const miniflare = new Miniflare({ + script: '', + modules: true, + compatibilityDate: '2025-09-25', + defaultPersistRoot: persistTo, + cachePersist: path.join(persistTo, 'cache'), + workflowsPersist: path.join(persistTo, 'workflows'), + d1Persist: path.join(persistTo, 'd1'), + d1Databases: Object.fromEntries( + config.d1_databases.map((_) => { + return [_.binding, _.database_id || ''] + }), + ), + }) - const SeedLive = Layer.orDie( - SqlD1.layer({ - db: DB, - transformQueryNames: String.camelToSnake, - transformResultNames: String.snakeToCamel, + return miniflare }), - ) + Effect.fnUntraced(function* (miniflare) { + yield* Effect.promise(() => miniflare.ready) - if (!seed || !seed.start || !Effect.isEffect(seed.start)) { - return yield* Effect.dieMessage('seed failed') - } + const dbBinding = config.d1_databases.find((_) => _.database_id === databaseId)?.binding || 'DB' + const DB = yield* Effect.promise(() => miniflare.getD1Database(dbBinding)) - yield* seed.start.pipe( - Effect.provide(SeedLive), - Effect.acquireRelease(() => Effect.promise(() => dev.dispose())), - Effect.withSpan('db.execute-d1-seed', { - attributes: { - projectName: workspace.projectName, - database: subcommand.database || 'default', - dbBinding, - seedPath, - }, + const SeedLive = Layer.orDie( + SqlD1.layer({ + db: DB, + transformQueryNames: String.camelToSnake, + transformResultNames: String.snakeToCamel, + }), + ) + + if (!seed || !seed.start || !Effect.isEffect(seed.start)) { + return yield* Effect.dieMessage('seed failed') + } + + yield* seed.start.pipe( + Effect.provide(SeedLive), + Effect.withSpan('db.execute-d1-seed', { + attributes: { + projectName: workspace.projectName, + database: subcommand.database || 'default', + dbBinding, + seedPath, + }, + }), + ) }), + (miniflare, exit) => { + if (Exit.isFailure(exit)) { + return Effect.promise(() => miniflare.dispose()).pipe(Effect.tap(Effect.logError(exit.cause)), Effect.ignore) + } + return Effect.promise(() => miniflare.dispose()).pipe(Effect.ignore) + }, ) + + return } else if (config.runtime === 'server') { // TODO: implement return yield* Effect.dieMessage('Not support seed in server') } yield* Effect.logInfo('Seed database done') + return }) export const dump = Effect.fn('db.dump')(function* ( @@ -630,15 +768,14 @@ export const dump = Effect.fn('db.dump')(function* ( ) { const path = yield* Path.Path const fs = yield* FileSystem.FileSystem - const { dbDir, config } = yield* detectDatabase(workspace) + const { dbDir, config } = yield* detectDatabase(workspace, { + databaseName: subcommand.database, + }) - if (config.runtime === 'browser' || config.runtime === 'server') { + if (config.runtime === 'd1') { + yield* dumpD1(workspace, subcommand) + } else if (config.runtime === 'browser' || config.runtime === 'server') { if (config.provider === 'sqlite') { - // sqlite export - if (!config.url) { - return yield* Effect.dieMessage('Missing database url') - } - const _formatSchema = Effect.gen(function* () { const content = yield* fs.readFileString(schemaOutput) @@ -652,13 +789,11 @@ export const dump = Effect.fn('db.dump')(function* ( ) }) - // "file:./xxx.db" - const dbUrl = config.url.replace('file:', '').replaceAll(`"`, '') - const dbFile = path.join(dbDir, dbUrl) + const localDevDbFile = path.join(dbDir, devDB) const schemaOutput = path.join(dbDir, 'schema.sql') const output = yield* shell` - $ sqlite3 ${dbFile} .schema + $ sqlite3 ${localDevDbFile} .schema ` if (output.stderr) { @@ -682,15 +817,13 @@ export const dump = Effect.fn('db.dump')(function* ( yield* fs.writeFileString(schemaOutput, `-- ${getMigrationDate()}\n${newFileContent}`) yield* Effect.logInfo('Dump sqlite schema done').pipe( - Effect.annotateLogs('file', dbFile), + Effect.annotateLogs('file', localDevDbFile), Effect.annotateLogs('output', schemaOutput), ) } } else { yield* Effect.logError('Not support database dump') } - } else if (config.runtime === 'd1') { - yield* d1Dump(workspace, subcommand) } yield* Effect.logInfo('Dump database schema done').pipe(Effect.annotateLogs('provider', config.provider)) @@ -700,86 +833,95 @@ export const push = Effect.fn('db.push')(function* ( workspace: Workspace.Workspace, subcommand: DatabasePushSubcommand, ) { - const { config, tables, dbDir } = yield* detectDatabase(workspace) - const { prismaPath, prisma } = yield* syncPrismaSchema(workspace, { dbDir }, config, tables) + const path = yield* Path.Path + const { config, tables, dbDir } = yield* detectDatabase(workspace, { + databaseName: subcommand.database, + }) + + yield* syncPrismaSchema(workspace, { dbDir }, config, tables) const schemaContext = yield* Effect.promise(() => - loadSchemaContext({ - schemaPathFromConfig: prismaPath, + PrismaMigrate.loadSchemaContext({ + schemaPath: { baseDir: dbDir }, + cwd: dbDir, }), ) if (config.runtime === 'd1') { - yield* d1Reset(workspace, { database: subcommand.database }) + const { databaseFile } = yield* getWranglerConfig(workspace, { + database: subcommand.database, + }) + + yield* resetD1(workspace, { database: subcommand.database }) - const from_: EngineArgs.MigrateDiffTarget = { + const from_: PrismaMigrate.EngineArgs.MigrateDiffTarget = { tag: 'empty', } - const to_: EngineArgs.MigrateDiffTarget = { + const to_: PrismaMigrate.EngineArgs.MigrateDiffTarget = { tag: 'schemaDatamodel', - files: [ - { - path: prismaPath, - content: prisma, - }, - ], + files: schemaContext.schemaFiles.map((loadedFile) => { + return { + path: loadedFile[0], + content: loadedFile[1], + } + }), } - - const captureStdout = new CaptureStdout() - captureStdout.startCapture() - let captureOutput: any - yield* Effect.acquireUseRelease( - Effect.promise(() => Migrate.setup({ schemaContext, configDir: dbDir, schemaEngineConfig: {} })), - (migrate) => - Effect.promise(() => - migrate.engine.migrateDiff({ - from: from_, - to: to_, - script: true, - exitCode: false, - shadowDatabaseUrl: `./${devDB}`, - filters: { - externalEnums: [], - externalTables: [], + const d1DbPath = `file:${databaseFile}` + const shadowDatabaseUrl = `file:${path.join(dbDir, devDB)}` + + const captureOutput = yield* Effect.acquireUseRelease( + Effect.gen(function* () { + const captureStdout = new CaptureStdout() + const migrate = yield* Effect.promise(() => + PrismaMigrate.SchemaEngineCLI.setup({ + schemaContext, + baseDir: dbDir, + datasource: { + url: d1DbPath, + shadowDatabaseUrl: shadowDatabaseUrl, }, }), - ), - (migrate, exit) => { - if (Exit.isFailure(exit)) { - return Effect.try(() => migrate.stop()).pipe( - Effect.tap(Effect.logError(exit.cause)), - Effect.ignore, - Effect.tap(() => { - captureStdout.stopCapture() + ) + captureStdout.startCapture() + + return { migrate, captureStdout } + }), + ({ migrate, captureStdout }) => + Effect.gen(function* () { + yield* Effect.promise(() => + migrate.migrateDiff({ + from: from_, + to: to_, + script: true, + exitCode: false, + shadowDatabaseUrl, + filters: { + externalEnums: [], + externalTables: [], + }, }), ) + const texts = captureStdout.getCapturedText() + captureStdout.stopCapture() + return texts + }), + ({ migrate, captureStdout }, exit) => { + captureStdout.stopCapture() + if (Exit.isFailure(exit)) { + return Effect.try(() => migrate.stop()).pipe(Effect.tap(Effect.logError(exit.cause)), Effect.ignore) } - return Effect.try(() => migrate.stop()).pipe( - Effect.tap(() => { - const text = captureStdout.getCapturedText() - captureStdout.stopCapture() - - if (!text) { - return Effect.dieMessage('Failed to migrate diff database') - } - - captureOutput = text - }), - Effect.ignore, - ) + return Effect.try(() => migrate.stop()).pipe(Effect.ignore) }, ) - const ensureOutputs = captureOutput - ? captureOutput.filter((_: any) => { - if (_.indexOf('empty migration') > -1) { - return false - } + const ensureOutputs = captureOutput.filter((_: any) => { + if (_.indexOf('empty migration') > -1) { + return false + } - return true - }) - : [] + return true + }) if (ensureOutputs.length === 0) { yield* Effect.logInfo('No migrations diff') @@ -787,37 +929,64 @@ export const push = Effect.fn('db.push')(function* ( return } - yield* d1Push(workspace, { + yield* pushD1(workspace, { sql: ensureOutputs.join('\n'), database: subcommand.database, }) + + return } else { - /** - * - 重置数据库 - * - Push (用最新的 schema.prisma 对比 Empty State 生成迁移并执行,不会产生 migration 记录) - */ + // browser/server + const datasource = + config.runtime === 'server' + ? { + url: config.url, + } + : { + url: `file:${path.join(dbDir, devDB)}`, + } + const prismaFilter = { externalEnums: [], externalTables: [] } + yield* Effect.acquireUseRelease( - Effect.promise(() => Migrate.setup({ schemaContext, configDir: dbDir, schemaEngineConfig: {} })), + Effect.promise(() => + PrismaMigrate.SchemaEngineCLI.setup({ + schemaContext, + baseDir: dbDir, + datasource, + }), + ), (migrate) => - Effect.tryPromise(() => migrate.reset()).pipe( + pipe( + Effect.tryPromise(() => migrate.reset({ filter: prismaFilter })), Effect.andThen( Effect.tryPromise(() => { - const output = migrate.push({ force: true }) as Promise<{ - executedSteps: number - warnings: string[] - unexecutable: string[] - }> - + const output = migrate.schemaPush({ + filters: prismaFilter, + force: true, + schema: { + files: schemaContext.schemaFiles.map((loadedFile) => { + return { + path: loadedFile[0], + content: loadedFile[1], + } + }), + }, + }) return output }), ), - Effect.tap((result) => Effect.logInfo('Force push done').pipe(Effect.annotateLogs(result))), + Effect.tap((result) => + Effect.logInfo('Force push done').pipe( + Effect.annotateLogs({ + ...result, + }), + ), + ), ), (migrate, exit) => { if (Exit.isFailure(exit)) { return Effect.try(() => migrate.stop()).pipe(Effect.tap(Effect.logError(exit.cause)), Effect.ignore) } - return Effect.try(() => migrate.stop()).pipe(Effect.ignore) }, ) @@ -845,28 +1014,28 @@ export const execute = Effect.fn('db.execute')(function* ( ) { const fs = yield* FileSystem.FileSystem const path = yield* Path.Path - const { config } = yield* detectDatabase(workspace) - const dbDir = path.join(workspace.projectPath, 'db') - const prismaPath = path.join(workspace.projectPath, dbDir, 'schema.prisma') - const prisma = yield* fs.readFileString(prismaPath) + const { config, dbDir } = yield* detectDatabase(workspace, { + databaseName: subcommand.database, + }) const schemaContext = yield* Effect.promise(() => - loadSchemaContext({ - schemaPathFromConfig: prismaPath, + PrismaMigrate.loadSchemaContext({ + schemaPath: { baseDir: dbDir }, + cwd: dbDir, }), ) if (config.runtime === 'd1') { - const { persistRoot, wranglerConfigPath, databaseName } = yield* wranglerConfig(workspace, { + const { persistRoot, wranglerConfigPath, databaseName } = yield* getWranglerConfig(workspace, { database: subcommand.database, }) let args = `--local --persist-to=${persistRoot} --config=${wranglerConfigPath} --json` - if (subcommand.file) { - args += ` --file=${subcommand.file}` - } + if (subcommand.sql) { - args += ` --command="${subcommand}` + args += ` --command="${subcommand.sql}` + } else if (subcommand.file) { + args += ` --file=${subcommand.file}` } const output = yield* shell` @@ -902,38 +1071,55 @@ export const execute = Effect.fn('db.execute')(function* ( }), ) } - } else if (config.runtime === 'browser') { - yield* Effect.logInfo('Skip browser database execute') - } else if (config.runtime === 'server') { - // TODO: more test - let script = '' + } else { + let executeScript = '' if (subcommand.sql) { - script = subcommand.sql - } - if (subcommand.file) { + executeScript = subcommand.sql + } else if (subcommand.file) { const inputPath = path.isAbsolute(subcommand.file) ? subcommand.file : path.resolve(process.cwd(), subcommand.file) - script = yield* fs.readFileString(inputPath) + executeScript = yield* fs.readFileString(inputPath) } - const datasourceType: EngineArgs.DbExecuteDatasourceType = { + const datasourceType: PrismaMigrate.EngineArgs.DbExecuteDatasourceType = { tag: 'schema', - files: [{ path: prismaPath, content: prisma }], - configDir: path.dirname(prismaPath), + files: schemaContext.schemaFiles.map((loadedFile) => { + return { + path: loadedFile[0], + content: loadedFile[1], + } + }), + configDir: dbDir, } + const datasource = + config.runtime === 'server' + ? { + url: config.url, + } + : { + url: `file:${path.join(dbDir, devDB)}`, + } + yield* Effect.acquireUseRelease( - Effect.promise(() => Migrate.setup({ schemaContext, configDir: dbDir, schemaEngineConfig: {} })), + Effect.promise(() => + PrismaMigrate.SchemaEngineCLI.setup({ + schemaContext, + baseDir: dbDir, + datasource, + }), + ), (migrate) => Effect.promise(() => - migrate.engine.dbExecute({ - script, + migrate.dbExecute({ + script: executeScript, datasourceType, }), ), (migrate, exit) => { + console.log(exit) if (Exit.isFailure(exit)) { return Effect.try(() => migrate.stop()).pipe(Effect.tap(Effect.logError(exit.cause)), Effect.ignore) } @@ -953,30 +1139,32 @@ export const dev = Effect.fn('db.dev')(function* ( subcommand: DatabaseMigrateDevSubcommand, ) { const path = yield* Path.Path - const { config, tables, dbDir, migrationsDir } = yield* detectDatabase(workspace) - const { prisma, prismaPath } = yield* syncPrismaSchema(workspace, { dbDir }, config, tables) + const { config, tables, dbDir, migrationsDir } = yield* detectDatabase(workspace, { + databaseName: subcommand.database, + }) const migrations = yield* getMigrations(migrationsDir) + yield* syncPrismaSchema(workspace, { dbDir }, config, tables) + const schemaContext = yield* Effect.promise(() => - loadSchemaContext({ - schemaPathFromConfig: prismaPath, + PrismaMigrate.loadSchemaContext({ + schemaPath: { baseDir: dbDir }, + cwd: dbDir, }), ) - // 从什么地方开始迁移 - let from_: EngineArgs.MigrateDiffTarget = { + const localDevDb = `file:${path.join(dbDir, devDB)}` + const localDevShadowDb = `file:${path.join(dbDir, 'shadow.db')}` + + // 全新的数据库 + let from_: PrismaMigrate.EngineArgs.MigrateDiffTarget = { tag: 'empty', } - // 全新的数据库 - if (migrations.length === 0) { - from_ = { - tag: 'empty', - } - } else { + if (migrations.length > 0) { // 从已有的 D1 sqlite 数据库迁移 if (config.runtime === 'd1') { - const { databaseFile } = yield* wranglerConfig(workspace, { + const { databaseFile } = yield* getWranglerConfig(workspace, { database: subcommand.database, }) @@ -985,13 +1173,11 @@ export const dev = Effect.fn('db.dev')(function* ( url: `file:${databaseFile}`, } } else if (config.runtime === 'browser') { - // 如果是 Browser 则用不到本地数据库默认 dev 数据库 - const localDBUrl = config.url ? config.url.replace('file:', '').replaceAll(`"`, '') : devDB - const db = path.join(dbDir, localDBUrl) + // 如果是 Browser 则用使用 dev 数据库 from_ = { tag: 'url', - url: `file:${db}`, + url: localDevDb, } } else if (config.runtime === 'server') { if (!config.url) { @@ -1006,62 +1192,57 @@ export const dev = Effect.fn('db.dev')(function* ( } // 用 schema.prisma 作为目标进行迁移 - const to_: EngineArgs.MigrateDiffTarget = { + const to_: PrismaMigrate.EngineArgs.MigrateDiffTarget = { tag: 'schemaDatamodel', - files: [ - { - path: prismaPath, - content: prisma, - }, - ], + files: schemaContext.schemaFiles.map((loadedFile) => { + return { + path: loadedFile[0], + content: loadedFile[1], + } + }), } - const captureStdout = new CaptureStdout() - captureStdout.startCapture() - let captureOutput: any + const datasource = { url: from_.tag === 'empty' ? localDevDb : from_.url } - yield* Effect.acquireUseRelease( - Effect.promise(() => Migrate.setup({ schemaContext, configDir: dbDir, schemaEngineConfig: {} })), - (migrate) => - Effect.promise(() => - migrate.engine.migrateDiff({ - from: from_, - to: to_, - script: true, - exitCode: false, - shadowDatabaseUrl: `./${devDB}`, - filters: { - externalEnums: [], - externalTables: [], - }, + const captureOutput = yield* Effect.acquireUseRelease( + Effect.gen(function* () { + const captureStdout = new CaptureStdout() + const migrate = yield* Effect.promise(() => + PrismaMigrate.SchemaEngineCLI.setup({ + schemaContext, + baseDir: dbDir, + datasource, }), - ), - (migrate, exit) => { - if (Exit.isFailure(exit)) { - return Effect.try(() => migrate.stop()).pipe( - Effect.tap(Effect.logError(exit.cause)), - Effect.ignore, - Effect.tap(() => { - const text = captureStdout.getCapturedText() - captureStdout.stopCapture() - - if (!text) { - return Effect.dieMessage('Failed to migrate diff database') - } + ) + captureStdout.startCapture() + + return { migrate, captureStdout } + }), + ({ migrate, captureStdout }) => + Effect.gen(function* () { + yield* Effect.promise(() => + migrate.migrateDiff({ + from: from_, + to: to_, + script: true, + exitCode: false, + shadowDatabaseUrl: localDevShadowDb, + filters: { + externalEnums: [], + externalTables: [], + }, }), ) + const text = captureStdout.getCapturedText() + captureStdout.stopCapture() + return text + }), + ({ migrate }, exit) => { + if (Exit.isFailure(exit)) { + return Effect.try(() => migrate.stop()).pipe(Effect.tap(Effect.logError(exit.cause)), Effect.ignore) } - return Effect.try(() => migrate.stop()).pipe( - Effect.ignore, - Effect.map(() => { - const text = captureStdout.getCapturedText() - captureStdout.stopCapture() - captureOutput = text - - return text - }), - ) + return Effect.try(() => migrate.stop()).pipe(Effect.ignore) }, ) @@ -1113,15 +1294,15 @@ export const dev = Effect.fn('db.dev')(function* ( * Apply migrations */ if (config.runtime === 'd1') { - yield* d1ApplyMigrations(workspace, { database: subcommand.database }) + yield* applyD1Migrations(workspace, { database: subcommand.database }) } else if (config.runtime === 'browser') { - yield* prismaApplyMigrations(workspace, { dbDir }) + yield* applyPrismaMigrations(workspace, { datasource, dbDir, migrations }) } else if (config.runtime === 'server') { const databaseUrl = config.url if (!databaseUrl) { return yield* Effect.dieMessage('Database url is required') } - yield* prismaApplyMigrations(workspace, { dbDir }) + yield* applyPrismaMigrations(workspace, { datasource, dbDir, migrations }) } yield* Effect.logInfo('Migrate database done') @@ -1144,15 +1325,37 @@ export const reset = Effect.fn('db.reset')(function* ( workspace: Workspace.Workspace, subcommand: DatabaseMigrateResetSubcommand, ) { - const { config, dbDir } = yield* detectDatabase(workspace) + const path = yield* Path.Path + const { config, dbDir, migrationsDir } = yield* detectDatabase(workspace, { + databaseName: subcommand.database, + }) + const migrations = yield* getMigrations(migrationsDir) if (config.runtime === 'd1') { - yield* d1Reset(workspace, { database: subcommand.database }) - yield* d1ApplyMigrations(workspace, { database: subcommand.database }) + yield* applyD1Migrations(workspace, { + database: subcommand.database, + reset: true, + }) } else if (config.runtime === 'browser') { - yield* prismaApplyMigrations(workspace, { dbDir }, { reset: true }) + const datasource = { + url: `file:${path.join(dbDir, devDB)}`, + } + yield* applyPrismaMigrations(workspace, { + dbDir, + datasource, + migrations, + reset: true, + }) } else if (config.runtime === 'server') { - yield* prismaApplyMigrations(workspace, { dbDir }, { reset: true }) + const datasource = { + url: config.url, + } + yield* applyPrismaMigrations(workspace, { + dbDir, + datasource, + migrations, + reset: true, + }) } yield* Effect.logInfo('Reset database done') @@ -1171,8 +1374,9 @@ export const deploy = Effect.fn('db.deploy')(function* ( workspace: Workspace.Workspace, subcommand: DatabaseMigrateDeploySubcommand, ) { - const { config, migrationsDir, dbDir } = yield* detectDatabase(workspace) - + const { config, migrationsDir, dbDir } = yield* detectDatabase(workspace, { + databaseName: subcommand.database, + }) const migrations = yield* getMigrations(migrationsDir) if (migrations.length === 0) { @@ -1180,14 +1384,17 @@ export const deploy = Effect.fn('db.deploy')(function* ( } if (config.runtime === 'd1') { - yield* d1ApplyMigrations(workspace, { - deploy: true, + yield* applyD1Migrations(workspace, { database: subcommand.database, + deploy: true, }) } else if (config.runtime === 'browser') { yield* Effect.logInfo('Skip browser database deploy') } else if (config.runtime === 'server') { - yield* prismaApplyMigrations(workspace, { dbDir }) + const datasource = { + url: config.url, + } + yield* applyPrismaMigrations(workspace, { dbDir, datasource, migrations }) } yield* Effect.logInfo('Deploy database done') diff --git a/scripts/thing/domain.ts b/scripts/thing/domain.ts index 23b2317..8986c5f 100644 --- a/scripts/thing/domain.ts +++ b/scripts/thing/domain.ts @@ -1,30 +1,8 @@ -import { Context, Data, type LogLevel, Schema } from 'effect' +import { Context, Data, Schema } from 'effect' +import { NodeEnv, Stage } from './core/env' +import { BuildReactRouterSchema, BuildReactRouterTarget } from './react-router/domain' -export const Stage = Schema.Literal('production', 'staging', 'test') -export type Stage = typeof Stage.Type - -export const NodeEnv = Schema.Literal('development', 'production') -export type NodeEnv = typeof NodeEnv.Type - -export const BuildReactRouterSchema = Schema.Struct({ - _tag: Schema.Literal('BuildReactRouter'), - /** - * Workers - * /client 静态资源部署到 Pages 上 - * /server 静态资源部署到 Workers 上 - * - * Pages - * /client 全部部署到 Pages 上 - */ - runtime: Schema.Literal('cloudflare-workers'), - options: Schema.Struct({ - isSpaMode: Schema.Boolean, - isDesktop: Schema.Boolean, - }), - stage: Stage, -}) -export interface BuildReactRouterTarget extends Schema.Schema.Type {} -export const BuildReactRouterTarget = Data.tagged('BuildReactRouter') +export { NodeEnv, Stage } from './core/env' export const BuildWorkersSchema = Schema.Struct({ _tag: Schema.Literal('BuildWorkers'), @@ -45,15 +23,6 @@ export type BuildProvider = BuildTarget export const TargetSchema = Schema.Union(BuildReactRouterSchema, BuildWorkersSchema) export type TargetSchema = typeof TargetSchema.Type -export interface BuildReactRouterParameters { - readonly nodeEnv: NodeEnv - readonly target: BuildReactRouterTarget - readonly env: Record -} -export const BuildReactRouterParameters = Context.GenericTag( - '@thing:build-react-router-parameters', -) - export interface BuildWorkersParameters { readonly nodeEnv: NodeEnv readonly target: BuildWorkersTarget @@ -105,106 +74,11 @@ export interface PreviewSubcommand { export const PreviewSubcommand = Data.tagged('PreviewSubcommand') +// ----- React Native ----- + // ----- Database ----- // DB -export interface DatabaseSeedSubcommand { - readonly _tag: 'DatabaseSeedSubcommand' - readonly cwd: string - readonly database: string | undefined - readonly file: string | undefined -} -export const DatabaseSeedSubcommand = Data.tagged('DatabaseSeedSubcommand') - -export interface DatabasePushSubcommand { - readonly _tag: 'DatabasePushSubcommand' - readonly cwd: string - readonly database: string | undefined - readonly skipSeed: boolean - readonly skipDump: boolean -} -export const DatabasePushSubcommand = Data.tagged('DatabasePushSubcommand') - -export interface DatabaseDumpSubcommand { - readonly _tag: 'DatabaseDumpSubcommand' - readonly cwd: string - readonly database: string | undefined -} -export const DatabaseDumpSubcommand = Data.tagged('DatabaseDumpSubcommand') - -export interface DatabaseExecuteSubcommand { - readonly _tag: 'DatabaseExecuteSubcommand' - readonly cwd: string - readonly sql: string - readonly file: string | undefined - readonly database: string | undefined -} -export const DatabaseExecuteSubcommand = Data.tagged('DatabaseExecuteSubcommand') - -// Migrate -export interface DatabaseMigrateDevSubcommand { - readonly _tag: 'DatabaseMigrateDevSubcommand' - readonly cwd: string - readonly database: string | undefined - readonly migrationName: string - readonly skipSeed: boolean - readonly skipDump: boolean -} -export const DatabaseMigrateDevSubcommand = Data.tagged('DatabaseMigrateDevSubcommand') - -export interface DatabaseMigrateResetSubcommand { - readonly _tag: 'DatabaseMigrateResetSubcommand' - readonly cwd: string - readonly database: string | undefined - readonly skipSeed: boolean -} -export const DatabaseMigrateResetSubcommand = Data.tagged( - 'DatabaseMigrateResetSubcommand', -) - -export interface DatabaseMigrateDeploySubcommand { - readonly _tag: 'DatabaseMigrateDeploySubcommand' - readonly cwd: string - readonly database: string | undefined -} -export const DatabaseMigrateDeploySubcommand = Data.tagged( - 'DatabaseMigrateDeploySubcommand', -) - -export interface DatabaseMigrateResolveSubcommand { - readonly _tag: 'DatabaseMigrateResolveSubcommand' - readonly cwd: string - readonly database: string | undefined -} -export const DatabaseMigrateResolveSubcommand = Data.tagged( - 'DatabaseMigrateResolveSubcommand', -) - // Email -export interface EmailBuildSubcommand { - readonly _tag: 'EmailBuildSubcommand' - readonly cwd: string -} -export const EmailBuildSubcommand = Data.tagged('EmailBuildSubcommand') - -export interface EmailDeploySubcommand { - readonly _tag: 'EmailDeploySubcommand' - readonly cwd: string - readonly stage: Stage -} -export const EmailDeploySubcommand = Data.tagged('EmailDeploySubcommand') - // Test - -export interface TestSubcommand { - readonly _tag: 'TestSubcommand' - - readonly project: string - readonly all: boolean - readonly mode: 'unit' | 'e2e' | 'browser' - readonly watch: boolean - readonly headless: boolean - readonly browser: 'chromium' | 'firefox' | 'webkit' | 'all' -} -export const TestSubcommand = Data.tagged('TestSubcommand') diff --git a/scripts/thing/emails/command.ts b/scripts/thing/emails/command.ts new file mode 100644 index 0000000..a94e2da --- /dev/null +++ b/scripts/thing/emails/command.ts @@ -0,0 +1,33 @@ +import { Command } from '@effect/cli' +import { Effect } from 'effect' +import { emailCwdOption, emailStageOption } from './options' +import { EmailBuildSubcommand, EmailDeploySubcommand } from './domain' +import * as Emails from './subcommand' +import * as Workspace from '../workspace' +import type { Workspace as WorkspaceModel } from '../workspace' + +const withWorkspace = (cwd: string, f: (workspace: WorkspaceModel) => Effect.Effect) => + Effect.gen(function* () { + const workspace = yield* Workspace.make(cwd) + return yield* f(workspace) + }) + +const emailBuildCommand = Command.make('build', { cwd: emailCwdOption }, (config) => + withWorkspace(config.cwd, (workspace) => Emails.build(workspace, EmailBuildSubcommand({ cwd: config.cwd }))), +) + +const emailDeployCommand = Command.make('deploy', { cwd: emailCwdOption, stage: emailStageOption }, (config) => + withWorkspace(config.cwd, (workspace) => + Emails.deploy( + workspace, + EmailDeploySubcommand({ + cwd: config.cwd, + stage: config.stage, + }), + ), + ), +) + +const emailCommand = Command.make('email').pipe(Command.withSubcommands([emailBuildCommand, emailDeployCommand])) + +export { emailCommand } diff --git a/scripts/thing/emails/domain.ts b/scripts/thing/emails/domain.ts new file mode 100644 index 0000000..ba94966 --- /dev/null +++ b/scripts/thing/emails/domain.ts @@ -0,0 +1,15 @@ +import { Data } from 'effect' +import { Stage } from '../domain' + +export interface EmailBuildSubcommand { + readonly _tag: 'EmailBuildSubcommand' + readonly cwd: string +} +export const EmailBuildSubcommand = Data.tagged('EmailBuildSubcommand') + +export interface EmailDeploySubcommand { + readonly _tag: 'EmailDeploySubcommand' + readonly cwd: string + readonly stage: Stage +} +export const EmailDeploySubcommand = Data.tagged('EmailDeploySubcommand') diff --git a/scripts/thing/emails/options.ts b/scripts/thing/emails/options.ts new file mode 100644 index 0000000..5a5aac8 --- /dev/null +++ b/scripts/thing/emails/options.ts @@ -0,0 +1,11 @@ +import { Options } from '@effect/cli' +const emailCwdOption = Options.text('cwd').pipe( + Options.withDescription('Absolute or relative path to the emails workspace'), +) + +const emailStageOption = Options.choice('stage', ['production', 'staging', 'test'] as const).pipe( + Options.withDescription('Deployment stage used for KV namespaces'), + Options.withDefault('test'), +) + +export { emailCwdOption, emailStageOption } diff --git a/scripts/thing/emails/subcommand.ts b/scripts/thing/emails/subcommand.ts new file mode 100644 index 0000000..2f913fe --- /dev/null +++ b/scripts/thing/emails/subcommand.ts @@ -0,0 +1,212 @@ +import type { KVNamespace } from '@cloudflare/workers-types' +import { FileSystem, Path } from '@effect/platform' +import { Effect } from 'effect' +import { CF } from '../cloudflare/api' +import type { EmailBuildSubcommand, EmailDeploySubcommand } from './domain' +import { shellInPath } from '../utils/shell' +import type { Workspace } from '../workspace' +import { EMAIL_TEMPLATE_PREFIX, EmailKV } from '../constants' + +interface CompiledTemplate { + name: string + content: string +} + +const getTemplates = Effect.fn('email.get-templates')(function* (projectRoot: string, options: { emailBin: string }) { + const path = yield* Path.Path + const fs = yield* FileSystem.FileSystem + const emailsPath = path.join(projectRoot, 'emails') + const templatesPath = path.join(emailsPath, 'templates') + const outputPath = path.join(emailsPath, '.rendered') + + const hasTemplates = yield* fs.exists(templatesPath).pipe( + Effect.withSpan('email.check-templates-exist', { + attributes: { + templatesPath, + projectRoot, + }, + }), + ) + + const templates: CompiledTemplate[] = [] + + if (!hasTemplates) { + yield* Effect.logInfo('Skip empty email templates') + return templates + } + + yield* Effect.logInfo('Starting email templates compilation...') + + yield* shellInPath(emailsPath)`$ ${options.emailBin} build ./templates`.pipe( + Effect.ignoreLogged, + Effect.withSpan('email.compile-templates', { + attributes: { + emailBin: options.emailBin, + emailsPath, + templatesPath, + outputPath, + }, + }), + ) + + yield* Effect.logInfo('Reading compiled templates...') + const compiledFiles = yield* fs.readDirectory(outputPath).pipe( + Effect.withSpan('email.read-compiled-directory', { + attributes: { + outputPath, + }, + }), + ) + + if (compiledFiles.length === 0) { + yield* Effect.logInfo('Skip empty compiled fields') + return templates + } + + yield* Effect.forEach( + compiledFiles.filter((file) => file.endsWith('.html')), + (file) => + Effect.gen(function* () { + const filePath = path.join(outputPath, file) + const content = yield* fs.readFileString(filePath) + const baseName = file.replace(/\.html$/, '') + + const existingTemplate = templates.find((t) => t.name === baseName) + + if (existingTemplate) { + existingTemplate.content = content + } else { + templates.push({ + name: baseName, + content, + }) + } + }), + { concurrency: 'unbounded' }, + ).pipe( + Effect.withSpan('email.process-template-files', { + attributes: { + fileCount: compiledFiles.filter((file) => file.endsWith('.html')).length, + outputPath, + }, + }), + ) + + yield* Effect.logInfo(`Processed ${templates.length} templates`) + + return templates +}) + +export const build = Effect.fn('email.build')(function* (workspace: Workspace, _subcommand: EmailBuildSubcommand) { + const path = yield* Path.Path + const emailBin = path.join(workspace.root, 'node_modules', '.bin', 'email') + const persistTo = path.join(workspace.root, '.wrangler/state/v3') + + const { Miniflare } = yield* Effect.promise(() => import('miniflare')).pipe( + Effect.withSpan('email.import-miniflare', { + attributes: { + projectName: workspace.projectName, + }, + }), + ) + + const miniflare = new Miniflare({ + script: '', + modules: true, + defaultPersistRoot: persistTo, + kvPersist: true, + kvNamespaces: { + KV: EmailKV.DevKV, + }, + }) + + const env: any = yield* Effect.promise(() => miniflare.getBindings()).pipe( + Effect.withSpan('email.miniflare-bindings', { + attributes: { + persistTo, + kvNamespace: EmailKV.DevKV, + projectName: workspace.projectName, + }, + }), + ) + + const kv = env.KV as KVNamespace + + const templates = yield* getTemplates(workspace.projectRoot, { emailBin }).pipe( + Effect.withSpan('email.get-templates', { + attributes: { + projectRoot: workspace.projectRoot, + emailBin, + projectName: workspace.projectName, + }, + }), + ) + + yield* Effect.forEach(templates, (item) => { + const key = `${EMAIL_TEMPLATE_PREFIX}::${workspace.projectPrefix}::${item.name}` + return Effect.tryPromise({ + try: () => kv.put(key, item.content), + catch: (error) => error as Error, + }) + }).pipe( + Effect.withSpan('email.put-templates-to-kv', { + attributes: { + templateCount: templates.length, + kvNamespace: EmailKV.DevKV, + projectPrefix: workspace.projectPrefix, + projectName: workspace.projectName, + }, + }), + ) + + yield* Effect.logInfo('Put email templates to local kv') +}) + +export const deploy = Effect.fn('email.deploy')(function* (workspace: Workspace, subcommand: EmailDeploySubcommand) { + const path = yield* Path.Path + const cf = yield* CF + + const emailBin = path.join(workspace.root, 'node_modules', '.bin', 'email') + const emailTemplateKV = subcommand.stage === 'test' ? EmailKV.DevKV : EmailKV.ProdKV + + const templates = yield* getTemplates(workspace.projectRoot, { emailBin }) + + yield* cf + .putKV( + emailTemplateKV, + templates.map((item) => { + const key = `${EMAIL_TEMPLATE_PREFIX}::${workspace.projectPrefix}::${item.name}` + + return { + key, + value: item.content, + } + }), + ) + .pipe( + Effect.withSpan('email.deploy-templates-to-kv', { + attributes: { + templateCount: templates.length, + kvNamespace: emailTemplateKV, + stage: subcommand.stage, + projectPrefix: workspace.projectPrefix, + projectName: workspace.projectName, + environment: subcommand.stage === 'test' ? 'development' : 'production', + }, + }), + ) + + yield* Effect.logInfo('Deploy email templates successfully') +}, Effect.provide(CF.Live)) + +export const existEmailProject = Effect.fn('email.exist-email-project')(function* (workspace: Workspace) { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + + const paths = [path.join(workspace.projectRoot, 'emails', 'project.json')] + + return yield* Effect.reduce(paths, false, (acc, path) => { + if (acc) return Effect.succeed(acc) + return fs.exists(path) + }) +}) diff --git a/scripts/thing/environment.ts b/scripts/thing/environment.ts index 4e26968..65ab601 100644 --- a/scripts/thing/environment.ts +++ b/scripts/thing/environment.ts @@ -152,6 +152,13 @@ export const make = Effect.fn('environment.make')(function* ({ process.env.SANITY_STUDIO_DATASET = allEnv.SANITY_STUDIO_DATASET process.env.SANITY_STUDIO_PROJECT_ID = allEnv.SANITY_STUDIO_PROJECT_ID + // Set all VITE_ prefixed environment variables to process.env + Object.entries(allEnv).forEach(([key, value]) => { + if (key.startsWith('VITE_')) { + process.env[key] = value + } + }) + return { env: { ...allEnv, ...envs }, configProvider: ConfigProvider.fromMap(envMap).pipe(ConfigProvider.orElse(() => ConfigProvider.fromEnv())), @@ -200,6 +207,13 @@ export const loadEnv = Effect.fn('environment.loadEnv')(function* ({ workspace } process.env.SANITY_STUDIO_DATASET = allEnv.SANITY_STUDIO_DATASET process.env.SANITY_STUDIO_PROJECT_ID = allEnv.SANITY_STUDIO_PROJECT_ID + // Set all VITE_ prefixed environment variables to process.env + Object.entries(allEnv).forEach(([key, value]) => { + if (key.startsWith('VITE_')) { + process.env[key] = value + } + }) + return { env: allEnv, configProvider: ConfigProvider.fromMap(envMap).pipe(ConfigProvider.orElse(() => ConfigProvider.fromEnv())), diff --git a/scripts/thing/git.ts b/scripts/thing/git.ts index a667265..be304c6 100644 --- a/scripts/thing/git.ts +++ b/scripts/thing/git.ts @@ -1,6 +1,34 @@ import { Config, Context, Effect, Layer, Option } from 'effect' -import { shell } from './utils' +import { spawnSync } from 'node:child_process' +import { shell } from './utils/shell' import type { Stage } from './domain' +import { workspaceRoot } from '@nx/devkit' + +export interface GitCommandOptions { + cwd?: string + trim?: boolean +} + +export const git = (args: string[], options?: GitCommandOptions) => { + const result = spawnSync('git', args, { + cwd: options?.cwd ?? workspaceRoot, + encoding: 'utf8', + env: process.env, + stdio: ['ignore', 'pipe', 'pipe'], + }) + + if (result.error) { + throw new Error(`[git] Failed to run git ${args.join(' ')}: ${result.error.message}`) + } + + if (result.status !== 0) { + const message = (result.stderr || result.stdout || '').trim() + throw new Error(`[git] Command git ${args.join(' ')} exited with code ${result.status}. ${message}`) + } + + const output = result.stdout ?? '' + return options?.trim === false ? output : output.trim() +} interface GitCommit { sha: string @@ -29,7 +57,7 @@ export class Git extends Context.Tag('@thing/Git')< static Github = Layer.effect( this, Effect.gen(function* () { - yield* Effect.logDebug('Use octokit') + yield* Effect.logInfo('Use octokit') const { GITHUB_BRANCH, GITHUB_OWNER, GITHUB_REPO, GITHUB_SHA, GITHUB_TOKEN } = yield* GitConfig @@ -102,7 +130,61 @@ export class Git extends Context.Tag('@thing/Git')< }) } -const formatBranch = (branch: string) => branch.replace('refs/heads/', '') +const formatBranch = (branch: string) => branch.replace(/^refs\/heads\//, '') + +const sanitizeChannelSegment = (value?: string) => { + if (!value) { + return '' + } + + const normalized = value + .toLowerCase() + .replace(/[^a-z0-9]+/gi, '-') + .replace(/-+/g, '-') + .replace(/^-+|-+$/g, '') + return normalized +} + +const resolvePreviewChannel = (branch: string) => { + const prRef = branch.match(/^refs\/pull\/(\d+)\/(head|merge)$/i) + if (prRef) { + return `preview/pr-${prRef[1]}` + } + + const shortPrRef = branch.match(/^pull\/(\d+)\/(head|merge)$/i) + if (shortPrRef) { + return `preview/pr-${shortPrRef[1]}` + } + + const previewLike = branch.match(/^(?:pr|preview)[/-](.+)$/i) + if (previewLike) { + const suffix = sanitizeChannelSegment(previewLike[1]) || 'pr' + return `preview/${suffix}` + } + + return undefined +} + +export const branchToNativeChannel = (branch: string, env?: string) => { + const formatted = formatBranch(branch) + const previewChannel = resolvePreviewChannel(formatted) + if (previewChannel) { + return previewChannel + } + + const branchSegment = sanitizeChannelSegment(formatted) || 'main' + if ( + branchSegment === 'main' || + branchSegment === 'staging' || + branchSegment === 'test' || + branchSegment.startsWith('feat-') + ) { + return branchSegment + } + + const envSegment = sanitizeChannelSegment(env) + return envSegment ? `${envSegment}-${branchSegment}` : branchSegment +} export const detectStage = Effect.fn('detectStage')(function* (defaultStage?: Option.Option) { const git = yield* Git diff --git a/scripts/thing/github.ts b/scripts/thing/github.ts index e754d73..de086c5 100644 --- a/scripts/thing/github.ts +++ b/scripts/thing/github.ts @@ -67,7 +67,7 @@ export class Github extends Context.Tag('@thing/github')< static Live = Layer.effect( this, Effect.gen(function* () { - yield* Effect.logDebug('Use octokit') + yield* Effect.logInfo('Use octokit') const { GITHUB_OWNER, GITHUB_REPO, GITHUB_SHA, GITHUB_TOKEN } = yield* GitConfig @@ -186,7 +186,7 @@ export class Github extends Context.Tag('@thing/github')< |Stage: ${stage} |Environment: ${environment}`) - yield* Effect.logDebug(`Create github deployment${msg}`) + yield* Effect.logInfo(`Create github deployment${msg}`) return { deploymentId: id, diff --git a/scripts/thing/package.json b/scripts/thing/package.json index 3a00148..64f30e1 100644 --- a/scripts/thing/package.json +++ b/scripts/thing/package.json @@ -2,5 +2,9 @@ "name": "@xstack/scripts-thing", "types": "module", "version": "0.0.0", - "sideEffects": true + "sideEffects": true, + "bin": "bin.ts", + "scripts": { + "postinstall": "ln -sf $(pwd)/bin.ts ../../node_modules/.bin/xdev" + } } diff --git a/scripts/thing/project.json b/scripts/thing/project.json index 2da4c1b..bd67d18 100644 --- a/scripts/thing/project.json +++ b/scripts/thing/project.json @@ -8,7 +8,7 @@ "executor": "nx:run-commands", "outputs": ["{workspaceRoot}/node_modules/.vitest/scripts/thing", "{workspaceRoot}/coverage/scripts/thing"], "options": { - "command": "pnpm xdev test --project scripts-thing" + "command": "xdev test --project scripts-thing" } }, "typecheck": { diff --git a/scripts/thing/react-native/command.ts b/scripts/thing/react-native/command.ts new file mode 100644 index 0000000..022f802 --- /dev/null +++ b/scripts/thing/react-native/command.ts @@ -0,0 +1,287 @@ +import { Command } from '@effect/cli' +import { Effect } from 'effect' +import { + nativeBundlerOption, + nativeHeadOption, + nativeBaseOption, + nativeBuildCacheOption, + nativeBuildClearCacheOption, + nativeBuildFreezeCredentialsOption, + nativeBuildJsonOption, + nativeBuildLocalOption, + nativeBuildLoggerOption, + nativeBuildMessageOption, + nativeBuildOutputOption, + nativeBuildPlatformOption, + nativeBuildProfileOption, + nativeBuildWaitOption, + nativeCleanOption, + nativeCwdOption, + nativeDeploySubmitBuildIdOption, + nativeDeploySubmitJsonOption, + nativeDeploySubmitLatestOption, + nativeDeploySubmitNonInteractiveOption, + nativeDeploySubmitPathOption, + nativeDeploySubmitPlatformOption, + nativeDeploySubmitProfileOption, + nativeDeploySubmitVerboseOption, + nativeDeploySubmitWaitOption, + nativeDeviceOption, + nativeAnalyzeMinifyOption, + nativeAnalyzeBytecodeOption, + nativeAnalyzeClearOption, + nativeAnalyzeDevOption, + nativeAnalyzePlatformOption, + nativeInstallOption, + nativeJsChannelOption, + nativeJsDryRunOption, + nativeJsEnvOption, + nativeJsForceOption, + nativeJsMessageOption, + nativeJsPlatformOption, + nativeJsTargetVersionOption, + nativeRunPlatformOption, + nativeRunPortOption, + nativeSchemeOption, + nativeVariantOption, + nativeXcodeConfigurationOption, + nativePrebuildCleanOption, +} from './options' +import { + ReactNativeAnalyzeSubcommand, + ReactNativeBuildSubcommand, + ReactNativeDeployCheckSubcommand, + ReactNativeDeploySubmitSubcommand, + ReactNativeDeployJsUpdateSubcommand, + ReactNativePrebuildSubcommand, + ReactNativeRunSubcommand, +} from './domain' +import * as Native from './subcommand' +import * as Workspace from '../workspace' +import type { Workspace as WorkspaceModel } from '../workspace' + +const withWorkspace = (cwd: string, f: (workspace: WorkspaceModel) => Effect.Effect) => + Effect.gen(function* () { + const workspace = yield* Workspace.make(cwd) + return yield* f(workspace) + }) + +const prebuildCommand = Command.make( + 'prebuild', + { + cwd: nativeCwdOption, + platform: nativeBuildPlatformOption, + install: nativeInstallOption, + clean: nativePrebuildCleanOption, + }, + (config) => + withWorkspace(config.cwd, (workspace) => + Native.prebuild( + workspace, + ReactNativePrebuildSubcommand({ + cwd: config.cwd, + platform: config.platform, + install: config.install, + clean: config.clean, + }), + ), + ), +) + +const buildCommand = Command.make( + 'build', + { + cwd: nativeCwdOption, + platform: nativeBuildPlatformOption, + profile: nativeBuildProfileOption, + local: nativeBuildLocalOption, + json: nativeBuildJsonOption, + wait: nativeBuildWaitOption, + clearCache: nativeBuildClearCacheOption, + message: nativeBuildMessageOption, + buildLoggerLevel: nativeBuildLoggerOption, + freezeCredentials: nativeBuildFreezeCredentialsOption, + output: nativeBuildOutputOption, + }, + (config) => + withWorkspace(config.cwd, (workspace) => + Native.build( + workspace, + ReactNativeBuildSubcommand({ + cwd: config.cwd, + platform: config.platform, + profile: config.profile, + local: config.local, + json: config.json, + wait: config.wait, + clearCache: config.clearCache, + message: config.message, + buildLoggerLevel: config.buildLoggerLevel, + freezeCredentials: config.freezeCredentials, + output: config.output, + }), + ), + ), +) + +const runCommand = Command.make( + 'run', + { + cwd: nativeCwdOption, + platform: nativeRunPlatformOption, + device: nativeDeviceOption, + scheme: nativeSchemeOption, + variant: nativeVariantOption, + xcodeConfiguration: nativeXcodeConfigurationOption, + port: nativeRunPortOption, + bundler: nativeBundlerOption, + buildCache: nativeBuildCacheOption, + clean: nativeCleanOption, + }, + (config) => + withWorkspace(config.cwd, (workspace) => + Native.run( + workspace, + ReactNativeRunSubcommand({ + cwd: config.cwd, + platform: config.platform, + device: config.device, + scheme: config.scheme, + variant: config.variant, + xcodeConfiguration: config.xcodeConfiguration, + port: config.port, + bundler: config.bundler, + buildCache: config.buildCache, + clean: config.clean, + }), + ), + ), +) + +const analyzeCommand = Command.make( + 'analyze', + { + cwd: nativeCwdOption, + base: nativeBaseOption, + head: nativeBaseOption, + platform: nativeRunPlatformOption, + minify: nativeAnalyzeMinifyOption, + bytecode: nativeAnalyzeBytecodeOption, + }, + (config) => + withWorkspace(config.cwd, (workspace) => + Native.analyze( + workspace, + ReactNativeAnalyzeSubcommand({ + cwd: config.cwd, + base: config.base, + head: config.head, + platform: config.platform, + minify: config.minify, + bytecode: config.bytecode, + }), + ), + ), +) + +const deployCheckCommand = Command.make( + 'deploy-check', + { + cwd: nativeCwdOption, + platform: nativeJsPlatformOption, + base: nativeBaseOption, + head: nativeHeadOption, + }, + (config) => + withWorkspace(config.cwd, (workspace) => + Native.deployCheck( + workspace, + ReactNativeDeployCheckSubcommand({ + cwd: config.cwd, + platform: config.platform, + base: config.base, + head: config.head, + }), + ), + ), +) + +const deployJsUpdateCommand = Command.make( + 'deploy-js-update', + { + cwd: nativeCwdOption, + env: nativeJsEnvOption, + base: nativeBaseOption, + head: nativeHeadOption, + channel: nativeJsChannelOption, + platform: nativeJsPlatformOption, + message: nativeJsMessageOption, + dryRun: nativeJsDryRunOption, + force: nativeJsForceOption, + targetVersion: nativeJsTargetVersionOption, + }, + (config) => + withWorkspace(config.cwd, (workspace) => + Native.deployJsUpdate( + workspace, + ReactNativeDeployJsUpdateSubcommand({ + cwd: config.cwd, + env: config.env, + base: config.base, + head: config.head, + channel: config.channel, + platform: config.platform, + message: config.message, + dryRun: config.dryRun, + force: config.force, + targetVersion: config.targetVersion, + }), + ), + ), +) + +const deploySubmitCommand = Command.make( + 'deploy-submit', + { + cwd: nativeCwdOption, + platform: nativeDeploySubmitPlatformOption, + profile: nativeDeploySubmitProfileOption, + path: nativeDeploySubmitPathOption, + buildId: nativeDeploySubmitBuildIdOption, + latest: nativeDeploySubmitLatestOption, + nonInteractive: nativeDeploySubmitNonInteractiveOption, + wait: nativeDeploySubmitWaitOption, + json: nativeDeploySubmitJsonOption, + verbose: nativeDeploySubmitVerboseOption, + }, + (config) => + withWorkspace(config.cwd, (workspace) => + Native.deploySubmit( + workspace, + ReactNativeDeploySubmitSubcommand({ + cwd: config.cwd, + platform: config.platform, + profile: config.profile, + path: config.path, + buildId: config.buildId, + latest: config.latest, + nonInteractive: config.nonInteractive, + wait: config.wait, + json: config.json, + verbose: config.verbose, + }), + ), + ), +) + +export const nativeCommand = Command.make('native').pipe( + Command.withSubcommands([ + prebuildCommand, + runCommand, + buildCommand, + analyzeCommand, + deployCheckCommand, + deployJsUpdateCommand, + deploySubmitCommand, + ]), +) diff --git a/scripts/thing/react-native/domain.ts b/scripts/thing/react-native/domain.ts new file mode 100644 index 0000000..6dd3e93 --- /dev/null +++ b/scripts/thing/react-native/domain.ts @@ -0,0 +1,106 @@ +import { Data, Schema } from 'effect' + +export const ReactNativeRunPlatform = Schema.Literal('ios', 'android') +export type ReactNativeRunPlatform = typeof ReactNativeRunPlatform.Type + +export const ReactNativeBuildPlatform = Schema.Literal('ios', 'android', 'all') +export type ReactNativeBuildPlatform = typeof ReactNativeBuildPlatform.Type + +export const ReactNativeExportPlatform = Schema.Literal('all', 'ios', 'android', 'web') +export type ReactNativeExportPlatform = typeof ReactNativeExportPlatform.Type + +export interface ReactNativePrebuildSubcommand { + readonly _tag: 'ReactNativePrebuildSubcommand' + readonly cwd: string + readonly platform: ReactNativeBuildPlatform + readonly clean: boolean + readonly install: boolean +} +export const ReactNativePrebuildSubcommand = Data.tagged('ReactNativePrebuildSubcommand') + +export interface ReactNativeBuildSubcommand { + readonly _tag: 'ReactNativeBuildSubcommand' + readonly cwd: string + readonly platform: ReactNativeBuildPlatform + readonly profile: string + readonly local: boolean + readonly json: boolean + readonly wait: boolean + readonly clearCache: boolean + readonly message?: string | undefined + readonly buildLoggerLevel?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | undefined + readonly freezeCredentials: boolean + readonly output?: string | undefined +} +export const ReactNativeBuildSubcommand = Data.tagged('ReactNativeBuildSubcommand') + +export interface ReactNativeRunSubcommand { + readonly _tag: 'ReactNativeRunSubcommand' + readonly cwd: string + readonly platform: ReactNativeRunPlatform + readonly device?: string | undefined + readonly scheme?: string | undefined + readonly xcodeConfiguration: string + readonly variant: string + readonly port: number + readonly bundler: boolean + readonly buildCache: boolean + readonly clean: boolean +} +export const ReactNativeRunSubcommand = Data.tagged('ReactNativeRunSubcommand') + +export interface ReactNativeAnalyzeSubcommand { + readonly _tag: 'ReactNativeAnalyzeSubcommand' + readonly cwd: string + readonly head?: string | undefined + readonly base?: string | undefined + readonly platform: ReactNativeBuildPlatform + readonly minify: boolean + readonly bytecode: boolean +} +export const ReactNativeAnalyzeSubcommand = Data.tagged('ReactNativeAnalyzeSubcommand') + +export interface ReactNativeDeployCheckSubcommand { + readonly _tag: 'ReactNativeDeployCheckSubcommand' + readonly cwd: string + readonly base?: string | undefined + readonly head?: string | undefined + readonly platform: ReactNativeBuildPlatform +} +export const ReactNativeDeployCheckSubcommand = Data.tagged( + 'ReactNativeDeployCheckSubcommand', +) + +export interface ReactNativeDeployJsUpdateSubcommand { + readonly _tag: 'ReactNativeJsUpdateSubcommand' + readonly cwd: string + readonly env: string + readonly base?: string | undefined + readonly head?: string | undefined + readonly channel?: string | undefined + readonly platform: ReactNativeBuildPlatform + readonly message?: string | undefined + readonly dryRun: boolean + readonly force: boolean + readonly targetVersion?: string | undefined +} +export const ReactNativeDeployJsUpdateSubcommand = Data.tagged( + 'ReactNativeJsUpdateSubcommand', +) + +export interface ReactNativeDeploySubmitSubcommand { + readonly _tag: 'ReactNativeDeploySubmitSubcommand' + readonly cwd: string + readonly platform: ReactNativeRunPlatform + readonly profile?: string | undefined + readonly path?: string | undefined + readonly buildId?: string | undefined + readonly latest: boolean + readonly nonInteractive: boolean + readonly wait: boolean + readonly json: boolean + readonly verbose: boolean +} +export const ReactNativeDeploySubmitSubcommand = Data.tagged( + 'ReactNativeDeploySubmitSubcommand', +) diff --git a/scripts/thing/react-native/options.ts b/scripts/thing/react-native/options.ts new file mode 100644 index 0000000..9223be3 --- /dev/null +++ b/scripts/thing/react-native/options.ts @@ -0,0 +1,218 @@ +import { Options } from '@effect/cli' + +export const nativeCwdOption = Options.text('cwd').pipe(Options.withDescription('Path to the Expo/React Native app')) + +export const nativeHeadOption = Options.text('head').pipe(Options.withDescription(''), Options.withDefault(undefined)) + +export const nativeBaseOption = Options.text('base').pipe(Options.withDescription(''), Options.withDefault(undefined)) + +export const nativeRunPlatformOption = Options.choice('platform', ['ios', 'android'] as const).pipe( + Options.withDescription('Target platform for expo run commands'), + Options.withDefault('ios'), +) + +export const nativeCleanOption = Options.boolean('clean').pipe( + Options.withDescription('Delete native folders and regenerate before running'), + Options.withDefault(false), +) + +export const nativeDeviceOption = Options.text('device').pipe( + Options.withDescription('Device name or identifier'), + Options.withDefault(undefined), +) + +export const nativeSchemeOption = Options.text('scheme').pipe( + Options.withDescription('Custom native scheme to run'), + Options.withDefault(undefined), +) + +export const nativeVariantOption = Options.text('variant').pipe( + Options.withDescription('Android build variant (e.g., debug, release)'), + Options.withDefault('debug'), +) + +export const nativeXcodeConfigurationOption = Options.text('xcode-configuration').pipe( + Options.withDescription('iOS build configuration (e.g., Debug, Release)'), + Options.withDefault('Debug'), +) + +export const nativeRunPortOption = Options.integer('port').pipe( + Options.withDescription('Port to start the Metro bundler on'), + Options.withDefault(8081), +) + +export const nativeInstallOption = Options.boolean('install').pipe( + Options.withDescription('Install the native binary before running'), + Options.withDefault(false), +) + +export const nativeBundlerOption = Options.boolean('bundler', { ifPresent: false }).pipe( + Options.withDescription('Start the metro bundler automatically'), + Options.withDefault(true), +) + +export const nativeBuildCacheOption = Options.boolean('build-cache', { ifPresent: false }).pipe( + Options.withDescription('Use derived data cache for builds'), + Options.withDefault(true), +) +export const nativeBuildPlatformOption = Options.choice('platform', ['ios', 'android', 'all'] as const).pipe( + Options.withDescription('Target platform for EAS build'), + Options.withDefault('ios'), +) + +export const nativeBuildProfileOption = Options.text('profile').pipe( + Options.withDescription('EAS build profile'), + Options.withDefault('preview'), +) + +export const nativeBuildLocalOption = Options.boolean('local', { ifPresent: false }).pipe( + Options.withDescription('Run EAS build locally'), + Options.withDefault(true), +) + +export const nativeBuildOutputOption = Options.text('output').pipe( + Options.withDescription('Override artifact output path'), + Options.withDefault(undefined), +) + +export const nativeBuildJsonOption = Options.boolean('json').pipe( + Options.withDescription('Enable JSON output for EAS build'), + Options.withDefault(false), +) + +export const nativeBuildWaitOption = Options.boolean('wait', { ifPresent: false }).pipe( + Options.withDescription('Wait for builds to complete'), + Options.withDefault(true), +) + +export const nativeBuildClearCacheOption = Options.boolean('clear-cache').pipe( + Options.withDescription('Clear build cache before running'), + Options.withDefault(false), +) + +export const nativeBuildMessageOption = Options.text('message').pipe( + Options.withDescription('Short description for the build'), + Options.withDefault(undefined), +) + +export const nativeBuildLoggerOption = Options.choice('build-logger-level', [ + 'trace', + 'debug', + 'info', + 'warn', + 'error', + 'fatal', +] as const).pipe(Options.withDescription('EAS build logger level'), Options.withDefault(undefined)) + +export const nativeBuildFreezeCredentialsOption = Options.boolean('freeze-credentials').pipe( + Options.withDescription('Prevent credential updates in non-interactive mode'), + Options.withDefault(false), +) + +export const nativePrebuildCleanOption = Options.boolean('clean').pipe( + Options.withDescription('Delete native folders before prebuild'), + Options.withDefault(false), +) + +export const nativeAnalyzePlatformOption = Options.choice('platform', ['all', 'ios', 'android', 'web'] as const).pipe( + Options.withDescription('Platform bundle to export'), + Options.withDefault('all'), +) + +export const nativeAnalyzeDevOption = Options.boolean('dev').pipe( + Options.withDescription('Generate a dev build during export'), + Options.withDefault(false), +) + +export const nativeAnalyzeClearOption = Options.boolean('clear').pipe( + Options.withDescription('Clear metro cache before export'), + Options.withDefault(false), +) + +export const nativeAnalyzeMinifyOption = Options.boolean('minify', { ifPresent: false }).pipe( + Options.withDescription('Minify bundle output'), + Options.withDefault(false), +) + +export const nativeAnalyzeBytecodeOption = Options.boolean('bytecode').pipe( + Options.withDescription('Emit Hermes bytecode bundles'), + Options.withDefault(false), +) + +export const nativeJsEnvOption = Options.text('env').pipe( + Options.withDescription('Deployment environment (maps to hot-updater channels)'), + Options.withDefault('staging'), +) + +export const nativeJsChannelOption = Options.text('channel').pipe( + Options.withDescription('Explicit hot-updater channel name'), + Options.withDefault(undefined), +) + +export const nativeJsPlatformOption = Options.choice('platform', ['ios', 'android', 'all'] as const).pipe( + Options.withDescription('Target platform(s) for JS updates'), + Options.withDefault('all'), +) + +export const nativeJsMessageOption = Options.text('message').pipe( + Options.withDescription('Custom release message for hot-updater deploys'), + Options.withDefault(undefined), +) + +export const nativeJsForceOption = Options.boolean('force', { ifPresent: false }).pipe( + Options.withDescription('Force publish even if target version is unchanged'), + Options.withDefault(false), +) + +export const nativeJsTargetVersionOption = Options.text('target-version').pipe( + Options.withDescription('Override runtime target version for JS updates'), + Options.withDefault(undefined), +) + +export const nativeJsDryRunOption = Options.boolean('dry-run').pipe( + Options.withDescription('Print commands without executing hot-updater'), + Options.withDefault(false), +) + +export const nativeDeploySubmitPlatformOption = Options.choice('platform', ['ios', 'android'] as const).pipe( + Options.withDescription('Platform to submit to the store'), + Options.withDefault('ios'), +) + +export const nativeDeploySubmitProfileOption = Options.text('profile').pipe( + Options.withDescription('EAS submit profile declared in eas.json'), + Options.withDefault('production'), +) + +export const nativeDeploySubmitPathOption = Options.text('path').pipe( + Options.withDescription('Path to a build artifact to submit'), + Options.withDefault(undefined), +) + +export const nativeDeploySubmitBuildIdOption = Options.text('build-id').pipe( + Options.withDescription('Use a specific EAS build ID for submission'), + Options.withDefault(undefined), +) + +export const nativeDeploySubmitLatestOption = Options.boolean('latest', { ifPresent: false }).pipe( + Options.withDescription('Submit the latest EAS build when no explicit artifact is provided'), + Options.withDefault(true), +) + +export const nativeDeploySubmitNonInteractiveOption = Options.boolean('non-interactive', { + ifPresent: false, +}).pipe(Options.withDescription('Disable interactive prompts during eas submit'), Options.withDefault(true)) + +export const nativeDeploySubmitWaitOption = Options.boolean('wait', { ifPresent: false }).pipe( + Options.withDescription('Wait for the submission to finish'), + Options.withDefault(true), +) + +export const nativeDeploySubmitJsonOption = Options.boolean('json').pipe( + Options.withDescription('Emit JSON output from eas submit'), + Options.withDefault(false), +) + +export const nativeDeploySubmitVerboseOption = Options.boolean('verbose', { + ifPresent: false, +}).pipe(Options.withDescription('Enable verbose logging for eas submit'), Options.withDefault(false)) diff --git a/scripts/thing/react-native/subcommand.ts b/scripts/thing/react-native/subcommand.ts new file mode 100644 index 0000000..8474a73 --- /dev/null +++ b/scripts/thing/react-native/subcommand.ts @@ -0,0 +1,548 @@ +import { FileSystem, Path, Command, CommandExecutor } from '@effect/platform' +import { diffFingerprints } from '@expo/fingerprint' +import type { Fingerprint, FingerprintDiffItem, Platform as ExpoPlatform } from '@expo/fingerprint' +import { Console, Effect, Schedule } from 'effect' +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { + type ReactNativePrebuildSubcommand, + type ReactNativeBuildSubcommand, + type ReactNativeRunSubcommand, + type ReactNativeAnalyzeSubcommand, + type ReactNativeDeployCheckSubcommand, + type ReactNativeDeployJsUpdateSubcommand, + type ReactNativeDeploySubmitSubcommand, +} from './domain' +import { execProcess, shellInPath } from '../utils/shell' +import { branchToNativeChannel, Git, git } from '../git' +import * as Workspace from '../workspace' +import { collectChangedFiles, resolveBase, resolveHead } from '../../ci/surface-detector' +import { createFingerprint } from '../utils/fingerprint' + +type NativePlatform = 'ios' | 'android' + +interface FingerprintSnapshot { + commit: string + hash: string + fingerprint: Fingerprint +} + +interface FingerprintSummary { + baseHash: string + headHash: string + changed: boolean + diff: FingerprintDiffItem[] +} + +interface DeployCheckPlatformReport { + platform: NativePlatform + jsFingerprint: FingerprintSummary + nativeFingerprint: FingerprintSummary + nativeFilesChanged: string[] + requiresNativeBuild: boolean + requiresStoreRelease: boolean + canHotUpdate: boolean +} + +const formatArgs = (args: Array) => + args.filter((part): part is string => typeof part === 'string' && part.length > 0).join(' ') + +const quote = (value: string) => JSON.stringify(value) + +const flagIfTrue = (name: string, value: boolean, val?: any) => { + if (value) { + if (val) { + return `--${name} ${quote(val)}` + } + return `--${name}` + } + + return undefined +} + +const flagIfFalse = (name: string, value?: unknown) => (!value ? `--no-${name}` : undefined) + +const resolvePath = (path: Path.Path, base: string, candidate?: string) => { + if (!candidate) { + return undefined + } + + return path.isAbsolute(candidate) ? candidate : path.join(base, candidate) +} + +const makeNativeEnv = Effect.fn('react-native.make-env')(function* (workspace: Workspace.Workspace) { + const path = yield* Path.Path + + const hermesDir = + process.env.REACT_NATIVE_OVERRIDE_HERMES_DIR ?? + path.join(workspace.root, 'node_modules', 'hermes-compiler', 'hermesc') + + const easLocalPlugin = + process.env.EAS_LOCAL_BUILD_PLUGIN_PATH ?? + path.join(workspace.root, 'node_modules', 'eas-cli-local-build-plugin', 'bin', 'run') + + const fixedWorkdir = process.env.EXPO_FIXED_BUILD_WORKDIR ?? workspace.root + + return { + ...process.env, + CI: process.env.CI ?? 'false', + // EXPO_DEBUG: process.env.EXPO_DEBUG ?? 'true', + EXPO_NO_TELEMETRY: process.env.EXPO_NO_TELEMETRY ?? 'true', + EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH: process.env.EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH ?? 'true', + EXPO_ATLAS: process.env.EXPO_ATLAS ?? 'true', + EXPO_FIXED_BUILD_WORKDIR: fixedWorkdir, + REACT_NATIVE_OVERRIDE_HERMES_DIR: hermesDir, + EAS_BUILD_DISABLE_EXPO_DOCTOR_STEP: process.env.EAS_BUILD_DISABLE_EXPO_DOCTOR_STEP ?? 'true', + EAS_LOCAL_BUILD_PLUGIN_PATH: easLocalPlugin, + EAS_NO_VCS_CHECK: process.env.EAS_NO_VCS_CHECK ?? '1', + } +}) + +export const prebuild = Effect.fn('react-native.prebuild')(function* ( + workspace: Workspace.Workspace, + subcommand: ReactNativePrebuildSubcommand, +) { + yield* Effect.annotateCurrentSpan({ + projectName: workspace.projectName, + command: 'expo prebuild', + platform: subcommand.platform, + clean: subcommand.clean, + install: subcommand.install, + }) + + const env = yield* makeNativeEnv(workspace) + const runShell = shellInPath(workspace.projectPath, env, true) + + const args = formatArgs([ + `--platform ${subcommand.platform}`, + flagIfTrue('clean', subcommand.clean), + flagIfFalse('install', subcommand.install), + ]) + + const command = ['expo prebuild'] + if (args.length > 0) { + command.push(args) + } + + return yield* runShell` + $$ ${command.join(' ')} + ` +}) + +export const build = Effect.fn('react-native.build')(function* ( + workspace: Workspace.Workspace, + subcommand: ReactNativeBuildSubcommand, +) { + if (subcommand.local && subcommand.platform === 'all') { + return yield* Effect.dieMessage('Local EAS builds require specifying a single platform (ios or android).') + } + + yield* Effect.annotateCurrentSpan({ + projectName: workspace.projectName, + command: 'eas build', + platform: subcommand.platform, + profile: subcommand.profile, + local: subcommand.local, + }) + + const env = yield* makeNativeEnv(workspace) + const path = yield* Path.Path + const artifactPath = resolvePath(path, workspace.root, subcommand.output) + const runShell = shellInPath(workspace.projectPath, env, true) + + const args = formatArgs([ + `--platform ${subcommand.platform}`, + `--profile ${quote(subcommand.profile)}`, + flagIfTrue('json', subcommand.json), + flagIfTrue('local', subcommand.local), + flagIfTrue('message', !!subcommand.message, subcommand.message), + flagIfTrue('build-logger-level', !!subcommand.buildLoggerLevel, subcommand.buildLoggerLevel), + flagIfTrue('freeze-credentials', subcommand.freezeCredentials), + flagIfTrue('reset-cache', subcommand.clearCache), + flagIfFalse('wait', subcommand.wait), + flagIfTrue('artifactPath', !!artifactPath, artifactPath), + '--interactive false', + ]) + + return yield* runShell` + $$ eas build ${args} + ` +}) + +export const run = Effect.fn('react-native.run')(function* ( + workspace: Workspace.Workspace, + subcommand: ReactNativeRunSubcommand, +) { + yield* Effect.annotateCurrentSpan({ + projectName: workspace.projectName, + command: 'expo run', + platform: subcommand.platform, + device: subcommand.device ?? 'auto', + clean: subcommand.clean, + }) + + const env = yield* makeNativeEnv(workspace) + const runShell = shellInPath(workspace.projectPath, env, true) + + const optionalArgs = formatArgs([ + flagIfTrue('device', !!subcommand.device, subcommand.device), + flagIfTrue('scheme', !!subcommand.scheme, subcommand.scheme), + flagIfTrue('configuration', subcommand.platform === 'ios', subcommand.xcodeConfiguration), + flagIfTrue('variant', subcommand.platform === 'android', subcommand.variant), + `--port ${subcommand.port}`, + flagIfFalse('bundler', subcommand.bundler), + '--install false', + flagIfFalse('build-cache', subcommand.buildCache), + flagIfTrue('reset-cache', subcommand.clean), + ]) + + const commandParts = [`pnpm exec expo run:${subcommand.platform}`] + if (optionalArgs.length > 0) { + commandParts.push(optionalArgs) + } + + return yield* runShell` + $$ ${commandParts.join(' ')} + ` +}) + +export const analyze = Effect.fn('react-native.analyze')(function* ( + workspace: Workspace.Workspace, + subcommand: ReactNativeAnalyzeSubcommand, +) { + const env = yield* makeNativeEnv(workspace) + + const head = resolveHead(subcommand.head) + const base = resolveBase(head, subcommand.base) + const changedFiles = collectChangedFiles(base, head) + + const outputDir = path.join(workspace.projectPath, '.expo') + const fs = yield* FileSystem.FileSystem + + yield* Effect.logInfo(`Preparing export output at ${outputDir}`).pipe( + Effect.andThen(fs.makeDirectory(outputDir, { recursive: true })), + Effect.orDie, + ) + + const exportCommandArgs = formatArgs([ + `--platform ${subcommand.platform}`, + '--dev false', + flagIfFalse('minify', subcommand.minify), + flagIfTrue('bundle-output', true, outputDir + '/index.bundle'), + flagIfTrue('assets-dest', true, outputDir), + '--eager', + flagIfTrue('bytecode', subcommand.bytecode), + '--unstable-transform-profile hermes', + ]) + + const exportCommandParts = ['pnpm exec expo export:embed'] + if (exportCommandArgs.length > 0) { + exportCommandParts.push(exportCommandArgs) + } + + const port = 9978 + const atlasJsonl = `.expo/atlas.jsonl` + const atlasJsonlPath = path.join(workspace.projectPath, atlasJsonl) + + const AtlasCommand = Command.make('expo-atlas', '.expo/atlas.jsonl', '--no-open', '--port', port.toString()).pipe( + Command.workingDirectory(workspace.projectPath), + Command.env(env), + Command.start, + ) + + const logMessages = `Expo Atlas is ready on: http://localhost:${port}` + + yield* Effect.logInfo(logMessages).pipe( + Effect.annotateLogs({ + file: atlasJsonlPath, + }), + ) + + yield* Effect.logInfo('Change files', changedFiles) + + yield* AtlasCommand.pipe( + Effect.tap(Effect.addFinalizer(() => Effect.logInfo('Expo Atlas stopped 🛏️'))), + Effect.zipRight(Effect.never), + Effect.orDie, + ) +}) + +export const deployCheck = Effect.fn('react-native.deploy-check')(function* ( + workspace: Workspace.Workspace, + subcommand: ReactNativeDeployCheckSubcommand, +) { + const pathService = yield* Path.Path + const projectRelative = pathService.relative(workspace.root, workspace.projectPath) || '.' + const head = resolveHead(subcommand.head) + const base = resolveBase(head, subcommand.base) + const changedFiles = collectChangedFiles(base, head) + + const platforms: NativePlatform[] = subcommand.platform === 'all' ? ['ios', 'android'] : [subcommand.platform] + + const results: DeployCheckPlatformReport[] = [] + + for (const platform of platforms) { + const nativeHeadSnapshot = yield* Effect.promise(() => + createFingerprintSnapshot(workspace.root, { + commit: head, + projectPath: projectRelative, + platform: platform, + }), + ) + + const nativeBaseSnapshot = + base === head + ? nativeHeadSnapshot + : yield* Effect.promise(() => + createFingerprintSnapshot(workspace.root, { + commit: base, + projectPath: projectRelative, + platform: platform, + }), + ) + + const diff = diffFingerprints(nativeBaseSnapshot.fingerprint, nativeHeadSnapshot.fingerprint) + const nativeChanged = nativeBaseSnapshot.hash !== nativeHeadSnapshot.hash + const nativeTouches = detectNativeTouches(changedFiles, platform) + + const headJs = yield* Effect.promise(() => + createFingerprintSnapshot(workspace.root, { + commit: head, + projectPath: projectRelative, + platform, + mode: 'ota', + }), + ) + + const baseJs = + base === head + ? headJs + : yield* Effect.promise(() => + createFingerprintSnapshot(workspace.root, { + commit: base, + projectPath: projectRelative, + platform, + mode: 'ota', + }), + ) + + const jsChanged = baseJs.hash !== headJs.hash + const requiresNativeBuild = nativeChanged || nativeTouches.length > 0 + const canHotUpdate = jsChanged && !requiresNativeBuild + + results.push({ + platform, + jsFingerprint: { + baseHash: baseJs.hash, + headHash: headJs.hash, + changed: jsChanged, + diff, + }, + nativeFingerprint: { + baseHash: nativeBaseSnapshot.hash, + headHash: nativeHeadSnapshot.hash, + changed: nativeChanged, + diff, + }, + nativeFilesChanged: nativeTouches, + requiresStoreRelease: nativeChanged, + requiresNativeBuild, + canHotUpdate, + }) + } +}) + +export const deployJsUpdate = Effect.fn('react-native.deploy-js-update')(function* ( + workspace: Workspace.Workspace, + subcommand: ReactNativeDeployJsUpdateSubcommand, +) { + const git = yield* Git + const head = resolveHead(subcommand.head) + const base = resolveBase(head, subcommand.base) + const branch = yield* git.branch + const lastCommit = yield* git.lastCommit + const channel = subcommand.channel ?? branchToNativeChannel(branch, subcommand.env) + const message = subcommand.message ?? lastCommit.message + + const platforms: Array<'ios' | 'android'> = subcommand.platform === 'all' ? ['ios', 'android'] : [subcommand.platform] + + yield* Effect.logInfo(`Publishing JS update for ${branch} -> channel ${channel} (${subcommand.env}) via hot-updater`) + + const runShell = shellInPath(workspace.root, process.env ?? {}, false) + + for (const platform of platforms) { + const args = ['exec hot-updater deploy', `-p ${platform}`, `-c ${channel}`, `-m ${quote(message)}`] + + if (subcommand.force || process.env.HOT_UPDATER_FORCE_UPDATE === 'true') { + args.push('-f') + } + + const targetVersion = subcommand.targetVersion ?? process.env.HOT_UPDATER_TARGET_VERSION + if (targetVersion) { + args.push(`-t ${quote(targetVersion)}`) + } + + yield* Effect.logInfo(`→ hot-updater deploy (${platform})`) + + if (subcommand.dryRun) { + yield* Effect.logInfo(`[dry-run] pnpm ${args.join(' ')}`) + continue + } + + yield* runShell` + $$ pnpm ${args.join(' ')} + ` + } +}) + +export const deploySubmit = Effect.fn('react-native.deploy-submit')(function* ( + workspace: Workspace.Workspace, + subcommand: ReactNativeDeploySubmitSubcommand, +) { + yield* Effect.annotateCurrentSpan({ + projectName: workspace.projectName, + command: 'eas submit', + platform: subcommand.platform, + profile: subcommand.profile ?? 'default', + }) + + const env = yield* makeNativeEnv(workspace) + const runShell = shellInPath(workspace.projectPath, env, true) + + const shouldUseLatest = subcommand.latest && !subcommand.path && !subcommand.buildId + + const args = formatArgs([ + `--platform ${subcommand.platform}`, + flagIfTrue('profile', !!subcommand.profile, subcommand.profile), + flagIfTrue('path', !!subcommand.path, subcommand.path), + flagIfTrue('id', !!subcommand.buildId, subcommand.buildId), + flagIfTrue('latest', shouldUseLatest), + flagIfTrue('non-interactive', subcommand.nonInteractive), + flagIfFalse('wait', subcommand.wait), + flagIfTrue('json', subcommand.json), + flagIfTrue('verbose', subcommand.verbose), + ]) + + return yield* runShell` + $$ eas submit ${args} + ` +}) + +const DEFAULT_NATIVE_PROJECT = 'apps/native' + +const PLATFORM_PATTERNS: Record = { + ios: [/^ios\//, /^apps\/native\/ios\//, /^apps\/native\/app\.config\.ios\./, /^apps\/native\/Info\.plist$/], + android: [ + /^android\//, + /^apps\/native\/android\//, + /^apps\/native\/app\.config\.android\./, + /^apps\/native\/AndroidManifest\.xml$/, + ], +} + +const COMMON_NATIVE_PATTERNS: RegExp[] = [ + /^apps\/native\/app\.json$/, + /^apps\/native\/eas\.json$/, + /^apps\/native\/package\.json$/, + /^apps\/native\/babel\.config\.js$/, + /^apps\/native\/metro\.config\.js$/, + /^packages\/expo-/, + /^packages\/react-native/, + /^package\.json$/, + /^pnpm-lock\.yaml$/, + /^pnpm-workspace\.yaml$/, +] + +const detectNativeTouches = (files: string[], platform: NativePlatform) => { + const patterns = [...PLATFORM_PATTERNS[platform], ...COMMON_NATIVE_PATTERNS] + return files.filter((file) => patterns.some((regex) => regex.test(file))) +} + +async function createFingerprintSnapshot( + workspaceRoot: string, + options: { + commit: string + projectPath: string + platform: NativePlatform + debug?: boolean + silent?: boolean + mode?: 'ota' | 'native' + }, +): Promise { + const fingerprint = await withCommitWorkspace(workspaceRoot, options.commit, async (root) => { + const projectPath = path.join(root, options.projectPath || DEFAULT_NATIVE_PROJECT) + if (!fs.existsSync(projectPath)) { + throw new Error(`Project root ${options.projectPath} is missing in commit ${options.commit}`) + } + + return createFingerprint(projectPath, { + platform: options.platform as ExpoPlatform, + ...(options.debug !== undefined ? { debug: options.debug } : {}), + ...(options.silent !== undefined ? { silent: options.silent } : {}), + ...(options.mode ? { mode: options.mode } : {}), + }) + }) + + return { + commit: options.commit, + hash: fingerprint.hash, + fingerprint, + } +} + +async function withCommitWorkspace(workspaceRoot: string, commit: string, task: (root: string) => Promise) { + const currentHead = git(['rev-parse', 'HEAD'], { cwd: workspaceRoot }) + if (commit === currentHead) { + return task(workspaceRoot) + } + + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'xstack-native-fingerprint-')) + let worktreeAdded = false + + try { + git(['worktree', 'add', '--detach', tempRoot, commit], { + cwd: workspaceRoot, + }) + worktreeAdded = true + linkSharedResources(workspaceRoot, tempRoot) + return await task(tempRoot) + } finally { + if (worktreeAdded) { + try { + git(['worktree', 'remove', '--force', tempRoot], { + cwd: workspaceRoot, + }) + } catch (error) { + console.warn( + `[fingerprint] Failed to remove worktree: ${error instanceof Error ? error.message : String(error)}`, + ) + } + } + + try { + fs.rmSync(tempRoot, { recursive: true, force: true }) + } catch { + // ignore cleanup errors + } + } +} + +const linkSharedResources = (workspaceRoot: string, worktreeRoot: string) => { + const shared = ['node_modules', '.expo'] + for (const entry of shared) { + const source = path.join(workspaceRoot, entry) + if (!fs.existsSync(source)) continue + const target = path.join(worktreeRoot, entry) + try { + if (fs.existsSync(target)) { + fs.rmSync(target, { recursive: true, force: true }) + } + type SymlinkType = Parameters[2] + const type = (process.platform === 'win32' ? 'junction' : 'dir') as SymlinkType + fs.symlinkSync(source, target, type) + } catch (error) { + console.warn(`[fingerprint] Failed to link ${entry}: ${error instanceof Error ? error.message : error}`) + } + } +} diff --git a/scripts/thing/react-router/build.ts b/scripts/thing/react-router/build.ts index 6b2d9c9..9ef801c 100644 --- a/scripts/thing/react-router/build.ts +++ b/scripts/thing/react-router/build.ts @@ -1,8 +1,9 @@ import { FileSystem } from '@effect/platform' import { Effect, pipe } from 'effect' import { Deployment } from '../deployment' -import { BuildReactRouterParameters, type BuildSubcommand } from '../domain' -import { shell, shellEnv, shellInPath } from '../utils' +import { type BuildSubcommand } from '../domain' +import { BuildReactRouterParameters } from './domain' +import { shell, shellInPath } from '../utils/shell' import { Workspace } from '../workspace' import { fixPwaSwScript } from './scripts' @@ -13,7 +14,7 @@ export const start = Effect.fn('react-router.build-start')(function* (subcommand return yield* Effect.dieMessage('Invalid provider') } - yield* Effect.logDebug('React router build started') + yield* Effect.logInfo('React router build started') const fs = yield* FileSystem.FileSystem const parameters = yield* BuildReactRouterParameters @@ -34,7 +35,7 @@ export const start = Effect.fn('react-router.build-start')(function* (subcommand if (isSpaMode || isDesktop) { const mode = isDesktop ? 'DESKTOP' : 'SPA' - const runShell = shellEnv(workspace.projectPath, { + const runShell = shellInPath(workspace.projectPath, { ...viteEnv, NODE_ENV: subcommand.nodeEnv, STAGE: subcommand.stage, @@ -42,7 +43,7 @@ export const start = Effect.fn('react-router.build-start')(function* (subcommand ANALYZE: analyze, MINIFY: minify, }) - yield* Effect.logDebug(`Build App (${mode})`).pipe( + yield* Effect.logInfo(`Build App (${mode})`).pipe( Effect.andThen( runShell` $ BUILD_TARGET=client vite build -c vite.config.ts @@ -62,7 +63,7 @@ export const start = Effect.fn('react-router.build-start')(function* (subcommand }), ) } else { - const runShell = shellEnv(workspace.projectPath, { + const runShell = shellInPath(workspace.projectPath, { ...viteEnv, NODE_ENV: subcommand.nodeEnv, STAGE: subcommand.stage, @@ -70,7 +71,7 @@ export const start = Effect.fn('react-router.build-start')(function* (subcommand ANALYZE: analyze, MINIFY: minify, }) - yield* Effect.logDebug('Build App (SSR MODE)').pipe( + yield* Effect.logInfo('Build App (SSR MODE)').pipe( Effect.andThen( runShell` $ BUILD_TARGET=client vite build -c vite.config.ts @@ -92,7 +93,7 @@ export const start = Effect.fn('react-router.build-start')(function* (subcommand } // fix server/index.js - yield* Effect.logDebug('Fix server/index.js').pipe( + yield* Effect.logInfo('Fix server/index.js').pipe( Effect.andThen(fs.readFileString(`${workspace.projectOutput.dist}/server/index.js`)), Effect.andThen((content) => { // dead import removal @@ -110,7 +111,7 @@ export const start = Effect.fn('react-router.build-start')(function* (subcommand ) // PWA fix - yield* Effect.logDebug('Fix PWA sw.js script').pipe( + yield* Effect.logInfo('Fix PWA sw.js script').pipe( Effect.andThen(fixPwaSwScript(workspace, { minify })), Effect.orDie, ) @@ -119,7 +120,7 @@ export const start = Effect.fn('react-router.build-start')(function* (subcommand yield* deployment.build - yield* Effect.logDebug('Cleanup build folder').pipe( + yield* Effect.logInfo('Cleanup build folder').pipe( Effect.andThen( shell` $ rm -rf ${workspace.projectOutput.dist}/package.json diff --git a/scripts/thing/react-router/cloudflare.ts b/scripts/thing/react-router/cloudflare.ts index d3586ff..10dfffb 100644 --- a/scripts/thing/react-router/cloudflare.ts +++ b/scripts/thing/react-router/cloudflare.ts @@ -7,9 +7,9 @@ import { formatSize } from '../cloudflare/utils' import { runWorkersDeploy } from '../cloudflare/workers' import { parseConfig } from '../cloudflare/wrangler' import { Deployment, DeploymentOutput } from '../deployment' -import { BuildReactRouterParameters } from '../domain' +import { BuildReactRouterParameters } from './domain' import { Git } from '../git' -import { shellInPath } from '../utils' +import { shellInPath } from '../utils/shell' import { Workspace } from '../workspace' const make = Effect.gen(function* () { @@ -366,7 +366,7 @@ const make = Effect.gen(function* () { $ rm -rf server `.pipe(Effect.withSpan('build.output-cleanup'), Effect.orDie) - return yield* Effect.logDebug('React router build finished').pipe( + return yield* Effect.logInfo('React router build finished').pipe( Effect.annotateLogs({ 'client assets size': clientJsAssetsSize.toString(), 'client sw.js size': clientInfo.swSize, diff --git a/scripts/thing/react-router/command.ts b/scripts/thing/react-router/command.ts new file mode 100644 index 0000000..ba7634d --- /dev/null +++ b/scripts/thing/react-router/command.ts @@ -0,0 +1,35 @@ +import type { Stage } from '../domain' +import { BuildReactRouterTarget } from './domain' +import * as ReactRouterSubcommand from './subcommand' +import type { BuildSubcommand, DeploySubcommand, PreviewSubcommand, ServeSubcommand } from '../domain' +import type { BuildReactRouterTarget as BuildReactRouterTargetType } from './domain' +import type { Workspace as WorkspaceModel } from '../workspace' + +interface ReactRouterTargetOptions { + readonly isSpaMode: boolean + readonly isDesktop: boolean +} + +const createReactRouterTarget = (stage: Stage, options: ReactRouterTargetOptions): BuildReactRouterTargetType => + BuildReactRouterTarget({ + runtime: 'cloudflare-workers', + options: { + isSpaMode: options.isSpaMode, + isDesktop: options.isDesktop, + }, + stage, + }) + +const runServe = (workspace: WorkspaceModel, subcommand: ServeSubcommand) => + ReactRouterSubcommand.serve(workspace, subcommand) + +const runBuild = (workspace: WorkspaceModel, subcommand: BuildSubcommand) => + ReactRouterSubcommand.build(workspace, subcommand) + +const runDeploy = (workspace: WorkspaceModel, subcommand: DeploySubcommand, target: BuildReactRouterTargetType) => + ReactRouterSubcommand.deploy(workspace, subcommand, target) + +const runPreview = (workspace: WorkspaceModel, subcommand: PreviewSubcommand, target: BuildReactRouterTargetType) => + ReactRouterSubcommand.preview(workspace, subcommand, target) + +export { createReactRouterTarget, runBuild, runDeploy, runPreview, runServe } diff --git a/scripts/thing/react-router/deploy.ts b/scripts/thing/react-router/deploy.ts index d5219c8..0fc145c 100644 --- a/scripts/thing/react-router/deploy.ts +++ b/scripts/thing/react-router/deploy.ts @@ -1,6 +1,8 @@ import { Cause, Effect, Exit, pipe } from 'effect' import { Deployment } from '../deployment' -import { DatabaseMigrateDeploySubcommand, type BuildReactRouterTarget, type DeploySubcommand } from '../domain' +import type { DeploySubcommand } from '../domain' +import { DatabaseMigrateDeploySubcommand } from '../database/domain' +import type { BuildReactRouterTarget } from './domain' import { Git } from '../git' import { Github } from '../github' import { Notification } from '../notification' @@ -17,7 +19,9 @@ export const start = Effect.fn('react-router.deploy-start')(function* ( const git = yield* Git const github = yield* Github - const [branch, lastCommit] = yield* Effect.all([git.branch, git.lastCommit], { concurrency: 'unbounded' }) + const [branch, lastCommit] = yield* Effect.all([git.branch, git.lastCommit], { + concurrency: 'unbounded', + }) const tags = `React router on ${buildTarget.runtime}` @@ -37,7 +41,7 @@ export const start = Effect.fn('react-router.deploy-start')(function* ( const fail = (message: string) => Effect.all( [ - Effect.logDebug('Deploy failed'), + Effect.logInfo('Deploy failed'), notification.failed({ projectName: workspace.projectName, branch, @@ -121,7 +125,7 @@ export const start = Effect.fn('react-router.deploy-start')(function* ( return fail(Cause.pretty(cause)) }, onSuccess() { - return Effect.logDebug('Deploy successful') + return Effect.logInfo('Deploy successful') }, }) }) diff --git a/scripts/thing/react-router/domain.ts b/scripts/thing/react-router/domain.ts new file mode 100644 index 0000000..c0c75f0 --- /dev/null +++ b/scripts/thing/react-router/domain.ts @@ -0,0 +1,23 @@ +import { Context, Data, Schema } from 'effect' +import { NodeEnv, Stage } from '../core/env' + +export const BuildReactRouterSchema = Schema.Struct({ + _tag: Schema.Literal('BuildReactRouter'), + runtime: Schema.Literal('cloudflare-workers'), + options: Schema.Struct({ + isSpaMode: Schema.Boolean, + isDesktop: Schema.Boolean, + }), + stage: Stage, +}) +export interface BuildReactRouterTarget extends Schema.Schema.Type {} +export const BuildReactRouterTarget = Data.tagged('BuildReactRouter') + +export interface BuildReactRouterParameters { + readonly nodeEnv: NodeEnv + readonly target: BuildReactRouterTarget + readonly env: Record +} +export const BuildReactRouterParameters = Context.GenericTag( + '@thing:build-react-router-parameters', +) diff --git a/scripts/thing/react-router/options.ts b/scripts/thing/react-router/options.ts new file mode 100644 index 0000000..bd37d88 --- /dev/null +++ b/scripts/thing/react-router/options.ts @@ -0,0 +1,13 @@ +import { Options } from '@effect/cli' + +const reactRouterSpaModeOption = Options.boolean('spa').pipe( + Options.withDescription('Treat the build as SPA-only (Pages assets deployment)'), + Options.withDefault(false), +) + +const reactRouterDesktopOption = Options.boolean('desktop').pipe( + Options.withDescription('Enable desktop bundling hints (e.g., Tauri build)'), + Options.withDefault(false), +) + +export { reactRouterDesktopOption, reactRouterSpaModeOption } diff --git a/scripts/thing/react-router/preview.ts b/scripts/thing/react-router/preview.ts index a5e9be7..6c5cbcc 100644 --- a/scripts/thing/react-router/preview.ts +++ b/scripts/thing/react-router/preview.ts @@ -1,7 +1,8 @@ import { FileSystem, Path } from '@effect/platform' import { Effect } from 'effect' import type { Unstable_DevOptions } from 'wrangler' -import { BuildReactRouterParameters, type BuildReactRouterTarget, type PreviewSubcommand } from '../domain' +import { type PreviewSubcommand } from '../domain' +import { BuildReactRouterParameters, type BuildReactRouterTarget } from './domain' import { Workspace } from '../workspace' import { unstableDev } from '../cloudflare/wrangler' diff --git a/scripts/thing/react-router/scripts.ts b/scripts/thing/react-router/scripts.ts index 7e2cd4a..8f0da5d 100644 --- a/scripts/thing/react-router/scripts.ts +++ b/scripts/thing/react-router/scripts.ts @@ -4,7 +4,7 @@ import * as traverse from '@babel/traverse' import { FileSystem, Path } from '@effect/platform' import { Config, Data, Effect } from 'effect' import * as R from 'remeda' -import { shellInPath } from '../utils' +import { shellInPath } from '../utils/shell' import type { Workspace } from '../workspace' type ReactRouterBuildEntry = { diff --git a/scripts/thing/react-router/serve.ts b/scripts/thing/react-router/serve.ts index 226bef1..1c94767 100644 --- a/scripts/thing/react-router/serve.ts +++ b/scripts/thing/react-router/serve.ts @@ -25,7 +25,8 @@ import { tsImport } from 'tsx/esm/api' import type { Connect } from 'vite' import { WebSocketServer, type WebSocket as WebSocketType } from 'ws' import { getPlatformProxy } from '../cloudflare/wrangler' -import { BuildReactRouterParameters, type ServeSubcommand } from '../domain' +import { type ServeSubcommand } from '../domain' +import { BuildReactRouterParameters } from './domain' import { Workspace } from '../workspace' import { launchEditor } from './launch-editor' import { otelForward } from './otel-forward' @@ -256,7 +257,7 @@ function log(out: boolean, method: string, path: string, status = 0, elapsed = ' return Effect.logError(str) } - return Effect.logTrace(str) + return Effect.logInfo(str) } interface OtelWebSocketMessage { @@ -283,10 +284,13 @@ export const start = Effect.fn('react-router.serve-start')(function* (subcommand */ const { contextBuilder } = yield* Effect.promise( () => - tsImport(contextFilePath, { parentURL: import.meta.url, tsconfig: tsconfigPath }) as Promise<{ + tsImport(contextFilePath, { + parentURL: import.meta.url, + tsconfig: tsconfigPath, + }) as Promise<{ contextBuilder: ReturnType }>, - ).pipe(Effect.tap(Effect.logDebug('Import Context Builder')), Effect.tapErrorCause(Effect.logError), Effect.orDie) + ).pipe(Effect.tap(Effect.logInfo('Import Context Builder')), Effect.tapErrorCause(Effect.logError), Effect.orDie) if (!contextBuilder) { return yield* Effect.dieMessage('No contextBuilder function export found') @@ -300,6 +304,7 @@ export const start = Effect.fn('react-router.serve-start')(function* (subcommand const server = createServer({ configFile: `${workspace.projectPath}/vite.config.ts`, appType: 'custom', + env: {}, server: { allowedHosts: true, middlewareMode: true, @@ -318,7 +323,7 @@ export const start = Effect.fn('react-router.serve-start')(function* (subcommand }), ).pipe( Effect.tapErrorCause(Effect.logError), - Effect.tap(Effect.logDebug('Create Vite Server')), + Effect.tap(Effect.logInfo('Create Vite Server')), Effect.withSpan('vite.createServer'), ) @@ -697,9 +702,11 @@ export const start = Effect.fn('react-router.serve-start')(function* (subcommand yield* Effect.addFinalizer( Effect.fn('react-router.serve-stop')(function* () { - yield* Effect.promise(() => viteDevServer.close()) + yield* Effect.promise(() => viteDevServer.close()).pipe(Effect.ignore) yield* Effect.try(() => ws.close()).pipe(Effect.ignore) yield* Effect.try(() => server.close()).pipe(Effect.ignore) + + yield* Effect.logInfo('Dev Server stopped 🛏️') }), ) diff --git a/scripts/thing/react-router/subcommand.ts b/scripts/thing/react-router/subcommand.ts index cd51dfe..b23bd72 100644 --- a/scripts/thing/react-router/subcommand.ts +++ b/scripts/thing/react-router/subcommand.ts @@ -2,23 +2,22 @@ import { FileSystem, Path } from '@effect/platform' import { Effect, Layer, pipe } from 'effect' import { ReactRouterOnCloudflare } from './cloudflare' import { - BuildReactRouterParameters, - type BuildReactRouterTarget, type BuildSubcommand, type BuildTarget, type DeploySubcommand, type PreviewSubcommand, type ServeSubcommand, } from '../domain' +import { BuildReactRouterParameters, type BuildReactRouterTarget } from './domain' import * as Environment from '../environment' -import { shell } from '../utils' +import { shell } from '../utils/shell' import * as Workspace from '../workspace' export const serve = Effect.fn('react-router.serve')(function* ( workspace: Workspace.Workspace, subcommand: ServeSubcommand, ) { - yield* Effect.logDebug(`Serve ${workspace.projectName}`) + yield* Effect.logInfo(`Serve ${workspace.projectName}`) if (subcommand.target._tag !== 'BuildReactRouter') { return yield* Effect.dieMessage('Invalid target') @@ -64,7 +63,7 @@ export const build = Effect.fn('react-router.build')(function* ( const fs = yield* FileSystem.FileSystem const path = yield* Path.Path - yield* Effect.logDebug('Start building app') + yield* Effect.logInfo('Start building app') if (subcommand.target._tag !== 'BuildReactRouter') { return yield* Effect.dieMessage('Invalid target') @@ -104,7 +103,7 @@ export const build = Effect.fn('react-router.build')(function* ( ) yield* pipe( - Effect.logDebug('Clean dist folder'), + Effect.logInfo('Clean dist folder'), Effect.andThen( shell` $ rm -rf ${workspace.projectOutput.dist} @@ -134,7 +133,7 @@ export const build = Effect.fn('react-router.build')(function* ( .start(subcommand) .pipe(Effect.provide(BuildLive), Effect.withConfigProvider(environment.configProvider)) - return yield* Effect.logDebug('Build finished') + return yield* Effect.logInfo('Build finished') }) export const deploy = Effect.fn('react-router.deploy')(function* ( @@ -142,7 +141,7 @@ export const deploy = Effect.fn('react-router.deploy')(function* ( subcommand: DeploySubcommand, buildTarget: BuildTarget, ) { - yield* Effect.logDebug(`Deploy ${workspace.projectName}`) + yield* Effect.logInfo(`Deploy ${workspace.projectName}`) if (buildTarget._tag !== 'BuildReactRouter') { return yield* Effect.dieMessage('Invalid build target') @@ -187,7 +186,7 @@ export const preview = Effect.fn('react-router.preview')(function* ( subcommand: PreviewSubcommand, buildTarget: BuildTarget, ) { - yield* Effect.logDebug(`Preview ${workspace.projectName}`) + yield* Effect.logInfo(`Preview ${workspace.projectName}`) if (buildTarget._tag !== 'BuildReactRouter') { return yield* Effect.dieMessage('Invalid build target') diff --git a/scripts/thing/tests/git.test.ts b/scripts/thing/tests/git.test.ts index 1ed974e..825b4d2 100644 --- a/scripts/thing/tests/git.test.ts +++ b/scripts/thing/tests/git.test.ts @@ -1,6 +1,6 @@ import { ConfigProvider, Effect } from 'effect' import { describe, expect, it } from '@effect/vitest' -import { Git } from '../git' +import { Git, branchToNativeChannel } from '../git' describe('git', () => { it('local', () => { @@ -44,3 +44,28 @@ describe('git', () => { return Effect.runPromise(main) }) }) + +describe('branchToNativeChannel', () => { + it('keeps main/staging/test channels without env prefixing', () => { + expect(branchToNativeChannel('main', 'staging')).toBe('main') + expect(branchToNativeChannel('staging', 'production')).toBe('staging') + expect(branchToNativeChannel('test', 'production')).toBe('test') + }) + + it('maps feature branches to feat-*', () => { + expect(branchToNativeChannel('feat/native/ui')).toBe('feat-native-ui') + }) + + it('detects PR refs and maps to preview channels', () => { + expect(branchToNativeChannel('refs/pull/42/head')).toBe('preview/pr-42') + expect(branchToNativeChannel('pr/amazing-change')).toBe('preview/amazing-change') + }) + + it('falls back to env-prefixed channel for other branches', () => { + expect(branchToNativeChannel('feature/native/rework', 'Staging')).toBe('staging-feature-native-rework') + }) + + it('defaults to main when empty', () => { + expect(branchToNativeChannel('///', 'production')).toBe('production-main') + }) +}) diff --git a/scripts/thing/tsconfig.json b/scripts/thing/tsconfig.json index a5471dd..415d02c 100644 --- a/scripts/thing/tsconfig.json +++ b/scripts/thing/tsconfig.json @@ -1,5 +1,5 @@ { "extends": "../../tsconfig.base.json", "include": ["**/*.ts", "vite.config.ts"], - "exclude": [] + "exclude": [], } diff --git a/scripts/thing/utils/capture-stdout.ts b/scripts/thing/utils/capture-stdout.ts new file mode 100644 index 0000000..5df24cf --- /dev/null +++ b/scripts/thing/utils/capture-stdout.ts @@ -0,0 +1,54 @@ +/** + * This class captures the stdout and stores each write in an array of strings. + */ +export class CaptureStdout { + private _capturedText: string[] + private _orig_stdout_write: typeof process.stdout.write | null + + constructor() { + this._capturedText = [] + this._orig_stdout_write = null + } + + /** + * Starts capturing the writes to process.stdout + */ + startCapture() { + this._orig_stdout_write = process.stdout.write + // @ts-ignore + process.stdout.write = this._writeCapture.bind(this) + } + + /** + * Stops capturing the writes to process.stdout. + */ + stopCapture() { + if (this._orig_stdout_write) { + process.stdout.write = this._orig_stdout_write + } + } + + /** + * Private method that is used as the replacement write function for process.stdout + * @param string + * @private + */ + _writeCapture(string: string) { + this._capturedText.push(string) + } + + /** + * Retrieve the text that has been captured since creation or since the last clear call + * @returns {Array} of Strings + */ + getCapturedText() { + return this._capturedText + } + + /** + * Clears all of the captured text + */ + clearCaptureText() { + this._capturedText = [] + } +} diff --git a/scripts/thing/utils/fingerprint.ts b/scripts/thing/utils/fingerprint.ts new file mode 100644 index 0000000..216d226 --- /dev/null +++ b/scripts/thing/utils/fingerprint.ts @@ -0,0 +1,161 @@ +import { SourceSkips, createFingerprintAsync, type HashSource, type Options } from '@expo/fingerprint' +import * as fs from 'node:fs' +import * as path from 'node:path' +import { globSync } from 'tinyglobby' + +/** + * Utility function that takes an array of extensions and generates glob patterns to allow those extensions. + * @param extensions Array of allowed extensions (e.g., ["*.swift", "*.kt", "*.java"]) + * @returns Array of glob patterns + */ +function allowExtensions(extensions: string[]) { + return extensions.map((ext) => `!**/${ext}`) +} + +/** + * Utility function that returns the default ignore paths. + * @returns Array of default ignore paths + */ +function getDefaultIgnorePaths() { + return ['**/*', '**/.build/**/*', '**/build/'] +} + +/** + * Processes extra source files and directories for fingerprinting. + * @param extraSources Array of file paths, directory paths, or glob patterns + * @param cwd Current working directory for resolving paths + * @returns Array of processed sources with their contents or directory information + */ +function processExtraSources(extraSources: string[], cwd: string): HashSource[] { + const processedSources: HashSource[] = [] + for (const source of extraSources) + try { + const matches = globSync(source, { + cwd, + ignore: [], + absolute: true, + onlyFiles: false, + }) + for (const absolutePath of matches) + if (fs.existsSync(absolutePath)) { + const stats = fs.statSync(absolutePath) + const relativePath = path.relative(cwd, absolutePath) + if (stats.isDirectory()) + processedSources.push({ + type: 'dir', + filePath: relativePath, + reasons: ['custom-user-config'], + }) + else + processedSources.push({ + type: 'contents', + id: relativePath, + contents: fs.readFileSync(absolutePath, 'utf-8'), + reasons: ['custom-user-config'], + }) + } + } catch (error) { + console.warn(`Error processing extra source "${source}": ${error}`) + } + return processedSources +} + +function getOtaFingerprintOptions( + platform: 'ios' | 'android', + path: string, + options: { + ignorePaths?: string[] | undefined + extraSources?: string[] | undefined + debug?: boolean | undefined + silent?: boolean | undefined + }, +): Options { + return { + useRNCoreAutolinkingFromExpo: false, + platforms: [platform], + ignorePaths: [ + ...getDefaultIgnorePaths(), + ...allowExtensions([ + '*.swift', + '*.h', + '*.m', + '*.mm', + '*.kt', + '*.java', + '*.cpp', + '*.hpp', + '*.c', + '*.cc', + '*.cxx', + '*.podspec', + '*.gradle', + '*.kts', + 'CMakeLists.txt', + 'Android.mk', + 'Application.mk', + '*.pro', + '*.mk', + '*.cmake', + '*.ninja', + 'Makefile', + '*.bazel', + '*.buck', + 'BUILD', + 'WORKSPACE', + 'BUILD.bazel', + 'WORKSPACE.bazel', + ]), + 'android/**/*', + 'ios/**/*', + ...(options.ignorePaths ?? []), + ], + sourceSkips: + SourceSkips.GitIgnore | + SourceSkips.PackageJsonScriptsAll | + SourceSkips.PackageJsonAndroidAndIosScriptsIfNotContainRun | + SourceSkips.ExpoConfigAll | + SourceSkips.ExpoConfigVersions | + SourceSkips.ExpoConfigNames | + SourceSkips.ExpoConfigRuntimeVersionIfString | + SourceSkips.ExpoConfigAssets | + SourceSkips.ExpoConfigExtraSection | + SourceSkips.ExpoConfigEASProject | + SourceSkips.ExpoConfigSchemes, + extraSources: processExtraSources(options.extraSources ?? [], path), + debug: options.debug ?? false, + silent: options.silent ?? true, + } +} + +export type FingerprintMode = 'ota' | 'native' + +interface FingerprintOptions { + platform?: 'ios' | 'android' + debug?: boolean | undefined + silent?: boolean | undefined + mode?: FingerprintMode + ignorePaths?: string[] + extraSources?: string[] +} + +export const createFingerprint = async (projectPath: string, inputOptions?: FingerprintOptions) => { + const { mode = 'ota', platform = 'ios', ...options } = inputOptions ?? {} + + if (mode === 'native') { + return await createFingerprintAsync(projectPath, { + platforms: [platform], + debug: options.debug ?? false, + silent: options.silent ?? true, + }) + } + + // const fingerprintConfig: any = (await import('../../apps/native/hot-updater.config')).default + + return await createFingerprintAsync( + projectPath, + getOtaFingerprintOptions(platform, projectPath, { + // ...fingerprintConfig, + ...options, + }), + ) +} diff --git a/scripts/thing/utils/shell.ts b/scripts/thing/utils/shell.ts new file mode 100644 index 0000000..a5ef157 --- /dev/null +++ b/scripts/thing/utils/shell.ts @@ -0,0 +1,63 @@ +import { spawn } from 'node:child_process' +import { Data, Effect } from 'effect' +import shellac from 'shellac' +import { CommandDescriptor } from '@effect/cli' + +declare type ShellacValueInterpolation = string | boolean | undefined | number | null +declare type ShellacInterpolations = + | ShellacValueInterpolation + | Promise + | ((a: string) => void) + | ((a: string) => Promise) + | (() => Promise) + +export class ShellExecuteError extends Data.TaggedError('ShellExecuteError')<{ + cause?: Error | undefined +}> {} + +export class ProcessExecuteError extends Data.TaggedError('ProcessExecuteError')<{ + command: string + args: string[] + exitCode: number + cause?: Error | undefined +}> {} + +export function shell(s: TemplateStringsArray, ...interps: Array) { + return Effect.tryPromise({ + try: () => shellac(s, ...interps), + catch: (error: any) => + new ShellExecuteError({ + cause: error, + }), + }).pipe(Effect.orDie) +} + +export function shellInPath(path: string, env?: Record | undefined, silent?: boolean | undefined) { + return (s: TemplateStringsArray, ...interps: Array) => + Effect.tryPromise({ + try: () => shellac.env(env ?? {}).in(path)(s, ...interps), + catch: (error: any) => + new ShellExecuteError({ + cause: silent ? new Error(`shell in ${path}`) : error, + }), + }).pipe(Effect.orDie) +} + +/** + * Execute a command using spawnSync with stdio inheritance + * This allows interactive processes like vitest to receive stdin properly + */ +export function execProcess(command: string, args: string[], cwd?: string) { + return Effect.async((resume, signal) => { + const result = spawn(command, args, { + cwd, + stdio: ['inherit', 'inherit', 'inherit'], + signal, + }) + + return Effect.sync(() => { + console.log('kill ') + return result.kill() + }) + }) +} diff --git a/scripts/thing/vitest/command.ts b/scripts/thing/vitest/command.ts new file mode 100644 index 0000000..73a9700 --- /dev/null +++ b/scripts/thing/vitest/command.ts @@ -0,0 +1,36 @@ +import { Command } from '@effect/cli' +import { TestSubcommand } from './domain' +import { + testAllOption, + testBrowserOption, + testHeadlessOption, + testModeOption, + testProjectOption, + testWatchOption, +} from './options' +import { runTest } from './subcommand' + +const testCommand = Command.make( + 'test', + { + project: testProjectOption, + mode: testModeOption, + all: testAllOption, + watch: testWatchOption, + browser: testBrowserOption, + headless: testHeadlessOption, + }, + (config) => + runTest( + TestSubcommand({ + project: config.project, + mode: config.mode, + all: config.all, + watch: config.watch, + browser: config.browser, + headless: config.headless, + }), + ), +) + +export { testCommand } diff --git a/scripts/thing/vitest/domain.ts b/scripts/thing/vitest/domain.ts new file mode 100644 index 0000000..76cde29 --- /dev/null +++ b/scripts/thing/vitest/domain.ts @@ -0,0 +1,13 @@ +import { Data } from 'effect' + +export interface TestSubcommand { + readonly _tag: 'TestSubcommand' + + readonly project: string + readonly all: boolean + readonly mode: 'unit' | 'e2e' | 'browser' + readonly watch: boolean + readonly headless: boolean + readonly browser: 'chromium' | 'firefox' | 'webkit' | 'all' +} +export const TestSubcommand = Data.tagged('TestSubcommand') diff --git a/scripts/thing/vitest/options.ts b/scripts/thing/vitest/options.ts new file mode 100644 index 0000000..1f1939f --- /dev/null +++ b/scripts/thing/vitest/options.ts @@ -0,0 +1,32 @@ +import { Options } from '@effect/cli' + +const testProjectOption = Options.text('project').pipe( + Options.withDescription('Nx project name whose Vitest config should run'), +) + +const testModeOption = Options.choice('mode', ['unit', 'e2e', 'browser'] as const).pipe( + Options.withDescription('Vitest mode to run'), + Options.withDefault('unit'), +) + +const testAllOption = Options.boolean('all').pipe( + Options.withDescription('Run all matching projects regardless of mode'), + Options.withDefault(false), +) + +const testWatchOption = Options.boolean('watch').pipe( + Options.withDescription('Enable Vitest watch mode'), + Options.withDefault(false), +) + +const testBrowserOption = Options.choice('browser', ['chromium', 'firefox', 'webkit', 'all'] as const).pipe( + Options.withDescription('Browser target when running in browser mode'), + Options.withDefault('chromium'), +) + +const testHeadlessOption = Options.boolean('headless', { negationNames: ['no-headless'] }).pipe( + Options.withDescription('Run browsers in headless mode'), + Options.withDefault(true), +) + +export { testAllOption, testBrowserOption, testHeadlessOption, testModeOption, testProjectOption, testWatchOption } diff --git a/scripts/thing/vitest/subcommand.ts b/scripts/thing/vitest/subcommand.ts index b8ada56..51108f0 100644 --- a/scripts/thing/vitest/subcommand.ts +++ b/scripts/thing/vitest/subcommand.ts @@ -1,7 +1,7 @@ import { Command, Path, FileSystem } from '@effect/platform' -import { workspaceRoot } from '@nx/devkit' import { Console, Effect, Stream } from 'effect' -import type { TestSubcommand } from '../domain' +import type { TestSubcommand } from './domain' +import { workspaceRoot } from '@nx/devkit' export const runTest = Effect.fn(function* (subcommand: TestSubcommand) { const path = yield* Path.Path @@ -26,7 +26,7 @@ export const runTest = Effect.fn(function* (subcommand: TestSubcommand) { const testProjects: any[] = vitestConfig.default?.test?.projects || [] if (testProjects.length === 0) { - return yield* Effect.logDebug('No test projects found in vitest.config.ts') + return yield* Effect.logInfo('No test projects found in vitest.config.ts') } // Get all available test project names @@ -61,8 +61,8 @@ export const runTest = Effect.fn(function* (subcommand: TestSubcommand) { }) if (matchingTests.length === 0) { - yield* Effect.logDebug(`No tests found for project: ${project}${mode ? ` with mode: ${mode}` : ''}`) - yield* Effect.logDebug(`Available test projects:`).pipe( + yield* Effect.logInfo(`No tests found for project: ${project}${mode ? ` with mode: ${mode}` : ''}`) + yield* Effect.logInfo(`Available test projects:`).pipe( Effect.annotateLogs({ projects: availableTests, }), @@ -70,7 +70,7 @@ export const runTest = Effect.fn(function* (subcommand: TestSubcommand) { return } - yield* Effect.logDebug(`Found ${matchingTests.length} matching test(s):`).pipe( + yield* Effect.logInfo(`Found ${matchingTests.length} matching test(s):`).pipe( Effect.annotateLogs({ projects: matchingTests, }), @@ -94,7 +94,7 @@ export const runTest = Effect.fn(function* (subcommand: TestSubcommand) { args.push(`--browser.headless=${headless}`) } - yield* Effect.logDebug('Running vitest with projects').pipe(Effect.annotateLogs({ args })) + yield* Effect.logInfo('Running vitest with projects').pipe(Effect.annotateLogs({ args })) const outStream = Command.make('vitest', ...args).pipe( Command.workingDirectory(workspaceRoot), diff --git a/scripts/thing/workers/cloudflare.ts b/scripts/thing/workers/cloudflare.ts index 37a08e0..306b86a 100644 --- a/scripts/thing/workers/cloudflare.ts +++ b/scripts/thing/workers/cloudflare.ts @@ -210,7 +210,7 @@ const make = Effect.gen(function* () { Effect.orDie, ) - return yield* Effect.logDebug('Workers build finished').pipe( + return yield* Effect.logInfo('Workers build finished').pipe( Effect.annotateLogs('workers bundle size', formatSize(outfileSize)), ) }).pipe(Effect.withSpan('build.cloudflare-build')) diff --git a/scripts/thing/workers/deploy.ts b/scripts/thing/workers/deploy.ts index 3b35610..0e290b8 100644 --- a/scripts/thing/workers/deploy.ts +++ b/scripts/thing/workers/deploy.ts @@ -1,18 +1,14 @@ import { Cause, Effect, Exit, pipe } from 'effect' import { Deployment } from '../deployment' -import { - BuildWorkersParameters, - DatabaseMigrateDeploySubcommand, - EmailDeploySubcommand, - type BuildWorkersTarget, - type DeploySubcommand, -} from '../domain' +import { BuildWorkersParameters, type BuildWorkersTarget, type DeploySubcommand } from '../domain' +import { DatabaseMigrateDeploySubcommand } from '../database/domain' +import { EmailDeploySubcommand } from '../emails/domain' import { Git } from '../git' import { Github } from '../github' import { Notification } from '../notification' import { Workspace } from '../workspace' import * as Database from '../database/subcommand' -import * as Email from '../email' +import * as Email from '../emails/subcommand' export const start = Effect.fn('workers.deploy-start')(function* ( subcommand: DeploySubcommand, @@ -25,7 +21,9 @@ export const start = Effect.fn('workers.deploy-start')(function* ( const git = yield* Git const github = yield* Github - const [branch, lastCommit] = yield* Effect.all([git.branch, git.lastCommit], { concurrency: 'unbounded' }) + const [branch, lastCommit] = yield* Effect.all([git.branch, git.lastCommit], { + concurrency: 'unbounded', + }) yield* Effect.annotateCurrentSpan({ projectName: workspace.projectName, @@ -54,7 +52,7 @@ export const start = Effect.fn('workers.deploy-start')(function* ( const fail = (message: string) => Effect.all( [ - Effect.logDebug('Deploy failed'), + Effect.logInfo('Deploy failed'), notification.failed({ projectName: workspace.projectName, branch, @@ -148,7 +146,7 @@ export const start = Effect.fn('workers.deploy-start')(function* ( return fail(Cause.pretty(cause)) }, onSuccess() { - return Effect.logDebug('Deploy successful') + return Effect.logInfo('Deploy successful') }, }) }) diff --git a/scripts/thing/workers/subcommand.ts b/scripts/thing/workers/subcommand.ts index 531a60f..3f647bd 100644 --- a/scripts/thing/workers/subcommand.ts +++ b/scripts/thing/workers/subcommand.ts @@ -11,14 +11,14 @@ import type { } from '../domain' import { BuildWorkersParameters } from '../domain' import * as Environment from '../environment' -import { shell } from '../utils' +import { shell } from '../utils/shell' import * as Workspace from '../workspace' export const serve = Effect.fn('workers.serve')(function* ( workspace: Workspace.Workspace, subcommand: ServeSubcommand, ) { - yield* Effect.logDebug(`Serve ${workspace.projectName}`) + yield* Effect.logInfo(`Serve ${workspace.projectName}`) if (subcommand.target._tag !== 'BuildWorkers') { return yield* Effect.dieMessage('Invalid target') @@ -61,7 +61,7 @@ export const build = Effect.fn('workers.build')(function* ( const fs = yield* FileSystem.FileSystem const path = yield* Path.Path - yield* Effect.logDebug('Start building app') + yield* Effect.logInfo('Start building app') if (subcommand.target._tag !== 'BuildWorkers') { return yield* Effect.dieMessage('Invalid target') @@ -99,7 +99,7 @@ export const build = Effect.fn('workers.build')(function* ( ) yield* pipe( - Effect.logDebug('Clean dist folder'), + Effect.logInfo('Clean dist folder'), Effect.andThen( shell` $ rm -rf ${workspace.projectOutput.dist} @@ -135,7 +135,7 @@ export const deploy = Effect.fn('workers.deploy')(function* ( subcommand: DeploySubcommand, buildTarget: BuildTarget, ) { - yield* Effect.logDebug(`Deploy ${workspace.projectName}`) + yield* Effect.logInfo(`Deploy ${workspace.projectName}`) if (buildTarget._tag !== 'BuildWorkers') { return yield* Effect.dieMessage('Invalid build target') @@ -155,7 +155,11 @@ export const deploy = Effect.fn('workers.deploy')(function* ( Layer.succeed(Workspace.Workspace, workspace), Layer.succeed( BuildWorkersParameters, - BuildWorkersParameters.of({ env: environment.env, target: buildTarget, nodeEnv: 'production' }), + BuildWorkersParameters.of({ + env: environment.env, + target: buildTarget, + nodeEnv: 'production', + }), ), ) const DeployLive = Layer.provideMerge(WorkersOnCloudflareLive, Base) @@ -174,7 +178,7 @@ export const preview = Effect.fn('workers.preview')(function* ( subcommand: PreviewSubcommand, buildTarget: BuildTarget, ) { - yield* Effect.logDebug(`Preview ${workspace.projectName}`) + yield* Effect.logInfo(`Preview ${workspace.projectName}`) if (buildTarget._tag !== 'BuildWorkers') { return yield* Effect.dieMessage('Invalid build target') @@ -186,7 +190,11 @@ export const preview = Effect.fn('workers.preview')(function* ( Layer.succeed(Workspace.Workspace, workspace), Layer.succeed( BuildWorkersParameters, - BuildWorkersParameters.of({ env: environment.env, target: buildTarget, nodeEnv: 'production' }), + BuildWorkersParameters.of({ + env: environment.env, + target: buildTarget, + nodeEnv: 'production', + }), ), ) diff --git a/scripts/ts-performance.sh b/scripts/ts-performance.sh index 5ff60be..5bc44f2 100755 --- a/scripts/ts-performance.sh +++ b/scripts/ts-performance.sh @@ -1,6 +1,6 @@ project='apps/web' -pnpm tsc -p $project/tsconfig.check.json --showConfig -pnpm tsc -p $project/tsconfig.check.json --noEmit --extendedDiagnostics --incremental false --generateTrace traceDir --generateCpuProfile ./traceDir/profile.cpuprofile -pnpm tsc -p $project/tsconfig.check.json --noEmit --traceResolution > ./traceDir/resolutions.txt +tsc -p $project/tsconfig.check.json --showConfig +tsc -p $project/tsconfig.check.json --noEmit --extendedDiagnostics --incremental false --generateTrace traceDir --generateCpuProfile ./traceDir/profile.cpuprofile +tsc -p $project/tsconfig.check.json --noEmit --traceResolution > ./traceDir/resolutions.txt pnpm dlx @typescript/analyze-trace traceDir diff --git a/tsconfig.base.json b/tsconfig.base.json index 9e54f91..f76cf1f 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -70,6 +70,7 @@ "@xstack/toaster": ["./packages/toaster/src/"], "@xstack/router/*": ["./packages/router/src/*"], "@xstack/router": ["./packages/router/src/"], + "@xstack/purchase/*": ["./packages/purchase/src/*"], "@xstack/emails/*": ["./packages/emails/src/*"], "@xstack/form/*": ["./packages/form/src/*"], "@xstack/i18n/*": ["./packages/i18n/src/*"], @@ -80,13 +81,13 @@ "@xstack/fx/*": ["./packages/fx/src/*"], "@xstack/fx": ["./packages/fx/src/"], "@/components/ui/*": ["./packages/lib/src/ui/*"], - "@/lib/*": ["./packages/lib/src/*"] + "@/lib/*": ["./packages/lib/src/*"], }, "plugins": [ { - "name": "@effect/language-service" - } - ] + "name": "@effect/language-service", + }, + ], }, - "exclude": ["node_modules", "dist", "build", ".wrangler", "coverage", "public"] + "exclude": ["node_modules", "dist", "build", ".wrangler", "coverage", "public"], } diff --git a/tsconfig.json b/tsconfig.json index 991adf6..1f7feea 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.base.json", "compilerOptions": { - "types": ["node"] + "types": ["node"], }, "include": ["./scripts/**/*.ts", "./scripts/**/*.tsx", "./scratchpad/**/*.ts", "./scratchpad/**/*.tsx"], - "exclude": ["node_modules", "dist", "build", "infra", "packages", "apps", "template"] + "exclude": ["node_modules", "dist", "build", "infra", "packages", "apps", "template"], }