diff --git a/.github/workflows/build-npm.yml b/.github/workflows/build-npm.yml index ed81f8ed43..25fd33d9aa 100644 --- a/.github/workflows/build-npm.yml +++ b/.github/workflows/build-npm.yml @@ -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.12 - name: Install root node dependencies run: yarn diff --git a/package.json b/package.json index 07de86c0d2..7741852483 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/package/.eslintrc b/package/.eslintrc index b8629a1d80..26cf69b443 100644 --- a/package/.eslintrc +++ b/package/.eslintrc @@ -4,6 +4,7 @@ "parserOptions": { "project": "./tsconfig.json" }, + "ignorePatterns": "build.ts", "rules": { "prefer-destructuring": [ "error", diff --git a/package/build.ts b/package/build.ts new file mode 100644 index 0000000000..e0a3f7c989 --- /dev/null +++ b/package/build.ts @@ -0,0 +1,110 @@ +// 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"; + +if (process.env.NODE_ENV !== "production") { + throw new Error("This script needs to be run with NODE_ENV=production."); +} + +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";' +); diff --git a/package/package.json b/package/package.json index 64058082d4..27f3e303ae 100644 --- a/package/package.json +++ b/package/package.json @@ -27,6 +27,8 @@ "android/src/**", "libs/android/**", "index.js", + "web.js", + "react-native-web.js", "jestSetup.js", "jestSetup.mjs", "jestEnv.mjs", @@ -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 && NODE_ENV=production bun build.ts", "release": "semantic-release" }, "repository": { diff --git a/package/react-native-web.js b/package/react-native-web.js new file mode 100644 index 0000000000..e4611fe041 --- /dev/null +++ b/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"); diff --git a/package/src/Platform/Platform.web.tsx b/package/src/Platform/Platform.web.tsx index 0177371a6b..41ad2a776b 100644 --- a/package/src/Platform/Platform.web.tsx +++ b/package/src/Platform/Platform.web.tsx @@ -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 @@ -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"); }, diff --git a/package/src/Platform/ResolveAssetWithNoDependency.tsx b/package/src/Platform/ResolveAssetWithNoDependency.tsx new file mode 100644 index 0000000000..7dc5e01037 --- /dev/null +++ b/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; +}; diff --git a/package/src/Platform/ResolveAssetWithRNDependency.tsx b/package/src/Platform/ResolveAssetWithRNDependency.tsx new file mode 100644 index 0000000000..15b13ead4c --- /dev/null +++ b/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; +}; diff --git a/package/src/external/reanimated/ReanimatedProxyPure.ts b/package/src/external/reanimated/ReanimatedProxyPure.ts new file mode 100644 index 0000000000..73d166c0fb --- /dev/null +++ b/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(() => { + throw new OptionalDependencyNotInstalledError("react-native-reanimated"); +}); + +// eslint-disable-next-line import/no-default-export +export default Reanimated; diff --git a/package/src/external/reanimated/reanimatedStatus.ts b/package/src/external/reanimated/reanimatedStatus.ts new file mode 100644 index 0000000000..1667ff522b --- /dev/null +++ b/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 }; +}; diff --git a/package/src/external/reanimated/reanimatedStatusPure.ts b/package/src/external/reanimated/reanimatedStatusPure.ts new file mode 100644 index 0000000000..7dcbd7c145 --- /dev/null +++ b/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 }; +}; diff --git a/package/src/external/reanimated/renderHelpers.ts b/package/src/external/reanimated/renderHelpers.ts index d9003a6a51..9920efdf71 100644 --- a/package/src/external/reanimated/renderHelpers.ts +++ b/package/src/external/reanimated/renderHelpers.ts @@ -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, unknown>(); diff --git a/package/src/skia/web/JsiSkDataFactory.ts b/package/src/skia/web/JsiSkDataFactory.ts index 662de472b1..6926e8c1bd 100644 --- a/package/src/skia/web/JsiSkDataFactory.ts +++ b/package/src/skia/web/JsiSkDataFactory.ts @@ -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. diff --git a/package/src/web/WithSkiaWeb.tsx b/package/src/web/WithSkiaWeb.tsx index 1a940481e4..3eb1bb385c 100644 --- a/package/src/web/WithSkiaWeb.tsx +++ b/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 { @@ -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( - " is only necessary on web. Consider not using on native." - ); - } + await LoadSkiaWeb(opts); return getComponent(); }), [getComponent, opts] diff --git a/package/src/web/for-bundling.ts b/package/src/web/for-bundling.ts new file mode 100644 index 0000000000..a2ad7e1cfc --- /dev/null +++ b/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"; diff --git a/package/tsconfig.json b/package/tsconfig.json index e4e4c1126d..ae60cba562 100644 --- a/package/tsconfig.json +++ b/package/tsconfig.json @@ -34,6 +34,7 @@ "metro.config.js", "jest.config.js", "lib", - "scripts" + "scripts", + "build.ts" ] } \ No newline at end of file diff --git a/package/web.js b/package/web.js new file mode 100644 index 0000000000..eb4abb286b --- /dev/null +++ b/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"); diff --git a/yarn.lock b/yarn.lock index 2d9f2a24eb..5970b9f373 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34,11 +34,39 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== +"@types/bun@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@types/bun/-/bun-1.1.0.tgz#227060a9b97a74213430f06ed1423aa943c75bca" + integrity sha512-QGK0yU4jh0OK1A7DyhPkQuKjHQCC5jSJa3dpWIEhHv/rPfb6zLfdArc4/uUUZBMTcjilsafRXnPWO+1owb572Q== + dependencies: + bun-types "1.1.0" + +"@types/node@*": + version "20.12.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.7.tgz#04080362fa3dd6c5822061aa3124f5c152cff384" + integrity sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg== + dependencies: + undici-types "~5.26.4" + "@types/node@16.11.7": version "16.11.7" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.7.tgz#36820945061326978c42a01e56b61cd223dfdc42" integrity sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw== +"@types/node@~20.11.3": + version "20.11.30" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.30.tgz#9c33467fc23167a347e73834f788f4b9f399d66f" + integrity sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw== + dependencies: + undici-types "~5.26.4" + +"@types/ws@~8.5.10": + version "8.5.10" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.10.tgz#4acfb517970853fa6574a3a6886791d04a396787" + integrity sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A== + dependencies: + "@types/node" "*" + acorn-walk@^8.1.1: version "8.3.2" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa" @@ -72,6 +100,14 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +bun-types@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/bun-types/-/bun-types-1.1.0.tgz#2a0bcabda1e7e6f7be94f622be5fc9e3bd279796" + integrity sha512-GhMDD7TosdJzQPGUOcQD5PZshvXVxDfwGAZs2dq+eSaPsRn3iUCzvpFlsg7Q51bXVzLAUs+FWHlnmpgZ5UggIg== + dependencies: + "@types/node" "~20.11.3" + "@types/ws" "~8.5.10" + clang-format@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/clang-format/-/clang-format-1.6.0.tgz#48fac4387712aeeae0f47b5d72f639f3fd95f4b6" @@ -218,6 +254,11 @@ typescript@5.3.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"