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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,5 @@ runs:
- name: Install dependencies
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: |
yarn install --cwd example --frozen-lockfile
yarn install --frozen-lockfile --ignore-engines
yarn install --immutable
shell: bash
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
run: yarn lint

- name: Typecheck files
run: yarn typescript
run: yarn typecheck

test:
runs-on: macos-latest
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,5 @@ android/generated

# React Native Nitro Modules
nitrogen/

coverage/
30 changes: 15 additions & 15 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,27 @@
"web": "expo start --web"
},
"dependencies": {
"@expo/metro-runtime": "~6.1.2",
"@shopify/react-native-skia": "2.2.12",
"@expo/metro-runtime": "~55.0.6",
"@shopify/react-native-skia": "2.4.18",
"echarts": "^6.0.0",
"expo": "~54.0.10",
"expo-status-bar": "~3.0.8",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.4",
"react-native-gesture-handler": "~2.28.0",
"react-native-reanimated": "~4.1.0",
"react-native-svg": "15.12.1",
"react-native-web": "~0.21.0",
"react-native-worklets": "^0.5.1",
"expo": "~55.0.8",
"expo-status-bar": "~55.0.4",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-native": "0.83.2",
"react-native-gesture-handler": "~2.30.0",
"react-native-reanimated": "4.2.1",
"react-native-svg": "15.15.3",
"react-native-web": "~0.21.2",
"react-native-worklets": "0.7.2",
"zrender": "^6.0.0"
},
"private": true,
"devDependencies": {
"react-native-builder-bob": "^0.40.13",
"react-native-monorepo-config": "^0.1.9"
"react-native-builder-bob": "^0.40.18",
"react-native-monorepo-config": "^0.3.3"
},
"resolutions": {
"tslib": "^2.6.1"
"tslib": "^2.8.1"
}
}
8 changes: 3 additions & 5 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
module.exports = {
preset: 'react-native',
testEnvironment: '<rootDir>/jest.skia-env.js',
modulePathIgnorePatterns: [
'<rootDir>/example/node_modules',
'<rootDir>/lib/',
],
transformIgnorePatterns: [
'node_modules/(?!(react-native|react-native.*|@react-native.*|@?react-navigation.*|@shopify/react-native-skia|zrender|echarts)/)',
],
setupFiles: [
'@shopify/react-native-skia/jestSetup.js',
'react-native-gesture-handler/jestSetup.js',
'./jestSetup.js',
],
setupFiles: ['react-native-gesture-handler/jestSetup.js'],
setupFilesAfterEnv: ['./jest.skia-setup.js', './jestSetup.js'],
testTimeout: 10000,
};
14 changes: 14 additions & 0 deletions jest.skia-env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const { TestEnvironment } = require('jest-environment-node');
const CanvasKitInit = require('canvaskit-wasm/bin/full/canvaskit');

let canvasKitPromise;

module.exports = class SkiaEnvironment extends TestEnvironment {
async setup() {
await super.setup();
canvasKitPromise ??= CanvasKitInit({});
const canvasKit = await canvasKitPromise;
this.global.CanvasKit = canvasKit;
global.CanvasKit = canvasKit;
}
};
228 changes: 228 additions & 0 deletions jest.skia-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
/* global jest */

const mockCanvasKit = global.CanvasKit;

