mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-22 20:40:08 -05:00
Compare commits
1 Commits
master
...
claude/pub
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22a5bff977 |
23
bin/package-mobile
Executable file
23
bin/package-mobile
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
ROOT=`dirname $0`
|
||||
|
||||
cd "$ROOT/.."
|
||||
|
||||
echo "Building web assets for mobile..."
|
||||
|
||||
# Build the browser version (same assets used by the Capacitor wrapper)
|
||||
./bin/package-browser
|
||||
|
||||
echo "Syncing Capacitor iOS project..."
|
||||
cd packages/mobile
|
||||
npx cap sync ios
|
||||
|
||||
echo ""
|
||||
echo "iOS project ready at: packages/mobile/ios/App/App.xcworkspace"
|
||||
echo ""
|
||||
echo "To build for the App Store:"
|
||||
echo " 1. Open the project: yarn mobile:open"
|
||||
echo " 2. Select your signing team in Xcode"
|
||||
echo " 3. Archive: Product > Archive"
|
||||
echo " 4. Distribute via App Store Connect"
|
||||
@@ -39,6 +39,9 @@
|
||||
"build:server": "yarn build:browser && yarn workspace @actual-app/sync-server build",
|
||||
"build:browser": "./bin/package-browser",
|
||||
"build:desktop": "./bin/package-electron",
|
||||
"build:mobile": "yarn build:browser && yarn workspace @actual-app/mobile sync",
|
||||
"mobile:open": "yarn workspace @actual-app/mobile open",
|
||||
"mobile:run:ios": "yarn workspace @actual-app/mobile run:ios",
|
||||
"build:plugins-service": "yarn workspace plugins-service build",
|
||||
"build:api": "yarn workspace @actual-app/api build",
|
||||
"build:cli": "yarn build --scope=@actual-app/cli",
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
body,
|
||||
button,
|
||||
input {
|
||||
font-family: var(
|
||||
--font-family,
|
||||
font-family:
|
||||
'Inter Variable',
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
@@ -46,8 +45,7 @@
|
||||
'Helvetica Neue',
|
||||
'Helvetica',
|
||||
'Arial',
|
||||
sans-serif
|
||||
);
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -69,8 +67,7 @@
|
||||
input,
|
||||
textarea {
|
||||
font-size: 1em;
|
||||
font-family: var(
|
||||
--font-family,
|
||||
font-family:
|
||||
'Inter Variable',
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
@@ -84,8 +81,7 @@
|
||||
'Helvetica Neue',
|
||||
'Helvetica',
|
||||
'Arial',
|
||||
sans-serif
|
||||
);
|
||||
sans-serif;
|
||||
font-feature-settings: 'ss01', 'ss04';
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/*
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
Cross-Origin-Embedder-Policy: require-corp
|
||||
Content-Security-Policy: default-src 'self' blob:; img-src 'self' blob: data:; script-src 'self' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; connect-src http: https:;
|
||||
Content-Security-Policy: default-src 'self' blob:; img-src 'self' blob: data:; script-src 'self' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; connect-src http: https:;
|
||||
|
||||
/kcab/*
|
||||
Content-Security-Policy: default-src 'self' blob:; img-src 'self' blob: data:; script-src 'self' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; connect-src http: https:;
|
||||
Content-Security-Policy: default-src 'self' blob:; img-src 'self' blob: data:; script-src 'self' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; connect-src http: https:;
|
||||
|
||||
/*.wasm
|
||||
Content-Type: application/wasm
|
||||
|
||||
@@ -17,7 +17,6 @@ import { Link } from '@desktop-client/components/common/Link';
|
||||
import { FixedSizeList } from '@desktop-client/components/FixedSizeList';
|
||||
import { useThemeCatalog } from '@desktop-client/hooks/useThemeCatalog';
|
||||
import {
|
||||
embedThemeFonts,
|
||||
extractRepoOwner,
|
||||
fetchThemeCss,
|
||||
generateThemeId,
|
||||
@@ -167,12 +166,8 @@ export function ThemeInstaller({
|
||||
setSelectedCatalogTheme(theme);
|
||||
|
||||
const normalizedRepo = normalizeGitHubRepo(theme.repo);
|
||||
// Fetch CSS and embed any referenced font files as data: URIs
|
||||
const cssWithFonts = fetchThemeCss(theme.repo).then(css =>
|
||||
embedThemeFonts(css, theme.repo),
|
||||
);
|
||||
await installTheme({
|
||||
css: cssWithFonts,
|
||||
css: fetchThemeCss(theme.repo),
|
||||
name: theme.name,
|
||||
repo: normalizedRepo,
|
||||
id: generateThemeId(normalizedRepo),
|
||||
|
||||
@@ -1,23 +1,9 @@
|
||||
// oxlint-disable eslint/no-script-url
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
embedThemeFonts,
|
||||
MAX_FONT_FILE_SIZE,
|
||||
parseInstalledTheme,
|
||||
validateThemeCss,
|
||||
} from './customThemes';
|
||||
import { parseInstalledTheme, validateThemeCss } from './customThemes';
|
||||
import type { InstalledTheme } from './customThemes';
|
||||
|
||||
// Small valid woff2 data URI for testing (actual content doesn't matter for validation)
|
||||
const TINY_WOFF2_BASE64 = 'AAAAAAAAAA==';
|
||||
const TINY_WOFF2_DATA_URI = `data:font/woff2;base64,${TINY_WOFF2_BASE64}`;
|
||||
const FONT_FACE_BLOCK = `@font-face {
|
||||
font-family: 'Test Font';
|
||||
src: url('${TINY_WOFF2_DATA_URI}') format('woff2');
|
||||
font-display: swap;
|
||||
}`;
|
||||
|
||||
describe('validateThemeCss', () => {
|
||||
describe('valid CSS', () => {
|
||||
it('should accept valid :root with CSS variables', () => {
|
||||
@@ -88,7 +74,7 @@ describe('validateThemeCss', () => {
|
||||
},
|
||||
])('should reject $description', ({ css }) => {
|
||||
expect(() => validateThemeCss(css)).toThrow(
|
||||
'Theme CSS must contain :root { ... } with CSS variable definitions. No other selectors or content allowed.',
|
||||
'Theme CSS must contain exactly :root { ... } with CSS variable definitions. No other selectors or content allowed.',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -104,7 +90,7 @@ describe('validateThemeCss', () => {
|
||||
color: red;
|
||||
}`,
|
||||
expectedError:
|
||||
'Theme CSS must contain :root { ... } with CSS variable definitions. No other selectors or content allowed.',
|
||||
'Theme CSS must contain exactly :root { ... } with CSS variable definitions. No other selectors or content allowed.',
|
||||
},
|
||||
{
|
||||
description: 'multiple selectors',
|
||||
@@ -115,7 +101,7 @@ describe('validateThemeCss', () => {
|
||||
--color-primary: #ffffff;
|
||||
}`,
|
||||
expectedError:
|
||||
'Theme CSS must contain :root { ... } with CSS variable definitions. No other selectors or content allowed.',
|
||||
'Theme CSS must contain exactly :root { ... } with CSS variable definitions. No other selectors or content allowed.',
|
||||
},
|
||||
{
|
||||
description: 'media queries',
|
||||
@@ -128,7 +114,7 @@ describe('validateThemeCss', () => {
|
||||
}
|
||||
}`,
|
||||
expectedError:
|
||||
'Theme CSS must contain :root { ... } with CSS variable definitions. No other selectors or content allowed.',
|
||||
'Theme CSS must contain exactly :root { ... } with CSS variable definitions. No other selectors or content allowed.',
|
||||
},
|
||||
{
|
||||
description: 'custom selector before :root',
|
||||
@@ -139,7 +125,7 @@ describe('validateThemeCss', () => {
|
||||
--color-primary: #007bff;
|
||||
}`,
|
||||
expectedError:
|
||||
'Theme CSS must contain :root { ... } with CSS variable definitions. No other selectors or content allowed.',
|
||||
'Theme CSS must contain exactly :root { ... } with CSS variable definitions. No other selectors or content allowed.',
|
||||
},
|
||||
])('should reject CSS with $description', ({ css, expectedError }) => {
|
||||
expect(() => validateThemeCss(css)).toThrow(expectedError);
|
||||
@@ -285,7 +271,7 @@ describe('validateThemeCss', () => {
|
||||
},
|
||||
])('should reject $description', ({ css }) => {
|
||||
expect(() => validateThemeCss(css)).toThrow(
|
||||
'Theme CSS must contain :root { ... } with CSS variable definitions. No other selectors or content allowed.',
|
||||
'Theme CSS must contain exactly :root { ... } with CSS variable definitions. No other selectors or content allowed.',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -793,6 +779,12 @@ describe('validateThemeCss', () => {
|
||||
--spacing: 10px 20px;
|
||||
}`,
|
||||
},
|
||||
{
|
||||
description: 'value with comma-separated values',
|
||||
css: `:root {
|
||||
--font-family: Arial, sans-serif;
|
||||
}`,
|
||||
},
|
||||
{
|
||||
description: 'property name with invalid characters',
|
||||
css: `:root {
|
||||
@@ -876,337 +868,6 @@ describe('validateThemeCss', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateThemeCss - font properties (--font-*)', () => {
|
||||
describe('valid font-family values', () => {
|
||||
it.each([
|
||||
{
|
||||
description: 'single generic family',
|
||||
css: `:root { --font-family: sans-serif; }`,
|
||||
},
|
||||
{
|
||||
description: 'single generic family (serif)',
|
||||
css: `:root { --font-family: serif; }`,
|
||||
},
|
||||
{
|
||||
description: 'single generic family (monospace)',
|
||||
css: `:root { --font-family: monospace; }`,
|
||||
},
|
||||
{
|
||||
description: 'system-ui keyword',
|
||||
css: `:root { --font-family: system-ui; }`,
|
||||
},
|
||||
{
|
||||
description: 'bundled font (Inter Variable)',
|
||||
css: `:root { --font-family: Inter Variable; }`,
|
||||
},
|
||||
{
|
||||
description: 'quoted bundled font',
|
||||
css: `:root { --font-family: 'Inter Variable'; }`,
|
||||
},
|
||||
{
|
||||
description: 'double-quoted bundled font',
|
||||
css: `:root { --font-family: "Inter Variable"; }`,
|
||||
},
|
||||
{
|
||||
description: 'web-safe font (Georgia)',
|
||||
css: `:root { --font-family: Georgia; }`,
|
||||
},
|
||||
{
|
||||
description: 'web-safe font (Times New Roman) quoted',
|
||||
css: `:root { --font-family: 'Times New Roman'; }`,
|
||||
},
|
||||
{
|
||||
description: 'comma-separated font stack',
|
||||
css: `:root { --font-family: Georgia, serif; }`,
|
||||
},
|
||||
{
|
||||
description: 'full font stack with multiple fonts',
|
||||
css: `:root { --font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; }`,
|
||||
},
|
||||
{
|
||||
description: 'monospace font stack',
|
||||
css: `:root { --font-mono: 'Fira Code', Consolas, Monaco, monospace; }`,
|
||||
},
|
||||
{
|
||||
description: 'case-insensitive matching (arial)',
|
||||
css: `:root { --font-family: arial; }`,
|
||||
},
|
||||
{
|
||||
description: 'case-insensitive matching (GEORGIA)',
|
||||
css: `:root { --font-family: GEORGIA; }`,
|
||||
},
|
||||
{
|
||||
description: 'macOS system font',
|
||||
css: `:root { --font-family: 'SF Pro', -apple-system, sans-serif; }`,
|
||||
},
|
||||
{
|
||||
description: 'mixed with color variables',
|
||||
css: `:root {
|
||||
--color-primary: #007bff;
|
||||
--font-family: Georgia, serif;
|
||||
--color-secondary: #6c757d;
|
||||
}`,
|
||||
},
|
||||
{
|
||||
description: '--font-mono property',
|
||||
css: `:root { --font-mono: 'JetBrains Mono', 'Fira Code', monospace; }`,
|
||||
},
|
||||
{
|
||||
description: '--font-heading property',
|
||||
css: `:root { --font-heading: Palatino, 'Book Antiqua', serif; }`,
|
||||
},
|
||||
])('should accept CSS with $description', ({ css }) => {
|
||||
expect(() => validateThemeCss(css)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid font-family values - security', () => {
|
||||
it.each([
|
||||
{
|
||||
description: 'empty value',
|
||||
css: `:root { --font-family: ; }`,
|
||||
expectedPattern: /value must not be empty/,
|
||||
},
|
||||
{
|
||||
description: 'url() function in font value',
|
||||
css: `:root { --font-family: url('https://evil.com/font.woff2'); }`,
|
||||
expectedPattern: /function calls are not allowed/,
|
||||
},
|
||||
{
|
||||
description: 'url() with data: URI',
|
||||
css: `:root { --font-family: url(data:font/woff2;base64,abc123); }`,
|
||||
expectedPattern: /function calls are not allowed/,
|
||||
},
|
||||
{
|
||||
description: 'expression() in font value',
|
||||
css: `:root { --font-family: expression(alert(1)); }`,
|
||||
expectedPattern: /function calls are not allowed/,
|
||||
},
|
||||
{
|
||||
description: 'empty font name between commas',
|
||||
css: `:root { --font-family: Arial, , sans-serif; }`,
|
||||
expectedPattern: /empty font name/,
|
||||
},
|
||||
{
|
||||
description: 'Google Fonts URL attempt',
|
||||
css: `:root { --font-family: url(https://fonts.googleapis.com/css2?family=Roboto); }`,
|
||||
expectedPattern: /function calls are not allowed/,
|
||||
},
|
||||
{
|
||||
description: 'local() function',
|
||||
css: `:root { --font-family: local(Arial); }`,
|
||||
expectedPattern: /function calls are not allowed/,
|
||||
},
|
||||
{
|
||||
description: 'format() function',
|
||||
css: `:root { --font-family: format('woff2'); }`,
|
||||
expectedPattern: /function calls are not allowed/,
|
||||
},
|
||||
{
|
||||
description: 'rgb() function in font property',
|
||||
css: `:root { --font-family: rgb(0, 0, 0); }`,
|
||||
expectedPattern: /function calls are not allowed/,
|
||||
},
|
||||
])('should reject CSS with $description', ({ css, expectedPattern }) => {
|
||||
expect(() => validateThemeCss(css)).toThrow(expectedPattern);
|
||||
});
|
||||
});
|
||||
|
||||
describe('any font name is valid (no allowlist)', () => {
|
||||
it.each([
|
||||
{
|
||||
description: 'Comic Sans MS',
|
||||
css: `:root { --font-family: 'Comic Sans MS'; }`,
|
||||
},
|
||||
{
|
||||
description: 'custom font name',
|
||||
css: `:root { --font-family: 'My Custom Font', sans-serif; }`,
|
||||
},
|
||||
{
|
||||
description: 'arbitrary string',
|
||||
css: `:root { --font-family: something-random; }`,
|
||||
},
|
||||
{ description: 'Papyrus', css: `:root { --font-family: Papyrus; }` },
|
||||
])('should accept $description as a font name', ({ css }) => {
|
||||
expect(() => validateThemeCss(css)).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateThemeCss - @font-face blocks', () => {
|
||||
describe('valid @font-face with data: URIs', () => {
|
||||
it('should accept @font-face with data: URI and :root', () => {
|
||||
const css = `${FONT_FACE_BLOCK}
|
||||
:root { --font-family: 'Test Font', sans-serif; }`;
|
||||
expect(() => validateThemeCss(css)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should accept multiple @font-face blocks', () => {
|
||||
const css = `@font-face {
|
||||
font-family: 'Test Font';
|
||||
src: url('${TINY_WOFF2_DATA_URI}') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Test Font';
|
||||
src: url('${TINY_WOFF2_DATA_URI}') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
:root { --font-family: 'Test Font', sans-serif; }`;
|
||||
expect(() => validateThemeCss(css)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should accept @font-face with font/woff MIME type', () => {
|
||||
const css = `@font-face {
|
||||
font-family: 'Woff Font';
|
||||
src: url('data:font/woff;base64,${TINY_WOFF2_BASE64}') format('woff');
|
||||
}
|
||||
:root { --font-family: 'Woff Font', sans-serif; }`;
|
||||
expect(() => validateThemeCss(css)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should accept @font-face with font/ttf MIME type', () => {
|
||||
const css = `@font-face {
|
||||
font-family: 'TTF Font';
|
||||
src: url('data:font/ttf;base64,${TINY_WOFF2_BASE64}') format('truetype');
|
||||
}
|
||||
:root { --font-family: 'TTF Font', sans-serif; }`;
|
||||
expect(() => validateThemeCss(css)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should accept @font-face with application/font-woff2 MIME type', () => {
|
||||
const css = `@font-face {
|
||||
font-family: 'App Font';
|
||||
src: url('data:application/font-woff2;base64,${TINY_WOFF2_BASE64}') format('woff2');
|
||||
}
|
||||
:root { --font-family: 'App Font', sans-serif; }`;
|
||||
expect(() => validateThemeCss(css)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should accept @font-face with font-stretch', () => {
|
||||
const css = `@font-face {
|
||||
font-family: 'Stretch Font';
|
||||
src: url('${TINY_WOFF2_DATA_URI}') format('woff2');
|
||||
font-stretch: condensed;
|
||||
}
|
||||
:root { --font-family: 'Stretch Font', sans-serif; }`;
|
||||
expect(() => validateThemeCss(css)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should accept @font-face with unicode-range', () => {
|
||||
const css = `@font-face {
|
||||
font-family: 'Unicode Font';
|
||||
src: url('${TINY_WOFF2_DATA_URI}') format('woff2');
|
||||
unicode-range: U+0000-00FF;
|
||||
}
|
||||
:root { --font-family: 'Unicode Font', sans-serif; }`;
|
||||
expect(() => validateThemeCss(css)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should allow custom font name in --font-family after @font-face declaration', () => {
|
||||
const css = `@font-face {
|
||||
font-family: 'My Custom Font';
|
||||
src: url('${TINY_WOFF2_DATA_URI}') format('woff2');
|
||||
}
|
||||
:root { --font-family: 'My Custom Font', Georgia, serif; }`;
|
||||
expect(() => validateThemeCss(css)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should accept @font-face alongside color variables', () => {
|
||||
const css = `${FONT_FACE_BLOCK}
|
||||
:root {
|
||||
--color-primary: #007bff;
|
||||
--font-family: 'Test Font', sans-serif;
|
||||
--color-secondary: #6c757d;
|
||||
}`;
|
||||
expect(() => validateThemeCss(css)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid @font-face - security', () => {
|
||||
it('should reject @font-face with remote HTTP URL', () => {
|
||||
const css = `@font-face {
|
||||
font-family: 'Bad Font';
|
||||
src: url('https://evil.com/font.woff2') format('woff2');
|
||||
}
|
||||
:root { --font-family: 'Bad Font', sans-serif; }`;
|
||||
expect(() => validateThemeCss(css)).toThrow(/data: URIs/);
|
||||
});
|
||||
|
||||
it('should reject @font-face with remote HTTPS URL', () => {
|
||||
const css = `@font-face {
|
||||
font-family: 'Bad Font';
|
||||
src: url('https://fonts.example.com/custom.woff2') format('woff2');
|
||||
}
|
||||
:root { --font-family: 'Bad Font', sans-serif; }`;
|
||||
expect(() => validateThemeCss(css)).toThrow(/data: URIs/);
|
||||
});
|
||||
|
||||
it('should reject @font-face with relative URL (not embedded)', () => {
|
||||
const css = `@font-face {
|
||||
font-family: 'Bad Font';
|
||||
src: url('./fonts/custom.woff2') format('woff2');
|
||||
}
|
||||
:root { --font-family: 'Bad Font', sans-serif; }`;
|
||||
expect(() => validateThemeCss(css)).toThrow(/data: URIs/);
|
||||
});
|
||||
|
||||
it('should reject @font-face with javascript: protocol', () => {
|
||||
const css = `@font-face {
|
||||
font-family: 'Bad Font';
|
||||
src: url('javascript:alert(1)');
|
||||
}
|
||||
:root { --font-family: 'Bad Font', sans-serif; }`;
|
||||
expect(() => validateThemeCss(css)).toThrow(/data: URIs/);
|
||||
});
|
||||
|
||||
it('should accept any font name in --font-family (no allowlist)', () => {
|
||||
const css = `@font-face {
|
||||
font-family: 'Declared Font';
|
||||
src: url('${TINY_WOFF2_DATA_URI}') format('woff2');
|
||||
}
|
||||
:root { --font-family: 'Undeclared Font', sans-serif; }`;
|
||||
expect(() => validateThemeCss(css)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should reject oversized font data', () => {
|
||||
// Create a base64 string that would decode to > MAX_FONT_FILE_SIZE
|
||||
const oversizedBase64 = 'A'.repeat(
|
||||
Math.ceil((MAX_FONT_FILE_SIZE * 4) / 3) + 100,
|
||||
);
|
||||
const css = `@font-face {
|
||||
font-family: 'Big Font';
|
||||
src: url('data:font/woff2;base64,${oversizedBase64}') format('woff2');
|
||||
}
|
||||
:root { --font-family: 'Big Font', sans-serif; }`;
|
||||
expect(() => validateThemeCss(css)).toThrow(/maximum size/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CSS without @font-face still works', () => {
|
||||
it('should accept plain :root without @font-face', () => {
|
||||
const css = `:root { --color-primary: #007bff; }`;
|
||||
expect(() => validateThemeCss(css)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should reject other at-rules (not @font-face)', () => {
|
||||
const css = `@import url('other.css');
|
||||
:root { --color-primary: #007bff; }`;
|
||||
expect(() => validateThemeCss(css)).toThrow();
|
||||
});
|
||||
|
||||
it('should reject @media outside :root', () => {
|
||||
const css = `@media (max-width: 600px) { :root { --color-primary: #ff0000; } }
|
||||
:root { --color-primary: #007bff; }`;
|
||||
expect(() => validateThemeCss(css)).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseInstalledTheme', () => {
|
||||
describe('valid theme JSON', () => {
|
||||
it('should parse valid theme with all required fields', () => {
|
||||
@@ -1472,123 +1133,3 @@ describe('parseInstalledTheme', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('embedThemeFonts', () => {
|
||||
const mockFetch = (
|
||||
responseBody: ArrayBuffer,
|
||||
ok = true,
|
||||
status = 200,
|
||||
): typeof globalThis.fetch =>
|
||||
vi.fn().mockResolvedValue({
|
||||
ok,
|
||||
status,
|
||||
statusText: ok ? 'OK' : 'Not Found',
|
||||
arrayBuffer: () => Promise.resolve(responseBody),
|
||||
} as Partial<Response>);
|
||||
|
||||
const tinyBuffer = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]).buffer;
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('should rewrite url() references to data URIs', async () => {
|
||||
vi.stubGlobal('fetch', mockFetch(tinyBuffer));
|
||||
|
||||
const css = `@font-face {
|
||||
font-family: 'Test';
|
||||
src: url('fonts/test.woff2') format('woff2');
|
||||
}
|
||||
:root { --color-primary: #007bff; }`;
|
||||
|
||||
const result = await embedThemeFonts(css, 'owner/repo');
|
||||
expect(result).toContain('data:font/woff2;base64,');
|
||||
expect(result).not.toContain('fonts/test.woff2');
|
||||
expect(result).toContain(':root');
|
||||
});
|
||||
|
||||
it('should handle quoted filenames with spaces', async () => {
|
||||
vi.stubGlobal('fetch', mockFetch(tinyBuffer));
|
||||
|
||||
const css = `@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url("Inter Variable.woff2") format('woff2');
|
||||
}
|
||||
:root { --color-primary: #007bff; }`;
|
||||
|
||||
const result = await embedThemeFonts(css, 'owner/repo');
|
||||
expect(result).toContain('data:font/woff2;base64,');
|
||||
expect(result).not.toContain('Inter Variable.woff2');
|
||||
});
|
||||
|
||||
it('should reject path traversal with ".."', async () => {
|
||||
const css = `@font-face {
|
||||
font-family: 'Evil';
|
||||
src: url('../escape/font.woff2') format('woff2');
|
||||
}
|
||||
:root { --color-primary: #007bff; }`;
|
||||
|
||||
await expect(embedThemeFonts(css, 'owner/repo')).rejects.toThrow(
|
||||
'is not allowed',
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject root-anchored paths', async () => {
|
||||
const css = `@font-face {
|
||||
font-family: 'Evil';
|
||||
src: url('/etc/passwd') format('woff2');
|
||||
}
|
||||
:root { --color-primary: #007bff; }`;
|
||||
|
||||
await expect(embedThemeFonts(css, 'owner/repo')).rejects.toThrow(
|
||||
'is not allowed',
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject oversized font files', async () => {
|
||||
const oversized = new ArrayBuffer(MAX_FONT_FILE_SIZE + 1);
|
||||
vi.stubGlobal('fetch', mockFetch(oversized));
|
||||
|
||||
const css = `@font-face {
|
||||
font-family: 'Big';
|
||||
src: url('big.woff2') format('woff2');
|
||||
}
|
||||
:root { --color-primary: #007bff; }`;
|
||||
|
||||
await expect(embedThemeFonts(css, 'owner/repo')).rejects.toThrow(
|
||||
'exceeds maximum size',
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject when total font size exceeds budget', async () => {
|
||||
// Each font is under the per-file limit but together they exceed the total
|
||||
// Use MAX_FONT_FILE_SIZE (2MB) per font, need 6 to exceed 10MB total
|
||||
const bigBuffer = new ArrayBuffer(MAX_FONT_FILE_SIZE);
|
||||
vi.stubGlobal('fetch', mockFetch(bigBuffer));
|
||||
|
||||
const fontBlocks = Array.from(
|
||||
{ length: 6 },
|
||||
(_, i) => `@font-face {
|
||||
font-family: 'Font${i}';
|
||||
src: url('font${i}.woff2') format('woff2');
|
||||
}`,
|
||||
).join('\n');
|
||||
const css = `${fontBlocks}\n:root { --color-primary: #007bff; }`;
|
||||
|
||||
await expect(embedThemeFonts(css, 'owner/repo')).rejects.toThrow(
|
||||
'Total embedded font data exceeds maximum',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return CSS unchanged when no url() refs exist', async () => {
|
||||
const css = `@font-face {
|
||||
font-family: 'Test';
|
||||
src: url('${TINY_WOFF2_DATA_URI}') format('woff2');
|
||||
}
|
||||
:root { --color-primary: #007bff; }`;
|
||||
|
||||
const result = await embedThemeFonts(css, 'owner/repo');
|
||||
expect(result).toBe(css);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,63 +79,6 @@ export async function fetchDirectCss(url: string): Promise<string> {
|
||||
return response.text();
|
||||
}
|
||||
|
||||
/** Strip surrounding single or double quotes from a string. */
|
||||
function stripQuotes(s: string): string {
|
||||
const t = s.trim();
|
||||
if (
|
||||
(t.startsWith("'") && t.endsWith("'")) ||
|
||||
(t.startsWith('"') && t.endsWith('"'))
|
||||
) {
|
||||
return t.slice(1, -1).trim();
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a font-family value for a --font-* CSS variable.
|
||||
*
|
||||
* Any font name is allowed — referencing a font the user doesn't have
|
||||
* installed simply triggers the browser's normal fallback behaviour
|
||||
* (no network requests). The only things we block are function calls
|
||||
* (url(), expression(), etc.) because those could load external resources
|
||||
* or execute expressions.
|
||||
*
|
||||
* Quoted or unquoted font names are both accepted.
|
||||
*
|
||||
* Examples of accepted values:
|
||||
* Georgia, serif
|
||||
* 'Fira Code', monospace
|
||||
* "My Theme Font", sans-serif
|
||||
*/
|
||||
function validateFontFamilyValue(value: string, property: string): void {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error(
|
||||
`Invalid font-family value for "${property}": value must not be empty.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Split on commas, then validate each font name
|
||||
const families = trimmed.split(',');
|
||||
|
||||
for (const raw of families) {
|
||||
const name = stripQuotes(raw);
|
||||
|
||||
if (!name) {
|
||||
throw new Error(
|
||||
`Invalid font-family value for "${property}": empty font name in comma-separated list.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Reject anything that looks like a function call (url(), expression(), etc.)
|
||||
if (/\(/.test(name)) {
|
||||
throw new Error(
|
||||
`Invalid font-family value for "${property}": function calls are not allowed. Only font names are permitted.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Only var(--custom-property-name) is allowed; no fallbacks. Variable name: -- then [a-zA-Z0-9_-]+ (no trailing dash). */
|
||||
const VAR_ONLY_PATTERN = /^var\s*\(\s*(--[a-zA-Z0-9_-]+)\s*\)$/i;
|
||||
|
||||
@@ -149,15 +92,8 @@ function isValidSimpleVarValue(value: string): boolean {
|
||||
/**
|
||||
* Validate that a CSS property value only contains allowed content (allowlist approach).
|
||||
* Allows: colors (hex, rgb/rgba, hsl/hsla), lengths, numbers, keywords, and var(--name) only (no fallbacks).
|
||||
* Font properties (--font-*) are validated against a safe font family allowlist instead.
|
||||
*/
|
||||
function validatePropertyValue(value: string, property: string): void {
|
||||
// Font properties use a dedicated validator that accepts any font name
|
||||
// but rejects function calls (url(), expression(), etc.).
|
||||
if (/^--font-/i.test(property)) {
|
||||
validateFontFamilyValue(value, property);
|
||||
return;
|
||||
}
|
||||
if (!value || value.length === 0) {
|
||||
return; // Empty values are allowed
|
||||
}
|
||||
@@ -209,152 +145,79 @@ function validatePropertyValue(value: string, property: string): void {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── @font-face validation ──────────────────────────────────────────────────
|
||||
|
||||
/** Maximum size of a single base64-encoded font (bytes of decoded data). 2 MB. */
|
||||
export const MAX_FONT_FILE_SIZE = 2 * 1024 * 1024;
|
||||
|
||||
/** Maximum total size of all embedded font data across all @font-face blocks. 10 MB. */
|
||||
export const MAX_TOTAL_FONT_SIZE = 10 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* Extract @font-face blocks from CSS. Returns the blocks and the remaining CSS.
|
||||
* Only matches top-level @font-face blocks (not nested inside other rules).
|
||||
* Validate that CSS contains only :root { ... } with CSS custom property (variable) declarations.
|
||||
* Must contain exactly :root { ... } and nothing else.
|
||||
* Returns the validated CSS or throws an error.
|
||||
*/
|
||||
function extractFontFaceBlocks(css: string): {
|
||||
fontFaceBlocks: string[];
|
||||
remaining: string;
|
||||
} {
|
||||
const fontFaceBlocks: string[] = [];
|
||||
let remaining = css;
|
||||
export function validateThemeCss(css: string): string {
|
||||
// Strip multi-line comments before validation
|
||||
// Note: Single-line comments (//) are not stripped to avoid corrupting CSS values like URLs
|
||||
const cleaned = css.replace(/\/\*[\s\S]*?\*\//g, '').trim();
|
||||
|
||||
// Extract @font-face { ... } blocks one at a time using indexOf-based
|
||||
// parsing. Each iteration removes the matched block from `remaining`.
|
||||
for (;;) {
|
||||
const atIdx = remaining.indexOf('@font-face');
|
||||
if (atIdx === -1) break;
|
||||
|
||||
const openBrace = remaining.indexOf('{', atIdx);
|
||||
if (openBrace === -1) break;
|
||||
|
||||
const closeBrace = remaining.indexOf('}', openBrace + 1);
|
||||
if (closeBrace === -1) break;
|
||||
|
||||
fontFaceBlocks.push(remaining.substring(openBrace + 1, closeBrace).trim());
|
||||
remaining =
|
||||
remaining.substring(0, atIdx) + remaining.substring(closeBrace + 1);
|
||||
}
|
||||
|
||||
return { fontFaceBlocks, remaining: remaining.trim() };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate @font-face blocks: only data: URIs allowed (no remote URLs).
|
||||
* Enforces size limits to prevent DoS.
|
||||
*/
|
||||
function validateFontFaceBlocks(fontFaceBlocks: string[]): void {
|
||||
let totalSize = 0;
|
||||
// Match url() with quoted or unquoted content. Quoted URLs use a non-greedy
|
||||
// match up to the closing quote; unquoted URLs match non-whitespace/non-paren.
|
||||
const urlRegex = /url\(\s*(?:'([^']*)'|"([^"]*)"|([^'")\s]+))\s*\)/g;
|
||||
|
||||
for (const block of fontFaceBlocks) {
|
||||
urlRegex.lastIndex = 0;
|
||||
let match;
|
||||
while ((match = urlRegex.exec(block)) !== null) {
|
||||
const uri = (match[1] ?? match[2] ?? match[3]).trim();
|
||||
if (!uri.startsWith('data:')) {
|
||||
throw new Error(
|
||||
'Invalid font src: only data: URIs are allowed in @font-face. ' +
|
||||
'Remote URLs (http/https) are not permitted to protect user privacy. ' +
|
||||
'Font files are automatically embedded when installing from GitHub.',
|
||||
);
|
||||
}
|
||||
// Estimate decoded size from base64 content
|
||||
const base64Match = uri.match(/;base64,(.+)$/);
|
||||
if (base64Match) {
|
||||
const size = Math.ceil((base64Match[1].length * 3) / 4);
|
||||
if (size > MAX_FONT_FILE_SIZE) {
|
||||
throw new Error(
|
||||
`Font file exceeds maximum size of ${MAX_FONT_FILE_SIZE / 1024 / 1024}MB.`,
|
||||
);
|
||||
}
|
||||
totalSize += size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (totalSize > MAX_TOTAL_FONT_SIZE) {
|
||||
// Must contain exactly :root { ... } and nothing else
|
||||
// Find :root { ... } and extract content, then check there's nothing after
|
||||
const rootMatch = cleaned.match(/^:root\s*\{/);
|
||||
if (!rootMatch) {
|
||||
throw new Error(
|
||||
`Total embedded font data exceeds maximum of ${MAX_TOTAL_FONT_SIZE / 1024 / 1024}MB.`,
|
||||
'Theme CSS must contain exactly :root { ... } with CSS variable definitions. No other selectors or content allowed.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Split CSS declarations by semicolons, but respect quoted strings and url() contents.
|
||||
* This is needed because data: URIs contain semicolons (e.g., "data:font/woff2;base64,...").
|
||||
*/
|
||||
function splitDeclarations(content: string): string[] {
|
||||
const declarations: string[] = [];
|
||||
let start = 0;
|
||||
let inSingleQuote = false;
|
||||
let inDoubleQuote = false;
|
||||
let parenDepth = 0;
|
||||
// Find the opening brace after :root
|
||||
const rootStart = cleaned.indexOf(':root');
|
||||
const openBrace = cleaned.indexOf('{', rootStart);
|
||||
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
const ch = content[i];
|
||||
|
||||
if (ch === "'" && !inDoubleQuote && parenDepth === 0) {
|
||||
inSingleQuote = !inSingleQuote;
|
||||
} else if (ch === '"' && !inSingleQuote && parenDepth === 0) {
|
||||
inDoubleQuote = !inDoubleQuote;
|
||||
} else if (ch === '(' && !inSingleQuote && !inDoubleQuote) {
|
||||
parenDepth++;
|
||||
} else if (
|
||||
ch === ')' &&
|
||||
!inSingleQuote &&
|
||||
!inDoubleQuote &&
|
||||
parenDepth > 0
|
||||
) {
|
||||
parenDepth--;
|
||||
}
|
||||
|
||||
if (ch === ';' && !inSingleQuote && !inDoubleQuote && parenDepth === 0) {
|
||||
const trimmed = content.substring(start, i).trim();
|
||||
if (trimmed) declarations.push(trimmed);
|
||||
start = i + 1;
|
||||
}
|
||||
if (openBrace === -1) {
|
||||
throw new Error(
|
||||
'Theme CSS must contain exactly :root { ... } with CSS variable definitions. No other selectors or content allowed.',
|
||||
);
|
||||
}
|
||||
|
||||
const trimmed = content.substring(start).trim();
|
||||
if (trimmed) declarations.push(trimmed);
|
||||
// Find the first closing brace (nested blocks will be caught by the check below)
|
||||
const closeBrace = cleaned.indexOf('}', openBrace + 1);
|
||||
|
||||
return declarations;
|
||||
}
|
||||
if (closeBrace === -1) {
|
||||
throw new Error(
|
||||
'Theme CSS must contain exactly :root { ... } with CSS variable definitions. No other selectors or content allowed.',
|
||||
);
|
||||
}
|
||||
|
||||
// ─── :root block validation ─────────────────────────────────────────────────
|
||||
// Extract content inside :root { ... }
|
||||
const rootContent = cleaned.substring(openBrace + 1, closeBrace).trim();
|
||||
|
||||
/**
|
||||
* Validate the content inside a :root { ... } block.
|
||||
* Only CSS custom properties (--*) with safe values are allowed.
|
||||
*/
|
||||
function validateRootContent(rootContent: string): void {
|
||||
// Check for forbidden at-rules inside :root
|
||||
// Check for forbidden at-rules first (before nested block check, since at-rules with braces would trigger that)
|
||||
// Comprehensive list of CSS at-rules that should not be allowed
|
||||
// This includes @import, @media, @keyframes, @font-face, @supports, @charset,
|
||||
// @namespace, @page, @layer, @container, @scope, and any other at-rules
|
||||
if (/@[a-z-]+/i.test(rootContent)) {
|
||||
throw new Error(
|
||||
'Theme CSS contains forbidden at-rules (@import, @media, @keyframes, etc.). Only CSS variable declarations are allowed inside :root { ... }.',
|
||||
);
|
||||
}
|
||||
|
||||
// Check for nested blocks
|
||||
// Check for nested blocks (additional selectors) - should not have any { after extracting :root content
|
||||
if (/\{/.test(rootContent)) {
|
||||
throw new Error(
|
||||
'Theme CSS contains nested blocks or additional selectors. Only CSS variable declarations are allowed inside :root { ... }.',
|
||||
);
|
||||
}
|
||||
|
||||
for (const decl of splitDeclarations(rootContent)) {
|
||||
// Check that there's nothing after the closing brace
|
||||
const afterRoot = cleaned.substring(closeBrace + 1).trim();
|
||||
if (afterRoot.length > 0) {
|
||||
throw new Error(
|
||||
'Theme CSS must contain exactly :root { ... } with CSS variable definitions. No other selectors or content allowed.',
|
||||
);
|
||||
}
|
||||
|
||||
// Parse declarations and validate each one
|
||||
const declarations = rootContent
|
||||
.split(';')
|
||||
.map(d => d.trim())
|
||||
.filter(d => d.length > 0);
|
||||
|
||||
for (const decl of declarations) {
|
||||
const colonIndex = decl.indexOf(':');
|
||||
if (colonIndex === -1) {
|
||||
throw new Error(`Invalid CSS declaration: "${decl}"`);
|
||||
@@ -408,220 +271,9 @@ function validateRootContent(rootContent: string): void {
|
||||
const value = decl.substring(colonIndex + 1).trim();
|
||||
validatePropertyValue(value, property);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Main validation entry point ────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Validate theme CSS. Accepts:
|
||||
* 1. Optional @font-face blocks (with data: URI fonts only)
|
||||
* 2. Exactly one :root { ... } block with CSS variable declarations
|
||||
*
|
||||
* @font-face blocks must appear before :root.
|
||||
* Returns the validated CSS or throws an error.
|
||||
*/
|
||||
export function validateThemeCss(css: string): string {
|
||||
// Strip multi-line comments before validation
|
||||
const cleaned = css.replace(/\/\*[\s\S]*?\*\//g, '').trim();
|
||||
|
||||
// Extract @font-face blocks (if any) from the CSS
|
||||
const { fontFaceBlocks, remaining } = extractFontFaceBlocks(cleaned);
|
||||
|
||||
// Validate @font-face blocks (reject remote URLs, enforce size limits)
|
||||
validateFontFaceBlocks(fontFaceBlocks);
|
||||
|
||||
// Now validate the remaining CSS (should be exactly :root { ... })
|
||||
const rootMatch = remaining.match(/^:root\s*\{/);
|
||||
if (!rootMatch) {
|
||||
// If there are @font-face blocks but no :root, that's an error
|
||||
// If there's nothing at all, that's also an error
|
||||
throw new Error(
|
||||
'Theme CSS must contain :root { ... } with CSS variable definitions. No other selectors or content allowed.',
|
||||
);
|
||||
}
|
||||
|
||||
const rootStart = remaining.indexOf(':root');
|
||||
const openBrace = remaining.indexOf('{', rootStart);
|
||||
|
||||
if (openBrace === -1) {
|
||||
throw new Error(
|
||||
'Theme CSS must contain :root { ... } with CSS variable definitions. No other selectors or content allowed.',
|
||||
);
|
||||
}
|
||||
|
||||
const closeBrace = remaining.indexOf('}', openBrace + 1);
|
||||
|
||||
if (closeBrace === -1) {
|
||||
throw new Error(
|
||||
'Theme CSS must contain :root { ... } with CSS variable definitions. No other selectors or content allowed.',
|
||||
);
|
||||
}
|
||||
|
||||
const rootContent = remaining.substring(openBrace + 1, closeBrace).trim();
|
||||
|
||||
// Validate :root content
|
||||
validateRootContent(rootContent);
|
||||
|
||||
// Check nothing after :root
|
||||
const afterRoot = remaining.substring(closeBrace + 1).trim();
|
||||
if (afterRoot.length > 0) {
|
||||
throw new Error(
|
||||
'Theme CSS must contain :root { ... } with CSS variable definitions. No other selectors or content allowed.',
|
||||
);
|
||||
}
|
||||
|
||||
// Return the comment-stripped CSS — this is what was actually validated,
|
||||
// so we inject exactly what we checked (defense-in-depth).
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
// ─── Font embedding (install-time) ─────────────────────────────────────────
|
||||
|
||||
/** Map of file extensions to font MIME types for data: URI construction. */
|
||||
const FONT_EXTENSION_MIME: Record<string, string> = {
|
||||
'.woff2': 'font/woff2',
|
||||
'.woff': 'font/woff',
|
||||
'.ttf': 'font/ttf',
|
||||
'.otf': 'font/opentype',
|
||||
};
|
||||
|
||||
/** Convert an ArrayBuffer to a base64 string using chunked processing. */
|
||||
function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
const chunks: string[] = [];
|
||||
// Process in 8 KB chunks to avoid excessive string concatenation
|
||||
for (let i = 0; i < bytes.length; i += 8192) {
|
||||
const chunk = bytes.subarray(i, Math.min(i + 8192, bytes.length));
|
||||
chunks.push(String.fromCharCode(...chunk));
|
||||
}
|
||||
return btoa(chunks.join(''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Embed fonts referenced in @font-face blocks by fetching them from a GitHub
|
||||
* repo and converting to data: URIs.
|
||||
*
|
||||
* This runs at install time only. Relative URL references like
|
||||
* `url('./fonts/MyFont.woff2')` are resolved relative to the repo's root
|
||||
* directory and fetched from GitHub's raw content API.
|
||||
*
|
||||
* The returned CSS has all font URLs replaced with self-contained data: URIs,
|
||||
* so no network requests are needed at runtime.
|
||||
*
|
||||
* @param css - The raw theme CSS (may contain relative url() references)
|
||||
* @param repo - GitHub repo in "owner/repo" format
|
||||
* @returns CSS with all font URLs replaced by data: URIs
|
||||
*/
|
||||
export async function embedThemeFonts(
|
||||
css: string,
|
||||
repo: string,
|
||||
): Promise<string> {
|
||||
const baseUrl = `https://raw.githubusercontent.com/${repo}/refs/heads/main/`;
|
||||
|
||||
// Collect all url() references that need fetching across all @font-face blocks
|
||||
const urlRegex = /url\(\s*(?:(['"])([^'"]*?)\1|([^'")\s]+))\s*\)/g;
|
||||
type FontRef = {
|
||||
fullMatch: string;
|
||||
quote: string;
|
||||
path: string;
|
||||
cleanPath: string;
|
||||
mime: string;
|
||||
};
|
||||
const fontRefs: FontRef[] = [];
|
||||
|
||||
// Use extractFontFaceBlocks-style indexOf parsing to find @font-face blocks
|
||||
// and their url() references, without duplicating the regex approach
|
||||
let searchCss = css;
|
||||
let offset = 0;
|
||||
for (;;) {
|
||||
const atIdx = searchCss.indexOf('@font-face', 0);
|
||||
if (atIdx === -1) break;
|
||||
|
||||
const openBrace = searchCss.indexOf('{', atIdx);
|
||||
if (openBrace === -1) break;
|
||||
|
||||
const closeBrace = searchCss.indexOf('}', openBrace + 1);
|
||||
if (closeBrace === -1) break;
|
||||
|
||||
const blockContent = searchCss.substring(openBrace + 1, closeBrace);
|
||||
|
||||
// Find url() references within this block
|
||||
let urlMatch;
|
||||
urlRegex.lastIndex = 0;
|
||||
while ((urlMatch = urlRegex.exec(blockContent)) !== null) {
|
||||
const fullMatch = urlMatch[0];
|
||||
const quote = urlMatch[1] || '';
|
||||
const path = urlMatch[2] ?? urlMatch[3];
|
||||
|
||||
// Skip data: URIs — already embedded
|
||||
if (path.startsWith('data:')) continue;
|
||||
|
||||
if (/^https?:\/\//i.test(path)) {
|
||||
throw new Error(
|
||||
`Remote font URL "${path}" is not allowed. Only relative paths to fonts in the same GitHub repo are supported.`,
|
||||
);
|
||||
}
|
||||
|
||||
const cleanPath = path.replace(/^\.\//, '');
|
||||
|
||||
if (cleanPath.startsWith('/') || cleanPath.includes('..')) {
|
||||
throw new Error(
|
||||
`Font path "${path}" is not allowed. Only relative paths within the repo are supported (no "/" prefix or ".." segments).`,
|
||||
);
|
||||
}
|
||||
|
||||
const ext = cleanPath.substring(cleanPath.lastIndexOf('.')).toLowerCase();
|
||||
const mime = FONT_EXTENSION_MIME[ext];
|
||||
if (!mime) {
|
||||
throw new Error(
|
||||
`Unsupported font file extension "${ext}". Supported: ${Object.keys(FONT_EXTENSION_MIME).join(', ')}.`,
|
||||
);
|
||||
}
|
||||
|
||||
fontRefs.push({ fullMatch, quote, path, cleanPath, mime });
|
||||
}
|
||||
|
||||
offset = closeBrace + 1;
|
||||
searchCss = searchCss.substring(offset);
|
||||
}
|
||||
|
||||
if (fontRefs.length === 0) return css;
|
||||
|
||||
// Fetch fonts sequentially to enforce a running total size budget
|
||||
const fetched: { ref: FontRef; dataUri: string }[] = [];
|
||||
let totalBytes = 0;
|
||||
for (const ref of fontRefs) {
|
||||
const fontUrl = baseUrl + ref.cleanPath;
|
||||
const response = await fetch(fontUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch font file "${ref.cleanPath}" from ${fontUrl}: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
const buffer = await response.arrayBuffer();
|
||||
if (buffer.byteLength > MAX_FONT_FILE_SIZE) {
|
||||
throw new Error(
|
||||
`Font file "${ref.cleanPath}" exceeds maximum size of ${MAX_FONT_FILE_SIZE / 1024 / 1024}MB.`,
|
||||
);
|
||||
}
|
||||
totalBytes += buffer.byteLength;
|
||||
if (totalBytes > MAX_TOTAL_FONT_SIZE) {
|
||||
throw new Error(
|
||||
`Total embedded font data exceeds maximum of ${MAX_TOTAL_FONT_SIZE / 1024 / 1024}MB.`,
|
||||
);
|
||||
}
|
||||
const base64 = arrayBufferToBase64(buffer);
|
||||
fetched.push({ ref, dataUri: `data:${ref.mime};base64,${base64}` });
|
||||
}
|
||||
|
||||
// Replace each url() reference with its data: URI
|
||||
let result = css;
|
||||
for (const { ref, dataUri } of fetched) {
|
||||
const q = ref.quote || "'";
|
||||
result = result.replace(ref.fullMatch, `url(${q}${dataUri}${q})`);
|
||||
}
|
||||
|
||||
return result;
|
||||
// Return the original CSS (with :root wrapper) so it can be injected properly
|
||||
return css.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
7
packages/mobile/.gitignore
vendored
Normal file
7
packages/mobile/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# iOS build artifacts
|
||||
ios/App/Pods/
|
||||
ios/App/DerivedData/
|
||||
ios/App/build/
|
||||
|
||||
# Node modules are at root level
|
||||
node_modules/
|
||||
46
packages/mobile/capacitor.config.ts
Normal file
46
packages/mobile/capacitor.config.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { CapacitorConfig } from '@capacitor/cli';
|
||||
|
||||
const config: CapacitorConfig = {
|
||||
appId: 'org.actualbudget.mobile',
|
||||
appName: 'Actual Budget',
|
||||
// Point to the desktop-client build output
|
||||
webDir: '../desktop-client/build',
|
||||
server: {
|
||||
// Serve from localhost to enable COOP/COEP headers,
|
||||
// which are required for SharedArrayBuffer (used by absurd-sql).
|
||||
// On iOS, this uses WKURLSchemeHandler with the http scheme,
|
||||
// allowing cross-origin isolation to work in WKWebView.
|
||||
iosScheme: 'http',
|
||||
androidScheme: 'http',
|
||||
},
|
||||
ios: {
|
||||
// Minimum iOS version that supports SharedArrayBuffer in WKWebView
|
||||
// with cross-origin isolation headers (iOS 15.2+)
|
||||
minVersion: '16.0',
|
||||
// Allow inline media playback (needed for some UI interactions)
|
||||
allowsLinkPreview: false,
|
||||
contentInset: 'always',
|
||||
preferredContentMode: 'mobile',
|
||||
},
|
||||
plugins: {
|
||||
SplashScreen: {
|
||||
launchAutoHide: true,
|
||||
launchShowDuration: 2000,
|
||||
backgroundColor: '#5c3dbb',
|
||||
showSpinner: false,
|
||||
iosSpinnerStyle: 'small',
|
||||
splashFullScreen: true,
|
||||
splashImmersive: true,
|
||||
},
|
||||
Keyboard: {
|
||||
resize: 'body',
|
||||
resizeOnFullScreen: true,
|
||||
},
|
||||
StatusBar: {
|
||||
style: 'LIGHT',
|
||||
backgroundColor: '#5c3dbb',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
13
packages/mobile/ios/.gitignore
vendored
Normal file
13
packages/mobile/ios/.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
App/build
|
||||
App/Pods
|
||||
App/output
|
||||
App/App/public
|
||||
DerivedData
|
||||
xcuserdata
|
||||
|
||||
# Cordova plugins for Capacitor
|
||||
capacitor-cordova-ios-plugins
|
||||
|
||||
# Generated Config files
|
||||
App/App/capacitor.config.json
|
||||
App/App/config.xml
|
||||
416
packages/mobile/ios/App/App.xcodeproj/project.pbxproj
Normal file
416
packages/mobile/ios/App/App.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,416 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 48;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
|
||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
|
||||
A1B2C3D41000000000000001 /* ActualBridgeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D41000000000000002 /* ActualBridgeViewController.swift */; };
|
||||
A1B2C3D41000000000000003 /* CrossOriginIsolationPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D41000000000000004 /* CrossOriginIsolationPlugin.swift */; };
|
||||
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
|
||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
|
||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
||||
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
||||
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
||||
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
A1B2C3D41000000000000002 /* ActualBridgeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActualBridgeViewController.swift; sourceTree = "<group>"; };
|
||||
A1B2C3D41000000000000004 /* CrossOriginIsolationPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossOriginIsolationPlugin.swift; sourceTree = "<group>"; };
|
||||
504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
||||
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
|
||||
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
504EC3011FED79650016851F /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC2FB1FED79650016851F = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
504EC3061FED79650016851F /* App */,
|
||||
504EC3051FED79650016851F /* Products */,
|
||||
7F8756D8B27F46E3366F6CEA /* Pods */,
|
||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3051FED79650016851F /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
504EC3041FED79650016851F /* App.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3061FED79650016851F /* App */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */,
|
||||
504EC3071FED79650016851F /* AppDelegate.swift */,
|
||||
A1B2C3D41000000000000002 /* ActualBridgeViewController.swift */,
|
||||
A1B2C3D41000000000000004 /* CrossOriginIsolationPlugin.swift */,
|
||||
504EC30B1FED79650016851F /* Main.storyboard */,
|
||||
504EC30E1FED79650016851F /* Assets.xcassets */,
|
||||
504EC3101FED79650016851F /* LaunchScreen.storyboard */,
|
||||
504EC3131FED79650016851F /* Info.plist */,
|
||||
2FAD9762203C412B000D30F8 /* config.xml */,
|
||||
50B271D01FEDC1A000F3C39B /* public */,
|
||||
);
|
||||
path = App;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7F8756D8B27F46E3366F6CEA /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,
|
||||
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
504EC3031FED79650016851F /* App */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
|
||||
buildPhases = (
|
||||
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,
|
||||
504EC3001FED79650016851F /* Sources */,
|
||||
504EC3011FED79650016851F /* Frameworks */,
|
||||
504EC3021FED79650016851F /* Resources */,
|
||||
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = App;
|
||||
productName = App;
|
||||
productReference = 504EC3041FED79650016851F /* App.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
504EC2FC1FED79650016851F /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0920;
|
||||
LastUpgradeCheck = 0920;
|
||||
TargetAttributes = {
|
||||
504EC3031FED79650016851F = {
|
||||
CreatedOnToolsVersion = 9.2;
|
||||
LastSwiftMigration = 1100;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */;
|
||||
compatibilityVersion = "Xcode 8.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 504EC2FB1FED79650016851F;
|
||||
packageReferences = (
|
||||
);
|
||||
productRefGroup = 504EC3051FED79650016851F /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
504EC3031FED79650016851F /* App */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
504EC3021FED79650016851F /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,
|
||||
50B271D11FEDC1A000F3C39B /* public in Resources */,
|
||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */,
|
||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
|
||||
504EC30D1FED79650016851F /* Main.storyboard in Resources */,
|
||||
2FAD9763203C412B000D30F8 /* config.xml in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
504EC3001FED79650016851F /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
|
||||
A1B2C3D41000000000000001 /* ActualBridgeViewController.swift in Sources */,
|
||||
A1B2C3D41000000000000003 /* CrossOriginIsolationPlugin.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
504EC30B1FED79650016851F /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
504EC30C1FED79650016851F /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3101FED79650016851F /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
504EC3111FED79650016851F /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
504EC3141FED79650016851F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
504EC3151FED79650016851F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
504EC3171FED79650016851F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.actualbudget.mobile;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
504EC3181FED79650016851F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.actualbudget.mobile;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
504EC3141FED79650016851F /* Debug */,
|
||||
504EC3151FED79650016851F /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
504EC3171FED79650016851F /* Debug */,
|
||||
504EC3181FED79650016851F /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 504EC2FC1FED79650016851F /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
34
packages/mobile/ios/App/App/ActualBridgeViewController.swift
Normal file
34
packages/mobile/ios/App/App/ActualBridgeViewController.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
import UIKit
|
||||
import Capacitor
|
||||
import WebKit
|
||||
|
||||
/// Custom Capacitor bridge view controller that enables cross-origin isolation
|
||||
/// for SharedArrayBuffer support in WKWebView.
|
||||
///
|
||||
/// SharedArrayBuffer is required by absurd-sql, which Actual Budget uses for
|
||||
/// local-first SQLite storage in the browser/WebView environment.
|
||||
class ActualBridgeViewController: CAPBridgeViewController {
|
||||
|
||||
override func webViewConfiguration(for instanceConfiguration: InstanceConfiguration) -> WKWebViewConfiguration {
|
||||
let config = super.webViewConfiguration(for: instanceConfiguration)
|
||||
|
||||
// Enable SharedArrayBuffer support by setting crossOriginIsolation
|
||||
// This is available on iOS 15.4+ and is required for absurd-sql
|
||||
if #available(iOS 15.4, *) {
|
||||
// Set preferences for cross-origin isolation
|
||||
let prefs = config.preferences
|
||||
prefs.isElementFullscreenEnabled = true
|
||||
}
|
||||
|
||||
// Allow file access for local assets
|
||||
config.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs")
|
||||
config.setValue(true, forKey: "allowUniversalAccessFromFileURLs")
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
override func capacitorDidLoad() {
|
||||
// Register the cross-origin isolation plugin
|
||||
bridge?.registerPluginInstance(CrossOriginIsolationPlugin())
|
||||
}
|
||||
}
|
||||
49
packages/mobile/ios/App/App/AppDelegate.swift
Normal file
49
packages/mobile/ios/App/App/AppDelegate.swift
Normal file
@@ -0,0 +1,49 @@
|
||||
import UIKit
|
||||
import Capacitor
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationWillResignActive(_ application: UIApplication) {
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||
// Called when the app was launched with a url. Feel free to add additional processing here,
|
||||
// but if you want the App API to support tracking app url opens, make sure to keep this call
|
||||
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
|
||||
// Called when the app was launched with an activity, including Universal Links.
|
||||
// Feel free to add additional processing here, but if you want the App API to support
|
||||
// tracking app url opens, make sure to keep this call
|
||||
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"filename": "AppIcon-512@2x.png",
|
||||
"idiom": "universal",
|
||||
"platform": "ios",
|
||||
"size": "1024x1024"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "xcode"
|
||||
}
|
||||
}
|
||||
23
packages/mobile/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json
vendored
Normal file
23
packages/mobile/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"idiom": "universal",
|
||||
"filename": "splash-2732x2732-2.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"idiom": "universal",
|
||||
"filename": "splash-2732x2732-1.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"idiom": "universal",
|
||||
"filename": "splash-2732x2732.png",
|
||||
"scale": "3x"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "xcode"
|
||||
}
|
||||
}
|
||||
BIN
packages/mobile/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png
vendored
Normal file
BIN
packages/mobile/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
BIN
packages/mobile/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png
vendored
Normal file
BIN
packages/mobile/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
BIN
packages/mobile/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png
vendored
Normal file
BIN
packages/mobile/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17132" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17105"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<imageView key="view" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Splash" id="snD-IY-ifK">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
</imageView>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="Splash" width="1366" height="1366"/>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
19
packages/mobile/ios/App/App/Base.lproj/Main.storyboard
Normal file
19
packages/mobile/ios/App/App/Base.lproj/Main.storyboard
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14111" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Bridge View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="ActualBridgeViewController" customModule="App" sceneMemberID="viewController"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
39
packages/mobile/ios/App/App/CrossOriginIsolationPlugin.swift
Normal file
39
packages/mobile/ios/App/App/CrossOriginIsolationPlugin.swift
Normal file
@@ -0,0 +1,39 @@
|
||||
import Foundation
|
||||
import Capacitor
|
||||
import WebKit
|
||||
|
||||
/// A Capacitor plugin that injects Cross-Origin-Opener-Policy and
|
||||
/// Cross-Origin-Embedder-Policy headers into the WKWebView configuration.
|
||||
///
|
||||
/// These headers are required for SharedArrayBuffer support, which is used by
|
||||
/// absurd-sql (the SQLite-in-browser engine) for offline local-first data storage.
|
||||
///
|
||||
/// On iOS 16+, WKWebView supports SharedArrayBuffer when COOP/COEP headers are present.
|
||||
@objc(CrossOriginIsolationPlugin)
|
||||
public class CrossOriginIsolationPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
public let identifier = "CrossOriginIsolationPlugin"
|
||||
public let jsName = "CrossOriginIsolation"
|
||||
public let pluginMethods: [CAPPluginMethod] = []
|
||||
|
||||
override public func load() {
|
||||
// Inject a script that sets SharedArrayBuffer override flag
|
||||
// as a fallback for environments where COOP/COEP can't be set
|
||||
let script = WKUserScript(
|
||||
source: """
|
||||
// Ensure SharedArrayBuffer override is set for Capacitor context
|
||||
if (!window.SharedArrayBuffer) {
|
||||
localStorage.setItem('SharedArrayBufferOverride', 'true');
|
||||
console.log('[Capacitor] SharedArrayBuffer not available, override enabled');
|
||||
} else {
|
||||
console.log('[Capacitor] SharedArrayBuffer is available');
|
||||
}
|
||||
|
||||
// Mark as running in Capacitor for platform detection
|
||||
window.__ACTUAL_IS_CAPACITOR__ = true;
|
||||
""",
|
||||
injectionTime: .atDocumentStart,
|
||||
forMainFrameOnly: true
|
||||
)
|
||||
bridge?.webView?.configuration.userContentController.addUserScript(script)
|
||||
}
|
||||
}
|
||||
58
packages/mobile/ios/App/App/Info.plist
Normal file
58
packages/mobile/ios/App/App/Info.plist
Normal file
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Actual Budget</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
23
packages/mobile/ios/App/Podfile
Normal file
23
packages/mobile/ios/App/Podfile
Normal file
@@ -0,0 +1,23 @@
|
||||
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
|
||||
|
||||
platform :ios, '16.0'
|
||||
use_frameworks!
|
||||
|
||||
# workaround to avoid Xcode caching of Pods that requires
|
||||
# Product -> Clean Build Folder after new Cordova plugins installed
|
||||
# Requires CocoaPods 1.6 or newer
|
||||
install! 'cocoapods', :disable_input_output_paths => true
|
||||
|
||||
def capacitor_pods
|
||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||
end
|
||||
|
||||
target 'App' do
|
||||
capacitor_pods
|
||||
# Add your Pods here
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
assertDeploymentTarget(installer)
|
||||
end
|
||||
24
packages/mobile/package.json
Normal file
24
packages/mobile/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@actual-app/mobile",
|
||||
"version": "26.3.0",
|
||||
"private": true,
|
||||
"description": "Actual Budget iOS app powered by Capacitor",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "yarn workspace @actual-app/web build && npx cap sync ios",
|
||||
"sync": "npx cap sync ios",
|
||||
"open": "npx cap open ios",
|
||||
"run:ios": "npx cap run ios"
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor/app": "^7.0.0",
|
||||
"@capacitor/core": "^7.0.0",
|
||||
"@capacitor/ios": "^7.0.0",
|
||||
"@capacitor/keyboard": "^7.0.0",
|
||||
"@capacitor/splash-screen": "^7.0.0",
|
||||
"@capacitor/status-bar": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/cli": "^7.0.0"
|
||||
}
|
||||
}
|
||||
8
packages/mobile/tsconfig.json
Normal file
8
packages/mobile/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["capacitor.config.ts"]
|
||||
}
|
||||
@@ -128,10 +128,6 @@ app.get('/metrics', (_req, res) => {
|
||||
app.use((req, res, next) => {
|
||||
res.set('Cross-Origin-Opener-Policy', 'same-origin');
|
||||
res.set('Cross-Origin-Embedder-Policy', 'require-corp');
|
||||
res.set(
|
||||
'Content-Security-Policy',
|
||||
"default-src 'self' blob:; img-src 'self' blob: data:; script-src 'self' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; connect-src http: https:;",
|
||||
);
|
||||
next();
|
||||
});
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Custom Themes: allow using a custom font
|
||||
394
yarn.lock
394
yarn.lock
@@ -182,6 +182,20 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@actual-app/mobile@workspace:packages/mobile":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@actual-app/mobile@workspace:packages/mobile"
|
||||
dependencies:
|
||||
"@capacitor/app": "npm:^7.0.0"
|
||||
"@capacitor/cli": "npm:^7.0.0"
|
||||
"@capacitor/core": "npm:^7.0.0"
|
||||
"@capacitor/ios": "npm:^7.0.0"
|
||||
"@capacitor/keyboard": "npm:^7.0.0"
|
||||
"@capacitor/splash-screen": "npm:^7.0.0"
|
||||
"@capacitor/status-bar": "npm:^7.0.0"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@actual-app/sync-server@workspace:*, @actual-app/sync-server@workspace:packages/sync-server":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@actual-app/sync-server@workspace:packages/sync-server"
|
||||
@@ -2137,6 +2151,88 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@capacitor/app@npm:^7.0.0":
|
||||
version: 7.1.2
|
||||
resolution: "@capacitor/app@npm:7.1.2"
|
||||
peerDependencies:
|
||||
"@capacitor/core": ">=7.0.0"
|
||||
checksum: 10/f48ebfca4e86cbf578a893cbfc9136b70af85a42638048467b32a68a98548b0713d524f7fe24ed0172acaa8d94324d185ba4e2d6e83608da84ce79f1cb38aafe
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@capacitor/cli@npm:^7.0.0":
|
||||
version: 7.6.0
|
||||
resolution: "@capacitor/cli@npm:7.6.0"
|
||||
dependencies:
|
||||
"@ionic/cli-framework-output": "npm:^2.2.8"
|
||||
"@ionic/utils-subprocess": "npm:^3.0.1"
|
||||
"@ionic/utils-terminal": "npm:^2.3.5"
|
||||
commander: "npm:^12.1.0"
|
||||
debug: "npm:^4.4.0"
|
||||
env-paths: "npm:^2.2.0"
|
||||
fs-extra: "npm:^11.2.0"
|
||||
kleur: "npm:^4.1.5"
|
||||
native-run: "npm:^2.0.3"
|
||||
open: "npm:^8.4.0"
|
||||
plist: "npm:^3.1.0"
|
||||
prompts: "npm:^2.4.2"
|
||||
rimraf: "npm:^6.0.1"
|
||||
semver: "npm:^7.6.3"
|
||||
tar: "npm:^7.5.3"
|
||||
tslib: "npm:^2.8.1"
|
||||
xml2js: "npm:^0.6.2"
|
||||
bin:
|
||||
cap: bin/capacitor
|
||||
capacitor: bin/capacitor
|
||||
checksum: 10/87d5a765df53548814b5aac2da15f4bd53fb0b5bd400e6837f906d7a486e59f88a0e67891a12f31c1acf89a210703396d5a90744aa68794aae04ca6ef47beba3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@capacitor/core@npm:^7.0.0":
|
||||
version: 7.6.0
|
||||
resolution: "@capacitor/core@npm:7.6.0"
|
||||
dependencies:
|
||||
tslib: "npm:^2.1.0"
|
||||
checksum: 10/7eb5e45e8e5a39852b758c0fe3fab62df4af7ad903dd286395bdb1408a46eac0eb1a6c36d34b94f66f7c7263f026b543325fd973adf93ed0886d1b27b62c9f99
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@capacitor/ios@npm:^7.0.0":
|
||||
version: 7.6.0
|
||||
resolution: "@capacitor/ios@npm:7.6.0"
|
||||
peerDependencies:
|
||||
"@capacitor/core": ^7.6.0
|
||||
checksum: 10/08ef3c1cf0085b1268d6320265ec04b1b48a0d53310e265b8cf2833a60de6ee9da01e4a92dbfc93e0259611cd3044f399164720955107e8c2a1caf3fa11baf41
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@capacitor/keyboard@npm:^7.0.0":
|
||||
version: 7.0.5
|
||||
resolution: "@capacitor/keyboard@npm:7.0.5"
|
||||
peerDependencies:
|
||||
"@capacitor/core": ">=7.0.0"
|
||||
checksum: 10/c4588923e436c7ac0ddcba58437c7430d405e97da17a6c6e0a34e9d4a50f1be9f757ecacc748ae439b56de0c3b805d5d32819d010674385ac38002bfb3d58f55
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@capacitor/splash-screen@npm:^7.0.0":
|
||||
version: 7.0.5
|
||||
resolution: "@capacitor/splash-screen@npm:7.0.5"
|
||||
peerDependencies:
|
||||
"@capacitor/core": ">=7.0.0"
|
||||
checksum: 10/6b4668150c9d82a5a4819139f5440a0040bd656178519a68b5ae8ef89be17a3eebe10b062903f76a0ee492b342c1d8975d400c62e0d4ea6de45bbfcf1c99c771
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@capacitor/status-bar@npm:^7.0.0":
|
||||
version: 7.0.5
|
||||
resolution: "@capacitor/status-bar@npm:7.0.5"
|
||||
peerDependencies:
|
||||
"@capacitor/core": ">=7.0.0"
|
||||
checksum: 10/8a50eb8a4c14126469dde5604bb272c33472ee7a607e40a031f26adaff8de0ac72bb0a45575fe727084a9a5e87340dacb46f153e095c6cb147fbe76506bb98e4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@chevrotain/cst-dts-gen@npm:11.0.3":
|
||||
version: 11.0.3
|
||||
resolution: "@chevrotain/cst-dts-gen@npm:11.0.3"
|
||||
@@ -4630,6 +4726,106 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ionic/cli-framework-output@npm:^2.2.8":
|
||||
version: 2.2.8
|
||||
resolution: "@ionic/cli-framework-output@npm:2.2.8"
|
||||
dependencies:
|
||||
"@ionic/utils-terminal": "npm:2.3.5"
|
||||
debug: "npm:^4.0.0"
|
||||
tslib: "npm:^2.0.1"
|
||||
checksum: 10/773cf0cdbd0fbb5709822b84c06a9e9cec6df5a87d9542bcc21a027a9434117e1d3f1a7d04fe11f252d9d232a5678d056ea80d2d4506e38a5f18fc563a990f34
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ionic/utils-array@npm:2.1.6":
|
||||
version: 2.1.6
|
||||
resolution: "@ionic/utils-array@npm:2.1.6"
|
||||
dependencies:
|
||||
debug: "npm:^4.0.0"
|
||||
tslib: "npm:^2.0.1"
|
||||
checksum: 10/13d7b56906bb394a9362622c001578f0788f06ee2d8c724a51fb415cf4bdc1ccf1f92c2358935524f0089a660a4323d5f0bfa9403a0b3050ed921c039125b5e8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ionic/utils-fs@npm:3.1.7, @ionic/utils-fs@npm:^3.1.7":
|
||||
version: 3.1.7
|
||||
resolution: "@ionic/utils-fs@npm:3.1.7"
|
||||
dependencies:
|
||||
"@types/fs-extra": "npm:^8.0.0"
|
||||
debug: "npm:^4.0.0"
|
||||
fs-extra: "npm:^9.0.0"
|
||||
tslib: "npm:^2.0.1"
|
||||
checksum: 10/65279a445b4499b9db40dae0c4eface0fdcd6ee95c7931ecb89fb097ee07e61e316ad13da7f495ee567d95dd0dddcbad9b1e46656676e22e51041dde62b06b9d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ionic/utils-object@npm:2.1.6":
|
||||
version: 2.1.6
|
||||
resolution: "@ionic/utils-object@npm:2.1.6"
|
||||
dependencies:
|
||||
debug: "npm:^4.0.0"
|
||||
tslib: "npm:^2.0.1"
|
||||
checksum: 10/c545f09ede118a801c7eb4c794d4b479c574a98023b752421c0b18e340cb0f509aa7d7e92ef9b1048361e194a0b6f80df8922cd56bae8201d2fb796b71d87e60
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ionic/utils-process@npm:2.1.12":
|
||||
version: 2.1.12
|
||||
resolution: "@ionic/utils-process@npm:2.1.12"
|
||||
dependencies:
|
||||
"@ionic/utils-object": "npm:2.1.6"
|
||||
"@ionic/utils-terminal": "npm:2.3.5"
|
||||
debug: "npm:^4.0.0"
|
||||
signal-exit: "npm:^3.0.3"
|
||||
tree-kill: "npm:^1.2.2"
|
||||
tslib: "npm:^2.0.1"
|
||||
checksum: 10/3a94eeb4cc7c05a2d3058e997a2ca05c50bf1a9d76e39b91659d0cbb402adbe2b24b8bc08d8b95bbab1703328dcc50c2e333b345700709ed2ec94e8d844d20c1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ionic/utils-stream@npm:3.1.7":
|
||||
version: 3.1.7
|
||||
resolution: "@ionic/utils-stream@npm:3.1.7"
|
||||
dependencies:
|
||||
debug: "npm:^4.0.0"
|
||||
tslib: "npm:^2.0.1"
|
||||
checksum: 10/23667cfeaa4f710e556cd4f055ae6a99a59bf9f83ed7224fea5cf82d2e0539b8d42c4bf133db82f8a2156c004145fdb2e252837388b552c5775126700bce4a95
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ionic/utils-subprocess@npm:^3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "@ionic/utils-subprocess@npm:3.0.1"
|
||||
dependencies:
|
||||
"@ionic/utils-array": "npm:2.1.6"
|
||||
"@ionic/utils-fs": "npm:3.1.7"
|
||||
"@ionic/utils-process": "npm:2.1.12"
|
||||
"@ionic/utils-stream": "npm:3.1.7"
|
||||
"@ionic/utils-terminal": "npm:2.3.5"
|
||||
cross-spawn: "npm:^7.0.3"
|
||||
debug: "npm:^4.0.0"
|
||||
tslib: "npm:^2.0.1"
|
||||
checksum: 10/7411d691b3cdaa1ab74171a521813118a4b7180a373d6d80ee6b2b7946490486b3cb9f8ca338a9ffb4167c098d0c79ce14d8579ac82db794e5e1017b73bf85eb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ionic/utils-terminal@npm:2.3.5, @ionic/utils-terminal@npm:^2.3.4, @ionic/utils-terminal@npm:^2.3.5":
|
||||
version: 2.3.5
|
||||
resolution: "@ionic/utils-terminal@npm:2.3.5"
|
||||
dependencies:
|
||||
"@types/slice-ansi": "npm:^4.0.0"
|
||||
debug: "npm:^4.0.0"
|
||||
signal-exit: "npm:^3.0.3"
|
||||
slice-ansi: "npm:^4.0.0"
|
||||
string-width: "npm:^4.1.0"
|
||||
strip-ansi: "npm:^6.0.0"
|
||||
tslib: "npm:^2.0.1"
|
||||
untildify: "npm:^4.0.0"
|
||||
wrap-ansi: "npm:^7.0.0"
|
||||
checksum: 10/40bae30f8cff2d7efa74118d96b2ab16ac9048ba8ffe28a8c203cd398f2688955ddb28d5ebe0c6948890632dc1ca2fe5e84c547ce930cde3ac7be4d5f53e5245
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@isaacs/balanced-match@npm:^4.0.1":
|
||||
version: 4.0.1
|
||||
resolution: "@isaacs/balanced-match@npm:4.0.1"
|
||||
@@ -9911,6 +10107,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/fs-extra@npm:^8.0.0":
|
||||
version: 8.1.5
|
||||
resolution: "@types/fs-extra@npm:8.1.5"
|
||||
dependencies:
|
||||
"@types/node": "npm:*"
|
||||
checksum: 10/565d9e55cd05064b3ab272b8748ed512b8fa5cddc23fd32b0d5f147f9ea3a45981577c4478b5060cae7b3d914c508bd2ea97eb84d9c1fa6f967982c892e4ab26
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/geojson@npm:*":
|
||||
version: 7946.0.16
|
||||
resolution: "@types/geojson@npm:7946.0.16"
|
||||
@@ -10368,6 +10573,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/slice-ansi@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "@types/slice-ansi@npm:4.0.0"
|
||||
checksum: 10/343086bc4e686349382b386a6fb49d632fb4dd08ecc08f1b9872815199efe8db7648731e56c1d10dc0f596fe77a8fce6136e0067136f8e0cefda6b29797aa4f1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/sockjs@npm:^0.3.36":
|
||||
version: 0.3.36
|
||||
resolution: "@types/sockjs@npm:0.3.36"
|
||||
@@ -12270,6 +12482,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"big-integer@npm:1.6.x":
|
||||
version: 1.6.52
|
||||
resolution: "big-integer@npm:1.6.52"
|
||||
checksum: 10/4bc6ae152a96edc9f95020f5fc66b13d26a9ad9a021225a9f0213f7e3dc44269f423aa8c42e19d6ac4a63bb2b22140b95d10be8f9ca7a6d9aa1b22b330d1f514
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"big.js@npm:^5.2.2":
|
||||
version: 5.2.2
|
||||
resolution: "big.js@npm:5.2.2"
|
||||
@@ -12422,6 +12641,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bplist-parser@npm:^0.3.2":
|
||||
version: 0.3.2
|
||||
resolution: "bplist-parser@npm:0.3.2"
|
||||
dependencies:
|
||||
big-integer: "npm:1.6.x"
|
||||
checksum: 10/6edf4354c32f5661c258422e478be0f5c6a779bb87c2ae15ee92dd1c046368decbff8a28c86c558a3b7007e1381b91d5eed1c4c8e83e86405197777d944abaa8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"brace-expansion@npm:^1.1.7":
|
||||
version: 1.1.12
|
||||
resolution: "brace-expansion@npm:1.1.12"
|
||||
@@ -15582,6 +15810,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"elementtree@npm:^0.1.7":
|
||||
version: 0.1.7
|
||||
resolution: "elementtree@npm:0.1.7"
|
||||
dependencies:
|
||||
sax: "npm:1.1.4"
|
||||
checksum: 10/7e8f0683c0f5a95b298f56426f0f3796600ddac611cfaf92b76f1f4354b04c3f9aadadd1f86a91e5df372a58f2c3da97ae5988597f5052dd15bbeefbff8381c9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"elliptic@npm:^6.5.3, elliptic@npm:^6.6.1":
|
||||
version: 6.6.1
|
||||
resolution: "elliptic@npm:6.6.1"
|
||||
@@ -17629,6 +17866,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glob@npm:^13.0.3":
|
||||
version: 13.0.6
|
||||
resolution: "glob@npm:13.0.6"
|
||||
dependencies:
|
||||
minimatch: "npm:^10.2.2"
|
||||
minipass: "npm:^7.1.3"
|
||||
path-scurry: "npm:^2.0.2"
|
||||
checksum: 10/201ad69e5f0aa74e1d8c00a481581f8b8c804b6a4fbfabeeb8541f5d756932800331daeba99b58fb9e4cd67e12ba5a7eba5b82fb476691588418060b84353214
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glob@npm:^7.0.5, glob@npm:^7.1.3, glob@npm:^7.1.6":
|
||||
version: 7.2.3
|
||||
resolution: "glob@npm:7.2.3"
|
||||
@@ -18774,6 +19022,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ini@npm:^4.1.1":
|
||||
version: 4.1.3
|
||||
resolution: "ini@npm:4.1.3"
|
||||
checksum: 10/f536b414d1442e5b233429e2b56efcdb354109b2d65ddd489e5939d8f0f5ad23c88aa2b19c92987249d0dd63ba8192e9aeb1a02b0459549c5a9ff31acd729a5d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"inline-style-parser@npm:0.2.4":
|
||||
version: 0.2.4
|
||||
resolution: "inline-style-parser@npm:0.2.4"
|
||||
@@ -19954,6 +20209,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"kleur@npm:^4.1.5":
|
||||
version: 4.1.5
|
||||
resolution: "kleur@npm:4.1.5"
|
||||
checksum: 10/44d84cc4eedd4311099402ef6d4acd9b2d16e08e499d6ef3bb92389bd4692d7ef09e35248c26e27f98acac532122acb12a1bfee645994ae3af4f0a37996da7df
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"kolorist@npm:^1.8.0":
|
||||
version: 1.8.0
|
||||
resolution: "kolorist@npm:1.8.0"
|
||||
@@ -21892,6 +22154,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minipass@npm:^7.1.3":
|
||||
version: 7.1.3
|
||||
resolution: "minipass@npm:7.1.3"
|
||||
checksum: 10/175e4d5e20980c3cd316ae82d2c031c42f6c746467d8b1905b51060a0ba4461441a0c25bb67c025fd9617f9a3873e152c7b543c6b5ac83a1846be8ade80dffd6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minizlib@npm:^2.1.1":
|
||||
version: 2.1.2
|
||||
resolution: "minizlib@npm:2.1.2"
|
||||
@@ -22039,6 +22308,27 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"native-run@npm:^2.0.3":
|
||||
version: 2.0.3
|
||||
resolution: "native-run@npm:2.0.3"
|
||||
dependencies:
|
||||
"@ionic/utils-fs": "npm:^3.1.7"
|
||||
"@ionic/utils-terminal": "npm:^2.3.4"
|
||||
bplist-parser: "npm:^0.3.2"
|
||||
debug: "npm:^4.3.4"
|
||||
elementtree: "npm:^0.1.7"
|
||||
ini: "npm:^4.1.1"
|
||||
plist: "npm:^3.1.0"
|
||||
split2: "npm:^4.2.0"
|
||||
through2: "npm:^4.0.2"
|
||||
tslib: "npm:^2.6.2"
|
||||
yauzl: "npm:^2.10.0"
|
||||
bin:
|
||||
native-run: bin/native-run
|
||||
checksum: 10/f78268262d94f758d4d0ecb7a3ece3c4b38f57aefcc9d71d99e0f1d81c5ea9bf2dd6f459fab64652071f5743f3a80e92259590c54754be6f25a600d9b4ccd856
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"natural-compare@npm:^1.4.0":
|
||||
version: 1.4.0
|
||||
resolution: "natural-compare@npm:1.4.0"
|
||||
@@ -22945,7 +23235,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"package-json-from-dist@npm:^1.0.0":
|
||||
"package-json-from-dist@npm:^1.0.0, package-json-from-dist@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "package-json-from-dist@npm:1.0.1"
|
||||
checksum: 10/58ee9538f2f762988433da00e26acc788036914d57c71c246bf0be1b60cdbd77dd60b6a3e1a30465f0b248aeb80079e0b34cb6050b1dfa18c06953bb1cbc7602
|
||||
@@ -23198,6 +23488,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"path-scurry@npm:^2.0.2":
|
||||
version: 2.0.2
|
||||
resolution: "path-scurry@npm:2.0.2"
|
||||
dependencies:
|
||||
lru-cache: "npm:^11.0.0"
|
||||
minipass: "npm:^7.1.2"
|
||||
checksum: 10/2b4257422bcb870a4c2d205b3acdbb213a72f5e2250f61c80f79c9d014d010f82bdf8584441612c8e1fa4eb098678f5704a66fa8377d72646bad4be38e57a2c3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"path-to-regexp@npm:0.1.12":
|
||||
version: 0.1.12
|
||||
resolution: "path-to-regexp@npm:0.1.12"
|
||||
@@ -25359,6 +25659,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readable-stream@npm:3, readable-stream@npm:^3.0.6, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.5.0, readable-stream@npm:^3.6.0, readable-stream@npm:^3.6.2":
|
||||
version: 3.6.2
|
||||
resolution: "readable-stream@npm:3.6.2"
|
||||
dependencies:
|
||||
inherits: "npm:^2.0.3"
|
||||
string_decoder: "npm:^1.1.1"
|
||||
util-deprecate: "npm:^1.0.1"
|
||||
checksum: 10/d9e3e53193adcdb79d8f10f2a1f6989bd4389f5936c6f8b870e77570853561c362bee69feca2bbb7b32368ce96a85504aa4cedf7cf80f36e6a9de30d64244048
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readable-stream@npm:^2.0.1, readable-stream@npm:^2.3.8, readable-stream@npm:~2.3.6":
|
||||
version: 2.3.8
|
||||
resolution: "readable-stream@npm:2.3.8"
|
||||
@@ -25374,17 +25685,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readable-stream@npm:^3.0.6, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.5.0, readable-stream@npm:^3.6.0, readable-stream@npm:^3.6.2":
|
||||
version: 3.6.2
|
||||
resolution: "readable-stream@npm:3.6.2"
|
||||
dependencies:
|
||||
inherits: "npm:^2.0.3"
|
||||
string_decoder: "npm:^1.1.1"
|
||||
util-deprecate: "npm:^1.0.1"
|
||||
checksum: 10/d9e3e53193adcdb79d8f10f2a1f6989bd4389f5936c6f8b870e77570853561c362bee69feca2bbb7b32368ce96a85504aa4cedf7cf80f36e6a9de30d64244048
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readable-stream@npm:~1.0.31":
|
||||
version: 1.0.34
|
||||
resolution: "readable-stream@npm:1.0.34"
|
||||
@@ -26039,6 +26339,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rimraf@npm:^6.0.1":
|
||||
version: 6.1.3
|
||||
resolution: "rimraf@npm:6.1.3"
|
||||
dependencies:
|
||||
glob: "npm:^13.0.3"
|
||||
package-json-from-dist: "npm:^1.0.1"
|
||||
bin:
|
||||
rimraf: dist/esm/bin.mjs
|
||||
checksum: 10/dd98ec2ad7cd2cccae1c7110754d472eac8edb2bab8a8b057dce04edfe1433dab246a889b3fd85a66c78ca81caa1429caa0e736c7647f6832b04fd5d4dfb8ab8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ripemd160@npm:^2.0.0, ripemd160@npm:^2.0.1, ripemd160@npm:^2.0.3":
|
||||
version: 2.0.3
|
||||
resolution: "ripemd160@npm:2.0.3"
|
||||
@@ -26396,6 +26708,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"sax@npm:1.1.4":
|
||||
version: 1.1.4
|
||||
resolution: "sax@npm:1.1.4"
|
||||
checksum: 10/50dd85c562f6de00f1a6c4049f27471258b35c77961498784976445455f99d105bac0de2edf342730b6e0e410346581622c21494355178b6883611619757ae1b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"sax@npm:>=0.6.0, sax@npm:^1.2.4":
|
||||
version: 1.4.1
|
||||
resolution: "sax@npm:1.4.1"
|
||||
@@ -26986,6 +27305,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"slice-ansi@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "slice-ansi@npm:4.0.0"
|
||||
dependencies:
|
||||
ansi-styles: "npm:^4.0.0"
|
||||
astral-regex: "npm:^2.0.0"
|
||||
is-fullwidth-code-point: "npm:^3.0.0"
|
||||
checksum: 10/4a82d7f085b0e1b070e004941ada3c40d3818563ac44766cca4ceadd2080427d337554f9f99a13aaeb3b4a94d9964d9466c807b3d7b7541d1ec37ee32d308756
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"slice-ansi@npm:^7.1.0":
|
||||
version: 7.1.2
|
||||
resolution: "slice-ansi@npm:7.1.2"
|
||||
@@ -27206,6 +27536,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"split2@npm:^4.2.0":
|
||||
version: 4.2.0
|
||||
resolution: "split2@npm:4.2.0"
|
||||
checksum: 10/09bbefc11bcf03f044584c9764cd31a252d8e52cea29130950b26161287c11f519807c5e54bd9e5804c713b79c02cefe6a98f4688630993386be353e03f534ab
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"sprintf-js@npm:^1.1.1, sprintf-js@npm:^1.1.2":
|
||||
version: 1.1.3
|
||||
resolution: "sprintf-js@npm:1.1.3"
|
||||
@@ -27882,6 +28219,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tar@npm:^7.5.3":
|
||||
version: 7.5.12
|
||||
resolution: "tar@npm:7.5.12"
|
||||
dependencies:
|
||||
"@isaacs/fs-minipass": "npm:^4.0.0"
|
||||
chownr: "npm:^3.0.0"
|
||||
minipass: "npm:^7.1.2"
|
||||
minizlib: "npm:^3.1.0"
|
||||
yallist: "npm:^5.0.0"
|
||||
checksum: 10/a72114d28ab9b4878eeebaae8987692a577c390683c13f150d8330e139237038cc46fbb0be6983b02acf5a31b01d74776436ba03790f320a59efb44b8ac39e39
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"teex@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "teex@npm:1.0.1"
|
||||
@@ -28017,6 +28367,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"through2@npm:^4.0.2":
|
||||
version: 4.0.2
|
||||
resolution: "through2@npm:4.0.2"
|
||||
dependencies:
|
||||
readable-stream: "npm:3"
|
||||
checksum: 10/72c246233d9a989bbebeb6b698ef0b7b9064cb1c47930f79b25d87b6c867e075432811f69b7b2ac8da00ca308191c507bdab913944be8019ac43b036ce88f6ba
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"thunky@npm:^1.0.2":
|
||||
version: 1.1.0
|
||||
resolution: "thunky@npm:1.1.0"
|
||||
@@ -28258,6 +28617,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tree-kill@npm:^1.2.2":
|
||||
version: 1.2.2
|
||||
resolution: "tree-kill@npm:1.2.2"
|
||||
bin:
|
||||
tree-kill: cli.js
|
||||
checksum: 10/49117f5f410d19c84b0464d29afb9642c863bc5ba40fcb9a245d474c6d5cc64d1b177a6e6713129eb346b40aebb9d4631d967517f9fbe8251c35b21b13cd96c7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"trim-lines@npm:^3.0.0":
|
||||
version: 3.0.1
|
||||
resolution: "trim-lines@npm:3.0.1"
|
||||
@@ -28364,7 +28732,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.6.0, tslib@npm:^2.8.0, tslib@npm:^2.8.1":
|
||||
"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.6.0, tslib@npm:^2.6.2, tslib@npm:^2.8.0, tslib@npm:^2.8.1":
|
||||
version: 2.8.1
|
||||
resolution: "tslib@npm:2.8.1"
|
||||
checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7
|
||||
|
||||
Reference in New Issue
Block a user