はじめに:AIのコードは、本当に「コピペ」で動くのか?
こんにちは!この記事は、「AIが作ったコードをコピー&ペーストすれば、誰でも簡単にアプリが作れるんじゃない?」という素朴な疑問から始まった、一つの壮大な実験の記録です。
Power Apps の AI(Generative Pages)が生成した本格的な「オセロゲーム」のソースコードを、Vite で作ったまっさらな React プロジェクトに移植し、最終的に Power Apps のクラウド環境で動かすことを目指します。
① Viteで空のReactプロジェクト作成 → ② Copilot生成コードを移植
③ ライブラリ追加と型エラー修正 → ④ Power Appsにビルド&プッシュ
つまり、AIが作ったReactコードをViteプロジェクトへ移植し、最終的にコードアプリとして動かすという流れです。
しかし、その道は「コピペで終わり」ほど甘くはありませんでした。なぜか動かないプログラム、次々と現れる真っ赤なエラーメッセージ…。この記事では、私が遭遇したすべてのつまずきと、その原因の特定から解決に至るまでの全プロセスを、初学者にも分かるように丁寧にまとめます。
検証環境(参考)
node -v # v18 / v20 など
npm -v
pac --version
npm ls react # 例: react@19.x または 18.x
--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
package.json に必要ライブラリがあるかをチェック。バージョン衝突は --legacy-peer-deps で回避できる場合があります。第3章:移植手術、執刀! 〜 TypeScript の集中治療室 〜
依存関係は片づいても、OthelloGame.tsx は TypeScript エラーの赤線だらけ。ここからはコードと向き合います。
つまずきポイント③:型のエラーたち
- 「'Theme' は型であり…」:型の import 方法が古い書き方だった。
- 「算術演算の左辺には…」:型が
stringかもしれない値をそのまま計算していた。
解決手順(例)
// 型の 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 で全体を包んでいない。
*なんのことはない作業し忘れ(笑)
解決手順(概要)
src/PowerProvider.tsxを用意(SDK のinitialize()を呼ぶ)。src/main.tsxで<PowerProvider>にアプリ全体をラップ。
pac code run のタイムアウトは解消されます。デプロイ最終手順
ビルド(完成品の作成)
npm run build
プッシュ(クラウドへ反映)
pac code push
ターミナルに表示された公開 URL(https://apps.powerapps.com/play/e/...)を開くと、Power Apps アプリとしてクラウド上で動作していることを確認できます!
ミニFAQ(よくある躓き)
- pac が見つからない:VS Code を再起動 → 新しいターミナルで
pac --version。拡張機能が有効か確認。 - ポート衝突(3000):
Ctrl+Cで停止 → 他の開発サーバーを終了 → もしくはvite.config.tsのportを変更。 - 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 URI(Base64)を埋め込む … 配置できない/CORS制約が厳しいとき
- 用途③:両対応(Data URIがあれば優先/無ければpublic) … 迷ったらこれ
audio.play() を呼ぶこと。自動再生ブロック回避のため。用途①:public の mp3 を使う(推奨)
配置
public/
rolling.mp3 # 抽選中BGM
hey.mp3 # 確定時SE
ヘルパ(サブパス対応の絶対URL化)
// 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時)
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 URI(Base64)をアプリに埋め込む
Data URI を定義(1行でフル貼り。改行・省略NG)
const ROLLING_MP3 = "data:audio/mpeg;base64,......";
const HEY_MP3 = "data:audio/mpeg;base64,......";
data: → blob: 変換(WebViewで安定再生)
// 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時)
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)
宣言(空文字は未使用扱い)
const ROLLING_MP3 = ""; // or "data:audio/mpeg;base64,...."
const HEY_MP3 = "";
ヘルパ
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時)
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));
}, []);
共通:ユーザー操作内での再生 & フェード停止
フェードアウトして停止(いきなり切らない)
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);
}, []);
ボタン押下内での再生(自動再生ブロック回避)
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.mp3 や public/hey.mp3 はクラウド上から配信されます。一方、Data URI 方式は mp3 の中身が JavaScript バンドルに埋め込まれるため、追加のファイルは不要ですが、バンドルサイズが大きくなります。
トラブル時チェックリスト
- 再生は必ずユーザー操作内(クリック/タップ)で
audio.play()。 public/rolling.mp3等のパスと MIME(audio/mpeg)が正しいか。- Data URI は改行なし1行・欠損なし。
- 迷ったら「用途①(public方式)」が最も手堅く、高速でデバッグしやすい。
```
重要:ライセンスについて
組織ポリシーや環境設定により、管理者の有効化や権限付与が必要になる場合があります。
・作業前に
pac auth list で正しい環境につながっているか確認・権限不足や機能未有効の場合は、テナント/環境の管理者にご相談ください
・評価用途は試用ライセンスの開始で可能な場合があります(組織設定に依存)