diff --git a/libs/with-zephyr/project.json b/libs/with-zephyr/project.json index 9009809a..7f99c615 100644 --- a/libs/with-zephyr/project.json +++ b/libs/with-zephyr/project.json @@ -9,6 +9,7 @@ "build": { "executor": "nx:run-commands", "outputs": ["{projectRoot}/dist"], + "dependsOn": ["^build"], "options": { "command": "pnpm build", "cwd": "libs/with-zephyr" diff --git a/libs/zephyr-agent/src/lib/deployment/aws-upload.strategy.ts b/libs/zephyr-agent/src/lib/deployment/aws-upload.strategy.ts index 31f3631d..f990c4e5 100644 --- a/libs/zephyr-agent/src/lib/deployment/aws-upload.strategy.ts +++ b/libs/zephyr-agent/src/lib/deployment/aws-upload.strategy.ts @@ -5,7 +5,7 @@ import { ZeErrors, ZephyrError } from '../errors'; import { getApplicationConfiguration } from '../edge-requests/get-application-configuration'; import { makeRequest } from '../http/http-request'; import { zeUploadSnapshot } from '../edge-actions'; -import {type UploadAssetsOptions, uploadBuildStatsAndEnableEnvs } from './upload-base'; +import { type UploadAssetsOptions, uploadBuildStatsAndEnableEnvs } from './upload-base'; import { update_hash_list } from '../edge-hash-list/distributed-hash-control'; import { white, whiteBright } from '../logging/picocolor'; import type { UploadFileProps } from '../http/upload-file'; @@ -15,7 +15,7 @@ const AWS_MAX_BODY_SIZE = 20971520; export async function awsUploadStrategy( zephyr_engine: ZephyrEngine, - { snapshot, getDashData, assets: { assetsMap, missingAssets } }: UploadOptions, + { snapshot, getDashData, assets: { assetsMap, missingAssets } }: UploadOptions ): Promise { const snapshotSize = Buffer.byteLength(JSON.stringify(snapshot), 'utf8'); if (snapshotSize > AWS_MAX_BODY_SIZE) { @@ -145,12 +145,13 @@ async function zeUploadAssets( return; } - const [ok, cause] = await makeRequest(result.url, + const [ok, cause] = await makeRequest( + result.url, { method: 'PUT', headers: { 'Content-Type': result.contentType, - } + }, }, asset.buffer ); @@ -174,7 +175,7 @@ async function zeUploadAssets( async function getUploadUrl( { hash, asset }: UploadFileProps, { EDGE_URL, jwt }: ZeApplicationConfig - ): Promise<{url: string; contentType: string; message?: string;}> { + ): Promise<{ url: string; contentType: string; message?: string }> { const type = 'uploadUrl'; const options: RequestInit = { method: 'POST', @@ -186,7 +187,7 @@ async function zeUploadAssets( }, }; - const [ok, cause, data] = await makeRequest<{url: string; contentType: string;}>( + const [ok, cause, data] = await makeRequest<{ url: string; contentType: string }>( { path: '/upload', base: EDGE_URL, diff --git a/libs/zephyr-agent/src/lib/node-persist/storage-keys.ts b/libs/zephyr-agent/src/lib/node-persist/storage-keys.ts index cccbc781..a461e095 100644 --- a/libs/zephyr-agent/src/lib/node-persist/storage-keys.ts +++ b/libs/zephyr-agent/src/lib/node-persist/storage-keys.ts @@ -3,15 +3,17 @@ import * as os from 'node:os'; import * as path from 'node:path'; export const ZE_PATH = path.resolve(os.homedir(), '.zephyr'); +export const ZE_PERSIST_PATH = path.resolve(ZE_PATH, 'persist'); export const ZE_SESSION_LOCK = path.resolve(ZE_PATH, 'session'); try { - // Ensures that the directory exists and lockfile is writable + // Ensures that the directories exist and lockfile is writable fs.mkdirSync(ZE_PATH, { recursive: true }); + fs.mkdirSync(ZE_PERSIST_PATH, { recursive: true }); } catch (error) { console.error( 'error', - `Could not create ~/.zephyr directory. Please check your permissions: ${error}` + `Could not create ~/.zephyr directories. Please check your permissions: ${error}` ); } diff --git a/libs/zephyr-agent/src/lib/node-persist/storage.ts b/libs/zephyr-agent/src/lib/node-persist/storage.ts index 38a7f22e..6c0892c6 100644 --- a/libs/zephyr-agent/src/lib/node-persist/storage.ts +++ b/libs/zephyr-agent/src/lib/node-persist/storage.ts @@ -1,10 +1,7 @@ import { init } from 'node-persist'; -import { ZE_PATH } from './storage-keys'; +import { ZE_PERSIST_PATH } from './storage-keys'; /** @internal */ export const storage = init({ - dir: ZE_PATH, - // node-persist thinks every file in .zephyr folder is a JSON valid file, - // since we use that folder for other purposes too, we need to set this to true - forgiveParseErrors: true, + dir: ZE_PERSIST_PATH, }); diff --git a/libs/zephyr-agent/src/tests/ze-agent.spec.ts b/libs/zephyr-agent/src/tests/ze-agent.spec.ts index 7badcc06..fd7bfffc 100644 --- a/libs/zephyr-agent/src/tests/ze-agent.spec.ts +++ b/libs/zephyr-agent/src/tests/ze-agent.spec.ts @@ -59,7 +59,7 @@ runner('ZeAgent', () => { if (fs.existsSync(zephyrAppFolder)) { const files = fs.readdirSync(zephyrAppFolder); files.forEach((file) => { - fs.rmSync(path.join(zephyrAppFolder, file)); + fs.rmSync(path.join(zephyrAppFolder, file), { recursive: true }); }); } await exec(`git config --add user.name "${gitUserName}"`); diff --git a/libs/zephyr-agent/src/zephyr-engine/index.ts b/libs/zephyr-agent/src/zephyr-engine/index.ts index e961ee52..d6ef1731 100644 --- a/libs/zephyr-agent/src/zephyr-engine/index.ts +++ b/libs/zephyr-agent/src/zephyr-engine/index.ts @@ -80,6 +80,7 @@ type ZephyrEngineBuilderTypes = | 'webpack' | 'rspack' | 'repack' + | 'metro' | 'vite' | 'rollup' | 'parcel' diff --git a/libs/zephyr-edge-contract/package.json b/libs/zephyr-edge-contract/package.json index 6f70d767..3b183f53 100644 --- a/libs/zephyr-edge-contract/package.json +++ b/libs/zephyr-edge-contract/package.json @@ -13,6 +13,14 @@ "url": "https://github.com/ZephyrCloudIO" }, "type": "commonjs", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.js", + "default": "./dist/index.js" + } + }, "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { diff --git a/libs/zephyr-edge-contract/src/index.ts b/libs/zephyr-edge-contract/src/index.ts index c897c89c..abed3879 100644 --- a/libs/zephyr-edge-contract/src/index.ts +++ b/libs/zephyr-edge-contract/src/index.ts @@ -21,7 +21,11 @@ export type { ZeApplicationList } from './lib/ze-api/app-list'; export type { ZeAppVersion, ZeAppVersionResponse } from './lib/ze-api/app-version'; export type { ConvertedGraph } from './lib/ze-api/converted-graph'; export type { LocalPackageJson } from './lib/ze-api/local-package-json'; -export type { ZephyrBuildStats, ZephyrDependency } from './lib/zephyr-build-stats'; +export type { + ApplicationConsumes, + ZephyrBuildStats, + ZephyrDependency, +} from './lib/zephyr-build-stats'; export type { Asset, SnapshotUploadRes, diff --git a/libs/zephyr-metro-plugin/.eslintrc.json b/libs/zephyr-metro-plugin/.eslintrc.json new file mode 100644 index 00000000..6c5a11b7 --- /dev/null +++ b/libs/zephyr-metro-plugin/.eslintrc.json @@ -0,0 +1,35 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx"], + "parserOptions": { + "project": ["libs/zephyr-metro-plugin/tsconfig.spec.json"] + }, + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": { + "@nx/enforce-module-boundaries": [ + "error", + { + "enforceBuildableLibDependency": false, + "allow": ["^zephyr-agent$"], + "depConstraints": [ + { + "sourceTag": "*", + "onlyDependOnLibsWithTags": ["*"] + } + ] + } + ] + } + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/zephyr-metro-plugin/.npmignore b/libs/zephyr-metro-plugin/.npmignore new file mode 100644 index 00000000..20b764de --- /dev/null +++ b/libs/zephyr-metro-plugin/.npmignore @@ -0,0 +1,5 @@ +src +tsconfig.* +jest.config.ts +project.json +.eslintrc.json diff --git a/libs/zephyr-metro-plugin/LICENSE b/libs/zephyr-metro-plugin/LICENSE new file mode 100644 index 00000000..bf5b2bdc --- /dev/null +++ b/libs/zephyr-metro-plugin/LICENSE @@ -0,0 +1,39 @@ + Apache License + Version 2.0, January 2004 +http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + ... + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same line as the copyright notice for each file. The "copyright" + word should be left as is (without quotes). + + Copyright [2023] [Zephyr Cloud] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/libs/zephyr-metro-plugin/README.md b/libs/zephyr-metro-plugin/README.md new file mode 100644 index 00000000..d02ebe83 --- /dev/null +++ b/libs/zephyr-metro-plugin/README.md @@ -0,0 +1,330 @@ +# Zephyr Metro Plugin + +
+ +[Zephyr Cloud](https://zephyr-cloud.io) | [Zephyr Docs](https://docs.zephyr-cloud.io/bundlers/metro) | [Discord](https://zephyr-cloud.io/discord) | [Twitter](https://x.com/ZephyrCloudIO) | [LinkedIn](https://www.linkedin.com/company/zephyr-cloud/) + +
+Zephyr Logo +
+ +A React Native Metro bundler plugin for deploying cross-platform applications with Zephyr Cloud. This plugin integrates seamlessly with Metro bundler and React Native to enable Over-The-Air (OTA) updates, Module Federation, and seamless deployment to Zephyr Cloud. + +## Get Started + +For setup instructions, see [TESTING.md](./TESTING.md) or refer to our [documentation](https://docs.zephyr-cloud.io/bundlers/metro) for Metro and comprehensive guides for React Native integration. + +## Installation + +Installing the `zephyr-metro-plugin` for your React Native application: + +```bash +# npm +npm install --save-dev zephyr-metro-plugin + +# yarn +yarn add --dev zephyr-metro-plugin + +# pnpm +pnpm add --dev zephyr-metro-plugin + +# bun +bun add --dev zephyr-metro-plugin +``` + +## Usage + +### Basic Configuration + +The Metro plugin provides two main integration points depending on your setup: + +#### 1. Using `withZephyr` Config Wrapper + +For Metro configuration file integration: + +```javascript +// metro.config.js +const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); +const { withZephyr } = require('zephyr-metro-plugin'); + +const baseConfig = getDefaultConfig(__dirname); + +module.exports = (async () => { + const zephyrConfig = await withZephyr({ + name: 'MyApp', + target: 'ios', // or 'android' + remotes: { + SharedComponents: 'SharedComponents@http://localhost:9000/remoteEntry.js', + }, + })(baseConfig); + + return mergeConfig(baseConfig, zephyrConfig); +})(); +``` + +#### 2. Using Command Wrapper + +For CLI-level integration with custom bundling commands: + +```javascript +const { zephyrCommandWrapper } = require('zephyr-metro-plugin'); + +// Wrap your Metro bundling function +const wrappedBundleCommand = zephyrCommandWrapper(originalBundleFunction, loadMetroConfig, updateManifest); +``` + +### Module Federation Configuration + +The plugin works with Metro's Module Federation setup. Configure your federated modules. + +> **Note:** Module Federation `exposes` configuration requires [@callstack/repack](https://re-pack.dev/) or a similar Metro Module Federation solution. This plugin handles the Zephyr Cloud deployment and OTA update aspects, while Re.Pack provides the underlying Module Federation runtime for React Native. + +#### Host Application Example + +```javascript +// metro.config.js +const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); +const { withZephyr } = require('zephyr-metro-plugin'); + +const baseConfig = getDefaultConfig(__dirname); + +module.exports = (async () => { + const zephyrConfig = await withZephyr({ + name: 'MobileHost', + target: 'ios', // or 'android' + remotes: { + MobileCart: 'MobileCart@http://localhost:9000/ios/MobileCart.bundle', + MobileCheckout: 'MobileCheckout@http://localhost:9001/ios/MobileCheckout.bundle', + }, + })(baseConfig); + + return mergeConfig(baseConfig, zephyrConfig); +})(); +``` + +#### Remote/Mini-App Example + +```javascript +// metro.config.js +const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); +const { withZephyr } = require('zephyr-metro-plugin'); + +const baseConfig = getDefaultConfig(__dirname); + +module.exports = (async () => { + const zephyrConfig = await withZephyr({ + name: 'MobileCart', + target: 'ios', + })(baseConfig); + + return mergeConfig(baseConfig, zephyrConfig); +})(); +``` + +### Platform-Specific Configuration + +The plugin supports both iOS and Android platforms: + +```javascript +// metro.config.js +const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); +const { withZephyr } = require('zephyr-metro-plugin'); + +const baseConfig = getDefaultConfig(__dirname); +const platform = process.env.PLATFORM || 'ios'; + +module.exports = (async () => { + const zephyrConfig = await withZephyr({ + name: 'MyApp', + target: platform, + })(baseConfig); + + return mergeConfig(baseConfig, zephyrConfig); +})(); +``` + +### Build Scripts + +Add these scripts to your `package.json`: + +```json +{ + "scripts": { + "ios": "react-native run-ios", + "android": "react-native run-android", + "build:ios": "PLATFORM=ios NODE_ENV=production react-native bundle", + "build:android": "PLATFORM=android NODE_ENV=production react-native bundle" + } +} +``` + +## Features + +- 📱 **Cross-platform React Native support** - iOS and Android +- 🚀 **Over-The-Air (OTA) updates** - Deploy updates without app store releases +- 🏗️ **Module Federation support** - Share code between micro-frontends +- ⚡ **Metro bundler integration** - Works seamlessly with Metro's fast refresh +- 🔧 **Zero-config setup** - Minimal configuration required +- 📊 **Build analytics and monitoring** - Track deployments and performance +- 🌐 **Global CDN distribution** - Fast asset delivery worldwide +- 🔄 **Hot Module Replacement** - Development workflow with fast refresh + +## Over-The-Air (OTA) Updates + +For detailed information about implementing OTA updates with the Metro plugin, see [OTA_SETUP.md](./OTA_SETUP.md). + +Key OTA features: + +- Automatic update checks +- Silent background updates +- Rollback capabilities +- Version management +- Platform-specific updates + +## Project Structure + +Your React Native Metro project should follow this structure: + +``` +my-react-native-app/ +├── android/ +├── ios/ +├── src/ +│ ├── components/ +│ ├── screens/ +│ └── App.tsx +├── metro.config.js +├── package.json +└── index.js +``` + +## Requirements + +- **React Native**: 0.70 or higher +- **Metro**: 0.80 or higher +- **Node.js**: 18 or higher +- **Zephyr Cloud account**: Sign up at [zephyr-cloud.io](https://zephyr-cloud.io) + +## Advanced Configuration + +### Custom Manifest Path + +```javascript +// metro.config.js +const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); +const { withZephyr } = require('zephyr-metro-plugin'); + +const baseConfig = getDefaultConfig(__dirname); + +module.exports = (async () => { + const zephyrConfig = await withZephyr({ + name: 'MyApp', + target: 'ios', + manifestPath: '/custom-manifest.json', // Custom manifest endpoint + })(baseConfig); + + return mergeConfig(baseConfig, zephyrConfig); +})(); +``` + +## Troubleshooting + +### Missing Metro Federation Config + +If you see `ERR_MISSING_METRO_FEDERATION_CONFIG`, ensure your Module Federation config is properly set up: + +```javascript +// Set global config before bundling +global.__METRO_FEDERATION_CONFIG = mfConfig; +``` + +### Platform-Specific Issues + +Make sure to specify the correct platform when building: + +```bash +# iOS +PLATFORM=ios react-native bundle + +# Android +PLATFORM=android react-native bundle +``` + +### Asset Loading Issues + +Ensure your Metro config is correctly set up: + +```javascript +module.exports = (async () => { + const zephyrConfig = await withZephyr({ + name: 'MyApp', + target: platform, + })(baseConfig); + + return mergeConfig(baseConfig, zephyrConfig); +})(); +``` + +## Examples + +Check out our [examples directory](../../examples/) for complete working examples: + +- [metro-react-native](../../examples/metro-react-native/) - Complete React Native setup with Zephyr + +## API Reference + +### `withZephyr(options)` + +Main configuration wrapper for Metro config. + +**Options (`ZephyrMetroOptions`):** + +| Option | Type | Description | +| --------------------- | ------------------------ | ---------------------------------------------------------------- | +| `name` | `string` | Application name (optional) | +| `target` | `'ios' \| 'android'` | Target platform (optional) | +| `remotes` | `Record` | Remote module configurations (optional) | +| `manifestPath` | `string` | Custom manifest endpoint path (default: `/zephyr-manifest.json`) | +| `entryFiles` | `string[]` | Custom entry file patterns for runtime injection (optional) | +| `failOnManifestError` | `boolean` | Throw error if manifest generation fails (default: `false`) | + +**Returns:** Async function that takes Metro `ConfigT` and returns enhanced config + +**Example:** + +```javascript +const enhancedConfig = await withZephyr({ + name: 'MyApp', + target: 'ios', + remotes: { + SharedUI: 'SharedUI@http://localhost:8081/remoteEntry.js', + }, +})(baseMetroConfig); +``` + +### `zephyrCommandWrapper(bundleFn, loadConfigFn, updateManifestFn)` + +Advanced CLI-level integration wrapper for custom bundling commands. + +**Parameters:** + +- `bundleFn` (function): Original Metro bundle function +- `loadConfigFn` (function): Metro config loader function +- `updateManifestFn` (function): Manifest update function + +**Returns:** Wrapped bundling function with Zephyr integration + +## Contributing + +We welcome contributions! Please read our [contributing guidelines](../../CONTRIBUTING.md) for more information. + +## License + +Licensed under the Apache-2.0 License. See [LICENSE](LICENSE) for more information. + +## Support + +- **Documentation**: [docs.zephyr-cloud.io](https://docs.zephyr-cloud.io) +- **Discord**: [Join our community](https://zephyr-cloud.io/discord) +- **GitHub Issues**: [Report bugs](https://github.com/ZephyrCloudIO/zephyr-packages/issues) +- **Twitter**: [@ZephyrCloudIO](https://x.com/ZephyrCloudIO) diff --git a/libs/zephyr-metro-plugin/TESTING.md b/libs/zephyr-metro-plugin/TESTING.md new file mode 100644 index 00000000..b480e340 --- /dev/null +++ b/libs/zephyr-metro-plugin/TESTING.md @@ -0,0 +1,153 @@ +# Testing Zephyr Metro Plugin + +Step-by-step guide to test the plugin with a Host + Remote setup. + +## Prerequisites + +- Node.js 18+ +- Xcode (iOS) or Android Studio (Android) +- Zephyr account: `npx zephyr login` + +## 1. Build the Plugin + +```bash +pnpm nx build zephyr-metro-plugin +``` + +## 2. Create Host App + +```bash +npx @react-native-community/cli init MetroHost +cd MetroHost +``` + +Install dependencies: + +```bash +npm install zephyr-metro-plugin @module-federation/runtime +# Or link local plugin: +npm install file:../path/to/zephyr-packages/libs/zephyr-metro-plugin +``` + +Update `metro.config.js`: + +```javascript +const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); +const { withZephyr } = require('zephyr-metro-plugin'); + +const baseConfig = getDefaultConfig(__dirname); + +module.exports = (async () => { + const zephyrConfig = await withZephyr({ + name: 'MetroHost', + target: 'ios', + remotes: { + MetroRemote: 'MetroRemote@http://localhost:9001/remoteEntry.js', + }, + })(baseConfig); + + return mergeConfig(baseConfig, zephyrConfig); +})(); +``` + +## 3. Create Remote App + +```bash +npx @react-native-community/cli init MetroRemote +cd MetroRemote +``` + +Install dependencies: + +```bash +npm install zephyr-metro-plugin @module-federation/runtime +``` + +Update `metro.config.js`: + +```javascript +const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); +const { withZephyr } = require('zephyr-metro-plugin'); + +const baseConfig = getDefaultConfig(__dirname); + +module.exports = (async () => { + const zephyrConfig = await withZephyr({ + name: 'MetroRemote', + target: 'ios', + })(baseConfig); + + return mergeConfig(baseConfig, zephyrConfig); +})(); +``` + +## 4. Start Remote + +```bash +cd MetroRemote +npx react-native start --port 9001 +``` + +## 5. Start Host + +```bash +cd MetroHost +npx react-native start +``` + +## 6. Verify Setup + +Check manifest endpoint: + +```bash +curl http://localhost:8081/zephyr-manifest.json +``` + +Should return JSON with remote dependencies. + +Run on simulator: + +```bash +npx react-native run-ios +``` + +## 7. Deploy to Zephyr + +Build and deploy: + +```bash +npx react-native bundle \ + --platform ios \ + --dev false \ + --entry-file index.js \ + --bundle-output dist/ios/main.bundle \ + --assets-dest dist/ios +``` + +Check for: + +- `assets/zephyr-manifest.json` generated +- Zephyr upload logs in console +- Deployment URL displayed + +## Verify Zephyr Integration + +| Check | Expected | +| --------------------------------------------------------- | ----------------------------- | +| Console shows `ZEPHYR` logs | Plugin initialized | +| `http://localhost:8081/zephyr-manifest.json` returns JSON | Manifest endpoint works | +| `assets/zephyr-manifest.json` exists after build | Production manifest generated | +| Build logs show upload progress | Zephyr deployment working | + +## Troubleshooting + +```bash +# Clear Metro cache +npx react-native start --reset-cache + +# Reset Zephyr state +rm -rf ~/.zephyr + +# Re-login to Zephyr +npx zephyr login +``` diff --git a/libs/zephyr-metro-plugin/jest.config.ts b/libs/zephyr-metro-plugin/jest.config.ts new file mode 100644 index 00000000..8ca192ad --- /dev/null +++ b/libs/zephyr-metro-plugin/jest.config.ts @@ -0,0 +1,10 @@ +export default { + displayName: 'zephyr-metro-plugin', + preset: '../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../coverage/libs/zephyr-metro-plugin', +}; diff --git a/libs/zephyr-metro-plugin/package.json b/libs/zephyr-metro-plugin/package.json new file mode 100644 index 00000000..9ddc931d --- /dev/null +++ b/libs/zephyr-metro-plugin/package.json @@ -0,0 +1,68 @@ +{ + "name": "zephyr-metro-plugin", + "version": "0.0.1", + "description": "Metro bundler plugin for deploying React Native applications with Zephyr Cloud - OTA updates, Module Federation, and seamless deployment", + "type": "commonjs", + "license": "Apache-2.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "require": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "author": { + "name": "ZephyrCloudIO", + "url": "https://github.com/ZephyrCloudIO" + }, + "peerDependencies": { + "metro": ">=0.70.0", + "react-native": ">=0.60.0", + "zephyr-xpack-internal": "workspace:*" + }, + "peerDependenciesMeta": { + "zephyr-xpack-internal": { + "optional": true + } + }, + "dependencies": { + "zephyr-agent": "workspace:*", + "zephyr-edge-contract": "workspace:*" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ZephyrCloudIO/zephyr-packages.git", + "directory": "libs/zephyr-metro-plugin" + }, + "devDependencies": { + "@rspack/core": "catalog:rspack", + "@types/find-package-json": "^1.2.6", + "@types/jest": "catalog:typescript", + "@typescript-eslint/eslint-plugin": "catalog:eslint", + "metro-config": "^0.83.3", + "metro-transform-worker": "^0.83.3", + "ts-jest": "catalog:typescript" + }, + "keywords": [ + "metro", + "react-native", + "zephyr", + "zephyr-cloud", + "module-federation", + "ota", + "over-the-air", + "deployment", + "bundler", + "ios", + "android", + "cross-platform", + "micro-frontends", + "plugin" + ], + "publishConfig": { + "access": "public", + "provenance": true + } +} diff --git a/libs/zephyr-metro-plugin/project.json b/libs/zephyr-metro-plugin/project.json new file mode 100644 index 00000000..03ac2f88 --- /dev/null +++ b/libs/zephyr-metro-plugin/project.json @@ -0,0 +1,42 @@ +{ + "name": "zephyr-metro-plugin", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/zephyr-metro-plugin/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "rootDir": "libs/zephyr-metro-plugin/src", + "outputPath": "libs/zephyr-metro-plugin/dist", + "main": "libs/zephyr-metro-plugin/src/index.ts", + "tsConfig": "libs/zephyr-metro-plugin/tsconfig.lib.json", + "assets": ["libs/zephyr-metro-plugin/*.md"] + } + }, + "nx-release-publish": { + "options": { + "packageRoot": "dist/{projectRoot}" + } + }, + "publish": { + "command": "node tools/scripts/publish.mjs zephyr-metro-plugin {args.ver} {args.tag}", + "dependsOn": ["build"] + }, + "release": { + "command": "pnpm dist-tag add zephyr-metro-plugin@$(npm view zephyr-metro-plugin@next version) latest" + }, + "lint": { + "executor": "@nx/eslint:lint" + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/zephyr-metro-plugin/jest.config.ts" + } + } + } +} diff --git a/libs/zephyr-metro-plugin/src/index.ts b/libs/zephyr-metro-plugin/src/index.ts new file mode 100644 index 00000000..9960b904 --- /dev/null +++ b/libs/zephyr-metro-plugin/src/index.ts @@ -0,0 +1,12 @@ +/* istanbul ignore file */ + +// Metro plugin exports +export { + withZephyr, + withZephyrMetro, + type ZephyrMetroOptions, + type ZephyrModuleFederationConfig, +} from './lib/with-zephyr'; + +// Transformer (usually not imported directly but referenced by path) +export { transform as zephyrTransformer } from './lib/zephyr-transformer'; diff --git a/libs/zephyr-metro-plugin/src/lib/__test__/with-zephyr.integration.spec.ts b/libs/zephyr-metro-plugin/src/lib/__test__/with-zephyr.integration.spec.ts new file mode 100644 index 00000000..adaf5a51 --- /dev/null +++ b/libs/zephyr-metro-plugin/src/lib/__test__/with-zephyr.integration.spec.ts @@ -0,0 +1,301 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/** Integration tests for withZephyr Metro configuration */ + +// Mock zephyr-agent - must be before imports +jest.mock('zephyr-agent', () => { + const mockEngine = { + env: { target: 'ios' as const }, + resolve_remote_dependencies: jest.fn().mockResolvedValue([ + { + name: 'RemoteApp', + version: 'latest', + resolved_url: 'http://cdn.example.com/remote.js', + }, + ]), + }; + + return { + ze_log: { + config: jest.fn(), + app: jest.fn(), + error: jest.fn(), + manifest: jest.fn(), + }, + ZephyrEngine: { + create: jest.fn().mockResolvedValue(mockEngine), + }, + ZephyrError: { + format: jest.fn().mockImplementation((err) => String(err)), + }, + ZeErrors: { + ERR_UNKNOWN: 'ERR_UNKNOWN', + }, + createManifestContent: jest + .fn() + .mockReturnValue(JSON.stringify({ version: '1.0.0' })), + }; +}); + +// Mock fs for manifest generation +jest.mock('fs', () => ({ + existsSync: jest.fn().mockReturnValue(true), + mkdirSync: jest.fn(), + promises: { + writeFile: jest.fn().mockResolvedValue(undefined), + }, +})); + +import { withZephyr, withZephyrMetro } from '../with-zephyr'; + +describe('withZephyr integration', () => { + // Sample Metro config + const baseMetroConfig: any = { + projectRoot: '/project', + transformer: { + babelTransformerPath: 'metro-react-native-babel-transformer', + }, + resolver: { + resolverMainFields: ['react-native', 'browser', 'main'], + }, + server: {}, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('config transformation', () => { + it('should return enhanced Metro config', async () => { + const enhancer = withZephyr({ name: 'TestApp' }); + const result = await enhancer(baseMetroConfig); + + expect(result).toBeDefined(); + expect(result.transformer).toBeDefined(); + expect(result.resolver).toBeDefined(); + expect(result.server).toBeDefined(); + }); + + it('should set custom transformer path', async () => { + const enhancer = withZephyr({ name: 'TestApp' }); + const result = await enhancer(baseMetroConfig); + + // The transformer path should be set to the zephyr-transformer module + expect(result.transformer?.babelTransformerPath).toBeDefined(); + expect(typeof result.transformer?.babelTransformerPath).toBe('string'); + }); + + it('should add zephyr to resolver main fields', async () => { + const enhancer = withZephyr({ name: 'TestApp' }); + const result = await enhancer(baseMetroConfig); + + expect(result.resolver?.resolverMainFields).toContain('zephyr'); + }); + + it('should preserve existing resolver main fields', async () => { + const enhancer = withZephyr({ name: 'TestApp' }); + const result = await enhancer(baseMetroConfig); + + expect(result.resolver?.resolverMainFields).toContain('react-native'); + expect(result.resolver?.resolverMainFields).toContain('browser'); + expect(result.resolver?.resolverMainFields).toContain('main'); + }); + + it('should add server middleware enhancement', async () => { + const enhancer = withZephyr({ name: 'TestApp' }); + const result = await enhancer(baseMetroConfig); + + expect(result.server?.enhanceMiddleware).toBeDefined(); + expect(typeof result.server?.enhanceMiddleware).toBe('function'); + }); + }); + + describe('manifest endpoint middleware', () => { + it('should serve manifest at default path', async () => { + const enhancer = withZephyr({ name: 'TestApp' }); + const result = await enhancer(baseMetroConfig); + + const mockReq = { url: '/zephyr-manifest.json' }; + const mockRes = { + setHeader: jest.fn(), + end: jest.fn(), + }; + const mockNext = jest.fn(); + const mockMiddleware = jest.fn(); + + result.server?.enhanceMiddleware?.(mockMiddleware, {})(mockReq, mockRes, mockNext); + + expect(mockRes.setHeader).toHaveBeenCalledWith('Content-Type', 'application/json'); + expect(mockRes.end).toHaveBeenCalled(); + expect(mockNext).not.toHaveBeenCalled(); + }); + + it('should serve manifest at custom path', async () => { + const enhancer = withZephyr({ + name: 'TestApp', + manifestPath: '/custom-manifest.json', + }); + const result = await enhancer(baseMetroConfig); + + const mockReq = { url: '/custom-manifest.json' }; + const mockRes = { + setHeader: jest.fn(), + end: jest.fn(), + }; + const mockNext = jest.fn(); + const mockMiddleware = jest.fn(); + + result.server?.enhanceMiddleware?.(mockMiddleware, {})(mockReq, mockRes, mockNext); + + expect(mockRes.end).toHaveBeenCalled(); + expect(mockNext).not.toHaveBeenCalled(); + }); + + it('should pass through non-manifest requests', async () => { + const enhancer = withZephyr({ name: 'TestApp' }); + const result = await enhancer(baseMetroConfig); + + const mockReq = { url: '/some-other-endpoint' }; + const mockRes = { + setHeader: jest.fn(), + end: jest.fn(), + }; + const mockNext = jest.fn(); + const mockMiddleware = jest.fn(); + + result.server?.enhanceMiddleware?.(mockMiddleware, {})(mockReq, mockRes, mockNext); + + expect(mockMiddleware).toHaveBeenCalledWith(mockReq, mockRes, mockNext); + }); + + it('should handle query strings in manifest URL', async () => { + const enhancer = withZephyr({ name: 'TestApp' }); + const result = await enhancer(baseMetroConfig); + + const mockReq = { url: '/zephyr-manifest.json?cacheBust=123' }; + const mockRes = { + setHeader: jest.fn(), + end: jest.fn(), + }; + const mockNext = jest.fn(); + const mockMiddleware = jest.fn(); + + result.server?.enhanceMiddleware?.(mockMiddleware, {})(mockReq, mockRes, mockNext); + + expect(mockRes.end).toHaveBeenCalled(); + }); + + it('should set no-cache header for manifest', async () => { + const enhancer = withZephyr({ name: 'TestApp' }); + const result = await enhancer(baseMetroConfig); + + const mockReq = { url: '/zephyr-manifest.json' }; + const mockRes = { + setHeader: jest.fn(), + end: jest.fn(), + }; + const mockNext = jest.fn(); + const mockMiddleware = jest.fn(); + + result.server?.enhanceMiddleware?.(mockMiddleware, {})(mockReq, mockRes, mockNext); + + expect(mockRes.setHeader).toHaveBeenCalledWith('Cache-Control', 'no-cache'); + }); + }); + + describe('transformer options', () => { + it('should pass manifest path to transformer', async () => { + const enhancer = withZephyr({ + name: 'TestApp', + manifestPath: '/custom-path.json', + }); + const result = await enhancer(baseMetroConfig); + + expect((result.transformer as any).zephyrTransformerOptions?.manifestPath).toBe( + '/custom-path.json' + ); + }); + + it('should pass entry files to transformer', async () => { + const enhancer = withZephyr({ + name: 'TestApp', + entryFiles: ['main.tsx', 'App.tsx'], + }); + const result = await enhancer(baseMetroConfig); + + expect((result.transformer as any).zephyrTransformerOptions?.entryFiles).toEqual([ + 'main.tsx', + 'App.tsx', + ]); + }); + }); + + describe('error handling', () => { + it('should return original config on ZephyrEngine.create error', async () => { + const { ZephyrEngine, ze_log } = require('zephyr-agent'); + ZephyrEngine.create.mockRejectedValueOnce(new Error('Engine init failed')); + + const enhancer = withZephyr({ name: 'TestApp' }); + const result = await enhancer(baseMetroConfig); + + expect(result).toEqual(baseMetroConfig); + expect(ze_log.error).toHaveBeenCalled(); + }); + }); + + describe('legacy export', () => { + it('should export withZephyrMetro as alias', () => { + expect(withZephyrMetro).toBe(withZephyr); + }); + }); + + describe('ZephyrEngine initialization', () => { + it('should create engine with metro builder', async () => { + const { ZephyrEngine } = require('zephyr-agent'); + + const enhancer = withZephyr({ name: 'TestApp' }); + await enhancer(baseMetroConfig); + + expect(ZephyrEngine.create).toHaveBeenCalledWith({ + builder: 'metro', + context: '/project', + }); + }); + + it('should use process.cwd when projectRoot not specified', async () => { + const { ZephyrEngine } = require('zephyr-agent'); + + const configWithoutRoot = { ...baseMetroConfig, projectRoot: undefined }; + const enhancer = withZephyr({ name: 'TestApp' }); + await enhancer(configWithoutRoot); + + expect(ZephyrEngine.create).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.any(String), + }) + ); + }); + }); + + describe('middleware chaining', () => { + it('should chain with existing enhanceMiddleware', async () => { + const existingMiddleware = jest.fn().mockReturnValue(jest.fn()); + const configWithMiddleware = { + ...baseMetroConfig, + server: { + enhanceMiddleware: existingMiddleware, + }, + }; + + const enhancer = withZephyr({ name: 'TestApp' }); + const result = await enhancer(configWithMiddleware); + + const mockReq = { url: '/other-path' }; + const mockRes = {}; + const mockNext = jest.fn(); + + result.server?.enhanceMiddleware?.(jest.fn(), {})(mockReq, mockRes, mockNext); + + expect(existingMiddleware).toHaveBeenCalled(); + }); + }); +}); diff --git a/libs/zephyr-metro-plugin/src/lib/__test__/zephyr-metro-command-wrapper.spec.ts b/libs/zephyr-metro-plugin/src/lib/__test__/zephyr-metro-command-wrapper.spec.ts new file mode 100644 index 00000000..5034a78b --- /dev/null +++ b/libs/zephyr-metro-plugin/src/lib/__test__/zephyr-metro-command-wrapper.spec.ts @@ -0,0 +1,350 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/** Unit tests for zephyrCommandWrapper */ + +// Mock zephyr-agent - must be before imports +jest.mock('zephyr-agent', () => ({ + ZephyrError: jest.fn().mockImplementation((error, options) => { + const err = new Error(options?.message || error); + (err as any).code = error; + return err; + }), + ZeErrors: { + ERR_UNKNOWN: 'ERR_UNKNOWN', + ERR_INVALID_MF_CONFIG: 'ERR_INVALID_MF_CONFIG', + }, +})); + +// Mock functions stored at module level for access in tests +let mockBeforeBuild: jest.Mock; +let mockAfterBuild: jest.Mock; + +// Mock ZephyrMetroPlugin +jest.mock('../zephyr-metro-plugin', () => { + mockBeforeBuild = jest.fn().mockResolvedValue({ name: 'TestApp' }); + mockAfterBuild = jest.fn().mockResolvedValue(undefined); + return { + ZephyrMetroPlugin: jest.fn().mockImplementation(() => ({ + beforeBuild: mockBeforeBuild, + afterBuild: mockAfterBuild, + })), + }; +}); + +// Mock internal errors +jest.mock('../internal/metro-errors', () => ({ + ERR_MISSING_METRO_FEDERATION_CONFIG: 'ERR_INVALID_MF_CONFIG', +})); + +import { zephyrCommandWrapper } from '../zephyr-metro-command-wrapper'; + +describe('zephyrCommandWrapper', () => { + // Mock functions + const mockBundleFederatedRemote = jest.fn().mockResolvedValue({ success: true }); + const mockLoadMetroConfig = jest.fn().mockResolvedValue({}); + const mockUpdateManifest = jest.fn(); + + // Sample args + const createMockArgs = (overrides: any = {}): any => [ + [{ mode: overrides.mode ?? 'production', platform: overrides.platform ?? 'ios' }], + { root: overrides.root ?? '/project', ...overrides.configOptions }, + { maxWorkers: 4, resetCache: false, config: 'metro.config.js' }, + ]; + + beforeEach(() => { + jest.clearAllMocks(); + // Reset global + (global as any).__METRO_FEDERATION_CONFIG = { + name: 'TestApp', + remotes: {}, + }; + }); + + afterEach(() => { + delete (global as any).__METRO_FEDERATION_CONFIG; + }); + + describe('wrapper creation', () => { + it('should return an async function', async () => { + const wrapper = await zephyrCommandWrapper( + mockBundleFederatedRemote, + mockLoadMetroConfig, + mockUpdateManifest + ); + + expect(typeof wrapper).toBe('function'); + }); + }); + + describe('execution flow', () => { + it('should load metro config with correct options', async () => { + const wrapper = await zephyrCommandWrapper( + mockBundleFederatedRemote, + mockLoadMetroConfig, + mockUpdateManifest + ); + + const args = createMockArgs(); + await wrapper(...args); + + expect(mockLoadMetroConfig).toHaveBeenCalledWith(args[1], { + maxWorkers: 4, + resetCache: false, + config: 'metro.config.js', + }); + }); + + it('should call beforeBuild on plugin', async () => { + const wrapper = await zephyrCommandWrapper( + mockBundleFederatedRemote, + mockLoadMetroConfig, + mockUpdateManifest + ); + + await wrapper(...createMockArgs()); + + expect(mockBeforeBuild).toHaveBeenCalled(); + }); + + it('should update manifest after beforeBuild', async () => { + const wrapper = await zephyrCommandWrapper( + mockBundleFederatedRemote, + mockLoadMetroConfig, + mockUpdateManifest + ); + + await wrapper(...createMockArgs()); + + expect(mockUpdateManifest).toHaveBeenCalled(); + }); + + it('should call original bundle function', async () => { + const wrapper = await zephyrCommandWrapper( + mockBundleFederatedRemote, + mockLoadMetroConfig, + mockUpdateManifest + ); + + const args = createMockArgs(); + await wrapper(...args); + + expect(mockBundleFederatedRemote).toHaveBeenCalledWith(...args); + }); + + it('should call afterBuild on plugin', async () => { + const wrapper = await zephyrCommandWrapper( + mockBundleFederatedRemote, + mockLoadMetroConfig, + mockUpdateManifest + ); + + await wrapper(...createMockArgs()); + + expect(mockAfterBuild).toHaveBeenCalled(); + }); + + it('should return result from bundle function', async () => { + mockBundleFederatedRemote.mockResolvedValue({ bundled: true, files: ['main.js'] }); + + const wrapper = await zephyrCommandWrapper( + mockBundleFederatedRemote, + mockLoadMetroConfig, + mockUpdateManifest + ); + + const result = await wrapper(...createMockArgs()); + + expect(result).toEqual({ bundled: true, files: ['main.js'] }); + }); + }); + + describe('platform handling', () => { + it('should pass iOS platform to plugin', async () => { + const { ZephyrMetroPlugin } = require('../zephyr-metro-plugin'); + + const wrapper = await zephyrCommandWrapper( + mockBundleFederatedRemote, + mockLoadMetroConfig, + mockUpdateManifest + ); + + await wrapper(...createMockArgs({ platform: 'ios' })); + + expect(ZephyrMetroPlugin).toHaveBeenCalledWith( + expect.objectContaining({ + platform: 'ios', + }) + ); + }); + + it('should pass Android platform to plugin', async () => { + const { ZephyrMetroPlugin } = require('../zephyr-metro-plugin'); + + const wrapper = await zephyrCommandWrapper( + mockBundleFederatedRemote, + mockLoadMetroConfig, + mockUpdateManifest + ); + + await wrapper(...createMockArgs({ platform: 'android' })); + + expect(ZephyrMetroPlugin).toHaveBeenCalledWith( + expect.objectContaining({ + platform: 'android', + }) + ); + }); + }); + + describe('mode handling', () => { + it('should set development mode when mode is truthy', async () => { + const { ZephyrMetroPlugin } = require('../zephyr-metro-plugin'); + + const wrapper = await zephyrCommandWrapper( + mockBundleFederatedRemote, + mockLoadMetroConfig, + mockUpdateManifest + ); + + await wrapper(...createMockArgs({ mode: 'development' })); + + expect(ZephyrMetroPlugin).toHaveBeenCalledWith( + expect.objectContaining({ + mode: 'development', + }) + ); + }); + + it('should set production mode when mode is falsy', async () => { + const { ZephyrMetroPlugin } = require('../zephyr-metro-plugin'); + + const wrapper = await zephyrCommandWrapper( + mockBundleFederatedRemote, + mockLoadMetroConfig, + mockUpdateManifest + ); + + await wrapper(...createMockArgs({ mode: '' })); + + expect(ZephyrMetroPlugin).toHaveBeenCalledWith( + expect.objectContaining({ + mode: 'production', + }) + ); + }); + }); + + describe('error handling', () => { + it('should throw ZephyrError when federation config is missing', async () => { + delete (global as any).__METRO_FEDERATION_CONFIG; + + const wrapper = await zephyrCommandWrapper( + mockBundleFederatedRemote, + mockLoadMetroConfig, + mockUpdateManifest + ); + + await expect(wrapper(...createMockArgs())).rejects.toThrow(); + }); + + it('should throw ZephyrError when bundle function fails', async () => { + mockBundleFederatedRemote.mockRejectedValue(new Error('Bundle failed')); + + const wrapper = await zephyrCommandWrapper( + mockBundleFederatedRemote, + mockLoadMetroConfig, + mockUpdateManifest + ); + + await expect(wrapper(...createMockArgs())).rejects.toThrow(); + }); + + it('should throw ZephyrError when beforeBuild fails', async () => { + mockBeforeBuild.mockRejectedValue(new Error('beforeBuild failed')); + + const wrapper = await zephyrCommandWrapper( + mockBundleFederatedRemote, + mockLoadMetroConfig, + mockUpdateManifest + ); + + await expect(wrapper(...createMockArgs())).rejects.toThrow(); + }); + + it('should throw ZephyrError when afterBuild fails', async () => { + mockAfterBuild.mockRejectedValue(new Error('afterBuild failed')); + + const wrapper = await zephyrCommandWrapper( + mockBundleFederatedRemote, + mockLoadMetroConfig, + mockUpdateManifest + ); + + await expect(wrapper(...createMockArgs())).rejects.toThrow(); + }); + }); + + describe('execution order', () => { + it('should execute hooks in correct order', async () => { + const executionOrder: string[] = []; + + mockLoadMetroConfig.mockImplementation(async () => { + executionOrder.push('loadConfig'); + return {}; + }); + + mockBeforeBuild.mockImplementation(async () => { + executionOrder.push('beforeBuild'); + return { name: 'TestApp' }; + }); + + mockUpdateManifest.mockImplementation(() => { + executionOrder.push('updateManifest'); + }); + + mockBundleFederatedRemote.mockImplementation(async () => { + executionOrder.push('bundle'); + return { success: true }; + }); + + mockAfterBuild.mockImplementation(async () => { + executionOrder.push('afterBuild'); + }); + + const wrapper = await zephyrCommandWrapper( + mockBundleFederatedRemote, + mockLoadMetroConfig, + mockUpdateManifest + ); + + await wrapper(...createMockArgs()); + + expect(executionOrder).toEqual([ + 'loadConfig', + 'beforeBuild', + 'updateManifest', + 'bundle', + 'afterBuild', + ]); + }); + }); + + describe('context handling', () => { + it('should use root from config as context', async () => { + const { ZephyrMetroPlugin } = require('../zephyr-metro-plugin'); + + const wrapper = await zephyrCommandWrapper( + mockBundleFederatedRemote, + mockLoadMetroConfig, + mockUpdateManifest + ); + + await wrapper(...createMockArgs({ root: '/custom/project/path' })); + + expect(ZephyrMetroPlugin).toHaveBeenCalledWith( + expect.objectContaining({ + context: '/custom/project/path', + }) + ); + }); + }); +}); diff --git a/libs/zephyr-metro-plugin/src/lib/__test__/zephyr-metro-plugin.spec.ts b/libs/zephyr-metro-plugin/src/lib/__test__/zephyr-metro-plugin.spec.ts new file mode 100644 index 00000000..c8939b00 --- /dev/null +++ b/libs/zephyr-metro-plugin/src/lib/__test__/zephyr-metro-plugin.spec.ts @@ -0,0 +1,308 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/** Unit tests for ZephyrMetroPlugin class */ + +// Mock zephyr-agent - must be before imports +jest.mock('zephyr-agent', () => ({ + ze_log: { + config: jest.fn(), + app: jest.fn(), + error: jest.fn(), + manifest: jest.fn(), + }, + ZephyrEngine: { + create: jest.fn().mockResolvedValue({ + env: { target: 'ios' as const }, + applicationProperties: { name: 'TestApp' }, + application_uid: 'test-app-uid', + npmProperties: { + dependencies: { react: '^18.0.0' }, + devDependencies: { typescript: '^5.0.0' }, + optionalDependencies: {}, + peerDependencies: {}, + }, + resolve_remote_dependencies: jest.fn().mockResolvedValue([ + { + name: 'RemoteApp', + version: 'latest', + resolved_url: 'http://cdn.example.com/remote.js', + }, + ]), + start_new_build: jest.fn().mockResolvedValue(undefined), + upload_assets: jest.fn().mockResolvedValue(undefined), + build_finished: jest.fn().mockResolvedValue(undefined), + }), + }, + buildAssetsMap: jest.fn().mockReturnValue({}), +})); + +// Mock internal dependencies +jest.mock('../internal/extract-mf-remotes', () => ({ + extract_remotes_dependencies: jest + .fn() + .mockReturnValue([{ name: 'RemoteApp', version: 'latest' }]), +})); + +jest.mock('../internal/mutate-mf-config', () => ({ + mutateMfConfig: jest.fn(), +})); + +jest.mock('../internal/metro-build-stats', () => ({ + createMinimalBuildStats: jest.fn().mockResolvedValue({ + id: 'test-build-id', + timestamp: Date.now(), + }), + resolveCatalogDependencies: jest.fn().mockReturnValue({}), +})); + +jest.mock('../internal/extract-modules-from-exposes', () => ({ + extractModulesFromExposes: jest.fn().mockReturnValue([]), +})); + +jest.mock('../internal/get-package-dependencies', () => ({ + getPackageDependencies: jest.fn().mockReturnValue([]), +})); + +jest.mock('../internal/parse-shared-dependencies', () => ({ + parseSharedDependencies: jest.fn().mockReturnValue({}), +})); + +jest.mock('../internal/load-static-entries', () => ({ + load_static_entries: jest.fn().mockResolvedValue([]), +})); + +import { ZephyrMetroPlugin } from '../zephyr-metro-plugin'; + +describe('ZephyrMetroPlugin', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('constructor', () => { + it('should create instance with valid config', () => { + const plugin = new ZephyrMetroPlugin({ + platform: 'ios', + mode: 'development', + context: '/project', + outDir: 'dist', + mfConfig: { + name: 'TestApp', + remotes: {}, + }, + }); + + expect(plugin).toBeInstanceOf(ZephyrMetroPlugin); + }); + + it('should accept android platform', () => { + const plugin = new ZephyrMetroPlugin({ + platform: 'android', + mode: 'production', + context: '/project', + outDir: 'dist', + mfConfig: undefined, + }); + + expect(plugin).toBeInstanceOf(ZephyrMetroPlugin); + }); + }); + + describe('beforeBuild', () => { + it('should initialize ZephyrEngine with metro builder', async () => { + const { ZephyrEngine } = require('zephyr-agent'); + + const plugin = new ZephyrMetroPlugin({ + platform: 'ios', + mode: 'development', + context: '/project', + outDir: 'dist', + mfConfig: { + name: 'TestApp', + remotes: { RemoteApp: 'http://localhost:9000/remote.js' }, + }, + }); + + await plugin.beforeBuild(); + + expect(ZephyrEngine.create).toHaveBeenCalledWith({ + builder: 'metro', + context: '/project', + }); + }); + + it('should resolve remote dependencies', async () => { + const { ZephyrEngine } = require('zephyr-agent'); + + const plugin = new ZephyrMetroPlugin({ + platform: 'ios', + mode: 'development', + context: '/project', + outDir: 'dist', + mfConfig: { + name: 'TestApp', + remotes: { RemoteApp: 'http://localhost:9000/remote.js' }, + }, + }); + + await plugin.beforeBuild(); + + // Engine should have been created + expect(ZephyrEngine.create).toHaveBeenCalled(); + }); + + it('should mutate MF config when provided', async () => { + const { mutateMfConfig } = require('../internal/mutate-mf-config'); + + const mfConfig = { + name: 'TestApp', + remotes: { RemoteApp: 'http://localhost:9000/remote.js' }, + }; + + const plugin = new ZephyrMetroPlugin({ + platform: 'ios', + mode: 'development', + context: '/project', + outDir: 'dist', + mfConfig, + }); + + await plugin.beforeBuild(); + + expect(mutateMfConfig).toHaveBeenCalled(); + }); + + it('should not mutate MF config when not provided', async () => { + const { mutateMfConfig } = require('../internal/mutate-mf-config'); + + const plugin = new ZephyrMetroPlugin({ + platform: 'ios', + mode: 'development', + context: '/project', + outDir: 'dist', + mfConfig: undefined, + }); + + await plugin.beforeBuild(); + + expect(mutateMfConfig).not.toHaveBeenCalled(); + }); + + it('should return mfConfig from beforeBuild', async () => { + const mfConfig = { + name: 'TestApp', + remotes: {}, + }; + + const plugin = new ZephyrMetroPlugin({ + platform: 'ios', + mode: 'development', + context: '/project', + outDir: 'dist', + mfConfig, + }); + + const result = await plugin.beforeBuild(); + + expect(result).toBe(mfConfig); + }); + + it('should log configuration', async () => { + const { ze_log } = require('zephyr-agent'); + + const plugin = new ZephyrMetroPlugin({ + platform: 'ios', + mode: 'development', + context: '/project', + outDir: 'dist', + mfConfig: undefined, + }); + + await plugin.beforeBuild(); + + expect(ze_log.config).toHaveBeenCalled(); + }); + }); + + describe('afterBuild', () => { + it('should complete build lifecycle', async () => { + const { ZephyrEngine } = require('zephyr-agent'); + + const plugin = new ZephyrMetroPlugin({ + platform: 'ios', + mode: 'production', + context: '/project', + outDir: 'dist', + mfConfig: { + name: 'TestApp', + remotes: {}, + }, + }); + + await plugin.beforeBuild(); + await plugin.afterBuild(); + + // The plugin should have called engine methods + expect(ZephyrEngine.create).toHaveBeenCalled(); + }); + }); + + describe('mode handling', () => { + it('should accept development mode', () => { + const plugin = new ZephyrMetroPlugin({ + platform: 'ios', + mode: 'development', + context: '/project', + outDir: 'dist', + mfConfig: undefined, + }); + + expect(plugin).toBeInstanceOf(ZephyrMetroPlugin); + }); + + it('should accept production mode', () => { + const plugin = new ZephyrMetroPlugin({ + platform: 'ios', + mode: 'production', + context: '/project', + outDir: 'dist', + mfConfig: undefined, + }); + + expect(plugin).toBeInstanceOf(ZephyrMetroPlugin); + }); + }); + + describe('context handling', () => { + it('should use provided context path', async () => { + const { ZephyrEngine } = require('zephyr-agent'); + + const plugin = new ZephyrMetroPlugin({ + platform: 'ios', + mode: 'development', + context: '/custom/project/path', + outDir: 'dist', + mfConfig: undefined, + }); + + await plugin.beforeBuild(); + + expect(ZephyrEngine.create).toHaveBeenCalledWith({ + builder: 'metro', + context: '/custom/project/path', + }); + }); + }); + + describe('outDir handling', () => { + it('should accept custom outDir', () => { + const plugin = new ZephyrMetroPlugin({ + platform: 'ios', + mode: 'production', + context: '/project', + outDir: 'build/output', + mfConfig: undefined, + }); + + expect(plugin).toBeInstanceOf(ZephyrMetroPlugin); + }); + }); +}); diff --git a/libs/zephyr-metro-plugin/src/lib/global.d.ts b/libs/zephyr-metro-plugin/src/lib/global.d.ts new file mode 100644 index 00000000..ad5b4d1c --- /dev/null +++ b/libs/zephyr-metro-plugin/src/lib/global.d.ts @@ -0,0 +1,125 @@ +/** + * Global type declarations for Zephyr Metro Plugin runtime. + * + * These globals are intentionally added to enable runtime plugin functionality in React + * Native applications. The runtime plugin is injected into entry files and uses these + * global variables to maintain state across the application. + * + * @see zephyr-transformer.ts for injection logic + * @see zephyr-xpack-internal for runtime plugin implementation + */ + +/* eslint-disable no-var */ + +/** Zephyr manifest structure for runtime updates */ +export interface ZephyrRuntimeManifest { + /** Semantic version of the manifest format */ + version: string; + /** Unix timestamp when manifest was generated */ + timestamp: number; + /** Map of remote module names to their resolved URLs */ + dependencies?: Record; + /** Additional manifest metadata */ + [key: string]: unknown; +} + +/** Callback type for manifest change notifications */ +export type ZephyrManifestChangeCallback = ( + newManifest: ZephyrRuntimeManifest, + oldManifest: ZephyrRuntimeManifest | null +) => void; + +/** + * Configuration options for creating the Zephyr runtime plugin. Passed to + * createZephyrRuntimePlugin from zephyr-xpack-internal. + */ +export interface ZephyrRuntimePluginOptions { + /** URL endpoint to fetch the manifest from (e.g., '/zephyr-manifest.json') */ + manifestUrl: string; + /** Polling interval in milliseconds for checking manifest updates (optional) */ + pollInterval?: number; + /** Whether to automatically apply updates when manifest changes (optional) */ + autoUpdate?: boolean; +} + +/** + * Interface for the Zephyr runtime plugin instance. Created by zephyr-xpack-internal's + * createZephyrRuntimePlugin function. + * + * Note: The actual implementation is in zephyr-xpack-internal which is an optional peer + * dependency. If not installed, runtime features will be disabled but the app will + * continue to work. + */ +export interface ZephyrRuntimePlugin { + /** Manually fetch and apply the latest manifest */ + refresh(): Promise; + /** Get the current manifest */ + getManifest(): ZephyrRuntimeManifest | null; + /** Register a callback for manifest changes */ + onManifestChange(callback: ZephyrManifestChangeCallback): () => void; + /** Start polling for manifest updates */ + startPolling(interval?: number): void; + /** Stop polling for manifest updates */ + stopPolling(): void; + /** Check if the plugin is initialized */ + isInitialized(): boolean; +} + +declare global { + /** + * Zephyr runtime plugin instance - created by zephyr-xpack-internal. Used for OTA + * updates and remote module resolution. + * + * This global is set by the code injected by zephyr-transformer.ts when the app starts. + * It will be undefined if: + * + * - Zephyr-xpack-internal is not installed + * - The runtime plugin failed to initialize + * - The code hasn't been executed yet + */ + var __ZEPHYR_RUNTIME_PLUGIN__: ZephyrRuntimePlugin | undefined; + + /** + * Zephyr runtime plugin singleton tracker. Prevents multiple initializations in the + * same runtime. Set to the same instance as **ZEPHYR_RUNTIME_PLUGIN** after + * initialization. + */ + var __ZEPHYR_RUNTIME_PLUGIN_INSTANCE__: ZephyrRuntimePlugin | undefined; + + /** + * Optional callback invoked when the manifest changes. Can be set by application code + * to react to OTA updates. + * + * @example + * ```typescript + * global.__ZEPHYR_MANIFEST_CHANGED__ = (newManifest, oldManifest) => { + * console.log('Manifest updated:', newManifest.version); + * // Trigger app reload or notify user + * }; + * ```; + */ + var __ZEPHYR_MANIFEST_CHANGED__: ZephyrManifestChangeCallback | undefined; + + /** + * Module Federation global config set by Metro bundler. Used by zephyrCommandWrapper to + * access the MF configuration. + */ + var __METRO_FEDERATION_CONFIG: + | { + name: string; + filename?: string; + remotes?: Record; + exposes?: Record; + shared?: Record; + } + | undefined; + + // Browser/Web environment support (for React Native Web) + interface Window { + __ZEPHYR_RUNTIME_PLUGIN__?: ZephyrRuntimePlugin; + __ZEPHYR_RUNTIME_PLUGIN_INSTANCE__?: ZephyrRuntimePlugin; + __ZEPHYR_MANIFEST_CHANGED__?: ZephyrManifestChangeCallback; + } +} + +export {}; diff --git a/libs/zephyr-metro-plugin/src/lib/internal/extract-mf-remotes.ts b/libs/zephyr-metro-plugin/src/lib/internal/extract-mf-remotes.ts new file mode 100644 index 00000000..fa8744bc --- /dev/null +++ b/libs/zephyr-metro-plugin/src/lib/internal/extract-mf-remotes.ts @@ -0,0 +1,29 @@ +import { + is_zephyr_dependency_pair, + readPackageJson, + type ZeDependencyPair, +} from 'zephyr-agent'; +import type { ZephyrCommandWrapperConfig } from '../zephyr-metro-plugin'; + +export function extract_remotes_dependencies( + config: ZephyrCommandWrapperConfig +): ZeDependencyPair[] { + const depsPairs: ZeDependencyPair[] = []; + + const { zephyrDependencies } = readPackageJson(config.context ?? process.cwd()); + if (zephyrDependencies) { + Object.entries(zephyrDependencies).map(([name, version]) => { + depsPairs.push({ name, version } as ZeDependencyPair); + }); + } + + if (config.mfConfig?.remotes) { + Object.entries(config.mfConfig.remotes).map(([name, remote]) => { + depsPairs.push({ name, version: remote } as ZeDependencyPair); + }); + } + + return depsPairs + .flat() + .filter((dep): dep is ZeDependencyPair => is_zephyr_dependency_pair(dep)); +} diff --git a/libs/zephyr-metro-plugin/src/lib/internal/extract-modules-from-exposes.ts b/libs/zephyr-metro-plugin/src/lib/internal/extract-modules-from-exposes.ts new file mode 100644 index 00000000..8119d863 --- /dev/null +++ b/libs/zephyr-metro-plugin/src/lib/internal/extract-modules-from-exposes.ts @@ -0,0 +1,87 @@ +import type { ModuleFederationPlugin, XFederatedSharedConfig } from './types'; + +/** + * Extracts exposed modules from Module Federation configuration Creates formatted module + * entries for the build stats + */ +export function extractModulesFromExposes( + mfConfig: ModuleFederationPlugin['config'] | undefined, + applicationID: string +): Array<{ + id: string; + name: string; + applicationID: string; + requires: string[]; + file: string; +}> { + if (!mfConfig?.exposes) { + return []; + } + + // Extract exposed modules from the Module Federation config + return Object.entries(mfConfig.exposes).map(([exposedPath, filePath]) => { + // Handle different formats of exposes configuration + // In Module Federation, exposes can be an object where key is the exposed path and value is the file path + // Example: { './Button': './src/Button' } + + // Normalize the file path (it might be an object in some federation implementations) + const normalizedFilePath = + typeof filePath === 'string' + ? filePath + : typeof filePath === 'object' && filePath !== null && 'import' in filePath + ? String((filePath as { import: string }).import) + : String(filePath); + + // Extract just the module name from the exposed path (removing './') + const name = exposedPath.startsWith('./') ? exposedPath.substring(2) : exposedPath; + + // Create a unique ID for this module in the format used by Module Federation Dashboard + const id = `${name}:${name}`; + + // Extract any potential requirements from shared dependencies + // In a more complete implementation, this would analyze the actual file to find imports + const requires: string[] = []; + + // If we have shared dependencies and they're an object with keys, use them as requirements + if (mfConfig.shared) { + if (Array.isArray(mfConfig.shared)) { + // Handle array format: ['react', 'react-dom'] + requires.push( + ...mfConfig.shared + .map((item: string | XFederatedSharedConfig) => { + return typeof item === 'string' + ? item + : typeof item === 'object' && item !== null && 'libraryName' in item + ? String(item.libraryName) + : ''; + }) + .filter(Boolean) + ); + } else if (typeof mfConfig.shared === 'object' && mfConfig.shared !== null) { + // Handle object format: { react: {...}, 'react-dom': {...} } + requires.push(...Object.keys(mfConfig.shared)); + } + } + + // Handle additionalShared format from Nx webpack module federation + if (mfConfig.additionalShared && Array.isArray(mfConfig.additionalShared)) { + requires.push( + ...mfConfig.additionalShared + .map((item: string | XFederatedSharedConfig) => + typeof item === 'object' && item !== null && 'libraryName' in item + ? String(item.libraryName) + : '' + ) + .filter(Boolean) + ); + } + + return { + id, + name, + applicationID, + requires, + file: normalizedFilePath, + }; + }); +} diff --git a/libs/zephyr-metro-plugin/src/lib/internal/get-package-dependencies.ts b/libs/zephyr-metro-plugin/src/lib/internal/get-package-dependencies.ts new file mode 100644 index 00000000..aed1a13f --- /dev/null +++ b/libs/zephyr-metro-plugin/src/lib/internal/get-package-dependencies.ts @@ -0,0 +1,6 @@ +export function getPackageDependencies( + dependencies: Record | undefined +): Array<{ name: string; version: string }> { + if (!dependencies) return []; + return Object.entries(dependencies).map(([name, version]) => ({ name, version })); +} diff --git a/libs/zephyr-metro-plugin/src/lib/internal/load-static-entries.ts b/libs/zephyr-metro-plugin/src/lib/internal/load-static-entries.ts new file mode 100644 index 00000000..9c4d7717 --- /dev/null +++ b/libs/zephyr-metro-plugin/src/lib/internal/load-static-entries.ts @@ -0,0 +1,47 @@ +import { readdirSync, readFile, statSync } from 'node:fs'; +import { relative, resolve } from 'node:path'; +import { promisify } from 'node:util'; +import type { OutputAsset } from './types'; + +// Metro-compatible path normalization (replaces vite's normalizePath) +function normalizePath(path: string): string { + return path.replace(/\\/g, '/'); +} + +interface LoadStaticEntriesOptions { + root: string; + outDir: string; +} + +export async function load_static_entries( + props: LoadStaticEntriesOptions +): Promise { + const { root } = props; + const publicAssets: OutputAsset[] = []; + + const root_dist_dir = resolve(root, props.outDir); + + const loadDir = async (destDir: string) => { + for (const file of readdirSync(destDir)) { + const destFile = resolve(destDir, file); + const stat = statSync(destFile); + if (stat.isDirectory()) { + await loadDir(destFile); + continue; + } + const fileName = normalizePath(relative(root_dist_dir, destFile)); + publicAssets.push({ + fileName, + name: file, + names: [file], + needsCodeReference: false, + source: await promisify(readFile)(destFile), + type: 'asset', + originalFileName: file, + originalFileNames: [file], + }); + } + }; + await loadDir(root_dist_dir); + return publicAssets; +} diff --git a/libs/zephyr-metro-plugin/src/lib/internal/metro-build-stats.ts b/libs/zephyr-metro-plugin/src/lib/internal/metro-build-stats.ts new file mode 100644 index 00000000..ef268022 --- /dev/null +++ b/libs/zephyr-metro-plugin/src/lib/internal/metro-build-stats.ts @@ -0,0 +1,59 @@ +import type { ZephyrEngine } from 'zephyr-agent'; +import type { ZephyrBuildStats } from 'zephyr-edge-contract'; + +// Create minimal build stats for Metro builds +export async function createMinimalBuildStats( + zephyr_engine: ZephyrEngine +): Promise> { + const app = zephyr_engine.applicationProperties; + const { git } = zephyr_engine.gitProperties; + const { isCI } = zephyr_engine.env; + + const version = (await zephyr_engine.snapshotId) || '0.0.0'; + const application_uid = zephyr_engine.application_uid; + const buildId = (await zephyr_engine.build_id) || 'unknown'; + const { EDGE_URL, PLATFORM, DELIMITER } = await zephyr_engine.application_configuration; + + return { + id: application_uid, + name: app.name, + version, + project: app.name || 'unknown-project', + app: Object.assign({}, app, { buildId }) as any, + git, + context: { isCI }, + tags: [], + edge: { url: EDGE_URL, delimiter: DELIMITER }, + platform: PLATFORM as any, + type: 'app' as any, + environment: '', + default: false, + overrides: [], + modules: [], + consumes: [], + dependencies: [], + devDependencies: [], + optionalDependencies: [], + peerDependencies: [], + remotes: [], + }; +} + +// Simple catalog dependencies resolver for Metro +export function resolveCatalogDependencies( + dependencies: Record = {} +): Record { + const resolved: Record = {}; + + for (const [name, version] of Object.entries(dependencies)) { + if (version.startsWith('catalog:')) { + // For Metro plugin, we'll just use a default version for catalog references + // In a real implementation, this would resolve from a catalog file + resolved[name] = '0.0.0'; + } else { + resolved[name] = version; + } + } + + return resolved; +} diff --git a/libs/zephyr-metro-plugin/src/lib/internal/metro-errors.ts b/libs/zephyr-metro-plugin/src/lib/internal/metro-errors.ts new file mode 100644 index 00000000..1d224c9b --- /dev/null +++ b/libs/zephyr-metro-plugin/src/lib/internal/metro-errors.ts @@ -0,0 +1,4 @@ +import { ZeErrors } from 'zephyr-agent'; + +// Use existing error from zephyr-agent +export const ERR_MISSING_METRO_FEDERATION_CONFIG = ZeErrors.ERR_INVALID_MF_CONFIG; diff --git a/libs/zephyr-metro-plugin/src/lib/internal/mutate-mf-config.ts b/libs/zephyr-metro-plugin/src/lib/internal/mutate-mf-config.ts new file mode 100644 index 00000000..39be3980 --- /dev/null +++ b/libs/zephyr-metro-plugin/src/lib/internal/mutate-mf-config.ts @@ -0,0 +1,77 @@ +import type { ZephyrEngine, ZeResolvedDependency } from 'zephyr-agent'; +import { ze_log } from 'zephyr-agent'; +import type { ZephyrPluginOptions } from 'zephyr-edge-contract'; + +export function mutateMfConfig( + zephyr_engine: ZephyrEngine, + config: Pick['mfConfig'], + resolvedDependencyPairs: ZeResolvedDependency[] | null, + delegate_module_template?: () => unknown | undefined +) { + // Lazy load zephyr-xpack-internal to avoid static import + const { + createMfRuntimeCode, + xpack_delegate_module_template, + } = require('zephyr-xpack-internal'); + const template = delegate_module_template || xpack_delegate_module_template; + if (!resolvedDependencyPairs?.length) { + ze_log.mf(`No resolved dependency pairs found, skipping...`); + return; + } + + const remotes = config?.remotes; + if (!remotes) { + ze_log.mf( + `No remotes found for plugin: ${JSON.stringify(config, null, 2)}`, + 'skipping...' + ); + return; + } + + Object.entries(remotes).map((remote) => { + const [remote_name, remote_version] = remote; + + if ( + !remote_name || + typeof remote_name !== 'string' || + !remote_version || + typeof remote_version !== 'string' + ) { + ze_log.mf(`Invalid remote configuration: ${JSON.stringify(remote)}, skipping...`); + return; + } + const resolved_dep = resolvedDependencyPairs.find( + (dep) => dep.name === remote_name && dep.version === remote_version + ); + + ze_log.mf(`remote_name: ${remote_name}, remote_version: ${remote_version}`); + + if (!resolved_dep) { + ze_log.mf( + `Resolved dependency pair not found for remote: ${JSON.stringify(remote, null, 2)}`, + 'skipping...' + ); + return; + } + + // todo: this is a version with named export logic, we should take this into account later + const [v_app] = remote_version.includes('@') + ? remote_version.split('@') + : [remote_name]; + + ze_log.mf(`v_app: ${v_app}`); + if (v_app) { + resolved_dep.remote_entry_url = [v_app, resolved_dep.remote_entry_url].join('@'); + ze_log.mf(`Adding version to remote entry url: ${resolved_dep.remote_entry_url}`); + } + + resolved_dep.name = remote_name; + + if (remotes[remote_name]) { + remotes[remote_name] = createMfRuntimeCode(zephyr_engine, resolved_dep, template); + ze_log.mf(`Setting runtime code for remote: ${remotes}`); + } + }); + + return config; +} diff --git a/libs/zephyr-metro-plugin/src/lib/internal/parse-shared-dependencies.ts b/libs/zephyr-metro-plugin/src/lib/internal/parse-shared-dependencies.ts new file mode 100644 index 00000000..123ba07b --- /dev/null +++ b/libs/zephyr-metro-plugin/src/lib/internal/parse-shared-dependencies.ts @@ -0,0 +1,65 @@ +import type { ZephyrEngine } from 'zephyr-agent'; +import { resolveCatalogDependencies } from './metro-build-stats'; +import type { XFederatedSharedConfig } from './types'; + +export function parseSharedDependencies( + name: string, + config: unknown, + zephyr_engine: ZephyrEngine +): { + id: string; + name: string; + version: string; + location: string; + applicationID: string; +} { + // Module Federation allows shared to be an object, array, or string + // Get version from package dependencies if available or from config + let version = '0.0.0'; + + if (zephyr_engine.npmProperties.dependencies?.[name]) { + // Resolve catalog reference in dependencies if present + const depVersion = zephyr_engine.npmProperties.dependencies[name]; + version = depVersion.startsWith('catalog:') + ? resolveCatalogDependencies({ [name]: depVersion })[name] + : depVersion; + } else if (zephyr_engine.npmProperties.peerDependencies?.[name]) { + // Resolve catalog reference in peer dependencies if present + const peerVersion = zephyr_engine.npmProperties.peerDependencies[name]; + version = peerVersion.startsWith('catalog:') + ? resolveCatalogDependencies({ [name]: peerVersion })[name] + : peerVersion; + } else if (typeof config === 'object' && config !== null) { + // Object format: { react: { requiredVersion: '18.0.0', singleton: true } } + if ((config as XFederatedSharedConfig).requiredVersion) { + const reqVersion = (config as XFederatedSharedConfig).requiredVersion; + + if (reqVersion) { + version = + typeof reqVersion === 'string' && reqVersion.startsWith('catalog:') + ? resolveCatalogDependencies({ [name]: reqVersion })[name] + : reqVersion; + } + } + } else if (typeof config === 'string') { + // String format: { react: '18.0.0' } + // Only use string value if we didn't find the package in dependencies + if ( + !zephyr_engine.npmProperties.dependencies?.[name] && + !zephyr_engine.npmProperties.peerDependencies?.[name] + ) { + version = config.startsWith('catalog:') + ? resolveCatalogDependencies({ [name]: config })[name] + : config; + } + } + // Array format is also possible but doesn't typically include version info + + return { + id: name, + name, + version, + location: name, + applicationID: name, + }; +} diff --git a/libs/zephyr-metro-plugin/src/lib/internal/types.ts b/libs/zephyr-metro-plugin/src/lib/internal/types.ts new file mode 100644 index 00000000..85b91917 --- /dev/null +++ b/libs/zephyr-metro-plugin/src/lib/internal/types.ts @@ -0,0 +1,61 @@ +// Metro-specific asset types +export interface OutputAsset { + fileName: string; + name: string; + names: string[]; + needsCodeReference: boolean; + source: string | Uint8Array; + type: 'asset'; + originalFileName: string; + originalFileNames: string[]; +} + +export interface XFederatedSharedConfig { + singleton?: boolean; + requiredVersion?: string; + version?: string; + eager?: boolean; + libraryName?: string; +} + +export interface XAdditionalSharedConfig { + libraryName: string; + sharedConfig?: { + singleton?: boolean; + requiredVersion?: string; + }; +} + +export interface XFederatedConfig { + name: string; + library?: + | { + type?: string; + } + | any; + shared?: Record | string[] | any; + remotes?: (string | RemotesObject)[] | RemotesObject | any; + exposes?: Record | any; + filename?: string; + bundle_name?: string; + additionalShared?: + | string[] + | Record + | XAdditionalSharedConfig[] + | any; +} + +export interface ModuleFederationPlugin { + apply: (compiler: unknown) => void; + _options?: XFederatedConfig | { config: XFederatedConfig }; + config?: XFederatedConfig; +} + +interface RemotesObject { + [index: string]: string | RemotesConfig | string[]; +} + +interface RemotesConfig { + external: string | string[]; + shareScope?: string; +} diff --git a/libs/zephyr-metro-plugin/src/lib/with-zephyr.spec.ts b/libs/zephyr-metro-plugin/src/lib/with-zephyr.spec.ts new file mode 100644 index 00000000..5e8b3d1d --- /dev/null +++ b/libs/zephyr-metro-plugin/src/lib/with-zephyr.spec.ts @@ -0,0 +1,123 @@ +/** Unit tests for Zephyr Metro Plugin configuration */ + +describe('with-zephyr', () => { + describe('extractMetroRemoteDependencies', () => { + // Helper function matching the implementation + const extractMetroRemoteDependencies = (remotes: Record) => { + return Object.entries(remotes).map(([name, url]) => { + const [remoteName, remoteUrl] = url.includes('@') ? url.split('@') : [name, url]; + + return { + name: remoteName, + version: 'latest', + remote_url: remoteUrl, + }; + }); + }; + + it('should extract simple remote URLs', () => { + const remotes = { + RemoteApp: 'http://localhost:9000/remoteEntry.js', + }; + const result = extractMetroRemoteDependencies(remotes); + expect(result).toEqual([ + { + name: 'RemoteApp', + version: 'latest', + remote_url: 'http://localhost:9000/remoteEntry.js', + }, + ]); + }); + + it('should extract remote URLs with name@url format', () => { + const remotes = { + SharedUI: 'SharedUILib@http://localhost:9001/remoteEntry.js', + }; + const result = extractMetroRemoteDependencies(remotes); + expect(result).toEqual([ + { + name: 'SharedUILib', + version: 'latest', + remote_url: 'http://localhost:9001/remoteEntry.js', + }, + ]); + }); + + it('should handle multiple remotes', () => { + const remotes = { + RemoteA: 'http://localhost:9000/a.js', + RemoteB: 'ModuleB@http://localhost:9001/b.js', + RemoteC: 'http://localhost:9002/c.js', + }; + const result = extractMetroRemoteDependencies(remotes); + expect(result).toHaveLength(3); + expect(result[0].name).toBe('RemoteA'); + expect(result[1].name).toBe('ModuleB'); + expect(result[2].name).toBe('RemoteC'); + }); + + it('should handle empty remotes', () => { + const result = extractMetroRemoteDependencies({}); + expect(result).toEqual([]); + }); + + it('should always set version to latest', () => { + const remotes = { + RemoteApp: 'RemoteApp@http://example.com/entry.js', + }; + const result = extractMetroRemoteDependencies(remotes); + expect(result[0].version).toBe('latest'); + }); + }); + + describe('ZephyrMetroOptions interface', () => { + it('should accept minimal options', () => { + const options = {}; + expect(options).toBeDefined(); + }); + + it('should accept full options', () => { + const options = { + name: 'MyApp', + target: 'ios' as const, + remotes: { + Remote1: 'http://localhost:9000/entry.js', + }, + manifestPath: '/custom-manifest.json', + entryFiles: ['main.tsx', 'App.tsx'], + failOnManifestError: true, + }; + expect(options.name).toBe('MyApp'); + expect(options.target).toBe('ios'); + expect(options.manifestPath).toBe('/custom-manifest.json'); + expect(options.entryFiles).toContain('main.tsx'); + expect(options.failOnManifestError).toBe(true); + }); + + it('should default failOnManifestError to false/undefined', () => { + const options = { + name: 'MyApp', + }; + expect(options.failOnManifestError).toBeUndefined(); + }); + }); + + describe('manifestPath configuration', () => { + it('should use default path when not specified', () => { + const defaultPath = '/zephyr-manifest.json'; + expect(defaultPath).toBe('/zephyr-manifest.json'); + }); + + it('should strip leading slash for filename generation', () => { + const endpoint = '/zephyr-manifest.json'; + const filename = endpoint.replace(/^\//, ''); + expect(filename).toBe('zephyr-manifest.json'); + }); + + it('should handle custom paths', () => { + const endpoint = '/custom/path/manifest.json'; + const filename = endpoint.replace(/^\//, ''); + expect(filename).toBe('custom/path/manifest.json'); + }); + }); +}); diff --git a/libs/zephyr-metro-plugin/src/lib/with-zephyr.ts b/libs/zephyr-metro-plugin/src/lib/with-zephyr.ts new file mode 100644 index 00000000..18030a61 --- /dev/null +++ b/libs/zephyr-metro-plugin/src/lib/with-zephyr.ts @@ -0,0 +1,196 @@ +import type { ConfigT } from 'metro-config'; +import { + ze_log, + ZephyrEngine, + ZephyrError, + ZeErrors, + createManifestContent, +} from 'zephyr-agent'; +import path from 'path'; +import fs from 'fs'; + +export interface ZephyrMetroOptions { + /** Application name */ + name?: string; + /** Remote dependencies configuration */ + remotes?: Record; + /** Target platform */ + target?: 'ios' | 'android'; + /** Custom manifest endpoint path (default: /zephyr-manifest.json) */ + manifestPath?: string; + /** Custom entry file patterns for runtime injection (more conservative targeting) */ + entryFiles?: string[]; + /** Throw an error if manifest generation fails (default: false - logs warning only) */ + failOnManifestError?: boolean; +} + +export interface ZephyrModuleFederationConfig { + name: string; + exposes?: Record; + remotes?: Record; + shared?: Record; +} + +/** Metro plugin configuration function for Zephyr */ +export function withZephyr(zephyrOptions: ZephyrMetroOptions = {}) { + return async (metroConfig: ConfigT): Promise => { + try { + return await applyZephyrToMetroConfig(metroConfig, zephyrOptions); + } catch (error) { + ze_log.error(ZephyrError.format(error)); + return metroConfig; // Return original config on error + } + }; +} + +async function applyZephyrToMetroConfig( + metroConfig: ConfigT, + zephyrOptions: ZephyrMetroOptions +): Promise { + const projectRoot = metroConfig.projectRoot || process.cwd(); + const manifestPath = zephyrOptions.manifestPath || '/zephyr-manifest.json'; + + // Initialize Zephyr Engine + const zephyr_engine = await ZephyrEngine.create({ + builder: 'metro', + context: projectRoot, + }); + + if (zephyrOptions.target) { + zephyr_engine.env.target = zephyrOptions.target; + } + + // Extract remote dependencies from zephyr options + const dependencyPairs = extractMetroRemoteDependencies(zephyrOptions.remotes || {}); + + // Resolve dependencies through Zephyr + const resolved_dependencies = + await zephyr_engine.resolve_remote_dependencies(dependencyPairs); + + // Enhanced metro config with Zephyr transformer options + const zephyrTransformerOptions = { + manifestPath, + entryFiles: zephyrOptions.entryFiles, + }; + + const enhancedConfig: ConfigT = { + ...metroConfig, + transformer: { + ...metroConfig.transformer, + babelTransformerPath: require.resolve('./zephyr-transformer'), + // Pass zephyr options to transformer via extra data + ...(metroConfig.transformer as any), + zephyrTransformerOptions, + }, + resolver: { + ...metroConfig.resolver, + // Add Zephyr-specific resolution logic + resolverMainFields: [ + ...(metroConfig.resolver?.resolverMainFields || [ + 'react-native', + 'browser', + 'main', + ]), + 'zephyr', + ], + }, + server: { + ...metroConfig.server, + // Enhance server with manifest endpoint + enhanceMiddleware: (middleware: any, server: any) => { + // Get the base middleware (either enhanced or original) + const baseMiddleware = metroConfig.server?.enhanceMiddleware + ? metroConfig.server.enhanceMiddleware(middleware, server) + : middleware; + + // Return a new middleware that intercepts manifest requests + return (req: any, res: any, next: any) => { + // Check if this is a manifest request + const url = req.url?.split('?')[0]; // Remove query string + if (url === manifestPath) { + try { + const manifestContent = createManifestContent(resolved_dependencies || []); + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Cache-Control', 'no-cache'); + res.end(manifestContent); + return; + } catch (error) { + ze_log.error(`Failed to serve manifest: ${error}`); + res.statusCode = 500; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ error: 'Failed to generate manifest' })); + return; + } + } + + // Pass through to base middleware + return baseMiddleware(req, res, next); + }; + }, + }, + }; + + // Generate manifest file for production builds + const manifestGenerated = await generateManifestFile( + projectRoot, + manifestPath, + resolved_dependencies || [] + ); + + if (!manifestGenerated) { + const errorMessage = + 'Manifest file generation failed - runtime updates may not work correctly'; + if (zephyrOptions.failOnManifestError) { + throw new ZephyrError(ZeErrors.ERR_UNKNOWN, { message: errorMessage }); + } + ze_log.error(errorMessage); + } + + ze_log.app('Zephyr Metro plugin configured successfully'); + + return enhancedConfig; +} + +/** Extract remote dependencies from Metro configuration */ +function extractMetroRemoteDependencies(remotes: Record) { + return Object.entries(remotes).map(([name, url]) => { + // Parse remote URL - could be just URL or name@url format + const [remoteName, remoteUrl] = url.includes('@') ? url.split('@') : [name, url]; + + return { + name: remoteName, + version: 'latest', // Metro doesn't have version concept like webpack MF + remote_url: remoteUrl, + }; + }); +} + +/** Generate zephyr-manifest.json file - returns true on success, false on failure */ +async function generateManifestFile( + projectRoot: string, + manifestEndpoint: string, + resolved_dependencies: any[] +): Promise { + try { + const manifestContent = createManifestContent(resolved_dependencies); + // Convert endpoint path to filename (e.g., /zephyr-manifest.json -> zephyr-manifest.json) + const manifestFilename = manifestEndpoint.replace(/^\//, ''); + const manifestFilePath = path.join(projectRoot, 'assets', manifestFilename); + + // Ensure assets directory exists + const assetsDir = path.dirname(manifestFilePath); + if (!fs.existsSync(assetsDir)) { + fs.mkdirSync(assetsDir, { recursive: true }); + } + + await fs.promises.writeFile(manifestFilePath, manifestContent, 'utf-8'); + ze_log.manifest(`Generated manifest at: ${manifestFilePath}`); + return true; + } catch (error) { + ze_log.error(`Failed to generate manifest file: ${ZephyrError.format(error)}`); + return false; + } +} + +/** Legacy function name for backward compatibility */ +export const withZephyrMetro = withZephyr; diff --git a/libs/zephyr-metro-plugin/src/lib/zephyr-metro-command-wrapper.ts b/libs/zephyr-metro-plugin/src/lib/zephyr-metro-command-wrapper.ts new file mode 100644 index 00000000..4c3a9ab5 --- /dev/null +++ b/libs/zephyr-metro-plugin/src/lib/zephyr-metro-command-wrapper.ts @@ -0,0 +1,72 @@ +import { ZephyrError, ZeErrors, type Platform } from 'zephyr-agent'; +import type { ZephyrPluginOptions } from 'zephyr-edge-contract'; +import { ERR_MISSING_METRO_FEDERATION_CONFIG } from './internal/metro-errors'; +import { ZephyrMetroPlugin } from './zephyr-metro-plugin'; + +export type MetroConfig = Record; +export type MetroFederationConfig = Pick['mfConfig']; + +interface MetroBundleOptions { + mode: string; + platform: Platform; +} + +interface MetroConfigOptions extends MetroConfig { + root: string; +} + +interface MetroCliOptions { + maxWorkers?: number; + resetCache?: boolean; + config?: string; +} + +type MetroCommandArgs = [[MetroBundleOptions], MetroConfigOptions, MetroCliOptions]; + +export async function zephyrCommandWrapper( + bundleFederatedRemote: (...args: MetroCommandArgs) => Promise, + loadMetroConfig: (config: MetroConfig, options: MetroCliOptions) => Promise, + updateManifest: () => void +) { + return async (...args: MetroCommandArgs) => { + try { + // before build + const isDev = args[0][0].mode; + const platform = args[0][0].platform; + + const context = args[1].root; + + await loadMetroConfig(args[1], { + maxWorkers: args[2].maxWorkers, + resetCache: args[2].resetCache, + config: args[2].config, + }); + + if (!(global as any).__METRO_FEDERATION_CONFIG) { + throw new ZephyrError(ERR_MISSING_METRO_FEDERATION_CONFIG); + } + + const zephyrMetroPlugin = new ZephyrMetroPlugin({ + platform, + mode: isDev ? 'development' : 'production', + context, + outDir: 'dist', + mfConfig: (global as any).__METRO_FEDERATION_CONFIG, + }); + + await zephyrMetroPlugin.beforeBuild(); + + updateManifest(); + + const res = await bundleFederatedRemote(...args); + + await zephyrMetroPlugin.afterBuild(); + + return res; + } catch (error) { + throw new ZephyrError(ZeErrors.ERR_UNKNOWN, { + message: JSON.stringify(error), + }); + } + }; +} diff --git a/libs/zephyr-metro-plugin/src/lib/zephyr-metro-plugin.ts b/libs/zephyr-metro-plugin/src/lib/zephyr-metro-plugin.ts new file mode 100644 index 00000000..bb3f94d5 --- /dev/null +++ b/libs/zephyr-metro-plugin/src/lib/zephyr-metro-plugin.ts @@ -0,0 +1,233 @@ +import type { Platform, ZeBuildAssetsMap } from 'zephyr-agent'; +import { buildAssetsMap, ze_log, ZephyrEngine } from 'zephyr-agent'; +import type { + ApplicationConsumes, + ZeBuildAsset, + ZephyrBuildStats, + ZephyrPluginOptions, +} from 'zephyr-edge-contract'; +import { extractModulesFromExposes } from './internal/extract-modules-from-exposes'; +import { getPackageDependencies } from './internal/get-package-dependencies'; +import { load_static_entries } from './internal/load-static-entries'; +import { + createMinimalBuildStats, + resolveCatalogDependencies, +} from './internal/metro-build-stats'; +import { parseSharedDependencies } from './internal/parse-shared-dependencies'; +import type { OutputAsset } from './internal/types'; +import { extract_remotes_dependencies } from './internal/extract-mf-remotes'; +import { mutateMfConfig } from './internal/mutate-mf-config'; + +export interface ZephyrCommandWrapperConfig { + platform: Platform; + mode: string; + context: string; + outDir: string; + mfConfig: Pick['mfConfig']; +} + +export class ZephyrMetroPlugin { + #config: ZephyrCommandWrapperConfig; + zephyr_engine!: ZephyrEngine; + + constructor(props: ZephyrCommandWrapperConfig) { + this.#config = props; + } + + async beforeBuild() { + this.zephyr_engine = await ZephyrEngine.create({ + builder: 'metro', + context: this.#config.context, + }); + ze_log.config('Configuring with Zephyr... \n config: ', this.#config); + + this.zephyr_engine.env.target = this.#config.platform; + + const dependency_pairs = extract_remotes_dependencies(this.#config); + + ze_log.config( + 'Resolving and building towards target by zephyr_engine.env.target: ', + this.zephyr_engine.env.target + ); + + const resolved_dependency_pairs = + await this.zephyr_engine.resolve_remote_dependencies(dependency_pairs); + + if (this.#config.mfConfig) { + mutateMfConfig( + this.zephyr_engine, + this.#config.mfConfig, + resolved_dependency_pairs + ); + } + + return this.#config.mfConfig; + } + + async afterBuild() { + await this.zephyr_engine.start_new_build(); + + const assetsMap = await this.makeAssetsMap(); + + const buildStats = await this.getBuildStats( + Object.values(assetsMap).filter( + (asset) => asset.extname === '.map' && !asset.path.includes('shared/') + ) + ); + + await this.zephyr_engine.upload_assets({ + assetsMap, + buildStats: buildStats as any, + mfConfig: this.#config.mfConfig, + }); + await this.zephyr_engine.build_finished(); + } + + private async getConsumeMap(bundleMaps: ZeBuildAsset[]) { + const consumeMap = new Map(); + + bundleMaps.forEach((asset) => { + try { + const sourceMap = JSON.parse(asset['buffer'].toString()); + if (sourceMap.sourcesContent && Array.isArray(sourceMap.sourcesContent)) { + // Filter out node_modules from sources and sourcesContent + const filteredSources: string[] = []; + const filteredSourcesContent: string[] = []; + + // Find indices of sources that contain node_modules + if (sourceMap.sources && Array.isArray(sourceMap.sources)) { + sourceMap.sources.forEach((source: string, sourceIndex: number) => { + if (typeof source === 'string' && !source.includes('node_modules')) { + // Keep non-node_modules sources + if (sourceMap.sourcesContent[sourceIndex]) { + filteredSources.push(source); + filteredSourcesContent.push(sourceMap.sourcesContent[sourceIndex]); + } + } + }); + } + + // Search for ES6 import statements: import ... from 'remote/component' + const searchPattern = + /import\s+(?:\{[^}]*\}|\w+)\s+from\s+['"]([^'"]+)\/([^'"]+)['"]/g; + + filteredSourcesContent.forEach((content: string, contentIndex: number) => { + let match; + while ((match = searchPattern.exec(content)) !== null) { + if (match.length >= 3) { + const remoteName = match[1]; + const componentName = match[2]; + + const fileUrl = filteredSources[contentIndex]; + + consumeMap.set(`${remoteName}-${componentName}`, { + consumingApplicationID: componentName, + applicationID: remoteName, + name: componentName, + usedIn: [ + { + file: fileUrl.replace(this.#config.context, ''), + url: fileUrl.replace(this.#config.context, ''), + }, + ], + }); + ze_log.app('Found remote import in promise chain', { + remoteName, + componentName, + file: fileUrl.replace(this.#config.context, ''), + }); + } + } + }); + } + } catch (error) { + ze_log.app('Error parsing bundle map for loadRemote calls', { + error, + chunkId: asset['path'], + }); + } + }); + + return consumeMap; + } + + private async getBuildStats(bundleMaps: ZeBuildAsset[]) { + const minimal_build_stats = await createMinimalBuildStats(this.zephyr_engine); + + const consumeMap = await this.getConsumeMap(bundleMaps); + + Object.assign(minimal_build_stats, { + name: this.#config.mfConfig?.name || this.zephyr_engine.applicationProperties.name, + remote: this.#config.mfConfig?.filename || 'remoteEntry.js', + remotes: this.#config.mfConfig?.remotes + ? Object.keys(this.#config.mfConfig.remotes) + : [], + metadata: { + hasFederation: !!this.#config.mfConfig, + }, + build_target: this.zephyr_engine.env.target, + }) as ZephyrBuildStats; + + // Extract shared dependencies from Module Federation config + const overrides = this.#config.mfConfig?.shared + ? Object.entries(this.#config.mfConfig.shared).map(([name, config]) => + parseSharedDependencies(name, config, this.zephyr_engine) + ) + : []; + + // Build the stats object + const buildStats = { + ...minimal_build_stats, + overrides, + modules: extractModulesFromExposes( + this.#config.mfConfig, + this.zephyr_engine.application_uid + ), + // Module Federation related data + dependencies: getPackageDependencies( + resolveCatalogDependencies(this.zephyr_engine.npmProperties.dependencies) + ), + devDependencies: getPackageDependencies( + resolveCatalogDependencies(this.zephyr_engine.npmProperties.devDependencies) + ), + optionalDependencies: getPackageDependencies( + resolveCatalogDependencies(this.zephyr_engine.npmProperties.optionalDependencies) + ), + peerDependencies: getPackageDependencies( + resolveCatalogDependencies(this.zephyr_engine.npmProperties.peerDependencies) + ), + consumes: Array.from(consumeMap.values()), + }; + + return buildStats; + } + + private async loadStaticAssets(): Promise> { + const assets = await load_static_entries({ + root: this.#config.context, + outDir: + this.#config.platform === 'ios' || this.#config.platform === 'android' + ? this.#config.outDir + `/${this.#config.platform}` + : this.#config.outDir, + }); + + return assets.reduce((acc, asset) => { + acc[asset.fileName] = asset; + return acc; + }, {} as any); + } + + private async makeAssetsMap(): Promise { + const assets = await this.loadStaticAssets(); + + return buildAssetsMap(assets, this.extractBuffer, this.getAssetType); + } + + private extractBuffer(asset: OutputAsset): string | undefined { + return asset.source?.toString(); + } + + private getAssetType(asset: OutputAsset): string { + return asset.type ?? 'asset'; + } +} diff --git a/libs/zephyr-metro-plugin/src/lib/zephyr-transformer.spec.ts b/libs/zephyr-metro-plugin/src/lib/zephyr-transformer.spec.ts new file mode 100644 index 00000000..b562b9ce --- /dev/null +++ b/libs/zephyr-metro-plugin/src/lib/zephyr-transformer.spec.ts @@ -0,0 +1,155 @@ +/** Unit tests for Zephyr Metro Transformer */ + +// Test the isZephyrTargetFile logic by extracting it +// Since the function is private, we test it indirectly through the transform function +// or export it for testing + +describe('zephyr-transformer', () => { + describe('isZephyrTargetFile logic', () => { + // Helper to test file targeting logic + const isZephyrTargetFile = ( + filename: string, + code: string, + entryFiles: string[] + ): boolean => { + const isEntryFile = entryFiles.some((pattern) => { + if (pattern.includes('*')) { + const regex = new RegExp(pattern.replace(/\*/g, '.*')); + return regex.test(filename); + } + return filename.endsWith(pattern) || filename.includes(`/${pattern}`); + }); + + if (isEntryFile) { + return true; + } + + if ( + code.includes('AppRegistry.registerComponent') || + code.includes('AppRegistry.runApplication') + ) { + return true; + } + + return false; + }; + + const defaultEntryFiles = ['index.js', 'index.ts', 'index.tsx', 'App.js', 'App.tsx']; + + it('should target default entry files', () => { + expect(isZephyrTargetFile('/project/index.js', '', defaultEntryFiles)).toBe(true); + expect(isZephyrTargetFile('/project/App.tsx', '', defaultEntryFiles)).toBe(true); + expect(isZephyrTargetFile('/project/src/App.js', '', defaultEntryFiles)).toBe(true); + }); + + it('should not target non-entry files', () => { + expect(isZephyrTargetFile('/project/utils/helper.js', '', defaultEntryFiles)).toBe( + false + ); + expect( + isZephyrTargetFile('/project/components/Button.tsx', '', defaultEntryFiles) + ).toBe(false); + }); + + it('should target files with AppRegistry.registerComponent', () => { + const code = ` + import { AppRegistry } from 'react-native'; + AppRegistry.registerComponent('MyApp', () => App); + `; + expect(isZephyrTargetFile('/project/customEntry.js', code, defaultEntryFiles)).toBe( + true + ); + }); + + it('should target files with AppRegistry.runApplication', () => { + const code = ` + import { AppRegistry } from 'react-native'; + AppRegistry.runApplication('MyApp', { rootTag: 1 }); + `; + expect(isZephyrTargetFile('/project/customEntry.js', code, defaultEntryFiles)).toBe( + true + ); + }); + + it('should not target files that just import AppRegistry', () => { + const code = ` + import { AppRegistry } from 'react-native'; + export const something = AppRegistry; + `; + expect( + isZephyrTargetFile('/project/utils/registry.js', code, defaultEntryFiles) + ).toBe(false); + }); + + it('should support custom entry file patterns', () => { + const customEntryFiles = ['main.tsx', 'entry.js']; + expect(isZephyrTargetFile('/project/main.tsx', '', customEntryFiles)).toBe(true); + expect(isZephyrTargetFile('/project/entry.js', '', customEntryFiles)).toBe(true); + expect(isZephyrTargetFile('/project/index.js', '', customEntryFiles)).toBe(false); + }); + + it('should support glob patterns in entry files', () => { + const globEntryFiles = ['src/*/index.tsx', '*.entry.js']; + expect(isZephyrTargetFile('/project/src/app/index.tsx', '', globEntryFiles)).toBe( + true + ); + expect(isZephyrTargetFile('/project/main.entry.js', '', globEntryFiles)).toBe(true); + }); + }); + + describe('generateRuntimePluginCode', () => { + // Helper to test runtime code generation + const generateRuntimePluginCode = (manifestPath: string): string => { + return `// Zephyr Runtime Plugin for React Native +(function() { + if (typeof global !== 'undefined' && !global.__ZEPHYR_RUNTIME_PLUGIN__) { + // Prevent multiple initializations + try { + var createZephyrRuntimePlugin = require('zephyr-xpack-internal').createZephyrRuntimePlugin; + + var plugin = createZephyrRuntimePlugin({ + manifestUrl: '${manifestPath}', + }); + + // Store globally + global.__ZEPHYR_RUNTIME_PLUGIN__ = plugin; + + if (__DEV__) { + console.log('[Zephyr] Runtime plugin initialized'); + } + } catch (error) { + // zephyr-xpack-internal is an optional peer dependency + if (__DEV__) { + console.warn('[Zephyr] Runtime plugin not available:', error.message); + } + } + } +})();`; + }; + + it('should generate code with default manifest path', () => { + const code = generateRuntimePluginCode('/zephyr-manifest.json'); + expect(code).toContain("manifestUrl: '/zephyr-manifest.json'"); + }); + + it('should generate code with custom manifest path', () => { + const code = generateRuntimePluginCode('/custom-manifest.json'); + expect(code).toContain("manifestUrl: '/custom-manifest.json'"); + }); + + it('should check for existing runtime plugin', () => { + const code = generateRuntimePluginCode('/zephyr-manifest.json'); + expect(code).toContain('!global.__ZEPHYR_RUNTIME_PLUGIN__'); + }); + + it('should use zephyr-xpack-internal', () => { + const code = generateRuntimePluginCode('/zephyr-manifest.json'); + expect(code).toContain("require('zephyr-xpack-internal')"); + }); + + it('should only log in development mode', () => { + const code = generateRuntimePluginCode('/zephyr-manifest.json'); + expect(code).toContain('if (__DEV__)'); + }); + }); +}); diff --git a/libs/zephyr-metro-plugin/src/lib/zephyr-transformer.ts b/libs/zephyr-metro-plugin/src/lib/zephyr-transformer.ts new file mode 100644 index 00000000..ba119f99 --- /dev/null +++ b/libs/zephyr-metro-plugin/src/lib/zephyr-transformer.ts @@ -0,0 +1,138 @@ +import type { JsTransformOptions, JsTransformerConfig } from 'metro-transform-worker'; +import { ze_log } from 'zephyr-agent'; +// Note: Global type declarations are in ./global.d.ts (ambient, no runtime import needed) + +interface ZephyrTransformerOptions { + /** Custom manifest endpoint path */ + manifestPath?: string; + /** Custom entry file patterns for more conservative targeting */ + entryFiles?: string[]; +} + +/** Default entry file patterns if none specified */ +const DEFAULT_ENTRY_FILES = ['index.js', 'index.ts', 'index.tsx', 'App.js', 'App.tsx']; + +/** Metro transformer that injects Zephyr runtime capabilities */ +export async function transform( + config: JsTransformerConfig & { zephyrTransformerOptions?: ZephyrTransformerOptions }, + projectRoot: string, + filename: string, + data: Buffer, + options: JsTransformOptions +): Promise<{ + ast: any; + code: string; + map: any; +}> { + // Use default Metro transformer first + const upstream = require('metro-react-native-babel-transformer'); + const result = await upstream.transform(config, projectRoot, filename, data, options); + + // Get Zephyr transformer options from config + const zephyrOptions = config.zephyrTransformerOptions; + const entryFiles = zephyrOptions?.entryFiles || DEFAULT_ENTRY_FILES; + + // Only enhance entry files - use configurable patterns for more conservative targeting + const shouldEnhance = isZephyrTargetFile(filename, result.code, entryFiles); + + if (shouldEnhance) { + const manifestPath = zephyrOptions?.manifestPath || '/zephyr-manifest.json'; + const enhancedCode = injectZephyrRuntime(result.code, filename, manifestPath); + + return { + ...result, + code: enhancedCode, + }; + } + + return result; +} + +/** Check if file should be enhanced with Zephyr runtime */ +function isZephyrTargetFile( + filename: string, + code: string, + entryFiles: string[] +): boolean { + // Check against configured entry file patterns + const isEntryFile = entryFiles.some((pattern) => { + // Support glob-like patterns (simple matching) + if (pattern.includes('*')) { + const regex = new RegExp(pattern.replace(/\*/g, '.*')); + return regex.test(filename); + } + return filename.endsWith(pattern) || filename.includes(`/${pattern}`); + }); + + if (isEntryFile) { + return true; + } + + // Target files that register the app (React Native entry point detection) + if ( + code.includes('AppRegistry.registerComponent') || + code.includes('AppRegistry.runApplication') + ) { + return true; + } + + return false; +} + +/** Inject Zephyr runtime plugin capabilities */ +function injectZephyrRuntime( + originalCode: string, + filename: string, + manifestPath: string +): string { + try { + // Create runtime plugin initialization code + const runtimePluginCode = generateRuntimePluginCode(manifestPath); + + // Inject at the top of the file + const injectedCode = ` +// === Zephyr Runtime Plugin Injection === +${runtimePluginCode} +// === End Zephyr Injection === + +${originalCode} +`; + + ze_log.misc(`Injected Zephyr runtime into: ${filename}`); + return injectedCode; + } catch (error) { + ze_log.error(`Failed to inject Zephyr runtime into ${filename}: ${error}`); + return originalCode; // Return original on error + } +} + +/** Generate runtime plugin initialization code */ +function generateRuntimePluginCode(manifestPath: string): string { + // Generate runtime initialization code using template strings + // This is more robust than function stringification which can break with minification + return `// Zephyr Runtime Plugin for React Native +(function() { + if (typeof global !== 'undefined' && !global.__ZEPHYR_RUNTIME_PLUGIN__) { + // Prevent multiple initializations + try { + var createZephyrRuntimePlugin = require('zephyr-xpack-internal').createZephyrRuntimePlugin; + + var plugin = createZephyrRuntimePlugin({ + manifestUrl: '${manifestPath}', + }); + + // Store globally + global.__ZEPHYR_RUNTIME_PLUGIN__ = plugin; + + if (__DEV__) { + console.log('[Zephyr] Runtime plugin initialized'); + } + } catch (error) { + // zephyr-xpack-internal is an optional peer dependency + if (__DEV__) { + console.warn('[Zephyr] Runtime plugin not available:', error.message); + } + } + } +})();`; +} diff --git a/libs/zephyr-metro-plugin/tsconfig.json b/libs/zephyr-metro-plugin/tsconfig.json new file mode 100644 index 00000000..0d48ab0c --- /dev/null +++ b/libs/zephyr-metro-plugin/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "lib": ["ES2020", "ES2021"], + "declaration": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "allowSyntheticDefaultImports": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.spec.json" + }, + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/libs/zephyr-metro-plugin/tsconfig.lib.json b/libs/zephyr-metro-plugin/tsconfig.lib.json new file mode 100644 index 00000000..33eca2c2 --- /dev/null +++ b/libs/zephyr-metro-plugin/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/zephyr-metro-plugin/tsconfig.spec.json b/libs/zephyr-metro-plugin/tsconfig.spec.json new file mode 100644 index 00000000..0d3c604e --- /dev/null +++ b/libs/zephyr-metro-plugin/tsconfig.spec.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "moduleResolution": "node10", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/zephyr-rsbuild-plugin/project.json b/libs/zephyr-rsbuild-plugin/project.json index b782a2be..42d9efb1 100644 --- a/libs/zephyr-rsbuild-plugin/project.json +++ b/libs/zephyr-rsbuild-plugin/project.json @@ -4,6 +4,7 @@ "sourceRoot": "libs/zephyr-rsbuild-plugin/src", "projectType": "library", "tags": [], + "implicitDependencies": ["zephyr-rspack-plugin"], "targets": { "build": { "executor": "@nx/js:tsc", diff --git a/libs/zephyr-rsbuild-plugin/src/index.ts b/libs/zephyr-rsbuild-plugin/src/index.ts index 959c3559..5c2f11d0 100644 --- a/libs/zephyr-rsbuild-plugin/src/index.ts +++ b/libs/zephyr-rsbuild-plugin/src/index.ts @@ -1,4 +1,4 @@ export { withZephyr } from './rsbuild-plugin/with-zephyr'; export { onDeploymentDone, resolveIndexHtml } from 'zephyr-rspack-plugin'; -export type { ZephyrBuildHooks, DeploymentInfo } from 'zephyr-agent'; +export type { ZephyrBuildHooks, DeploymentInfo } from 'zephyr-rspack-plugin'; diff --git a/libs/zephyr-rspack-plugin/project.json b/libs/zephyr-rspack-plugin/project.json index ec1f5328..b131672b 100644 --- a/libs/zephyr-rspack-plugin/project.json +++ b/libs/zephyr-rspack-plugin/project.json @@ -4,6 +4,7 @@ "sourceRoot": "libs/zephyr-rspack-plugin/src", "projectType": "library", "tags": [], + "implicitDependencies": ["zephyr-agent", "zephyr-xpack-internal"], "targets": { "build": { "executor": "@nx/js:tsc", diff --git a/libs/zephyr-xpack-internal/src/index.ts b/libs/zephyr-xpack-internal/src/index.ts index 7f8aaaa8..6a608c44 100644 --- a/libs/zephyr-xpack-internal/src/index.ts +++ b/libs/zephyr-xpack-internal/src/index.ts @@ -25,3 +25,8 @@ export { setupManifestEmission } from './hooks/ze-emit-manifest'; export { xpack_zephyr_agent } from './xpack-extract/ze-xpack-upload-agent'; export { detectAndStoreBaseHref } from './basehref/basehref-integration'; + +export { + createZephyrRuntimePlugin, + type ZephyrRuntimePluginOptions, +} from './xpack-extract/runtime-plugin'; diff --git a/libs/zephyr-xpack-internal/src/xpack-extract/runtime-plugin.ts b/libs/zephyr-xpack-internal/src/xpack-extract/runtime-plugin.ts index 110c6c57..38353c86 100644 --- a/libs/zephyr-xpack-internal/src/xpack-extract/runtime-plugin.ts +++ b/libs/zephyr-xpack-internal/src/xpack-extract/runtime-plugin.ts @@ -5,34 +5,58 @@ import type { RemoteWithEntry, } from '../types/module-federation.types'; -// Ensure only one fetch is done by the app -const globalKey = '__ZEPHYR_MANIFEST_PROMISE__'; -const _global = typeof window !== 'undefined' ? window : globalThis; - -function getGlobalManifestPromise(): Promise | undefined { - return (_global as any)[globalKey]; -} - -function setGlobalManifestPromise(promise: Promise): void { - (_global as any)[globalKey] = promise; +/** Options for basic runtime plugin */ +export interface ZephyrRuntimePluginOptions { + /** Custom manifest URL (defaults to /zephyr-manifest.json) */ + manifestUrl?: string; } /** - * Zephyr Runtime Plugin for Module Federation This plugin handles dynamic remote URL - * resolution at runtime using beforeRequest hook to mutate URLs on the fly + * Basic Zephyr Runtime Plugin (no OTA features) Suitable for web applications that don't + * need OTA updates + * + * Features: + * + * - Simple manifest fetching + * - Remote URL resolution + * - Session storage override support + * + * For mobile applications with OTA support, use createZephyrRuntimePluginMobile */ -export function createZephyrRuntimePlugin(): FederationRuntimePlugin { +export function createZephyrRuntimePlugin( + options: ZephyrRuntimePluginOptions = {} +): FederationRuntimePlugin { + const { manifestUrl = '/zephyr-manifest.json' } = options; + let processedRemotes: Record | undefined; - // Start fetching manifest immediately - let zephyrManifestPromise = getGlobalManifestPromise(); + /** Fetches the zephyr-manifest.json file (basic version without OTA) */ + async function fetchManifest(url: string): Promise { + try { + const response = await fetch(url); + + if (!response.ok) { + return; + } - if (!zephyrManifestPromise) { - zephyrManifestPromise = fetchZephyrManifest(); - setGlobalManifestPromise(zephyrManifestPromise); + const manifest = await response.json().catch(() => undefined); + + if (!manifest) { + console.error('[Zephyr] Failed to parse manifest JSON'); + return; + } + + return manifest; + } catch (error) { + console.error('[Zephyr] Unexpected error fetching manifest:', error); + return; + } } - return { + // Initialize manifest fetching + const zephyrManifestPromise = fetchManifest(manifestUrl); + + const plugin: FederationRuntimePlugin = { name: 'zephyr-runtime-remote-resolver', async beforeRequest(args) { const zephyrManifest = await zephyrManifestPromise; @@ -66,30 +90,8 @@ export function createZephyrRuntimePlugin(): FederationRuntimePlugin { return args; }, }; -} - -/** Fetches the zephyr-manifest.json file and returns the runtime plugin data */ -async function fetchZephyrManifest(): Promise { - try { - // Fetch the manifest from the same origin - const response = await fetch('/zephyr-manifest.json'); - - if (!response.ok) { - return; - } - const manifest = await response.json().catch(() => undefined); - - if (!manifest) { - console.error('Failed to parse manifest JSON'); - return; - } - - return manifest; - } catch { - console.error('Unexpected error fetching manifest'); - return; - } + return plugin; } function identifyRemotes( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d66520e4..8ab64c5d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1510,6 +1510,46 @@ importers: specifier: catalog:typescript version: 29.3.0(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.1)(jest@30.0.5(@types/node@22.13.13)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.1))(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.13.13)(typescript@5.9.3)))(typescript@5.9.3) + libs/zephyr-metro-plugin: + dependencies: + metro: + specifier: '>=0.70.0' + version: 0.83.3 + react-native: + specifier: '>=0.60.0' + version: 0.82.1(@babel/core@7.28.4)(@types/react@19.2.2)(react@19.2.0) + zephyr-agent: + specifier: workspace:* + version: link:../zephyr-agent + zephyr-edge-contract: + specifier: workspace:* + version: link:../zephyr-edge-contract + zephyr-xpack-internal: + specifier: workspace:* + version: link:../zephyr-xpack-internal + devDependencies: + '@rspack/core': + specifier: ^1.5.5 + version: 1.5.5(@swc/helpers@0.5.17) + '@types/find-package-json': + specifier: ^1.2.6 + version: 1.2.6 + '@types/jest': + specifier: catalog:typescript + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: catalog:eslint + version: 8.27.0(@typescript-eslint/parser@8.36.0(eslint@9.23.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.23.0(jiti@2.6.1))(typescript@5.9.3) + metro-config: + specifier: ^0.83.3 + version: 0.83.3 + metro-transform-worker: + specifier: ^0.83.3 + version: 0.83.3 + ts-jest: + specifier: catalog:typescript + version: 29.3.0(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.1)(jest@30.0.5(@types/node@24.5.2)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.1))(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@24.5.2)(typescript@5.9.3)))(typescript@5.9.3) + libs/zephyr-modernjs-plugin: dependencies: '@modern-js/app-tools': @@ -3144,6 +3184,10 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} + '@isaacs/ttlcache@1.4.1': + resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==} + engines: {node: '>=12'} + '@istanbuljs/load-nyc-config@1.1.0': resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} @@ -3169,6 +3213,10 @@ packages: node-notifier: optional: true + '@jest/create-cache-key-function@29.7.0': + resolution: {integrity: sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/create-cache-key-function@30.0.5': resolution: {integrity: sha512-W1kmkwPq/WTMQWgvbzWSCbXSqvjI6rkqBQCxuvYmd+g6o4b5gHP98ikfh/Ei0SKzHvWdI84TOXp0hRcbpr8Q0w==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -5074,6 +5122,62 @@ packages: '@polka/url@1.0.0-next.28': resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} + '@react-native/assets-registry@0.82.1': + resolution: {integrity: sha512-B1SRwpntaAcckiatxbjzylvNK562Ayza05gdJCjDQHTiDafa1OABmyB5LHt7qWDOpNkaluD+w11vHF7pBmTpzQ==} + engines: {node: '>= 20.19.4'} + + '@react-native/codegen@0.82.1': + resolution: {integrity: sha512-ezXTN70ygVm9l2m0i+pAlct0RntoV4afftWMGUIeAWLgaca9qItQ54uOt32I/9dBJvzBibT33luIR/pBG0dQvg==} + engines: {node: '>= 20.19.4'} + peerDependencies: + '@babel/core': '*' + + '@react-native/community-cli-plugin@0.82.1': + resolution: {integrity: sha512-H/eMdtOy9nEeX7YVeEG1N2vyCoifw3dr9OV8++xfUElNYV7LtSmJ6AqxZUUfxGJRDFPQvaU/8enmJlM/l11VxQ==} + engines: {node: '>= 20.19.4'} + peerDependencies: + '@react-native-community/cli': '*' + '@react-native/metro-config': '*' + peerDependenciesMeta: + '@react-native-community/cli': + optional: true + '@react-native/metro-config': + optional: true + + '@react-native/debugger-frontend@0.82.1': + resolution: {integrity: sha512-a2O6M7/OZ2V9rdavOHyCQ+10z54JX8+B+apYKCQ6a9zoEChGTxUMG2YzzJ8zZJVvYf1ByWSNxv9Se0dca1hO9A==} + engines: {node: '>= 20.19.4'} + + '@react-native/debugger-shell@0.82.1': + resolution: {integrity: sha512-fdRHAeqqPT93bSrxfX+JHPpCXHApfDUdrXMXhoxlPgSzgXQXJDykIViKhtpu0M6slX6xU/+duq+AtP/qWJRpBw==} + engines: {node: '>= 20.19.4'} + + '@react-native/dev-middleware@0.82.1': + resolution: {integrity: sha512-wuOIzms/Qg5raBV6Ctf2LmgzEOCqdP3p1AYN4zdhMT110c39TVMbunpBaJxm0Kbt2HQ762MQViF9naxk7SBo4w==} + engines: {node: '>= 20.19.4'} + + '@react-native/gradle-plugin@0.82.1': + resolution: {integrity: sha512-KkF/2T1NSn6EJ5ALNT/gx0MHlrntFHv8YdooH9OOGl9HQn5NM0ZmQSr86o5utJsGc7ME3R6p3SaQuzlsFDrn8Q==} + engines: {node: '>= 20.19.4'} + + '@react-native/js-polyfills@0.82.1': + resolution: {integrity: sha512-tf70X7pUodslOBdLN37J57JmDPB/yiZcNDzS2m+4bbQzo8fhx3eG9QEBv5n4fmzqfGAgSB4BWRHgDMXmmlDSVA==} + engines: {node: '>= 20.19.4'} + + '@react-native/normalize-colors@0.82.1': + resolution: {integrity: sha512-CCfTR1uX+Z7zJTdt3DNX9LUXr2zWXsNOyLbwupW2wmRzrxlHRYfmLgTABzRL/cKhh0Ubuwn15o72MQChvCRaHw==} + + '@react-native/virtualized-lists@0.82.1': + resolution: {integrity: sha512-f5zpJg9gzh7JtCbsIwV+4kP3eI0QBuA93JGmwFRd4onQ3DnCjV2J5pYqdWtM95sjSKK1dyik59Gj01lLeKqs1Q==} + engines: {node: '>= 20.19.4'} + peerDependencies: + '@types/react': ^19.1.1 + react: '*' + react-native: '*' + peerDependenciesMeta: + '@types/react': + optional: true + '@remix-run/router@1.20.0': resolution: {integrity: sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==} engines: {node: '>=14.0.0'} @@ -7057,6 +7161,9 @@ packages: ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + anser@1.4.10: + resolution: {integrity: sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==} + ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} @@ -7226,6 +7333,9 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + async-limiter@1.0.1: + resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==} + async-sema@3.1.1: resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} @@ -7357,6 +7467,9 @@ packages: peerDependencies: styled-components: '>= 2' + babel-plugin-syntax-hermes-parser@0.32.0: + resolution: {integrity: sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg==} + babel-plugin-syntax-jsx@6.18.0: resolution: {integrity: sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==} @@ -7737,10 +7850,21 @@ packages: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} + chrome-launcher@0.15.2: + resolution: {integrity: sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==} + engines: {node: '>=12.13.0'} + hasBin: true + chrome-trace-event@1.0.4: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} + chromium-edge-launcher@0.2.0: + resolution: {integrity: sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==} + + ci-info@2.0.0: + resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -9048,6 +9172,9 @@ packages: resolution: {integrity: sha512-xvHszRavo28ejws8FpemjhwswGj4w/BetHIL8cU49u4sGyXDw2+p3YbeDbj6xzlxi6kWTjIRSTJ+9sNXPnF0Zg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + exponential-backoff@3.1.3: + resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + express@4.21.2: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} @@ -9104,6 +9231,11 @@ packages: resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} engines: {node: '>=0.8.0'} + fb-dotslash@0.5.8: + resolution: {integrity: sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA==} + engines: {node: '>=20'} + hasBin: true + fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} @@ -9233,6 +9365,9 @@ packages: flexsearch@0.7.43: resolution: {integrity: sha512-c5o/+Um8aqCSOXGcZoqZOm+NqtVwNsvVpWv6lfmSclU954O3wvQKxxK8zj74fPaSJbXpSLTs4PRhh+wnoCXnKg==} + flow-enums-runtime@0.0.6: + resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==} + follow-redirects@1.15.9: resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} engines: {node: '>=4.0'} @@ -9603,6 +9738,15 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true + hermes-compiler@0.0.0: + resolution: {integrity: sha512-boVFutx6ME/Km2mB6vvsQcdnazEYYI/jV1pomx1wcFUG/EVqTkr5CU0CW9bKipOA/8Hyu3NYwW3THg2Q1kNCfA==} + + hermes-estree@0.32.0: + resolution: {integrity: sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==} + + hermes-parser@0.32.0: + resolution: {integrity: sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==} + highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} @@ -9866,6 +10010,11 @@ packages: engines: {node: '>=0.10.0'} hasBin: true + image-size@1.2.1: + resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==} + engines: {node: '>=16.x'} + hasBin: true + immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} @@ -10524,6 +10673,10 @@ packages: resolution: {integrity: sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-validate@30.0.5: resolution: {integrity: sha512-ouTm6VFHaS2boyl+k4u+Qip4TSH7Uld5tyD8psQ8abGgt2uYYB8VwVfAHWHjHc0NWmGGbwO5h0sCPOGHHevefw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -10609,6 +10762,9 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsc-safe-url@0.2.4: + resolution: {integrity: sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==} + jsdom@20.0.3: resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} engines: {node: '>=14'} @@ -10760,6 +10916,9 @@ packages: lie@3.1.1: resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} + lighthouse-logger@1.4.2: + resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==} + lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} @@ -10978,6 +11137,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.throttle@4.1.1: + resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} + lodash.unionby@4.8.0: resolution: {integrity: sha512-e60kn4GJIunNkw6v9MxRnUuLYI/Tyuanch7ozoCtk/1irJTYBj+qNTxr5B3qVflmJhwStJBv387Cb+9VOfABMg==} @@ -11085,6 +11247,9 @@ packages: markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + marky@1.3.0: + resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -11229,6 +11394,9 @@ packages: resolution: {integrity: sha512-RG+4HMGyIVp6UWDWbFmZ38yKrSzblPnfJu0PyPt0hw52KW4PPlPp+HdV4qZBG0hLDuYVnf8wfQT4NymKXnlQjA==} engines: {node: '>= 4.0.0'} + memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} @@ -11243,6 +11411,64 @@ packages: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} + metro-babel-transformer@0.83.3: + resolution: {integrity: sha512-1vxlvj2yY24ES1O5RsSIvg4a4WeL7PFXgKOHvXTXiW0deLvQr28ExXj6LjwCCDZ4YZLhq6HddLpZnX4dEdSq5g==} + engines: {node: '>=20.19.4'} + + metro-cache-key@0.83.3: + resolution: {integrity: sha512-59ZO049jKzSmvBmG/B5bZ6/dztP0ilp0o988nc6dpaDsU05Cl1c/lRf+yx8m9WW/JVgbmfO5MziBU559XjI5Zw==} + engines: {node: '>=20.19.4'} + + metro-cache@0.83.3: + resolution: {integrity: sha512-3jo65X515mQJvKqK3vWRblxDEcgY55Sk3w4xa6LlfEXgQ9g1WgMh9m4qVZVwgcHoLy0a2HENTPCCX4Pk6s8c8Q==} + engines: {node: '>=20.19.4'} + + metro-config@0.83.3: + resolution: {integrity: sha512-mTel7ipT0yNjKILIan04bkJkuCzUUkm2SeEaTads8VfEecCh+ltXchdq6DovXJqzQAXuR2P9cxZB47Lg4klriA==} + engines: {node: '>=20.19.4'} + + metro-core@0.83.3: + resolution: {integrity: sha512-M+X59lm7oBmJZamc96usuF1kusd5YimqG/q97g4Ac7slnJ3YiGglW5CsOlicTR5EWf8MQFxxjDoB6ytTqRe8Hw==} + engines: {node: '>=20.19.4'} + + metro-file-map@0.83.3: + resolution: {integrity: sha512-jg5AcyE0Q9Xbbu/4NAwwZkmQn7doJCKGW0SLeSJmzNB9Z24jBe0AL2PHNMy4eu0JiKtNWHz9IiONGZWq7hjVTA==} + engines: {node: '>=20.19.4'} + + metro-minify-terser@0.83.3: + resolution: {integrity: sha512-O2BmfWj6FSfzBLrNCXt/rr2VYZdX5i6444QJU0fFoc7Ljg+Q+iqebwE3K0eTvkI6TRjELsXk1cjU+fXwAR4OjQ==} + engines: {node: '>=20.19.4'} + + metro-resolver@0.83.3: + resolution: {integrity: sha512-0js+zwI5flFxb1ktmR///bxHYg7OLpRpWZlBBruYG8OKYxeMP7SV0xQ/o/hUelrEMdK4LJzqVtHAhBm25LVfAQ==} + engines: {node: '>=20.19.4'} + + metro-runtime@0.83.3: + resolution: {integrity: sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw==} + engines: {node: '>=20.19.4'} + + metro-source-map@0.83.3: + resolution: {integrity: sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg==} + engines: {node: '>=20.19.4'} + + metro-symbolicate@0.83.3: + resolution: {integrity: sha512-F/YChgKd6KbFK3eUR5HdUsfBqVsanf5lNTwFd4Ca7uuxnHgBC3kR/Hba/RGkenR3pZaGNp5Bu9ZqqP52Wyhomw==} + engines: {node: '>=20.19.4'} + hasBin: true + + metro-transform-plugins@0.83.3: + resolution: {integrity: sha512-eRGoKJU6jmqOakBMH5kUB7VitEWiNrDzBHpYbkBXW7C5fUGeOd2CyqrosEzbMK5VMiZYyOcNFEphvxk3OXey2A==} + engines: {node: '>=20.19.4'} + + metro-transform-worker@0.83.3: + resolution: {integrity: sha512-Ztekew9t/gOIMZX1tvJOgX7KlSLL5kWykl0Iwu2cL2vKMKVALRl1hysyhUw0vjpAvLFx+Kfq9VLjnHIkW32fPA==} + engines: {node: '>=20.19.4'} + + metro@0.83.3: + resolution: {integrity: sha512-+rP+/GieOzkt97hSJ0MrPOuAH/jpaS21ZDvL9DJ35QYRDlQcwzcvUlGUf79AnQxq/2NPiS/AULhhM4TKutIt8Q==} + engines: {node: '>=20.19.4'} + hasBin: true + micromark-core-commonmark@1.1.0: resolution: {integrity: sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==} @@ -11565,6 +11791,11 @@ packages: resolution: {integrity: sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==} engines: {node: '>= 18'} + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + mkdirp@3.0.1: resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} engines: {node: '>=10'} @@ -11791,6 +12022,10 @@ packages: engines: {node: '>=18'} hasBin: true + ob1@0.83.3: + resolution: {integrity: sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA==} + engines: {node: '>=20.19.4'} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -11880,6 +12115,10 @@ packages: resolution: {integrity: sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==} engines: {node: '>=18'} + open@7.4.2: + resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} + engines: {node: '>=8'} + open@8.4.2: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} @@ -12762,6 +13001,9 @@ packages: promise@7.3.1: resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} + promise@8.3.0: + resolution: {integrity: sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -12864,6 +13106,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + queue@6.0.2: + resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + quick-lru@5.1.1: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} @@ -12896,6 +13141,9 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true + react-devtools-core@6.1.5: + resolution: {integrity: sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==} + react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: @@ -12937,6 +13185,17 @@ packages: react-lazy-with-preload@2.2.1: resolution: {integrity: sha512-ONSb8gizLE5jFpdHAclZ6EAAKuFX2JydnFXPPPjoUImZlLjGtKzyBS8SJgJq7CpLgsGKh9QCZdugJyEEOVC16Q==} + react-native@0.82.1: + resolution: {integrity: sha512-tFAqcU7Z4g49xf/KnyCEzI4nRTu1Opcx05Ov2helr8ZTg1z7AJR/3sr2rZ+AAVlAs2IXk+B0WOxXGmdD3+4czA==} + engines: {node: '>= 20.19.4'} + hasBin: true + peerDependencies: + '@types/react': ^19.1.1 + react: ^19.1.1 + peerDependenciesMeta: + '@types/react': + optional: true + react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} @@ -13070,6 +13329,9 @@ packages: regenerate@1.4.2: resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} @@ -13801,6 +14063,9 @@ packages: scheduler@0.25.0: resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -13882,6 +14147,10 @@ packages: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} + serialize-error@2.1.0: + resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==} + engines: {node: '>=0.10.0'} + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -14083,6 +14352,10 @@ packages: source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -14132,6 +14405,10 @@ packages: stackframe@1.3.4: resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + stacktrace-parser@0.1.11: + resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==} + engines: {node: '>=6'} + statuses@1.5.0: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} @@ -14463,6 +14740,9 @@ packages: peerDependencies: tslib: ^2 + throat@5.0.0: + resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==} + through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -14744,6 +15024,10 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} + type-fest@0.7.1: + resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} + engines: {node: '>=8'} + type-fest@0.8.1: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} @@ -15230,6 +15514,9 @@ packages: jsdom: optional: true + vlq@1.0.1: + resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==} + vm-browserify@1.1.2: resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} @@ -15359,6 +15646,9 @@ packages: resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} engines: {node: '>=12'} + whatwg-fetch@3.6.20: + resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} @@ -15456,6 +15746,17 @@ packages: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ws@6.2.3: + resolution: {integrity: sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@7.5.10: resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} engines: {node: '>=8.3.0'} @@ -15906,14 +16207,14 @@ snapshots: '@babel/helper-member-expression-to-functions@7.25.9': dependencies: '@babel/traverse': 7.28.4(supports-color@5.5.0) - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color '@babel/helper-member-expression-to-functions@7.27.1': dependencies: '@babel/traverse': 7.28.4(supports-color@5.5.0) - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color @@ -15951,11 +16252,11 @@ snapshots: '@babel/helper-optimise-call-expression@7.25.9': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@babel/helper-plugin-utils@7.26.5': {} @@ -16018,7 +16319,7 @@ snapshots: dependencies: '@babel/template': 7.27.2 '@babel/traverse': 7.28.4(supports-color@5.5.0) - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color @@ -16765,8 +17066,8 @@ snapshots: '@babel/template@7.26.9': dependencies: '@babel/code-frame': 7.26.2 - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 '@babel/template@7.27.2': dependencies: @@ -17146,6 +17447,8 @@ snapshots: dependencies: minipass: 7.1.2 + '@isaacs/ttlcache@1.4.1': {} + '@istanbuljs/load-nyc-config@1.1.0': dependencies: camelcase: 5.3.1 @@ -17318,6 +17621,10 @@ snapshots: - supports-color - ts-node + '@jest/create-cache-key-function@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@jest/create-cache-key-function@30.0.5': dependencies: '@jest/types': 30.0.5 @@ -21129,6 +21436,73 @@ snapshots: '@polka/url@1.0.0-next.28': {} + '@react-native/assets-registry@0.82.1': {} + + '@react-native/codegen@0.82.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/parser': 7.28.5 + glob: 13.0.0 + hermes-parser: 0.32.0 + invariant: 2.2.4 + nullthrows: 1.1.1 + yargs: 17.7.2 + + '@react-native/community-cli-plugin@0.82.1': + dependencies: + '@react-native/dev-middleware': 0.82.1 + debug: 4.4.3 + invariant: 2.2.4 + metro: 0.83.3 + metro-config: 0.83.3 + metro-core: 0.83.3 + semver: 7.7.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@react-native/debugger-frontend@0.82.1': {} + + '@react-native/debugger-shell@0.82.1': + dependencies: + cross-spawn: 7.0.6 + fb-dotslash: 0.5.8 + + '@react-native/dev-middleware@0.82.1': + dependencies: + '@isaacs/ttlcache': 1.4.1 + '@react-native/debugger-frontend': 0.82.1 + '@react-native/debugger-shell': 0.82.1 + chrome-launcher: 0.15.2 + chromium-edge-launcher: 0.2.0 + connect: 3.7.0 + debug: 4.4.3 + invariant: 2.2.4 + nullthrows: 1.1.1 + open: 7.4.2 + serve-static: 1.16.2 + ws: 6.2.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@react-native/gradle-plugin@0.82.1': {} + + '@react-native/js-polyfills@0.82.1': {} + + '@react-native/normalize-colors@0.82.1': {} + + '@react-native/virtualized-lists@0.82.1(@types/react@19.2.2)(react-native@0.82.1(@babel/core@7.28.4)(@types/react@19.2.2)(react@19.2.0))(react@19.2.0)': + dependencies: + invariant: 2.2.4 + nullthrows: 1.1.1 + react: 19.2.0 + react-native: 0.82.1(@babel/core@7.28.4)(@types/react@19.2.2)(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@remix-run/router@1.20.0': {} '@remix-run/router@1.22.0': {} @@ -23740,6 +24114,8 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + anser@1.4.10: {} + ansi-align@3.0.1: dependencies: string-width: 4.2.3 @@ -24008,6 +24384,8 @@ snapshots: async-function@1.0.0: {} + async-limiter@1.0.1: {} + async-sema@3.1.1: {} async@3.2.6: {} @@ -24150,7 +24528,7 @@ snapshots: babel-plugin-jest-hoist@30.0.1: dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@types/babel__core': 7.20.5 babel-plugin-macros@3.1.0: @@ -24227,6 +24605,10 @@ snapshots: - '@babel/core' - supports-color + babel-plugin-syntax-hermes-parser@0.32.0: + dependencies: + hermes-parser: 0.32.0 + babel-plugin-syntax-jsx@6.18.0: {} babel-plugin-transform-react-remove-prop-types@0.4.24: {} @@ -24271,7 +24653,7 @@ snapshots: babel-walk@3.0.0-canary-5: dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 background-only@0.0.1: {} @@ -24655,8 +25037,30 @@ snapshots: chownr@3.0.0: {} + chrome-launcher@0.15.2: + dependencies: + '@types/node': 24.5.2 + escape-string-regexp: 4.0.0 + is-wsl: 2.2.0 + lighthouse-logger: 1.4.2 + transitivePeerDependencies: + - supports-color + chrome-trace-event@1.0.4: {} + chromium-edge-launcher@0.2.0: + dependencies: + '@types/node': 24.5.2 + escape-string-regexp: 4.0.0 + is-wsl: 2.2.0 + lighthouse-logger: 1.4.2 + mkdirp: 1.0.4 + rimraf: 3.0.2 + transitivePeerDependencies: + - supports-color + + ci-info@2.0.0: {} + ci-info@3.9.0: {} ci-info@4.2.0: {} @@ -24851,8 +25255,8 @@ snapshots: constantinople@4.0.1: dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 constants-browserify@1.0.0: {} @@ -26140,6 +26544,8 @@ snapshots: jest-mock: 30.0.5 jest-util: 30.0.5 + exponential-backoff@3.1.3: {} + express@4.21.2: dependencies: accepts: 1.3.8 @@ -26227,6 +26633,8 @@ snapshots: dependencies: websocket-driver: 0.7.4 + fb-dotslash@0.5.8: {} + fb-watchman@2.0.2: dependencies: bser: 2.1.1 @@ -26365,6 +26773,8 @@ snapshots: flexsearch@0.7.43: {} + flow-enums-runtime@0.0.6: {} + follow-redirects@1.15.9(debug@4.4.0): optionalDependencies: debug: 4.4.0 @@ -26900,6 +27310,14 @@ snapshots: he@1.2.0: {} + hermes-compiler@0.0.0: {} + + hermes-estree@0.32.0: {} + + hermes-parser@0.32.0: + dependencies: + hermes-estree: 0.32.0 + highlight.js@10.7.3: {} highlightjs-vue@1.0.0: {} @@ -27209,6 +27627,10 @@ snapshots: image-size@0.5.5: optional: true + image-size@1.2.1: + dependencies: + queue: 6.0.2 + immediate@3.0.6: {} immutable@5.0.3: {} @@ -28406,7 +28828,7 @@ snapshots: '@babel/generator': 7.28.3 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.4) - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@jest/expect-utils': 30.1.2 '@jest/get-type': 30.1.0 '@jest/snapshot-utils': 30.1.2 @@ -28444,6 +28866,15 @@ snapshots: graceful-fs: 4.2.11 picomatch: 4.0.3 + jest-validate@29.7.0: + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + jest-validate@30.0.5: dependencies: '@jest/get-type': 30.0.1 @@ -28597,6 +29028,8 @@ snapshots: dependencies: argparse: 2.0.1 + jsc-safe-url@0.2.4: {} + jsdom@20.0.3: dependencies: abab: 2.0.6 @@ -28781,6 +29214,13 @@ snapshots: dependencies: immediate: 3.0.6 + lighthouse-logger@1.4.2: + dependencies: + debug: 2.6.9 + marky: 1.3.0 + transitivePeerDependencies: + - supports-color + lightningcss-android-arm64@1.30.2: optional: true @@ -28969,6 +29409,8 @@ snapshots: lodash.merge@4.6.2: {} + lodash.throttle@4.1.1: {} + lodash.unionby@4.8.0: {} lodash.uniq@4.5.0: {} @@ -29079,6 +29521,8 @@ snapshots: markdown-table@3.0.4: {} + marky@1.3.0: {} + math-intrinsics@1.1.0: {} md5.js@1.3.5: @@ -29442,6 +29886,8 @@ snapshots: tree-dump: 1.1.0(tslib@2.8.1) tslib: 2.8.1 + memoize-one@5.2.1: {} + merge-descriptors@1.0.3: {} merge-stream@2.0.0: {} @@ -29450,6 +29896,181 @@ snapshots: methods@1.1.2: {} + metro-babel-transformer@0.83.3: + dependencies: + '@babel/core': 7.28.4 + flow-enums-runtime: 0.0.6 + hermes-parser: 0.32.0 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + + metro-cache-key@0.83.3: + dependencies: + flow-enums-runtime: 0.0.6 + + metro-cache@0.83.3: + dependencies: + exponential-backoff: 3.1.3 + flow-enums-runtime: 0.0.6 + https-proxy-agent: 7.0.6 + metro-core: 0.83.3 + transitivePeerDependencies: + - supports-color + + metro-config@0.83.3: + dependencies: + connect: 3.7.0 + flow-enums-runtime: 0.0.6 + jest-validate: 29.7.0 + metro: 0.83.3 + metro-cache: 0.83.3 + metro-core: 0.83.3 + metro-runtime: 0.83.3 + yaml: 2.8.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + metro-core@0.83.3: + dependencies: + flow-enums-runtime: 0.0.6 + lodash.throttle: 4.1.1 + metro-resolver: 0.83.3 + + metro-file-map@0.83.3: + dependencies: + debug: 4.4.3 + fb-watchman: 2.0.2 + flow-enums-runtime: 0.0.6 + graceful-fs: 4.2.11 + invariant: 2.2.4 + jest-worker: 29.7.0 + micromatch: 4.0.8 + nullthrows: 1.1.1 + walker: 1.0.8 + transitivePeerDependencies: + - supports-color + + metro-minify-terser@0.83.3: + dependencies: + flow-enums-runtime: 0.0.6 + terser: 5.44.0 + + metro-resolver@0.83.3: + dependencies: + flow-enums-runtime: 0.0.6 + + metro-runtime@0.83.3: + dependencies: + '@babel/runtime': 7.26.10 + flow-enums-runtime: 0.0.6 + + metro-source-map@0.83.3: + dependencies: + '@babel/traverse': 7.28.4(supports-color@5.5.0) + '@babel/traverse--for-generate-function-map': '@babel/traverse@7.28.4(supports-color@5.5.0)' + '@babel/types': 7.28.5 + flow-enums-runtime: 0.0.6 + invariant: 2.2.4 + metro-symbolicate: 0.83.3 + nullthrows: 1.1.1 + ob1: 0.83.3 + source-map: 0.5.7 + vlq: 1.0.1 + transitivePeerDependencies: + - supports-color + + metro-symbolicate@0.83.3: + dependencies: + flow-enums-runtime: 0.0.6 + invariant: 2.2.4 + metro-source-map: 0.83.3 + nullthrows: 1.1.1 + source-map: 0.5.7 + vlq: 1.0.1 + transitivePeerDependencies: + - supports-color + + metro-transform-plugins@0.83.3: + dependencies: + '@babel/core': 7.28.4 + '@babel/generator': 7.28.3 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.4(supports-color@5.5.0) + flow-enums-runtime: 0.0.6 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + + metro-transform-worker@0.83.3: + dependencies: + '@babel/core': 7.28.4 + '@babel/generator': 7.28.3 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + flow-enums-runtime: 0.0.6 + metro: 0.83.3 + metro-babel-transformer: 0.83.3 + metro-cache: 0.83.3 + metro-cache-key: 0.83.3 + metro-minify-terser: 0.83.3 + metro-source-map: 0.83.3 + metro-transform-plugins: 0.83.3 + nullthrows: 1.1.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + metro@0.83.3: + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/core': 7.28.4 + '@babel/generator': 7.28.3 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.4(supports-color@5.5.0) + '@babel/types': 7.28.5 + accepts: 1.3.8 + chalk: 4.1.2 + ci-info: 2.0.0 + connect: 3.7.0 + debug: 4.4.3 + error-stack-parser: 2.1.4 + flow-enums-runtime: 0.0.6 + graceful-fs: 4.2.11 + hermes-parser: 0.32.0 + image-size: 1.2.1 + invariant: 2.2.4 + jest-worker: 29.7.0 + jsc-safe-url: 0.2.4 + lodash.throttle: 4.1.1 + metro-babel-transformer: 0.83.3 + metro-cache: 0.83.3 + metro-cache-key: 0.83.3 + metro-config: 0.83.3 + metro-core: 0.83.3 + metro-file-map: 0.83.3 + metro-resolver: 0.83.3 + metro-runtime: 0.83.3 + metro-source-map: 0.83.3 + metro-symbolicate: 0.83.3 + metro-transform-plugins: 0.83.3 + metro-transform-worker: 0.83.3 + mime-types: 2.1.35 + nullthrows: 1.1.1 + serialize-error: 2.1.0 + source-map: 0.5.7 + throat: 5.0.0 + ws: 7.5.10 + yargs: 17.7.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + micromark-core-commonmark@1.1.0: dependencies: decode-named-character-reference: 1.1.0 @@ -30069,6 +30690,8 @@ snapshots: minipass: 7.1.2 rimraf: 5.0.10 + mkdirp@1.0.4: {} + mkdirp@3.0.1: {} mlly@1.6.1: @@ -30356,6 +30979,10 @@ snapshots: transitivePeerDependencies: - supports-color + ob1@0.83.3: + dependencies: + flow-enums-runtime: 0.0.6 + object-assign@4.1.1: {} object-hash@3.0.0: {} @@ -30462,6 +31089,11 @@ snapshots: is-inside-container: 1.0.0 is-wsl: 3.1.0 + open@7.4.2: + dependencies: + is-docker: 2.2.1 + is-wsl: 2.2.0 + open@8.4.2: dependencies: define-lazy-prop: 2.0.0 @@ -31377,6 +32009,10 @@ snapshots: dependencies: asap: 2.0.6 + promise@8.3.0: + dependencies: + asap: 2.0.6 + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -31514,6 +32150,10 @@ snapshots: queue-microtask@1.2.3: {} + queue@6.0.2: + dependencies: + inherits: 2.0.4 + quick-lru@5.1.1: {} radix3@1.1.2: {} @@ -31547,6 +32187,14 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 + react-devtools-core@6.1.5: + dependencies: + shell-quote: 1.8.2 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 @@ -31591,6 +32239,54 @@ snapshots: react-lazy-with-preload@2.2.1: {} + react-native@0.82.1(@babel/core@7.28.4)(@types/react@19.2.2)(react@19.2.0): + dependencies: + '@jest/create-cache-key-function': 29.7.0 + '@react-native/assets-registry': 0.82.1 + '@react-native/codegen': 0.82.1(@babel/core@7.28.4) + '@react-native/community-cli-plugin': 0.82.1 + '@react-native/gradle-plugin': 0.82.1 + '@react-native/js-polyfills': 0.82.1 + '@react-native/normalize-colors': 0.82.1 + '@react-native/virtualized-lists': 0.82.1(@types/react@19.2.2)(react-native@0.82.1(@babel/core@7.28.4)(@types/react@19.2.2)(react@19.2.0))(react@19.2.0) + abort-controller: 3.0.0 + anser: 1.4.10 + ansi-regex: 5.0.1 + babel-jest: 29.7.0(@babel/core@7.28.4) + babel-plugin-syntax-hermes-parser: 0.32.0 + base64-js: 1.5.1 + commander: 12.1.0 + flow-enums-runtime: 0.0.6 + glob: 13.0.0 + hermes-compiler: 0.0.0 + invariant: 2.2.4 + jest-environment-node: 29.7.0 + memoize-one: 5.2.1 + metro-runtime: 0.83.3 + metro-source-map: 0.83.3 + nullthrows: 1.1.1 + pretty-format: 29.7.0 + promise: 8.3.0 + react: 19.2.0 + react-devtools-core: 6.1.5 + react-refresh: 0.14.2 + regenerator-runtime: 0.13.11 + scheduler: 0.26.0 + semver: 7.7.3 + stacktrace-parser: 0.1.11 + whatwg-fetch: 3.6.20 + ws: 6.2.3 + yargs: 17.7.2 + optionalDependencies: + '@types/react': 19.2.2 + transitivePeerDependencies: + - '@babel/core' + - '@react-native-community/cli' + - '@react-native/metro-config' + - bufferutil + - supports-color + - utf-8-validate + react-refresh@0.14.2: {} react-refresh@0.16.0: {} @@ -31775,6 +32471,8 @@ snapshots: regenerate@1.4.2: {} + regenerator-runtime@0.13.11: {} + regenerator-runtime@0.14.1: {} regenerator-transform@0.15.2: @@ -32565,6 +33263,8 @@ snapshots: scheduler@0.25.0: {} + scheduler@0.26.0: {} + scheduler@0.27.0: {} schema-utils@3.3.0: @@ -32650,6 +33350,8 @@ snapshots: transitivePeerDependencies: - supports-color + serialize-error@2.1.0: {} + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -32955,6 +33657,8 @@ snapshots: buffer-from: 1.1.2 source-map: 0.6.1 + source-map@0.5.7: {} + source-map@0.6.1: {} source-map@0.7.4: {} @@ -33009,6 +33713,10 @@ snapshots: stackframe@1.3.4: {} + stacktrace-parser@0.1.11: + dependencies: + type-fest: 0.7.1 + statuses@1.5.0: {} statuses@2.0.1: {} @@ -33503,6 +34211,8 @@ snapshots: dependencies: tslib: 2.8.1 + throat@5.0.0: {} + through@2.3.8: {} thunky@1.1.0: {} @@ -33648,7 +34358,7 @@ snapshots: json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.7.2 + semver: 7.7.3 type-fest: 4.41.0 typescript: 5.8.3 yargs-parser: 21.1.1 @@ -33669,7 +34379,7 @@ snapshots: json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.7.2 + semver: 7.7.3 type-fest: 4.41.0 typescript: 5.9.3 yargs-parser: 21.1.1 @@ -33690,7 +34400,7 @@ snapshots: json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.7.2 + semver: 7.7.3 type-fest: 4.41.0 typescript: 5.8.3 yargs-parser: 21.1.1 @@ -33711,7 +34421,7 @@ snapshots: json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.7.2 + semver: 7.7.3 type-fest: 4.41.0 typescript: 5.9.3 yargs-parser: 21.1.1 @@ -33727,7 +34437,7 @@ snapshots: chalk: 4.1.2 enhanced-resolve: 5.18.1 micromatch: 4.0.8 - semver: 7.7.2 + semver: 7.7.3 typescript: 5.8.3 webpack: 5.99.9(@swc/core@1.13.5(@swc/helpers@0.5.17))(esbuild@0.25.1) @@ -33736,7 +34446,7 @@ snapshots: chalk: 4.1.2 enhanced-resolve: 5.18.1 micromatch: 4.0.8 - semver: 7.7.2 + semver: 7.7.3 typescript: 5.9.3 webpack: 5.99.9(@swc/core@1.13.5(@swc/helpers@0.5.17))(esbuild@0.25.1) @@ -33937,6 +34647,8 @@ snapshots: type-fest@0.21.3: {} + type-fest@0.7.1: {} + type-fest@0.8.1: {} type-fest@2.19.0: {} @@ -34477,6 +35189,8 @@ snapshots: - tsx - yaml + vlq@1.0.1: {} + vm-browserify@1.1.2: {} void-elements@3.1.0: {} @@ -34778,6 +35492,8 @@ snapshots: dependencies: iconv-lite: 0.6.3 + whatwg-fetch@3.6.20: {} + whatwg-mimetype@3.0.0: {} whatwg-url@11.0.0: @@ -34860,8 +35576,8 @@ snapshots: with@7.0.2: dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 assert-never: 1.4.0 babel-walk: 3.0.0-canary-5 @@ -34910,6 +35626,10 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 4.1.0 + ws@6.2.3: + dependencies: + async-limiter: 1.0.1 + ws@7.5.10: {} ws@8.17.1: {} diff --git a/tsconfig.base.json b/tsconfig.base.json index 5af04c0c..3f07111b 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -20,6 +20,7 @@ "vite-plugin-zephyr": ["libs/vite-plugin-zephyr/src/index.ts"], "zephyr-agent": ["libs/zephyr-agent/src/index.ts"], "zephyr-edge-contract": ["libs/zephyr-edge-contract/src/index.ts"], + "zephyr-metro-plugin": ["libs/zephyr-metro-plugin/src/index.ts"], "zephyr-repack-plugin": ["libs/zephyr-repack-plugin/src/index.ts"], "zephyr-rspack-plugin": ["libs/zephyr-rspack-plugin/src/index.ts"], "zephyr-webpack-plugin": ["libs/zephyr-webpack-plugin/src/index.ts"],