建設アシストのコードのセキュリティ対策として、CSP(Content Security Policy)でunsafe-evalを禁止した結果、Alpine.js の式評価がブロックされ動作不能になった問題を解決します
1.パッケージ切り替え・ビルド設定
package.json / resources/js/app.js
| 変更内容 | 詳細 |
| alpinejs → @alpinejs/cspに差し替え | npm install @alpinejs/cspを実行、app.jsのインポートを変更 |
| グローバル Alpine コンポーネントを app.jsに集約 | alpine:initイベント内で Alpine.data()を登録 |
app.jsに登録したグローバルコンポーネント:
| コンポーネント名 | 用途 |
| scheduleInitModal(chartType, switchUrl) | 工程表 初期選択モーダル |
| flashMessage | プロフィール保存後フラッシュメッセージ(2秒フェードアウト) |
| dropdownMenu | ナビゲーション ドロップダウン |
| laravelModal(initialShow, modalName, focusable) | Breeze 標準モーダル(フォーカストラップ付き) |
2.変更ファイル一覧(12ファイル)
2-1. resources/js/app.js
- インポートをalpinejs → @alpinejs/cspに変更
- alpine:initブロックに 4 つのグローバルコンポーネントを登録
2-2. resources/views/company_admin/index.blade.php
- 着手前に対応済み(本作業の起点)
- x-data=”companyAdmin”形式で Alpine.data 登録済み
2-3. resources/views/schedules/barchart.blade.php
barChart() 関数に CSP ヘルパーメソッド 32 件を追加。
| 変更前(HTML) | 変更後 |
| @click=”viewMode=’edit’; renderAll()” | @click=”switchToEdit()” |
| :style=”viewMode===’edit’ ? … : …” | :style=”viewModeStyle(‘edit’)” |
| :style=”mode===’DRAW_BAR’ ? … : …” | :style=”modeDrawBarStyle()” |
| @input=”setZoom($event.target.value/100)” | @input=”setZoomFromInput($event)” |
| x-text=”Math.round(zoom*100)+’%'” | x-text=”zoomText()” |
| :disabled=”!scheduleId” | :disabled=”scheduleNotSelected()” |
| :style=”`width:${canvasWidth}px…`” | :style=”hscrollStyle()” |
| :style=”`left:${ctxMenu.x}px…`” | :style=”ctxMenuPosStyle()” |
| x-text=”ctxMenu.type===’bar’ ? … : …” | x-text=”ctxMenuTitle()” |
| x-text=”barModal.startDate + ‘ → ‘ + …” | x-text=”barStartEndText()” |
| :disabled=”!modalBasisTotal()” | :disabled=”applyBasisDisabled()” |
| :class=”barModal.pattern === p.v ? …” | :class=”patternBtnClass(p.v)” |
| x-text=”annotModal.isConnMode ? …” | x-text=”annotModalTitle()” |
| @click=”annotModal.bold=!annotModal.bold” | @click=”toggleAnnotBold()” |
| :style=”cell && cell.isExtra ? … : …”(5段ネスト) | :style=”calCellStyle(cell)” |
| x-show=”createModal.extraHolidays.length > 0″ | x-show=”hasExtraHolidays()” |
| x-text=”createModal.calendarYear + ‘年 ‘ + …” | x-text=”calendarMonthTitle()” |
2-7. resources/views/components/schedule-init-modal.blade.php
scheduleInitModal(chartType, switchUrl)をapp.js`に登録。savedSchedules /openCreateModal() /openLoadModal()`は Alpine プロキシ経由で親スコープ(barChart / networkSchedule)から参照。
| 変更前 | 変更後 |
| x-data=”{ selectedType:{{ $chartType }}’ }” | | x-data=”scheduleInitModal(‘{{ $chartType }}, {{ $switchUrl }})” |
| @click=”selectedType ===…&& savedSchedules.length === 0 ? …” | :class=”loadBtnClass()” |
| x-text=”(+savedSchedules.length+件)” | x-text=”savedCountText()” |
2-10. resources/views/audits/findings/edit.blade.php
findingsEditor()` にヘルパーメソッド 10 件を追加。PHP@foreach でベイクインされた{ $idx }} /{{ $sys->id }} をメソッド引数として渡す形式に変換。
| 変更前 | 変更後 |
| :class=”{{ $idx }} === activeTab ? … : …” | :class=”tabBtnClass({{ $idx }})” |
| x-text=”‘(‘ + (findingsByIso[{{ $sys->id }}]?.length || 0) + ‘)'” | x-text=”isoCountText({{ $sys->id }})” |
| x-show=”activeTab === {{ $idx }}” | x-show=”isActiveTabIdx({{ $idx }})” |
| :key=”‘{{ $sys->id }}-‘ + (f.id || (‘new-‘ + fi))” | :key=”findingKey({{ $sys->id }}, f, fi)” |
| x-if=”(findingsByIso[{{ $sys->id }}]?.length || 0) === 0″ | x-if=”isoHasNoFindings({{ $sys->id }})” |
| x-text=”modal.isNew ? ‘…’ : ‘…’ + (modal.index + 1) + ‘…'” | x-text=”modalHeaderText()” |
| x-text=”c.clause_no + (c.short_label ? ‘ ’ + c.short_label : ”)” | x-text=”clauseOptionText(c)” |
| x-text=”modalClauseSummary() || ‘…'” | x-text=”modalClauseSummaryText()” |
| x-show=”modal.form.finding_rank && …startsWith(‘nonconformity’)” | x-show=”isNonconformity()” |
2-12. resources/views/profile/partials/update-password-form.blade.php
2-13. resources/views/profile/partials/update-profile-information-form.blade.php
両ファイル同一パターン。flashMessageコンポーネントを app.jsに登録して対応。
| 変更前 | 変更後 |
| x-data=”{ show: true }” | x-data=”flashMessage” |
| x-init=”setTimeout(() => show = false, 2000)” | (削除 — init()メソッド内に移行) |
4.確認事項
全変更後 npm run buildが成功(エラーなし)
@alpinejs/csp ^3.15.12をpackage.json のdependencies に追加済み
標準 alpinejsはdevDependenciesに残存(他依存なければ削除可)
2-4. resources/views/schedules/network.blade.php
networkSchedule() 関数に CSP ヘルパーメソッド 39 件を追加。barchart と共通の変換に加え以下を対応。
| 変更前 | 変更後 |
| @change=”renumberNodes(); renderAll()” | @change=”renumberAndRender()” |
| :class=”mode===’IDLE’ ? ‘text-gray-300: …” | :class=”hintBarClass()” |
| :style=”modal.lineType===’solid ? …” | :style=”lineTypeStyle(‘solid’)” |
| :style=”modal.arrowShape===TL ? …” | :style=”arrowShapeStyle(TL)”` |(6種類) |
| x-text=”fmtDate(modal.startDate)+’ → ‘+calcEndDate()” | x-text=”arrowDateRange()” |
| x-text=”modalCeilDays() ? … + ‘日’ : ‘-'” | x-text=”ceilDaysText()” |
| x-text=”modalBasisTotal() ? … + ‘日’ : ‘-'” | x-text=”basisTotalText()” |
2-5. resources/views/components/modal.blade.php
Breeze 標準モーダルコンポーネントをLaravelModalに完全移行。
| 変更前 | 変更後 |
| x-data=”{show: @js($show), focusables() {…}, …}”|init()`としてapp.js のlaravelModal`内に移行 | x-data=”laravelModal(@js($show), ‘{{ $name }}’, …)” |
| x-init=”$watch(‘show’, value => {…})” | x-on:open-modal.window=”… ? show = true : null” |
| x-on:open-modal.window=”handleOpenModal($event)” | x-on:keydown.tab.prevent=”$event.shiftKey || …” |
| x-on:keydown.tab.prevent=”onTab($event)” | x-on:click=”show = false” |x-on:click=”closeModal()” |
2-6. resources/views/components/dropdown.blade.php
| 変更前 | 変更後 |
| x-data=”{ open: false }” | x-data=”dropdownMenu” |
| @click.outside=”open = false” | @click.outside=”closeMenu()” |
| @click=”open = ! open” | @click=”toggleMenu()” |
2-8. resources/views/admin/users/log_modal.blade.php
logModal() /changeLogModal()は既に function 形式のためx-data変更不要。各関数にヘルパーメソッドを追加。
| 追加メソッド | 変換した式 |
| logHeaderText() | userName + のログイン・ログアウト履歴 |
| noLogs() | !loading && logs.length === 0 |
| hasLogs() | !loading && logs.length > 0 |
| showLimit() | logs.length >= 100 |
| logoutTimeClass(log) | log.logout_time === — ? … : … |
| changeHeaderText() | userName + ‘ のプロフィール変更履歴’ |
2-9. resources/views/audits/checklists/edit.blade.php
checklistEditor() にヘルパーメソッド 8 件を追加。
| 変更前 | 変更後 |
| x-if=”tabs.length > 0″ | x-if=”hasTabs()” |
| x-if=”tabs.length === 0″ | x-if=”noTabs()” |
| :class=”ti === activeTab ? … : …” | :class=”tabClass(ti)” |
| x-text=”‘(‘ + (itemsByTab[t]?.length || 0) + ‘)'” | x-text=”tabCountText(t)” |
| :key=”‘pane-‘ + t” | :key=”tabPaneKey(t)” |
| x-show=”ti === activeTab” | x-show=”isActiveTab(ti)” |
| x-show=”(itemsByTab[t]?.length || 0) === 0″ | x-show=”tabHasNoItems(t)” |
| x-text=”idx + 1″ | x-text=”rowNumber(idx)” |
2-11. resources/views/audits/reports/show.blade.php
<script> ブロックにauditReport(initialType) 関数を新規追加。
| 変更前 | 変更後 |
| x-data=”{ targetType:{{ $report->target_type ?:department’ }}’ }” | x-data=”auditReport(‘{{ $report->target_type ?:department’ }}’)” |
| x-show=”targetType===department” | x-show=”isDepartment()” |
| x-show=”targetType===project'” | x-show=”isProject()” |
| x-if=”targetType===’department'” | x-if=”isDepartment()” |
| x-if=”targetType===’project'” | x-if=”isProject()” |
3.変換ルール まとめ
| パターン | 変換方針 |
| x-data=”{ key: val }”` インラインオブジェクト | Alpine.data(‘name’, …) をapp.jsまたは<script> ブロックに登録しx-data=”name” または x-data=”name(params)”に変更 |
| :style/:class 三項演算子 | データオブジェクトにメソッドを追加し :style=”methodName()”に変換 |
| @click=”a = !a” 代入式 | toggleXxx() メソッドに変換 |
| @input=”fn($event.target.value/100)”` 算術式 | ラッパーメソッド setXxxFromInput($event)に変換 |
| x-text=”a + ‘text’ + b”` 文字列結合 | xxxText()` メソッドに変換 |
| x-show=”a && b.length > 0″ 複合論理式 | hasXxx()/noXxx() メソッドに変換 |
| x-if=”arr.length === 0″ 比較式 | hasXxx()/noXxx() メソッドに変換 |
| :style=”`left:${x}px`”テンプレートリテラル | xxxStyle() メソッドに変換(最優先・確実にNGのため) |
| x-init=”setTimeout(() => …, n)”` アロー関数 | init()` メソッド内に移行 |



