diff --git a/src/components/FileManager.tsx b/src/components/FileManager.tsx index 7cc8e2d..28a69a6 100644 --- a/src/components/FileManager.tsx +++ b/src/components/FileManager.tsx @@ -1,12 +1,16 @@ -import { useStore } from '@nanostores/react' +import { useStore } from '@nanostores/react'; import tutorialStore from 'tutorialkit:store'; import { useRef, useEffect } from 'react'; import { webcontainer } from 'tutorialkit:core'; import type { WebContainer } from '@webcontainer/api'; +import { RAILS_WASM_PACKAGE_VERSION } from '../templates/default/lib/constants'; export function FileManager() { const files = useStore(tutorialStore.files); const processedFiles = useRef(new Set()); + const wasmCached = useRef(false); + const cachingInterval = useRef(null); + const VERSIONED_RAILS_WASM_FILE_NAME = `rails-${RAILS_WASM_PACKAGE_VERSION}.wasm`; async function chmodx(wc: WebContainer, path: string) { const process = await wc.spawn('chmod', ['+x', path]); @@ -20,12 +24,69 @@ export function FileManager() { } } + async function fetchCachedWasmFile(): Promise { + try { + const opfsRoot = await navigator.storage.getDirectory(); + const fileHandle = await opfsRoot.getFileHandle(VERSIONED_RAILS_WASM_FILE_NAME); + const file = await fileHandle.getFile(); + console.log(`Found cached WASM version ${RAILS_WASM_PACKAGE_VERSION}`); + return new Uint8Array(await file.arrayBuffer()); + } catch { + return null; + } + } + + async function persistWasmFile(wasmData: Uint8Array): Promise { + try { + const opfsRoot = await navigator.storage.getDirectory(); + const fileHandle = await opfsRoot.getFileHandle(VERSIONED_RAILS_WASM_FILE_NAME, { create: true }); + const writable = await fileHandle.createWritable(); + await writable.write(wasmData); + await writable.close(); + console.log(`Rails WASM v${RAILS_WASM_PACKAGE_VERSION} cached`); + } catch (error) { + console.error('Failed to persist Rails WASM:', error); + } + } + + async function cacheWasmFile(wc: WebContainer): Promise { + if (cachingInterval.current) return; + + console.log(`Caching WASM file v${RAILS_WASM_PACKAGE_VERSION}...`); + + cachingInterval.current = window.setInterval(async () => { + try { + const wasmData = await wc.fs.readFile('/node_modules/@rails-tutorial/wasm/dist/rails.wasm'); + if (wasmData && wasmData.length > 0) { + clearInterval(cachingInterval.current!); + cachingInterval.current = null; + + await persistWasmFile(wasmData); + wasmCached.current = true; + } + } catch (error) { + // File not ready yet, continue checking + } + }, 1000); + } + useEffect(() => { if (!files) return; (async () => { const wc = await webcontainer; + if (!wasmCached.current) { + const cachedWasm = await fetchCachedWasmFile(); + if (cachedWasm) { + await wc.fs.writeFile(VERSIONED_RAILS_WASM_FILE_NAME, cachedWasm); + console.log(`Rails WASM v${RAILS_WASM_PACKAGE_VERSION} loaded from cache`); + wasmCached.current = true; + } else { + await cacheWasmFile(wc); + } + } + Object.entries(files).forEach(([_, fd]) => { const dir = fd.path.split('/').filter(Boolean).slice(-2, -1)[0]; if (dir === "bin" && !processedFiles.current.has(fd.path)) { @@ -34,6 +95,13 @@ export function FileManager() { } }); })(); + + return () => { + if (cachingInterval.current) { + clearInterval(cachingInterval.current); + cachingInterval.current = null; + } + }; }, [files]); return null; diff --git a/src/templates/default/lib/constants.js b/src/templates/default/lib/constants.js new file mode 100644 index 0000000..077aede --- /dev/null +++ b/src/templates/default/lib/constants.js @@ -0,0 +1 @@ +export const RAILS_WASM_PACKAGE_VERSION = '8.0.2-rc.1'; diff --git a/src/templates/default/package.json b/src/templates/default/package.json index f91bcfe..060fd4b 100644 --- a/src/templates/default/package.json +++ b/src/templates/default/package.json @@ -4,6 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { + "preinstall": "node scripts/preinstall.js", "dev": "vite", "test": "node --disable-warning=ExperimentalWarning ./scripts/test.js", "test:watch": "node --disable-warning=ExperimentalWarning --watch --watch-path=./workspace ./scripts/test.js", @@ -12,7 +13,6 @@ "dependencies": { "@electric-sql/pglite": "^0.3.1", "@ruby/wasm-wasi": "^2.7.1", - "@rails-tutorial/wasm": "8.0.2-rc.1", "express": "^5.1.0", "multer": "^2.0.1", "set-cookie-parser": "^2.7.1" diff --git a/src/templates/default/scripts/preinstall.js b/src/templates/default/scripts/preinstall.js new file mode 100755 index 0000000..fa8b1f3 --- /dev/null +++ b/src/templates/default/scripts/preinstall.js @@ -0,0 +1,63 @@ +#!/usr/bin/env node + +import fs from 'fs/promises'; +import path from 'path'; +import { execSync } from 'child_process'; +import { RAILS_WASM_PACKAGE_VERSION } from '../lib/constants.js'; +const VERSIONED_RAILS_WASM_PATH = `./rails-${RAILS_WASM_PACKAGE_VERSION}.wasm`; +const TARGET_DIR = 'node_modules/@rails-tutorial/wasm/dist'; +const TARGET_FILE = path.join(TARGET_DIR, 'rails.wasm'); + +async function checkIfFileExists(filePath) { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +} + +async function moveFile(src, dest) { + try { + await fs.mkdir(path.dirname(dest), { recursive: true }); + await fs.rename(src, dest); + console.log(`✓ Moved ${src} to ${dest}`); + return true; + } catch (error) { + console.error(`✗ Failed to move ${src} to ${dest}:`, error.message); + return false; + } +} + +async function installPackage(packageName, version) { + try { + console.log(`Installing ${packageName}@${version}...`); + execSync(`npm install ${packageName}@${version}`, { stdio: 'inherit' }); + console.log(`✓ Installed ${packageName}@${version}`); + return true; + } catch (error) { + console.error(`✗ Failed to install ${packageName}@${version}:`, error.message); + return false; + } +} + +async function main() { + const versionedWasmExists = await checkIfFileExists(VERSIONED_RAILS_WASM_PATH); + + if (versionedWasmExists) { + const success = await moveFile(VERSIONED_RAILS_WASM_PATH, TARGET_FILE); + if (!success) { + process.exit(1); + } + } else { + const success = await installPackage('@rails-tutorial/wasm', RAILS_WASM_PACKAGE_VERSION); + if (!success) { + process.exit(1); + } + } +} + +main().catch(error => { + console.error('Preinstall script failed:', error); + process.exit(1); +});