dai365のお部屋

酒と業務効率化をこよなく愛する50代オヤジ、PowerPlatformと生成AIで業務効率化が究極に進んだ世の中が理想。半ランク上(笑)のPower Appsオジサンを目指していたらMicrosoft MVPになりました。好きなものは競馬、プロレス。嫌いなものは手書き。 奇想天外ビリビリ☆Power Apps同好会やってます。https://biribiri.connpass.com/

【奮闘記】AIが作ったオセロコードをReactプロジェクトに移植したら・・・のお話

はじめに:AIのコードは、本当に「コピペ」で動くのか?

こんにちは!この記事は、「AIが作ったコードをコピー&ペーストすれば、誰でも簡単にアプリが作れるんじゃない?」という素朴な疑問から始まった、一つの壮大な実験の記録です。

Power Apps の AI(Generative Pages)が生成した本格的な「オセロゲーム」のソースコードを、Vite で作ったまっさらな React プロジェクトに移植し、最終的に Power Apps のクラウド環境で動かすことを目指します。

このページでやること(1分で把握)
① Viteで空のReactプロジェクト作成 → ② Copilot生成コードを移植
③ ライブラリ追加と型エラー修正 → ④ Power Appsにビルド&プッシュ
ライセンス前提:Code Apps の利用・公開には Power Apps Premium ライセンスが必要です。詳細は文末の 「重要:ライセンスについて」 を参照してください。
Generative Pagesが生成したオセロのプレビュー画面
Generative Pagesで生成したオセロ(移植前の参考表示)

つまり、AIが作ったReactコードをViteプロジェクトへ移植し、最終的にコードアプリとして動かすという流れです。

しかし、その道は「コピペで終わり」ほど甘くはありませんでした。なぜか動かないプログラム、次々と現れる真っ赤なエラーメッセージ…。この記事では、私が遭遇したすべてのつまずきと、その原因の特定から解決に至るまでの全プロセスを、初学者にも分かるように丁寧にまとめます。

合言葉:エラーは怖くない。ひとつずつ原因を見つけて直せば、必ず前に進みます!

検証環境(参考)

node -v         # v18 / v20 など
npm -v
pac --version
npm ls react     # 例: react@19.x または 18.x
注意:UIライブラリが React 18 想定のことがあります。React 19 で衝突する場合は --legacy-peer-deps を検討してください。

第1章:船の建造と、AIからの設計図の入手

目標:移植先となる空の React プロジェクトを作る。

操作手順

npm create vite@latest my-second-code-app -- --template react-ts
cd my-second-code-app
npm install

ここで、Power Apps の AI Copilot が生成したオセロゲームのコードを src/OthelloGame.tsx に保存し、App.tsx から呼び出すように設定しました。

期待:「よし、これで npm run dev を実行すれば、オセロゲームが表示されるはず!」

第2章:最初の拒絶反応 〜 なぜコピペだけでは動かないのか? 〜

つまずきポイント①:Module Not Found(そんな部品、持ってないよ!)

遭遇したエラー:

  • Failed to resolve import "@mui/material"
  • Module not found: Can't resolve '@fluentui/react-icons'

原因:AI のコードは MUI(Material-UI)や Fluent UI などの外部 UI ライブラリを使っていましたが、私の Vite プロジェクトにはそれらが入っていませんでした。

解決手順

コード先頭の import を確認し、@mui/...@fluentui/...npm install で追加します。

npm install @mui/material @emotion/react @emotion/styled @mui/icons-material @fluentui/react-icons

つまずきポイント②:ERESOLVE(部品の「世代間対立」)

遭遇したエラー: npm error ERESOLVE unable to resolve dependency tree

原因:プロジェクトは React 19、ライブラリは React 18 を前提としており、バージョンの不一致で停止。

解決手順

--legacy-peer-deps を付けて、互換チェックを緩めてインストールします。

npm install @mui/material @emotion/react @emotion/styled @mui/icons-material @fluentui/react-icons --legacy-peer-deps
教訓:AI のコードをコピペしたら、まず package.json に必要ライブラリがあるかをチェック。バージョン衝突は --legacy-peer-deps で回避できる場合があります。

第3章:移植手術、執刀! 〜 TypeScript の集中治療室 〜

依存関係は片づいても、OthelloGame.tsx は TypeScript エラーの赤線だらけ。ここからはコードと向き合います。

つまずきポイント③:型のエラーたち

  • 「'Theme' は型であり…」:型の import 方法が古い書き方だった。
  • 「算術演算の左辺には…」:型が string かもしれない値をそのまま計算していた。

解決手順(例)

TypeScript(修正例)
// 型の import は 'import type' を使う
import type { Theme } from '@mui/material/styles';

// 数値だと明示して計算する
const radius = (theme.shape.borderRadius as number) * 2;

VS Code のエラーメッセージを頼りに、一つずつ修正。赤線が消えたら npm run dev を再実行し、ローカルで動けば「移植手術」完了です!

第4章:Power Platform への挑戦と、最後の壁

ローカル動作したオセロゲームを、Power Apps のアプリとしてクラウド公開します。

