Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

@Shopify/react-native-skia/web #2394

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/build-npm.yml
Expand Up @@ -18,8 +18,11 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: "lts/*"
cache: 'yarn'
cache-dependency-path: 'package/yarn.lock'
cache: "yarn"
cache-dependency-path: "package/yarn.lock"
- uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.3

- name: Install root node dependencies
run: yarn
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -7,6 +7,7 @@
"example": "example"
},
"devDependencies": {
"@types/bun": "1.1.0",
"@types/node": "16.11.7",
"clang-format": "1.6.0",
"rimraf": "3.0.2",
Expand Down
1 change: 1 addition & 0 deletions package/.eslintrc
Expand Up @@ -4,6 +4,7 @@
"parserOptions": {
"project": "./tsconfig.json"
},
"ignorePatterns": "build.ts",
"rules": {
"prefer-destructuring": [
"error",
Expand Down
106 changes: 106 additions & 0 deletions package/build.ts
@@ -0,0 +1,106 @@
// Execute this file with `bun build.ts`

// Generates two files:
// `@Shopify/react-native-skia/web` -> `lib/web/pure.js`
// -> A version of RN Skia that has no React Native dependency
// and therefore no Webpack override is necessary
// `@Shopify/react-native-skia/react-native-web` -> lib/web/react-native-web.js
// -> A version of RN Skia for the web that has a React Native dependency
// that can be overriden with Webpack in order to support React Native-style assets

import { build, BunPlugin } from "bun";
import pathModule from "path";

export const bundleSkia = async (
noReactNativeDependency: boolean,
output: string
) => {
const reactNativePlugin: BunPlugin = {
name: "Resolve .web.ts first",
setup(build) {
build.onResolve(
{ filter: /.*/ },
async ({ importer, namespace, path }) => {
// If there should be no react-native dependency,
// override it with a no-dependency version
if (noReactNativeDependency) {
path = path.replace(
"ResolveAssetWithRNDependency",
"ResolveAssetWithNoDependency"
);
path = path.replace("ReanimatedProxy", "ReanimatedProxyPure");
path = path.replace("reanimatedStatus", "reanimatedStatusPure");
}
const resolved = pathModule.resolve(importer, "..", path);

// First resolve web.ts
const extensions = [".web.ts", ".web.tsx", ".ts", ".tsx"];
const resolvedWithExtensions = extensions.map(
(ext) => resolved + ext
);

// Override with .web.tsx if it exists
for (const resolvedWithExtension of resolvedWithExtensions) {
if (await Bun.file(resolvedWithExtension).exists()) {
return Promise.resolve({
namespace,
path: resolvedWithExtension,
});
}
}
return undefined;
}
);
},
};

const outputs = await build({
plugins: [reactNativePlugin],
entrypoints: ["./src/web/for-bundling.ts"],
// Don't bundle these dependencies
external: [
"react-native",
"canvaskit-wasm",
"react",
"scheduler",
"react-reconciler",
"react-native-reanimated",
],
});

if (!outputs.success) {
console.error(outputs.logs);
throw new Error("Build failed");
}
return outputs.outputs[0].text();
};

const PURE_VERSION = "lib/web/pure.js";
const REACT_NATIVE_WEB_VERSION = "lib/web/react-native-web.js";

// 1. Bundle a pure version with no React Native dependencies
const pureVersion = await bundleSkia(true, PURE_VERSION);
await Bun.write(PURE_VERSION, pureVersion);
// Test pure version: Should not import React Native dependencies
// or react-native-reanimated
if (
pureVersion.includes(`__require("react-native`) ||
pureVersion.includes(`from "react-native`)
) {
throw new Error("Pure version should not include React Native dependencies");
}

// 2. Bundle a version with React Native dependencies
const rnwVersion = await bundleSkia(false, REACT_NATIVE_WEB_VERSION);
// Test RNW version: Should import React Native dependencies
await Bun.write(REACT_NATIVE_WEB_VERSION, rnwVersion);

// 3. Map the types to the correct files
await Bun.write(
"lib/web/pure.d.ts",
'export * from "../typescript/src/web/for-bundling";'
);
await Bun.write(
"lib/web/react-native-web.d.ts",
'export * from "../typescript/src/web/for-bundling";'
);
4 changes: 3 additions & 1 deletion package/package.json
Expand Up @@ -27,6 +27,8 @@
"android/src/**",
"libs/android/**",
"index.js",
"web.js",
"react-native-web.js",
"jestSetup.js",
"jestSetup.mjs",
"jestEnv.mjs",
Expand All @@ -48,7 +50,7 @@
"lint": "eslint . --ext .ts,.tsx --max-warnings 0 --cache",
"test": "jest",
"e2e": "E2E=true yarn test -i e2e",
"build": "bob build && merge-dirs lib/typescript/src lib/commonjs && merge-dirs lib/typescript/src lib/module",
"build": "bob build && merge-dirs lib/typescript/src lib/commonjs && merge-dirs lib/typescript/src lib/module && bun build.ts",
"release": "semantic-release"
},
"repository": {
Expand Down
3 changes: 3 additions & 0 deletions package/react-native-web.js
@@ -0,0 +1,3 @@
// For maximum backwards compatibility, not using "exports" field
// and using a commonjs export
module.exports = require("./lib/web/react-native-web");
21 changes: 2 additions & 19 deletions package/src/Platform/Platform.web.tsx
Expand Up @@ -2,9 +2,7 @@ import type { RefObject, CSSProperties } from "react";
import React, { useLayoutEffect, useMemo, useRef } from "react";
import type { LayoutChangeEvent, ViewComponent, ViewProps } from "react-native";

import type { DataModule } from "../skia/types";
import { isRNModule } from "../skia/types";

import { resolveAsset } from "./ResolveAssetWithRNDependency";
import type { IPlatform } from "./IPlatform";

// eslint-disable-next-line max-len
Expand Down Expand Up @@ -127,22 +125,7 @@ const View = (({ children, onLayout, style: rawStyle }: ViewProps) => {
export const Platform: IPlatform = {
OS: "web",
PixelRatio: typeof window !== "undefined" ? window.devicePixelRatio : 1, // window is not defined on node
resolveAsset: (source: DataModule) => {
if (isRNModule(source)) {
if (typeof source === "number" && typeof require === "function") {
const {
getAssetByID,
} = require("react-native/Libraries/Image/AssetRegistry");
const { httpServerLocation, name, type } = getAssetByID(source);
const uri = `${httpServerLocation}/${name}.${type}`;
return uri;
}
throw new Error(
"Asset source is a number - this is not supported on the web"
);
}
return source.default;
},
resolveAsset: resolveAsset,
findNodeHandle: () => {
throw new Error("findNodeHandle is not supported on the web");
},
Expand Down
15 changes: 15 additions & 0 deletions package/src/Platform/ResolveAssetWithNoDependency.tsx
@@ -0,0 +1,15 @@
// In `package/build.ts`, this file will replace `ResolveAssetWithRNDependency.tsx`
// in order to remove React Native dependencies.

import type { DataModule } from "../skia";

import type { resolveAsset as original } from "./ResolveAssetWithRNDependency";

export const resolveAsset: typeof original = (source: DataModule) => {
if (typeof source === "number") {
throw new Error(
"Asset loading is not implemented in pure web - use React Native Web implementation"
);
}
return source.default;
};
22 changes: 22 additions & 0 deletions package/src/Platform/ResolveAssetWithRNDependency.tsx
@@ -0,0 +1,22 @@
// In `package/build.ts`, this file will be replaced with
// `ResolveAssetWithNoDependency.tsx` in order to remove React Native dependencies.

import type { DataModule } from "../skia/types";
import { isRNModule } from "../skia/types";

export const resolveAsset = (source: DataModule) => {
if (isRNModule(source)) {
if (typeof source === "number" && typeof require === "function") {
const {
getAssetByID,
} = require("react-native/Libraries/Image/AssetRegistry");
const { httpServerLocation, name, type } = getAssetByID(source);
const uri = `${httpServerLocation}/${name}.${type}`;
return uri;
}
throw new Error(
"Asset source is a number - this is not supported on the web"
);
}
return source.default;
};
16 changes: 16 additions & 0 deletions package/src/external/reanimated/ReanimatedProxyPure.ts
@@ -0,0 +1,16 @@
import type * as ReanimatedT from "react-native-reanimated";

import {
createModuleProxy,
OptionalDependencyNotInstalledError,
} from "../ModuleProxy";

import type original from "./ReanimatedProxy";
type TReanimated = typeof ReanimatedT;

const Reanimated: typeof original = createModuleProxy<TReanimated>(() => {
throw new OptionalDependencyNotInstalledError("react-native-reanimated");
});

// eslint-disable-next-line import/no-default-export
export default Reanimated;
20 changes: 20 additions & 0 deletions package/src/external/reanimated/reanimatedStatus.ts
@@ -0,0 +1,20 @@
export const getReanimatedStatus = () => {
let HAS_REANIMATED = false;
let HAS_REANIMATED_3 = false;
try {
require("react-native-reanimated");
HAS_REANIMATED = true;
const reanimatedVersion =
require("react-native-reanimated/package.json").version;
if (
reanimatedVersion &&
(reanimatedVersion >= "3.0.0" || reanimatedVersion.includes("3.0.0-"))
) {
HAS_REANIMATED_3 = true;
}
} catch (e) {
HAS_REANIMATED = false;
}

return { HAS_REANIMATED, HAS_REANIMATED_3 };
};
5 changes: 5 additions & 0 deletions package/src/external/reanimated/reanimatedStatusPure.ts
@@ -0,0 +1,5 @@
import type { getReanimatedStatus as original } from "./reanimatedStatus";

export const getReanimatedStatus: typeof original = () => {
return { HAS_REANIMATED: false, HAS_REANIMATED_3: false };
};
18 changes: 2 additions & 16 deletions package/src/external/reanimated/renderHelpers.ts
Expand Up @@ -5,23 +5,9 @@ import type { AnimatedProps } from "../../renderer/processors";
import type { Node } from "../../dom/types";

import Rea from "./ReanimatedProxy";
import { getReanimatedStatus } from "./reanimatedStatus";

let HAS_REANIMATED = false;
let HAS_REANIMATED_3 = false;
try {
require("react-native-reanimated");
HAS_REANIMATED = true;
const reanimatedVersion =
require("react-native-reanimated/package.json").version;
if (
reanimatedVersion &&
(reanimatedVersion >= "3.0.0" || reanimatedVersion.includes("3.0.0-"))
) {
HAS_REANIMATED_3 = true;
}
} catch (e) {
HAS_REANIMATED = false;
}
const { HAS_REANIMATED, HAS_REANIMATED_3 } = getReanimatedStatus();

const _bindings = new WeakMap<Node<unknown>, unknown>();

Expand Down
3 changes: 2 additions & 1 deletion package/src/skia/web/JsiSkDataFactory.ts
Expand Up @@ -21,7 +21,8 @@ export class JsiSkDataFactory extends Host implements DataFactory {
* @param bytes An array of bytes representing the data
*/
fromBytes(bytes: Uint8Array) {
return new JsiSkData(this.CanvasKit, bytes);
// FIXME: Bun type error, might be resolved with a newer version
return new JsiSkData(this.CanvasKit, bytes as unknown as ArrayBuffer);
}
/**
* Creates a new Data object from a base64 encoded string.
Expand Down
10 changes: 1 addition & 9 deletions package/src/web/WithSkiaWeb.tsx
@@ -1,8 +1,6 @@
import type { ComponentProps, ComponentType } from "react";
import React, { useMemo, lazy, Suspense } from "react";

import { Platform } from "../Platform";

import { LoadSkiaWeb } from "./LoadSkiaWeb";

interface WithSkiaProps {
Expand All @@ -21,13 +19,7 @@ export const WithSkiaWeb = ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(): any =>
lazy(async () => {
if (Platform.OS === "web") {
await LoadSkiaWeb(opts);
} else {
console.warn(
"<WithSkiaWeb /> is only necessary on web. Consider not using on native."
);
}
await LoadSkiaWeb(opts);
return getComponent();
}),
[getComponent, opts]
Expand Down
11 changes: 11 additions & 0 deletions package/src/web/for-bundling.ts
@@ -0,0 +1,11 @@
// This file can get bundled with package/build.ts
// and two versions will get generated:
// @Shopify/react-native-skia/web -> A Pure JS version with no webpack override necessary
// @Shopify/react-native-skia/react-native-web ->
// A React Native Web version
// that supports React Native assets but
// but needs a react-native-web Webpack override

export * from "./LoadSkiaWeb";
export * from "./WithSkiaWeb";
export * from "../index";
3 changes: 2 additions & 1 deletion package/tsconfig.json
Expand Up @@ -34,6 +34,7 @@
"metro.config.js",
"jest.config.js",
"lib",
"scripts"
"scripts",
"build.ts"
]
}
8 changes: 8 additions & 0 deletions package/web.js
@@ -0,0 +1,8 @@
// The entry point for `@Shopify/react-native-skia/web`
// A version of RN Skia that requires no Webpack override
// The `lib/web/pure` file gets generated at build time
// using `bun build.ts`

// For maximum backwards compatibility, not using "exports" field
// and using a commonjs export
module.exports = require("./lib/web/pure");