merge: integrate mobile-app-bootstrap into offline-sync
This commit is contained in:
13
.env.example
13
.env.example
@@ -92,6 +92,19 @@ IFRAME_EMBED_ALLOWED=false
|
|||||||
# Example: https://intranet.example.com,https://portal.example.com
|
# Example: https://intranet.example.com,https://portal.example.com
|
||||||
IFRAME_ALLOWED_ORIGINS=
|
IFRAME_ALLOWED_ORIGINS=
|
||||||
|
|
||||||
|
# Comma-separated list of additional origins allowed to call the API via CORS.
|
||||||
|
# The APP_URL origin and native mobile (Capacitor) origins are always allowed.
|
||||||
|
# Leave empty for a same-origin (web-only) deployment.
|
||||||
|
CORS_ALLOWED_ORIGINS=
|
||||||
|
|
||||||
|
# Expose OpenAPI/Swagger docs at /api/docs (development/debugging aid only).
|
||||||
|
SWAGGER_ENABLED=false
|
||||||
|
|
||||||
|
# Capacitor (mobile shell): hosted client URL loaded by the iOS shell so the
|
||||||
|
# AGPL web client is NOT bundled into the .ipa (see docs/mobile-app-plan.md §9).
|
||||||
|
# Leave empty for Android bundled mode / local development.
|
||||||
|
CAP_SERVER_URL=
|
||||||
|
|
||||||
# Enable debug logging in production (default: false)
|
# Enable debug logging in production (default: false)
|
||||||
DEBUG_MODE=false
|
DEBUG_MODE=false
|
||||||
|
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -45,3 +45,8 @@ lerna-debug.log*
|
|||||||
|
|
||||||
# TypeScript incremental build artifacts
|
# TypeScript incremental build artifacts
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Capacitor native platform projects (generated locally via 'npx cap add ios|android')
|
||||||
|
/ios
|
||||||
|
/android
|
||||||
|
.capacitor
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
<meta name="theme-color" content="#0E1117" media="(prefers-color-scheme: dark)" />
|
<meta name="theme-color" content="#0E1117" media="(prefers-color-scheme: dark)" />
|
||||||
<meta name="theme-color" content="#f6f7f9" media="(prefers-color-scheme: light)" />
|
<meta name="theme-color" content="#f6f7f9" media="(prefers-color-scheme: light)" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
<link rel="apple-touch-icon" href="/icons/app-icon-192x192.png" />
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-touch-fullscreen" content="yes" />
|
<meta name="apple-touch-fullscreen" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Gitmost" />
|
<meta name="apple-mobile-web-app-title" content="Gitmost" />
|
||||||
|
|||||||
@@ -1,30 +1,19 @@
|
|||||||
{
|
{
|
||||||
|
"id": "/",
|
||||||
"name": "Gitmost",
|
"name": "Gitmost",
|
||||||
"short_name": "Gitmost",
|
"short_name": "Gitmost",
|
||||||
|
"description": "Gitmost - open-source collaborative documentation and knowledge base.",
|
||||||
|
"lang": "en",
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
|
"scope": "/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
|
"orientation": "any",
|
||||||
"background_color": "#0E1117",
|
"background_color": "#0E1117",
|
||||||
"theme_color": "#0E1117",
|
"theme_color": "#0E1117",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{ "src": "icons/favicon-16x16.png", "type": "image/png", "sizes": "16x16" },
|
||||||
"src": "icons/favicon-16x16.png",
|
{ "src": "icons/favicon-32x32.png", "type": "image/png", "sizes": "32x32" },
|
||||||
"type": "image/png",
|
{ "src": "icons/app-icon-192x192.png", "type": "image/png", "sizes": "192x192", "purpose": "any" },
|
||||||
"sizes": "16x16"
|
{ "src": "icons/app-icon-512x512.png", "type": "image/png", "sizes": "512x512", "purpose": "any" }
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/favicon-32x32.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "32x32"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/app-icon-192x192.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "180x180 192x192"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/app-icon-512x512.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "512x512"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
82
apps/client/public/sw.js
Normal file
82
apps/client/public/sw.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
// Gitmost PWA service worker.
|
||||||
|
// Conservative strategy:
|
||||||
|
// - Never intercept API, websocket or collaboration traffic (always network).
|
||||||
|
// - Navigations: network-first, fall back to the cached app shell offline.
|
||||||
|
// - Other same-origin GET assets: stale-while-revalidate.
|
||||||
|
// Bump CACHE_VERSION to invalidate stale assets on deploy.
|
||||||
|
const CACHE_VERSION = "gitmost-v1";
|
||||||
|
const APP_SHELL_URL = "/";
|
||||||
|
|
||||||
|
// Path prefixes that must always hit the network (auth/state/realtime).
|
||||||
|
const NETWORK_ONLY_PREFIXES = ["/api", "/socket.io", "/collab"];
|
||||||
|
|
||||||
|
self.addEventListener("install", (event) => {
|
||||||
|
// Activate this worker immediately without waiting for old tabs to close.
|
||||||
|
self.skipWaiting();
|
||||||
|
event.waitUntil(
|
||||||
|
caches
|
||||||
|
.open(CACHE_VERSION)
|
||||||
|
.then((cache) => cache.add(APP_SHELL_URL))
|
||||||
|
.catch(() => {}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("activate", (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
(async () => {
|
||||||
|
const keys = await caches.keys();
|
||||||
|
await Promise.all(
|
||||||
|
keys
|
||||||
|
.filter((key) => key !== CACHE_VERSION)
|
||||||
|
.map((key) => caches.delete(key)),
|
||||||
|
);
|
||||||
|
await self.clients.claim();
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("fetch", (event) => {
|
||||||
|
const { request } = event;
|
||||||
|
|
||||||
|
// Only handle same-origin GET requests; everything else goes to the network.
|
||||||
|
if (request.method !== "GET") return;
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
if (url.origin !== self.location.origin) return;
|
||||||
|
if (NETWORK_ONLY_PREFIXES.some((prefix) => url.pathname.startsWith(prefix)))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Navigations: network-first with an offline fallback to the cached shell.
|
||||||
|
if (request.mode === "navigate") {
|
||||||
|
event.respondWith(
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
return await fetch(request);
|
||||||
|
} catch {
|
||||||
|
const cache = await caches.open(CACHE_VERSION);
|
||||||
|
const cached = await cache.match(APP_SHELL_URL);
|
||||||
|
return cached || Response.error();
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static assets: stale-while-revalidate.
|
||||||
|
event.respondWith(
|
||||||
|
(async () => {
|
||||||
|
const cache = await caches.open(CACHE_VERSION);
|
||||||
|
const cached = await cache.match(request);
|
||||||
|
const network = fetch(request)
|
||||||
|
.then((response) => {
|
||||||
|
// Only cache successful, same-origin (basic) responses.
|
||||||
|
if (response && response.status === 200 && response.type === "basic") {
|
||||||
|
cache.put(request, response.clone());
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch(() => undefined);
|
||||||
|
return cached || (await network) || Response.error();
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -84,3 +84,13 @@ root.render(
|
|||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
</BrowserRouter>,
|
</BrowserRouter>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Register the service worker for PWA installability and an offline app shell.
|
||||||
|
// Production only: in dev the Vite server and HMR must not be intercepted.
|
||||||
|
if (import.meta.env.PROD && "serviceWorker" in navigator) {
|
||||||
|
window.addEventListener("load", () => {
|
||||||
|
navigator.serviceWorker.register("/sw.js").catch((err) => {
|
||||||
|
console.error("Service worker registration failed:", err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
"@nestjs/platform-fastify": "^11.1.19",
|
"@nestjs/platform-fastify": "^11.1.19",
|
||||||
"@nestjs/platform-socket.io": "^11.1.19",
|
"@nestjs/platform-socket.io": "^11.1.19",
|
||||||
"@nestjs/schedule": "^6.1.3",
|
"@nestjs/schedule": "^6.1.3",
|
||||||
|
"@nestjs/swagger": "^11.2.0",
|
||||||
"@nestjs/terminus": "^11.1.1",
|
"@nestjs/terminus": "^11.1.1",
|
||||||
"@nestjs/throttler": "^6.5.0",
|
"@nestjs/throttler": "^6.5.0",
|
||||||
"@nestjs/websockets": "^11.1.19",
|
"@nestjs/websockets": "^11.1.19",
|
||||||
|
|||||||
@@ -95,6 +95,12 @@ export class AuthController {
|
|||||||
} else if (mfaResult.authToken) {
|
} else if (mfaResult.authToken) {
|
||||||
// User doesn't have MFA and workspace doesn't require it
|
// User doesn't have MFA and workspace doesn't require it
|
||||||
this.setAuthCookie(res, mfaResult.authToken);
|
this.setAuthCookie(res, mfaResult.authToken);
|
||||||
|
// Opt-in body token for native clients (Bearer auth). The response is
|
||||||
|
// wrapped by TransformHttpResponseInterceptor, so clients read it at
|
||||||
|
// `data.authToken`. Web clients omit returnToken and keep the cookie.
|
||||||
|
if (loginInput.returnToken) {
|
||||||
|
return { authToken: mfaResult.authToken };
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,6 +108,12 @@ export class AuthController {
|
|||||||
|
|
||||||
const authToken = await this.authService.login(loginInput, workspace.id);
|
const authToken = await this.authService.login(loginInput, workspace.id);
|
||||||
this.setAuthCookie(res, authToken);
|
this.setAuthCookie(res, authToken);
|
||||||
|
// Opt-in body token for native clients (Bearer auth). The response is wrapped
|
||||||
|
// by TransformHttpResponseInterceptor, so clients read it at `data.authToken`.
|
||||||
|
// Web clients omit returnToken and keep using the httpOnly cookie only.
|
||||||
|
if (loginInput.returnToken) {
|
||||||
|
return { authToken };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(SetupGuard)
|
@UseGuards(SetupGuard)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
|
import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class LoginDto {
|
export class LoginDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@@ -8,4 +8,13 @@ export class LoginDto {
|
|||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
password: string;
|
password: string;
|
||||||
|
|
||||||
|
// When true, the access token is returned in the response body (in addition
|
||||||
|
// to the httpOnly cookie) so native/mobile clients can store it in
|
||||||
|
// Keychain/Keystore and send it as 'Authorization: Bearer'. Web clients omit
|
||||||
|
// this flag and keep using the cookie. Opt-in only: the token is never put in
|
||||||
|
// the body otherwise.
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
returnToken?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -320,4 +320,19 @@ export class EnvironmentService {
|
|||||||
.map((o) => o.trim())
|
.map((o) => o.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCorsAllowedOrigins(): string[] {
|
||||||
|
const raw = this.configService.get<string>('CORS_ALLOWED_ORIGINS', '');
|
||||||
|
return raw
|
||||||
|
.split(',')
|
||||||
|
.map((o) => o.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
isSwaggerEnabled(): boolean {
|
||||||
|
const enabled = this.configService
|
||||||
|
.get<string>('SWAGGER_ENABLED', 'false')
|
||||||
|
.toLowerCase();
|
||||||
|
return enabled === 'true';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { InternalLogFilter } from './common/logger/internal-log-filter';
|
|||||||
import { EnvironmentService } from './integrations/environment/environment.service';
|
import { EnvironmentService } from './integrations/environment/environment.service';
|
||||||
import { resolveFrameHeader } from './common/helpers';
|
import { resolveFrameHeader } from './common/helpers';
|
||||||
import { resolveTrustProxy } from './integrations/environment/trust-proxy.util';
|
import { resolveTrustProxy } from './integrations/environment/trust-proxy.util';
|
||||||
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create<NestFastifyApplication>(
|
const app = await NestFactory.create<NestFastifyApplication>(
|
||||||
@@ -142,8 +143,49 @@ async function bootstrap() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
app.enableCors();
|
// Configure CORS explicitly (replaces the previous unconfigured enableCors()).
|
||||||
|
// The web client is same-origin in production; an explicit allowlist lets
|
||||||
|
// native/mobile WebView origins (Capacitor) and any configured cross-origin
|
||||||
|
// clients call the API, while everything else is rejected.
|
||||||
|
const corsAllowedOrigins = new Set<string>([
|
||||||
|
environmentService.getAppUrl(),
|
||||||
|
...environmentService.getCorsAllowedOrigins(),
|
||||||
|
// Capacitor / Ionic WebView origins used by the native shell.
|
||||||
|
'capacitor://localhost',
|
||||||
|
'ionic://localhost',
|
||||||
|
'http://localhost',
|
||||||
|
'https://localhost',
|
||||||
|
]);
|
||||||
|
|
||||||
|
app.enableCors({
|
||||||
|
// Allow requests with no Origin header (curl, server-to-server, some native
|
||||||
|
// WebView requests) and any origin in the allowlist; reject the rest.
|
||||||
|
origin: (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => {
|
||||||
|
if (!origin || corsAllowedOrigins.has(origin)) {
|
||||||
|
callback(null, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback(null, false);
|
||||||
|
},
|
||||||
|
credentials: true,
|
||||||
|
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE', 'OPTIONS'],
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization'],
|
||||||
|
});
|
||||||
|
|
||||||
app.useGlobalInterceptors(new TransformHttpResponseInterceptor(reflector));
|
app.useGlobalInterceptors(new TransformHttpResponseInterceptor(reflector));
|
||||||
|
|
||||||
|
if (environmentService.isSwaggerEnabled()) {
|
||||||
|
// Optional OpenAPI docs to speed up typed mobile-client generation.
|
||||||
|
const swaggerConfig = new DocumentBuilder()
|
||||||
|
.setTitle('Gitmost API')
|
||||||
|
.setDescription('Gitmost REST API (RPC-style POST endpoints).')
|
||||||
|
.setVersion(process.env.APP_VERSION || '0.0.0')
|
||||||
|
.addBearerAuth()
|
||||||
|
.build();
|
||||||
|
const document = SwaggerModule.createDocument(app, swaggerConfig);
|
||||||
|
SwaggerModule.setup('api/docs', app, document);
|
||||||
|
}
|
||||||
|
|
||||||
app.enableShutdownHooks();
|
app.enableShutdownHooks();
|
||||||
|
|
||||||
const logger = new Logger('NestApplication');
|
const logger = new Logger('NestApplication');
|
||||||
|
|||||||
29
capacitor.config.ts
Normal file
29
capacitor.config.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type { CapacitorConfig } from "@capacitor/cli";
|
||||||
|
|
||||||
|
// Capacitor configuration for the Gitmost mobile shell.
|
||||||
|
//
|
||||||
|
// AGPL / App Store note (see docs/mobile-app-plan.md section 9): the AGPL web
|
||||||
|
// client must NOT be bundled into the iOS .ipa. On iOS, point the shell at a
|
||||||
|
// hosted client via CAP_SERVER_URL (server.url) so the AGPL bytes are served
|
||||||
|
// from our own server rather than redistributed under Apple's DRM/usage-rules.
|
||||||
|
// Android may bundle the local web build (webDir) directly.
|
||||||
|
const serverUrl = process.env.CAP_SERVER_URL?.trim();
|
||||||
|
|
||||||
|
const config: CapacitorConfig = {
|
||||||
|
appId: "xyz.vvzvlad.gitmost",
|
||||||
|
appName: "Gitmost",
|
||||||
|
// Web build output of apps/client (Android bundled mode / local assets).
|
||||||
|
// Build it with `pnpm run client:build` before `cap sync`.
|
||||||
|
webDir: "apps/client/dist",
|
||||||
|
...(serverUrl
|
||||||
|
? {
|
||||||
|
// iOS / hosted mode: load the client from our server (AGPL-clean).
|
||||||
|
server: {
|
||||||
|
url: serverUrl,
|
||||||
|
cleartext: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
48
docs/mobile-bootstrap.md
Normal file
48
docs/mobile-bootstrap.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# Mobile app bootstrap
|
||||||
|
|
||||||
|
Purpose: this document records what has been bootstrapped in the repo to enable a
|
||||||
|
mobile app for Gitmost, per the first-step checklist in
|
||||||
|
[docs/mobile-app-plan.md](./mobile-app-plan.md) section 12.
|
||||||
|
|
||||||
|
## What is in the repo now
|
||||||
|
|
||||||
|
- **PWA**: web app manifest, a hand-rolled service worker, and production-only
|
||||||
|
service worker registration in the client. This lets the existing responsive
|
||||||
|
web UI be installed and run as a Progressive Web App.
|
||||||
|
- **Backend mobile auth**: opt-in token return from the login flow. The login
|
||||||
|
request accepts a `returnToken` flag (must be sent as a JSON boolean) that makes
|
||||||
|
the server include the auth token in the response body, and the server already
|
||||||
|
accepts a `Bearer` token in the `Authorization` header. Note the global response
|
||||||
|
interceptor wraps every payload, so the native client reads the token at
|
||||||
|
`response.data.authToken` (not at the top level). A native client can store this
|
||||||
|
token (Keychain / Keystore) and send it as `Authorization: Bearer` on each request.
|
||||||
|
- **Explicit CORS allowlist**: the server reads a `CORS_ALLOWED_ORIGINS` env
|
||||||
|
variable for the allowed origins, and always allows the native WebView origins
|
||||||
|
(`capacitor://localhost`, `ionic://localhost`, `http://localhost`,
|
||||||
|
`https://localhost`) so the mobile shell can call the API.
|
||||||
|
- **Optional OpenAPI / Swagger**: an opt-in OpenAPI/Swagger surface gated behind
|
||||||
|
the `SWAGGER_ENABLED` env flag, useful for developing the native client.
|
||||||
|
- **Capacitor config**: [capacitor.config.ts](../capacitor.config.ts) at the
|
||||||
|
repo root. It targets the `apps/client` web build output (`apps/client/dist`)
|
||||||
|
for the Android bundled mode, and on iOS loads the client from a hosted server
|
||||||
|
via `CAP_SERVER_URL` (`server.url`) so the AGPL web client is not bundled into
|
||||||
|
the `.ipa` (see mobile-app-plan section 9).
|
||||||
|
|
||||||
|
## Remaining MANUAL / local steps (require Xcode / external accounts, out of scope here)
|
||||||
|
|
||||||
|
- Run `pnpm install` to fetch the Capacitor packages and `@nestjs/swagger`.
|
||||||
|
- Run `pnpm run client:build` to produce the web build in `apps/client/dist`.
|
||||||
|
- Run `npx cap add ios` and/or `npx cap add android` to generate the native
|
||||||
|
platform projects (these live outside version control; see `.gitignore`).
|
||||||
|
- Set `CAP_SERVER_URL` for the iOS build so the shell loads the hosted client
|
||||||
|
(AGPL-clean), then run `pnpm run mobile:build` / `cap sync`.
|
||||||
|
- Set up push notifications: APNs for iOS and FCM for Android.
|
||||||
|
- Obtain an Apple Developer account and the App Store / Play Console listings.
|
||||||
|
- Confirm the AGPL iOS distribution decision (mobile-app-plan section 9) before
|
||||||
|
shipping anything to the App Store.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
For the full background, rationale, and the licensing analysis, see
|
||||||
|
[docs/mobile-app-plan.md](./mobile-app-plan.md) (section 12 for the bootstrap
|
||||||
|
checklist, section 9 for the AGPL / App Store licensing path).
|
||||||
11
package.json
11
package.json
@@ -16,10 +16,18 @@
|
|||||||
"server:start": "nx run server:start:prod",
|
"server:start": "nx run server:start:prod",
|
||||||
"email:dev": "nx run server:email:dev",
|
"email:dev": "nx run server:email:dev",
|
||||||
"dev": "pnpm concurrently -n \"frontend,backend\" -c \"cyan,green\" \"pnpm run client:dev\" \"pnpm run server:dev\"",
|
"dev": "pnpm concurrently -n \"frontend,backend\" -c \"cyan,green\" \"pnpm run client:dev\" \"pnpm run server:dev\"",
|
||||||
"clean": "rm -rf apps/*/dist packages/*/dist apps/client/node_modules/.vite"
|
"clean": "rm -rf apps/*/dist packages/*/dist apps/client/node_modules/.vite",
|
||||||
|
"cap:copy": "cap copy",
|
||||||
|
"cap:sync": "cap sync",
|
||||||
|
"cap:ios": "cap open ios",
|
||||||
|
"cap:android": "cap open android",
|
||||||
|
"mobile:build": "pnpm run client:build && cap sync"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braintree/sanitize-url": "^7.1.2",
|
"@braintree/sanitize-url": "^7.1.2",
|
||||||
|
"@capacitor/android": "^7.0.0",
|
||||||
|
"@capacitor/core": "^7.0.0",
|
||||||
|
"@capacitor/ios": "^7.0.0",
|
||||||
"@casl/ability": "6.8.0",
|
"@casl/ability": "6.8.0",
|
||||||
"@docmost/editor-ext": "workspace:*",
|
"@docmost/editor-ext": "workspace:*",
|
||||||
"@floating-ui/dom": "^1.7.3",
|
"@floating-ui/dom": "^1.7.3",
|
||||||
@@ -78,6 +86,7 @@
|
|||||||
"yjs": "^13.6.30"
|
"yjs": "^13.6.30"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@capacitor/cli": "^7.0.0",
|
||||||
"@nx/js": "22.6.1",
|
"@nx/js": "22.6.1",
|
||||||
"@types/bytes": "^3.1.5",
|
"@types/bytes": "^3.1.5",
|
||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
|
|||||||
Reference in New Issue
Block a user