jest.mock('@shopify/react-native-skia', () => {
jest.mock('@shopify/react-native-skia/lib/commonjs/skia/NativeSetup', () => {
return {};
});

jest.mock('@shopify/react-native-skia/lib/commonjs/Platform', () => {
const Noop = () => undefined;
const Platform = {
OS: 'web',
PixelRatio: 1,
requireNativeComponent: Noop,
resolveAsset: Noop,
findNodeHandle: Noop,
NativeModules: Noop,
View: Noop,
};

return {
Platform,
};
});

jest.mock('@shopify/react-native-skia/lib/commonjs/skia/core/Font', () => {
return {
useFont: () => null,
matchFont: () => null,
listFontFamilies: () => [],
useFonts: () => null,
};
});

const mockReact = require('react');
const mockView = require('react-native').View;
const skiaMock = require('@shopify/react-native-skia/lib/commonjs/mock').Mock(
mockCanvasKit
);
const testParagraphFamilies = new Set();
const paragraphFontCandidates = [
{
path: '/System/Library/Fonts/Hiragino Sans GB.ttc',
families: [
'PingFang SC',
'Hiragino Sans GB',
'Helvetica Neue',
'sans-serif',
'System',
],
},
{
path: '/System/Library/Fonts/Supplemental/Arial.ttf',
families: ['Arial', 'Helvetica', 'Helvetica Neue', 'sans-serif'],
},
{
path: '/System/Library/Fonts/Helvetica.ttc',
families: ['Helvetica', 'Helvetica Neue', 'sans-serif'],
},
];
let cachedParagraphFontProvider = null;
let cachedParagraphTypeface = null;

const registerParagraphFamily = (family) => {
if (
!family ||
!cachedParagraphFontProvider ||
!cachedParagraphTypeface ||
testParagraphFamilies.has(family)
) {
return;
}

cachedParagraphFontProvider.registerFont(cachedParagraphTypeface, family);
testParagraphFamilies.add(family);
};
const ensureParagraphFontProvider = (families = []) => {
if (!cachedParagraphFontProvider) {
cachedParagraphFontProvider = skiaMock.Skia.TypefaceFontProvider.Make();

const fs = require('fs');
for (const candidate of paragraphFontCandidates) {
if (!fs.existsSync(candidate.path)) {
continue;
}

try {
const bytes = fs.readFileSync(candidate.path);
const data = skiaMock.Skia.Data.fromBytes(
new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength)
);
const typeface =
skiaMock.Skia.Typeface.MakeFreeTypeFaceFromData(data);

if (!typeface) {
continue;
}

cachedParagraphTypeface = typeface;
candidate.families.forEach(registerParagraphFamily);
break;
} catch {}
}
}

families.forEach(registerParagraphFamily);

return cachedParagraphFontProvider;
};
const originalParagraphBuilderMake = skiaMock.Skia.ParagraphBuilder.Make.bind(
skiaMock.Skia.ParagraphBuilder
);

skiaMock.Skia.ParagraphBuilder.Make = (paragraphStyle, typefaceProvider) => {
const provider = typefaceProvider ?? ensureParagraphFontProvider();
const builder = originalParagraphBuilderMake(paragraphStyle, provider);
const originalPushStyle = builder.pushStyle.bind(builder);

builder.pushStyle = (style, ...args) => {
if (!typefaceProvider && Array.isArray(style?.fontFamilies)) {
ensureParagraphFontProvider(style.fontFamilies);
}

return originalPushStyle(style, ...args);
};

return builder;
};

const sanitizeValue = (value, key) => {
if (
value == null ||
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean'
) {
return value;
}

if (typeof value === 'function') {
return '[Function]';
}

if (Array.isArray(value)) {
return value.map((item) => sanitizeValue(item, key));
}

if (mockReact.isValidElement(value)) {
return sanitizeElement(value);
}

if (key === 'image') {
return '[SkImage]';
}

if (key === 'paragraph') {
return '[SkParagraph]';
}

if (key === 'path') {
return '[SkPath]';
}

if (key === 'clip') {
return '[SkClip]';
}

const ctorName = value?.constructor?.name;
if (ctorName && ctorName !== 'Object') {
return `[${ctorName}]`;
}

return Object.fromEntries(
Object.entries(value).map(([nestedKey, nestedValue]) => [
nestedKey,
sanitizeValue(nestedValue, nestedKey),
])
);
};
const sanitizeElement = (element, fallbackKey) => {
if (!mockReact.isValidElement(element)) {
return sanitizeValue(element);
}

const { children, ...props } = element.props || {};
const sanitizedChildren = mockReact.Children.toArray(children).map(
(child, index) => sanitizeElement(child, `${fallbackKey}-child-${index}`)
);

return mockReact.createElement(
element.type,
{
key: element.key ?? fallbackKey,
...Object.fromEntries(
Object.entries(props).map(([key, value]) => [
key,
sanitizeValue(value, key),
])
),
},
...sanitizedChildren
);
};
const MockCanvas = mockReact.forwardRef(({ children, ...props }, ref) => {
const sanitizedChildren = mockReact.Children.toArray(children).map(
(child, index) => sanitizeElement(child, `canvas-child-${index}`)
);

if (sanitizedChildren.length === 0) {
return null;
}

return mockReact.createElement(
mockView,
{ ...props, ref },
...sanitizedChildren
);
});

MockCanvas.displayName = 'SkiaMockCanvas';

return {
...skiaMock,
...require('@shopify/react-native-skia/lib/commonjs/renderer/Offscreen'),
Canvas: MockCanvas,
};
});
Loading
Loading