つまずきポイント④:「アプリがタイムアウトしました」

症状:Power Apps のテスト画面に「このアプリは準備が完了し、起動できることを示すシグナルを送信しませんでした。」と表示。

原因:アプリが「準備完了」のシグナルを送る PowerProvider で全体を包んでいない。

*なんのことはない作業し忘れ(笑)

解決手順(概要)

  1. src/PowerProvider.tsx を用意(SDKinitialize() を呼ぶ)。
  2. src/main.tsx<PowerProvider> にアプリ全体をラップ。
これで pac code runタイムアウトは解消されます。

デプロイ最終手順

ビルド(完成品の作成)

npm run build

プッシュ(クラウドへ反映)

pac code push

ターミナルに表示された公開 URL(https://apps.powerapps.com/play/e/...)を開くと、Power Apps アプリとしてクラウド上で動作していることを確認できます!

Power Apps上で動作するオセロアプリのスクリーンショット
Power Apps の紫ヘッダー下で動作していれば成功!

ミニFAQ(よくある躓き)

  • pac が見つからない:VS Code を再起動 → 新しいターミナルで pac --version拡張機能が有効か確認。
  • ポート衝突(3000):Ctrl+Cで停止 → 他の開発サーバーを終了 → もしくは vite.config.tsport を変更。
  • Power Apps側でタイムアウトPowerProvider でアプリ全体をラップしているか再確認。

さいごに

AI のコードをコピペするだけでは動きませんでした。しかし、その過程で出会ったエラーは、依存関係・TypeScript の型・Power Platform 連携の仕組みを学ぶ最高の教材でした。

メッセージ:エラーは「成長の地図」。この奮闘記が、あなたの冒険の道しるべになりますように。

 

補足:Power Apps(Code Apps)で音を鳴らす ― React実装メモ

ビンゴゲームに音源実装して少し調べたので備忘のため補足

AI何でも教えてくれますね(笑)

要点
✔ 公開用は public/ に mp3 を置くのが一番簡単・高速(ブラウザキャッシュ可)
✔ サーバに置けない/CORSなどで詰まる場合は Data URI を埋め込む(ただしバンドル肥大化)
✔ 実装は「Data URIがあれば優先、なければpublicへフォールバック」が安定(PowerAppsのWebViewでもOK)

使い分け早見表

  • 用途①:public の mp3 を使う(推奨) … 軽い・差し替え簡単・キャッシュ可
  • 用途②:Data URIBase64)を埋め込む … 配置できない/CORS制約が厳しいとき
  • 用途③:両対応(Data URIがあれば優先/無ければpublic) … 迷ったらこれ
重要:どの方式でも ユーザー操作内(クリック等)で audio.play() を呼ぶこと。自動再生ブロック回避のため。

用途①:public の mp3 を使う(推奨)

配置

構成
public/
  rolling.mp3   # 抽選中BGM
  hey.mp3       # 確定時SE

ヘルパ(サブパス対応の絶対URL化)

