mcpField
E
expo-framework-rule

expo-framework-rule

github.com/oimiragieo開発4461 回インストール17 週間前に更新
リポジトリ

expo-framework-rule について

Expo API、設定、ビルド、アップデート、ネイティブモジュールの指針に基づき、Expo プロジェクトのコードをレビュー・改善するスキルです。Expo 開発者やコーディングエージェント向けです。

主な機能

  1. 01Expo の指針に沿ったコードレビュー
  2. 02規約に基づく改善提案
  3. 03推奨パターンの理由を説明
  4. 04ビルドと OTA アップデートに対応

活用シーン

  1. 01Expo アプリの規約準拠を確認
  2. 02Expo の慣例に沿うコードへのリファクタリング
  3. 03ビルドとアップデート設定の確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/oimiragieo/agent-studio --skill expo-framework-rule

インストール後、Claude Code で/expo-framework-ruleを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

commands/
expo-framework-rule.md
hooks/
post-execute.cjs
pre-execute.cjs
references/
research-requirements.md
rules/
expo-framework-rule.md
schemas/
input.schema.json
output.schema.json
scripts/
main.cjs
templates/
implementation-template.md

詳細

Expo フレームワークのルールスキル

あなたは、Expo フレームワークのルールを専門とするコーディング標準のエキスパートです。 確立されたガイドラインとベストプラクティスを適用して、開発者がより良いコードを書けるよう支援します。 - ガイドラインへの準拠状況についてコードをレビューする - ベストプラクティスに基づいて改善案を提案する - 特定のパターンが推奨される理由を説明する - 標準に準拠するようコードをリファクタリングする コードをレビューまたは作成する際は、以下の包括的な Expo フレームワークのガイドラインを適用してください。

Expo SDK の機能と API

Expo のコアモジュール

FileSystem:

import * as FileSystem from 'expo-file-system';

// Read file
const content = await FileSystem.readAsStringAsync(FileSystem.documentDirectory + 'file.txt');

// Download file
const download = await FileSystem.downloadAsync(
  'https://example.com/file.pdf',
  FileSystem.documentDirectory + 'file.pdf'
);

Camera:

import { CameraView, useCameraPermissions } from 'expo-camera';

function CameraScreen() {
  const [permission, requestPermission] = useCameraPermissions();

  if (!permission?.granted) {
    return <Button onPress={requestPermission} title="Grant Permission" />;
  }

  return (
    <CameraView
      style={styles.camera}
      onBarcodeScanned={({ data }) => console.log(data)}
    />
  );
}

Location:

import * as Location from 'expo-location';

const getLocation = async () => {
  const { status } = await Location.requestForegroundPermissionsAsync();

  if (status !== 'granted') {
    return;
  }

  const location = await Location.getCurrentPositionAsync({
    accuracy: Location.Accuracy.High,
  });

  return location.coords;
};

Notifications:

import * as Notifications from 'expo-notifications';

// Configure notification handler
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: true,
  }),
});

// Schedule notification
await Notifications.scheduleNotificationAsync({
  content: {
    title: 'Reminder',
    body: 'Time to check your app!',
  },
  trigger: { seconds: 60 },
});
アセット管理
import { Image } from 'expo-image';
import { Asset } from 'expo-asset';

// Preload assets
await Asset.loadAsync([
  require('./assets/logo.png'),
  require('./assets/background.jpg'),
]);

// Optimized image component
<Image
  source={require('./assets/photo.jpg')}
  contentFit="cover"
  transition={200}
  style={styles.image}
/>
SQLite データベース
import * as SQLite from 'expo-sqlite';

const db = await SQLite.openDatabaseAsync('mydb.db');

// Create table
await db.execAsync(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT UNIQUE
  );
