Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions homebrew/Formula/mininas.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ def install
libexec.install "packages/api/dist/cli.js"
libexec.install "packages/api/dist/schema.sql"

# Install web static assets
(libexec/"web").install Dir["packages/web/dist/*"]
# Install web static assets (unified Expo app)
(libexec/"web").install Dir["packages/app/dist/*"]

# Install node_modules for native addons (better-sqlite3, sharp)
cd "packages/api" do
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"scripts": {
"dev": "turbo dev",
"build": "turbo build",
"start": "pnpm build && (pnpm --filter api start & pnpm --filter web preview)",
"start": "pnpm build && WEB_DIST_DIR=packages/app/dist pnpm --filter api start",
"lint": "biome check .",
"lint:fix": "biome check --fix .",
"format": "biome format .",
Expand Down
15 changes: 5 additions & 10 deletions packages/api/src/middleware/static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,19 +66,14 @@ export function createStaticMiddleware(distDir: string) {

let filePath = tryFile(distDir, pathname)

// SPA fallback: /volumes/X/Y/Z -> /volumes/index.html
if (!filePath && pathname.startsWith('/volumes/') && !path.extname(pathname)) {
filePath = path.join(distDir, 'volumes', 'index.html')
// SPA fallback: any non-file route → root index.html
if (!filePath && !path.extname(pathname)) {
filePath = path.join(distDir, 'index.html')
if (!isFile(filePath)) return next()
}

if (!filePath) {
// Try index.html for root
if (pathname === '/') {
filePath = path.join(distDir, 'index.html')
if (!isFile(filePath)) return next()
} else {
return next()
}
return next()
}

const ext = path.extname(filePath)
Expand Down
8 changes: 8 additions & 0 deletions packages/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.expo/
dist/

# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
# The following patterns were generated by expo-cli

expo-env.d.ts
# @end expo-cli
45 changes: 45 additions & 0 deletions packages/app/app.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
export default {
expo: {
name: 'MiniNAS',
slug: 'mininas',
version: '0.1.0',
orientation: 'default',
scheme: 'mininas',
userInterfaceStyle: 'light',
newArchEnabled: true,
ios: {
supportsTablet: true,
bundleIdentifier: 'com.mininas.app',
},
android: {
adaptiveIcon: {
backgroundColor: '#ffffff',
},
package: 'com.mininas.app',
},
web: {
bundler: 'metro',
output: 'static',
favicon: './src/assets/favicon.png',
},
plugins: [
'expo-router',
[
'expo-build-properties',
{
ios: {
deploymentTarget: '15.1',
},
},
],
],
experiments: {
typedRoutes: true,
},
extra: {
router: {
origin: false,
},
},
},
}
75 changes: 75 additions & 0 deletions packages/app/metro.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
const { getDefaultConfig } = require('expo/metro-config')
const path = require('path')

const projectRoot = __dirname
const monorepoRoot = path.resolve(projectRoot, '../..')

module.exports = (() => {
const config = getDefaultConfig(projectRoot)
const { resolver } = config

// pnpm monorepo: tell Metro where to find packages
config.watchFolders = [monorepoRoot]
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, 'node_modules'),
path.resolve(monorepoRoot, 'node_modules'),
]

// Dev server middleware: proxy /api and /dav requests to the MiniNAS backend
config.server = {
...config.server,
enhanceMiddleware: (middleware) => {
return (req, res, next) => {
// Proxy API requests to backend
if (req.url?.startsWith('/api/') || req.url?.startsWith('/dav/')) {
const http = require('http')
const proxyReq = http.request(
{
hostname: 'localhost',
port: 3001,
path: req.url,
method: req.method,
headers: req.headers,
},
(proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers)
proxyRes.pipe(res, { end: true })
},
)
req.pipe(proxyReq, { end: true })
return
}
return middleware(req, res, next)
}
},
}

config.resolver = {
...resolver,
resolveRequest: (context, moduleName, platform) => {
if (platform === 'web') {
// Alias react-native to react-native-web on web
if (moduleName === 'react-native') {
return context.resolveRequest(context, 'react-native-web', platform)
}

// Mock native-only modules on web
const webMocks = {
'expo-haptics': path.resolve(__dirname, 'mocks/expo-haptics.web.js'),
'expo-splash-screen': path.resolve(__dirname, 'mocks/expo-splash-screen.web.js'),
'react-native-screens': path.resolve(__dirname, 'mocks/react-native-screens.web.js'),
}

if (webMocks[moduleName]) {
return {
type: 'sourceFile',
filePath: webMocks[moduleName],
}
}
}
return context.resolveRequest(context, moduleName, platform)
},
}

return config
})()
11 changes: 11 additions & 0 deletions packages/app/mocks/expo-haptics.web.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = {
impactAsync: () => {},
notificationAsync: () => {},
selectionAsync: () => {},
ImpactFeedbackStyle: { Light: 'light', Medium: 'medium', Heavy: 'heavy' },
NotificationFeedbackType: {
Success: 'success',
Warning: 'warning',
Error: 'error',
},
}
4 changes: 4 additions & 0 deletions packages/app/mocks/expo-splash-screen.web.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
preventAutoHideAsync: () => Promise.resolve(),
hideAsync: () => Promise.resolve(),
}
4 changes: 4 additions & 0 deletions packages/app/mocks/react-native-screens.web.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
enableScreens: () => {},
enableFreeze: () => {},
}
54 changes: 54 additions & 0 deletions packages/app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "@mininas/app",
"version": "0.0.1",
"private": true,
"main": "expo-router/entry",
"scripts": {
"dev": "expo start",
"dev:web": "expo start --web --port 4321",
"ios": "expo run:ios",
"android": "expo run:android",
"build": "expo export --platform web",
"build:web": "expo export --platform web"
},
"dependencies": {
"expo": "~53.0.0",
"expo-router": "~5.1.0",
"expo-constants": "~17.1.0",
"expo-linking": "~7.1.0",
"expo-status-bar": "~2.2.0",
"expo-splash-screen": "~0.30.0",
"expo-clipboard": "~7.1.0",
"expo-document-picker": "~13.1.0",
"expo-file-system": "~18.1.0",
"expo-sharing": "~13.1.0",
"expo-haptics": "~14.1.0",
"expo-image": "~2.4.0",
"expo-build-properties": "~0.14.0",
"expo-font": "~13.3.0",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-native": "0.79.6",
"react-native-web": "~0.20.0",
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.11.0",
"react-native-gesture-handler": "~2.24.0",
"react-native-reanimated": "~3.17.0",
"@react-navigation/native": "^7.0.0",
"@tanstack/react-query": "^5.66.0",
"@simplewebauthn/browser": "^13.1.0",
"tus-js-client": "^4.3.0",
"lucide-react-native": "^0.474.0",
"react-native-svg": "15.12.1",
"tamagui": "2.0.0-rc.17",
"@tamagui/animations-css": "2.0.0-rc.17",
"@tamagui/animations-react-native": "2.0.0-rc.17",
"@tamagui/config": "2.0.0-rc.17"
},
"devDependencies": {
"@babel/core": "^7.26.0",
"@types/react": "~19.0.0",
"@types/react-dom": "~19.0.0",
"typescript": "^5.7.0"
}
}
21 changes: 21 additions & 0 deletions packages/app/src/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { Stack } from 'expo-router'
import { StatusBar } from 'expo-status-bar'
import { SafeAreaProvider } from 'react-native-safe-area-context'

const queryClient = new QueryClient({
defaultOptions: {
queries: { staleTime: 30_000, retry: 1 },
},
})

export default function RootLayout() {
return (
<SafeAreaProvider>
<QueryClientProvider client={queryClient}>
<StatusBar style="dark" />
<Stack screenOptions={{ headerShown: false }} />
</QueryClientProvider>
</SafeAreaProvider>
)
}
Loading