TypeScript
// public配下のファイルに対して “どこにホストしても壊れない” 絶対URLを作る
function assetUrl(name: string): string {
  const baseAbs = new URL((import.meta as any)?.env?.BASE_URL ?? "./", window.location.href);
  return new URL(name.replace(/^\//, ""), baseAbs).toString();
}

Audioの準備(mount時)

React / TypeScript
const rollingRef = useRef<HTMLAudioElement | null>(null);
const heyRef = useRef<HTMLAudioElement | null>(null);

useEffect(() => {
  // BGM(ループ・事前読み込み)
  const rolling = new Audio(assetUrl("rolling.mp3"));
  rolling.preload = "auto";
  rolling.loop = true;

  // SE(事前読み込みのみ)
  const hey = new Audio(assetUrl("hey.mp3"));
  hey.preload = "auto";

  rollingRef.current = rolling;
  heyRef.current = hey;
}, []);

用途②:Data URIBase64)をアプリに埋め込む

Data URI を定義1行でフル貼り。改行・省略NG

TypeScript
const ROLLING_MP3 = "data:audio/mpeg;base64,......";
const HEY_MP3     = "data:audio/mpeg;base64,......";

data: → blob: 変換(WebViewで安定再生)

TypeScript
// data: を Blobにして blob: URL を作る(PowerAppsのWebViewでも安定)
function dataToBlobUrl(src?: string | null): string | null {
  if (!src || !src.startsWith("data:")) return null;
  const [head, b64] = src.split(",", 2);
  const mime = head.match(/^data:(.*?);base64$/)?.[1] ?? "application/octet-stream";
  const bin = atob(b64);
  const bytes = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
  return URL.createObjectURL(new Blob([bytes], { type: mime }));
}

Audioの準備(mount時)

React / TypeScript
const rollingRef = useRef<HTMLAudioElement | null>(null);
const heyRef = useRef<HTMLAudioElement | null>(null);

useEffect(() => {
  const blobUrls: string[] = [];

  // Data URI を blob: に変換
  const rollingSrc = dataToBlobUrl(ROLLING_MP3)!; blobUrls.push(rollingSrc);
  const heySrc     = dataToBlobUrl(HEY_MP3)!;     blobUrls.push(heySrc);

  const rolling = new Audio(rollingSrc);
  rolling.preload = "auto";
  rolling.loop = true;

  const hey = new Audio(heySrc);
  hey.preload = "auto";

  rollingRef.current = rolling;
  heyRef.current = hey;

  // アンマウント時に blob: を開放(メモリリーク防止)
  return () => blobUrls.forEach(u => URL.revokeObjectURL(u));
}, []);

用途③:両対応(Data URI 優先 / 無ければ public)

宣言(空文字は未使用扱い)

TypeScript
const ROLLING_MP3 = ""; // or "data:audio/mpeg;base64,...."
const HEY_MP3     = "";

ヘルパ

TypeScript
function assetUrl(name: string): string {
  const baseAbs = new URL((import.meta as any)?.env?.BASE_URL ?? "./", window.location.href);
  return new URL(name.replace(/^\//, ""), baseAbs).toString();
}
function dataToBlobUrl(src?: string | null): string | null {
  if (!src || !src.startsWith("data:")) return null;
  const [head, b64] = src.split(",", 2);
  const mime = head.match(/^data:(.*?);base64$/)?.[1] ?? "application/octet-stream";
  const bin = atob(b64);
  const bytes = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
  return URL.createObjectURL(new Blob([bytes], { type: mime }));
}

Audioの準備(mount時)

React / TypeScript
const rollingRef = useRef<HTMLAudioElement | null>(null);
const heyRef = useRef<HTMLAudioElement | null>(null);

useEffect(() => {
  const blobUrls: string[] = [];

  // 1) Data URI があれば blob: を採用、2) なければ public の mp3 へ
  const rollingSrc = dataToBlobUrl(ROLLING_MP3) ?? assetUrl("rolling.mp3");
  const heySrc     = dataToBlobUrl(HEY_MP3)     ?? assetUrl("hey.mp3");

  if (rollingSrc.startsWith("blob:")) blobUrls.push(rollingSrc);
  if (heySrc.startsWith("blob:"))     blobUrls.push(heySrc);

  const rolling = new Audio(rollingSrc);
  rolling.preload = "auto";
  rolling.loop = true;

  const hey = new Audio(heySrc);
  hey.preload = "auto";

  rollingRef.current = rolling;
  heyRef.current = hey;

  return () => blobUrls.forEach(u => URL.revokeObjectURL(u));
}, []);

共通:ユーザー操作内での再生 & フェード停止

フェードアウトして停止(いきなり切らない)

React / TypeScript
const fadeOutAndStop = useCallback(() => {
  const a = rollingRef.current;
  if (!a || a.paused) return;           // すでに止まっていれば何もしない
  const dur = 800, iv = 50;
  const step = a.volume / (dur / iv);   // 1ステップの減衰量
  const id = setInterval(() => {
    if (a.volume > step) a.volume -= step;
    else {
      clearInterval(id);
      a.pause();
      a.currentTime = 0;                // 次回は先頭から
      a.volume = 1;                     // 音量リセット
    }
  }, iv);
}, []);

ボタン押下内での再生(自動再生ブロック回避)

React / TypeScript
const onCall = useCallback(() => {
  // --- BGM開始 ---
  const bgm = rollingRef.current;
  if (bgm) {
    bgm.currentTime = 0;            // 毎回先頭から
    bgm.volume = 1;
    bgm.play().catch(() => { /* 自動再生ブロック等の例外は握りつぶす */ });
    setTimeout(fadeOutAndStop, 10500); // 10.5秒後にフェード停止
  }

  // --- 決定時SE(例:10秒後) ---
  const se = heyRef.current;
  setTimeout(() => se?.play().catch(() => {}), 10000);
}, [fadeOutAndStop]);

ビルド時に音源は含まれる?

はい、含まれます。
Viteでは public/ 配下のファイルはビルド時にそのまま dist/ にコピーされ、Power Apps(Code Apps)へプッシュするとアプリの静的アセットとしてクラウドに保存されます。したがって public/rolling.mp3public/hey.mp3クラウド上から配信されます。
一方、Data URI 方式は mp3 の中身が JavaScript バンドルに埋め込まれるため、追加のファイルは不要ですが、バンドルサイズが大きくなります。

トラブル時チェックリスト

  • 再生は必ずユーザー操作内(クリック/タップ)で audio.play()
  • public/rolling.mp3 等のパスと MIME(audio/mpeg)が正しいか。
  • Data URI改行なし1行・欠損なし。
  • 迷ったら「用途①(public方式)」が最も手堅く、高速でデバッグしやすい。

```

重要:ライセンスについて

Code Apps の利用・公開には、Power Apps の Premium ライセンスが必要です。
組織ポリシーや環境設定により、管理者の有効化や権限付与が必要になる場合があります。
チェックポイント
・作業前に pac auth list で正しい環境につながっているか確認
・権限不足や機能未有効の場合は、テナント/環境の管理者にご相談ください
・評価用途は試用ライセンスの開始で可能な場合があります(組織設定に依存)