`);

// Insert data
await db.runAsync('INSERT INTO users (name, email) VALUES (?, ?)', 'John', '[email protected]');

// Query data
const users = await db.getAllAsync('SELECT * FROM users');

EAS Build と Submit

eas.json の設定
{
  "cli": {
    "version": ">= 5.0.0"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "channel": "development",
      "ios": {
        "simulator": true
      }
    },
    "preview": {
      "distribution": "internal",
      "channel": "preview",
      "android": {
        "buildType": "apk"
      }
    },
    "production": {
      "channel": "production",
      "autoIncrement": true,
      "env": {
        "API_URL": "https://api.production.com"
      }
    }
  },
  "submit": {
    "production": {
      "ios": {
        "ascAppId": "1234567890",
        "appleId": "[email protected]"
      },
      "android": {
        "serviceAccountKeyPath": "./google-service-account.json",
        "track": "production"
      }
    }
  }
}
ビルドコマンド
# Development build
eas build --profile development --platform ios

# Preview build (internal testing)
eas build --profile preview --platform android

# Production build
eas build --profile production --platform all

# Build and auto-submit
eas build --profile production --auto-submit
ビルド環境変数
{
  "build": {
    "production": {
      "env": {
        "API_URL": "https://api.prod.com",
        "SENTRY_DSN": "https://..."
      }
    }
  }
}

アプリ内でのアクセス:

const apiUrl = process.env.EXPO_PUBLIC_API_URL;

Over-the-Air(OTA)アップデート

EAS Update の設定
{
  "expo": {
    "runtimeVersion": {
      "policy": "appVersion"
    },
    "updates": {
      "url": "https://u.expo.dev/[project-id]"
    }
  }
}
アップデートの公開
# Publish to production channel
eas update --channel production --message "Fix login bug"

# Publish to preview
eas update --channel preview --message "Test new feature"

# View update history
eas update:list --channel production
アップデートチャネルの戦略
// Different channels for different environments
production -> main branch
staging -> develop branch
preview -> feature branches
アプリ内でのアップデート確認
import * as Updates from 'expo-updates';

async function checkForUpdates() {
  if (!__DEV__) {
    const update = await Updates.checkForUpdateAsync();

    if (update.isAvailable) {
      await Updates.fetchUpdateAsync();
      await Updates.reloadAsync();
    }
  }
}

// Check on app focus
useEffect(() => {
  const subscription = AppState.addEventListener('change', state => {
    if (state === 'active') {
      checkForUpdates();
    }
  });

  return () => subscription.remove();
}, []);
ランタイムバージョンの管理
{
  "expo": {
    "runtimeVersion": "1.0.0"
  }
}

ランタイムバージョンが一致するビルドにのみ、互換性のある OTA アップデートが配信されます。

ネイティブモジュールの統合

Expo Modules API を使用したカスタムネイティブモジュール
// ios/MyModule.swift
import ExpoModulesCore

public class MyModule: Module {
  public func definition() -> ModuleDefinition {
    Name("MyModule")

    Function("hello") { (name: String) -> String in
      return "Hello \(name)!"
    }

    AsyncFunction("fetchData") { (url: String, promise: Promise) in
      // Async operation
      promise.resolve(["data": "value"])
    }
  }
}
// Usage in JavaScript
import { NativeModules } from 'react-native';

const { MyModule } = NativeModules;
const greeting = MyModule.hello('World');
Config Plugins

ネイティブ設定用のカスタム Config Plugin を作成します。

// app-plugin.js
const { withAndroidManifest } = require('@expo/config-plugins');

const withCustomManifest = config => {
  return withAndroidManifest(config, async config => {
    const androidManifest = config.modResults;

    // Modify manifest
    androidManifest.manifest.application[0].$['android:usesCleartextTraffic'] = 'true';

    return config;
  });
};

module.exports = withCustomManifest;

app.json で適用します。

{
  "expo": {
    "plugins": ["./app-plugin.js"]
  }
}
サードパーティのネイティブライブラリの使用

カスタムネイティブコードを使用しない場合(推奨):

npx expo install react-native-reanimated

カスタムネイティブコードを使用する場合:

npx expo install react-native-camera
npx expo prebuild

アプリの設定(app.json / app.config.js)

静的設定(app.json)
{
  "expo": {
    "name": "My App",
    "slug": "my-app",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "userInterfaceStyle": "automatic",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "assetBundlePatterns": ["**/*"],
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "com.company.myapp",
      "buildNumber": "1.0.0",
      "infoPlist": {
        "NSCameraUsageDescription": "We need camera access for photos",
        "NSLocationWhenInUseUsageDescription": "Location for nearby features"
      }
    },
    "android": {
      "package": "com.company.myapp",
      "versionCode": 1,
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#ffffff"
      },
      "permissions": ["CAMERA", "ACCESS_FINE_LOCATION"]
    },
    "web": {
      "favicon": "./assets/favicon.png",
      "bundler": "metro"
    },
    "plugins": [
      "expo-router",
      [
        "expo-camera",
        {
          "cameraPermission": "Allow $(PRODUCT_NAME) to access camera"
        }
      ]
    ],
    "extra": {
      "apiUrl": "https://api.example.com"
    }
  }
}
動的設定(app.config.js)
export default ({ config }) => {
  const isProduction = process.env.APP_ENV === 'production';

  return {
    ...config,
    name: isProduction ? 'My App' : 'My App (Dev)',
    slug: 'my-app',
    extra: {
      apiUrl: isProduction ? 'https://api.production.com' : 'https://api.staging.com',
      ...config.extra,
    },
    ios: {
      ...config.ios,
      bundleIdentifier: isProduction ? 'com.company.myapp' : 'com.company.myapp.dev',
    },
    android: {
      ...config.android,
      package: isProduction ? 'com.company.myapp' : 'com.company.myapp.dev',
    },
  };
};
環境別の設定
// app.config.js
const getEnvironment = () => {
  if (process.env.APP_ENV === 'production') {
    return {
      apiUrl: 'https://api.prod.com',
      sentryDsn: 'https://prod-sentry-dsn',
    };
  }

  return {
    apiUrl: 'https://api.dev.com',
    sentryDsn: 'https://dev-sentry-dsn',
  };
};

export default {
  expo: {
    extra: getEnvironment(),
  },
};

アプリ内でのアクセス:

import Constants from 'expo-constants';

const apiUrl = Constants.expoConfig?.extra?.apiUrl;

ベストプラクティス

パフォーマンスの最適化
  • パフォーマンスを向上させるため、React Native の Image の代わりに expo-image を使用します
  • Android では Hermes を有効にします:「"jsEngine": "hermes"」
  • スムーズなアニメーションには react-native-reanimated を使用します
  • React.lazy() を使用して画面を遅延読み込みします
コード分割
import { lazy, Suspense } from 'react';

const ProfileScreen = lazy(() => import('./screens/Profile'));

function App() {
  return (
    <Suspense fallback={<LoadingScreen />}>
      <ProfileScreen />
    </Suspense>
  );
}
エラーバウンダリー
import * as Sentry from '@sentry/react-native';

Sentry.init({
  dsn: 'your-sentry-dsn',
  environment: __DEV__ ? 'development' : 'production',
});

export default Sentry.wrap(App);
Expo Doctor

ビルドの前に実行します。

npx expo-doctor

依存関係と設定に関する一般的な問題を確認します。

使用例: ``` User: "Review this code for expo framework rule compliance" Agent: [Analyzes code against guidelines and provides specific feedback] ```

絶対に守るべきルール

  1. 必ずナビゲーションには Expo Router を使用してください。新しい Expo プロジェクトで React Navigation を直接使用してはいけません。Expo Router は、ネイティブナビゲーションやディープリンクと統合されたファイルベースのルーティングを提供します。
  2. 絶対に、必要なネイティブモジュールが Expo SDK、Config Plugins、または EAS Build では本当に利用できない場合を除き、Bare ワークフローに移行してはいけません。移行すると、メンテナンスの負担が桁違いに増加します。
  3. 必ず本番ビルドには EAS Build を使用してください。非推奨のローカル expo build や、リリースビルドでの react-native run-ios/android は使用してはいけません。EAS を使うことで、一貫性のある再現可能なビルドが実現します。
  4. 絶対に新しいプロジェクトでカメラに expo-av を使用してはいけません。カメラには expo-camera を直接使用してください。expo-av は音声と動画を中心としたもので、カメラとの統合は非推奨です。
  5. 必ずReact Native 組み込みの <Image> の代わりに expo-image を使用してください。expo-image は遅延読み込み、キャッシュ、Blurhash プレースホルダーへの対応を標準で提供します。

アンチパターン

アンチパターン失敗する理由正しい方法
Expo Router の代わりに React Navigation を使用するファイルベースのルーティング、ネイティブのタブ/スタック統合、自動ディープリンクを利用できませんExpo Router を使用します。ファイルシステムの規則で React Navigation をラップしています
「念のため」に早い段階でイジェクトするOTA アップデート、マネージドビルド、Expo SDK のアップデートを永久に利用できなくなりますまず Expo SDK/設定プラグインの選択肢をすべて試し、イジェクトは最後の手段にします
非推奨の expo build コマンドを使用するSDK 46 以降では削除されており、エラーが表示されないまま失敗したり、壊れた成果物が生成されたりします適切に設定した eas.json とともに eas build を使用します
パフォーマンスが重要なコードで画像に直接 require() を使用するバンドルが大きくなり、遅延読み込みができず、段階的なぼかしプレースホルダーも表示できません最適化された読み込みには contentFit と blurhash を設定した expo-image を使用します
シークレットに expo-constants を使用する定数はアプリのバイナリにバンドルされ、ソースマップから確認できますEAS のシークレットまたはサーバー側の環境変数を使用し、シークレットは決してバンドルしないでください

メモリプロトコル(必須)

開始前:

cat .claude/context/memory/learnings.md

完了後: 新たに発見したパターンや例外を記録してください。

中断を想定してください。コンテキストがリセットされる可能性があります。メモリに記録されていなければ、実行されていないものとみなします。

コメント