feat(react-native): upgrade react native to 0.73 (#20896)

This commit is contained in:
Emily Xiong 2024-01-31 15:38:02 -05:00 committed by GitHub
parent d43d5365c8
commit 65e86eacac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
221 changed files with 3685 additions and 3424 deletions

View File

@ -7531,6 +7531,14 @@
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "serve",
"path": "/nx-api/expo/executors/serve",
"name": "serve",
"children": [],
"isExternal": false,
"disableCollapsible": false
}
],
"isExternal": false,
@ -9007,6 +9015,14 @@
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "upgrade",
"path": "/nx-api/react-native/executors/upgrade",
"name": "upgrade",
"children": [],
"isExternal": false,
"disableCollapsible": false
}
],
"isExternal": false,
@ -9080,6 +9096,14 @@
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "web-configuration",
"path": "/nx-api/react-native/generators/web-configuration",
"name": "web-configuration",
"children": [],
"isExternal": false,
"disableCollapsible": false
}
],
"isExternal": false,

View File

@ -904,6 +904,15 @@
"originalFilePath": "/packages/expo/src/executors/submit/schema.json",
"path": "/nx-api/expo/executors/submit",
"type": "executor"
},
"/nx-api/expo/executors/serve": {
"description": "Serve up the Expo web app locally",
"file": "generated/packages/expo/executors/serve.json",
"hidden": false,
"name": "serve",
"originalFilePath": "/packages/expo/src/executors/serve/schema.json",
"path": "/nx-api/expo/executors/serve",
"type": "executor"
}
},
"generators": {
@ -2365,11 +2374,20 @@
"originalFilePath": "/packages/react-native/src/executors/pod-install/schema.json",
"path": "/nx-api/react-native/executors/pod-install",
"type": "executor"
},
"/nx-api/react-native/executors/upgrade": {
"description": "upgrade executor",
"file": "generated/packages/react-native/executors/upgrade.json",
"hidden": false,
"name": "upgrade",
"originalFilePath": "/packages/react-native/src/executors/upgrade/schema.json",
"path": "/nx-api/react-native/executors/upgrade",
"type": "executor"
}
},
"generators": {
"/nx-api/react-native/generators/init": {
"description": "Initialize the `@nrwl/react-native` plugin.",
"description": "Initialize the `@nx/react-native` plugin.",
"file": "generated/packages/react-native/generators/init.json",
"hidden": true,
"name": "init",
@ -2405,7 +2423,7 @@
"type": "generator"
},
"/nx-api/react-native/generators/storybook-configuration": {
"description": "Set up Storybook for a React-native application or library.",
"description": "Set up Storybook for a React Native application or library.",
"file": "generated/packages/react-native/generators/storybook-configuration.json",
"hidden": false,
"name": "storybook-configuration",
@ -2414,7 +2432,7 @@
"type": "generator"
},
"/nx-api/react-native/generators/component-story": {
"description": "Generate Storybook story for a React-native component.",
"description": "Generate Storybook story for a React Native component.",
"file": "generated/packages/react-native/generators/component-story.json",
"hidden": false,
"name": "component-story",
@ -2423,7 +2441,7 @@
"type": "generator"
},
"/nx-api/react-native/generators/stories": {
"description": "Create stories/specs for all components declared in an application or library.",
"description": "Create stories for all components declared in an application or library.",
"file": "generated/packages/react-native/generators/stories.json",
"hidden": false,
"name": "stories",
@ -2439,6 +2457,15 @@
"originalFilePath": "/packages/react-native/src/generators/upgrade-native/schema.json",
"path": "/nx-api/react-native/generators/upgrade-native",
"type": "generator"
},
"/nx-api/react-native/generators/web-configuration": {
"description": "Set up web configuration for a React Native app",
"file": "generated/packages/react-native/generators/web-configuration.json",
"hidden": false,
"name": "web-configuration",
"originalFilePath": "/packages/react-native/src/generators/web-configuration/schema.json",
"path": "/nx-api/react-native/generators/web-configuration",
"type": "generator"
}
},
"path": "/nx-api/react-native"

View File

@ -890,6 +890,15 @@
"originalFilePath": "/packages/expo/src/executors/submit/schema.json",
"path": "expo/executors/submit",
"type": "executor"
},
{
"description": "Serve up the Expo web app locally",
"file": "generated/packages/expo/executors/serve.json",
"hidden": false,
"name": "serve",
"originalFilePath": "/packages/expo/src/executors/serve/schema.json",
"path": "expo/executors/serve",
"type": "executor"
}
],
"generators": [
@ -2339,11 +2348,20 @@
"originalFilePath": "/packages/react-native/src/executors/pod-install/schema.json",
"path": "react-native/executors/pod-install",
"type": "executor"
},
{
"description": "upgrade executor",
"file": "generated/packages/react-native/executors/upgrade.json",
"hidden": false,
"name": "upgrade",
"originalFilePath": "/packages/react-native/src/executors/upgrade/schema.json",
"path": "react-native/executors/upgrade",
"type": "executor"
}
],
"generators": [
{
"description": "Initialize the `@nrwl/react-native` plugin.",
"description": "Initialize the `@nx/react-native` plugin.",
"file": "generated/packages/react-native/generators/init.json",
"hidden": true,
"name": "init",
@ -2379,7 +2397,7 @@
"type": "generator"
},
{
"description": "Set up Storybook for a React-native application or library.",
"description": "Set up Storybook for a React Native application or library.",
"file": "generated/packages/react-native/generators/storybook-configuration.json",
"hidden": false,
"name": "storybook-configuration",
@ -2388,7 +2406,7 @@
"type": "generator"
},
{
"description": "Generate Storybook story for a React-native component.",
"description": "Generate Storybook story for a React Native component.",
"file": "generated/packages/react-native/generators/component-story.json",
"hidden": false,
"name": "component-story",
@ -2397,7 +2415,7 @@
"type": "generator"
},
{
"description": "Create stories/specs for all components declared in an application or library.",
"description": "Create stories for all components declared in an application or library.",
"file": "generated/packages/react-native/generators/stories.json",
"hidden": false,
"name": "stories",
@ -2413,6 +2431,15 @@
"originalFilePath": "/packages/react-native/src/generators/upgrade-native/schema.json",
"path": "react-native/generators/upgrade-native",
"type": "generator"
},
{
"description": "Set up web configuration for a React Native app",
"file": "generated/packages/react-native/generators/web-configuration.json",
"hidden": false,
"name": "web-configuration",
"originalFilePath": "/packages/react-native/src/generators/web-configuration/schema.json",
"path": "react-native/generators/web-configuration",
"type": "generator"
}
],
"githubRoot": "https://github.com/nrwl/nx/blob/master",

View File

@ -14,40 +14,38 @@
"platform": {
"description": "Choose the platform to compile for",
"enum": ["ios", "android", "all", "web"],
"default": "all",
"alias": "p",
"x-priority": "important"
},
"dev": {
"type": "boolean",
"description": "Bundle for development environments without minifying code or stripping the __DEV__ boolean. Configure static files for developing locally using a non-https server."
"description": "Configure static files for developing locally using a non-https server"
},
"clear": {
"type": "boolean",
"description": "Clear the bundler cache before exporting"
},
"minify": { "type": "boolean", "description": "Minify source" },
"outputDir": {
"type": "string",
"description": "The directory to export the static files to. Default: dist"
"description": "Relative to workspace root, the directory to export the static files to. Default: dist"
},
"maxWorkers": {
"type": "number",
"description": "Maximum number of tasks to allow Metro to spawn"
"description": "When bundler is metro, the maximum number of tasks to allow the bundler to spawn"
},
"dumpAssetmap": {
"type": "boolean",
"description": "Dump the asset map for further processing"
"description": "When bundler is metro, whether to dump the asset map for further processing"
},
"dumpSourcemap": {
"sourceMaps": {
"type": "boolean",
"description": "Dump the source map for debugging the JS bundle"
},
"bundler": {
"enum": ["metro", "webpack"],
"description": "Choose the bundler to compile for",
"default": "metro"
"description": "When bundler is metro, whether to emit JavaScript source maps"
}
},
"required": [],
"required": ["platform"],
"examplesFile": "`project.json`:\n\n```json\n{\n \"name\": \"mobile\",\n //...\n \"targets\": {\n //...\n \"export\": {\n \"executor\": \"@nx/expo:export\",\n \"options\": {\n \"outputs\": [\"{options.outputDir}\"],\n \"platform\": \"all\",\n \"outputDir\": \"dist/apps/mobile\"\n },\n \"dependsOn\": [\"sync-deps\"]\n }\n //...\n }\n}\n```\n\n```shell\nnx run mobile:export\n```\n\n## Examples\n\n{% tabs %}\n{% tab label=\"Specify outputDir\" %}\nThe `outputDir` option allows you to specify the output directory of your bundle:\n\n```json\n \"export\": {\n \"executor\": \"@nx/expo:export\",\n \"outputs\": [\"{options.outputDir}\"],\n \"options\": {\n \"platform\": \"all\",\n \"bundler\": \"metro\",\n \"outputDir\": \"dist/apps/mobile\"\n },\n \"dependsOn\": [\"sync-deps\"]\n },\n```\n\nor run command: `nx run mobile:export --outputDir=dist/apps/mobile`.\n\n{% /tab %}\n{% tab label=\"Specify the platform\" %}\nThe `platform` option allows you to specify the platform to compile with metro bundler: \"ios\", \"android\", \"all\", and \"web\".\n\nFor example, to bundle for web:\n\n```json\n \"export\": {\n \"executor\": \"@nx/expo:export\",\n \"outputs\": [\"{options.outputDir}\"],\n \"options\": {\n \"platform\": \"web\",\n \"bundler\": \"metro\",\n \"outputDir\": \"dist/apps/dogs\"\n },\n \"dependsOn\": [\"sync-deps\"]\n },\n```\n\nor run command `nx export mobile --platform=web`.\n\n{% /tab %}\n{% tab label=\"Bundle for development\" %}\n\nThe `dev` option allows you to bundle for development environments.\n\n```json\n \"export\": {\n \"executor\": \"@nx/expo:export\",\n \"outputs\": [\"{options.outputDir}\"],\n \"options\": {\n \"platform\": \"web\",\n \"bundler\": \"metro\",\n \"outputDir\": \"dist/apps/dogs\",\n \"dev\": true\n },\n \"dependsOn\": [\"sync-deps\"]\n },\n```\n\nor run command `nx export mobile --dev`.\n\n{% /tab %}\n{% tab label=\"Clear bundle cache\" %}\n\nThe `clear` option allows you to clear bundle cache.\n\n```json\n \"export\": {\n \"executor\": \"@nx/expo:export\",\n \"outputs\": [\"{options.outputDir}\"],\n \"options\": {\n \"platform\": \"web\",\n \"bundler\": \"metro\",\n \"outputDir\": \"dist/apps/dogs\",\n \"clear\": true\n },\n \"dependsOn\": [\"sync-deps\"]\n },\n```\n\nor run command `nx export mobile --clear`.\n\n{% /tab %}\n{% /tabs %}\n",
"presets": []
},
"description": "Export the JavaScript and assets for your app using Metro/webpack bundler",

View File

@ -34,6 +34,7 @@
"description": "Project template to clone from. File path pointing to a local tar file or a github repo"
}
},
"required": ["platform"],
"examplesFile": "The `prebuild` command generates native code before a native app can compile.\n\n`project.json`:\n\n```json\n{\n \"name\": \"mobile\",\n //...\n \"targets\": {\n //...\n \"prebuild\": {\n \"executor\": \"@nx/expo:prebuild\",\n \"options\": {}\n }\n //...\n }\n}\n```\n\n```shell\nnx run mobile:prebuild\n```\n\n## Examples\n\n{% tabs %}\n{% tab label=\"Generate Native Code for Different Platforms\" %}\nThe `platform` option allows you to specify the platform to generate native code for (e.g. android, ios, all).\n\n```json\n \"prebuild\": {\n \"executor\": \"@nx/expo:prebuild\",\n \"options\": {\n \"platform\": \"android\"\n }\n }\n```\n\n{% /tab %}\n{% tab label=\"Regenerate Native Code\" %}\n\nThe `clean` option allows you to delete the native folders and regenerate them before apply changes.\n\n```json\n \"prebuild\": {\n \"executor\": \"@nx/expo:prebuild\",\n \"options\": {\n \"clean\": true\n }\n }\n```\n\n{% /tab %}\n{% tab label=\"Install NPM Packages and CocoaPods\" %}\n\nThe `install` option allows you to install NPM Packages and CocoaPods.\n\n```json\n \"prebuild\": {\n \"executor\": \"@nx/expo:prebuild\",\n \"options\": {\n \"install\": true\n }\n }\n```\n\n{% /tab %}\n{% /tabs %}\n\n---\n",
"presets": []
},

View File

@ -0,0 +1,45 @@
{
"name": "serve",
"implementation": "/packages/expo/src/executors/serve/serve.impl.ts",
"schema": {
"version": 2,
"outputCapture": "direct-nodejs",
"cli": "nx",
"$id": "NxExpoServe",
"$schema": "http://json-schema.org/schema",
"title": "Serve web app for Expo",
"description": "Packager Server target options.",
"type": "object",
"properties": {
"port": {
"type": "number",
"description": "Port to start the native Metro bundler on (does not apply to web or tunnel)",
"default": 19000,
"alias": "p"
},
"clear": {
"type": "boolean",
"description": "Clear the Metro bundler cache",
"alias": "c"
},
"maxWorkers": {
"type": "number",
"description": "Maximum number of tasks to allow Metro to spawn"
},
"dev": {
"type": "boolean",
"description": "Turn development mode on or off"
},
"minify": {
"type": "boolean",
"description": "Whether or not to minify code"
}
},
"presets": []
},
"description": "Serve up the Expo web app locally",
"aliases": [],
"hidden": false,
"path": "/packages/expo/src/executors/serve/schema.json",
"type": "executor"
}

View File

@ -86,7 +86,7 @@
"description": "Allows this command to run while offline"
}
},
"examplesFile": "`project.json`:\n\n```json\n{\n \"name\": \"mobile\",\n //...\n \"targets\": {\n //...\n \"start\": {\n \"executor\": \"@nx/expo:start\",\n \"options\": {\n \"port\": 8081\n }\n }\n //...\n }\n}\n```\n\n```shell\nnx run mobile:start\n```\n\n## Examples\n\n{% tabs %}\n{% tab label=\"Specify starting on platform\" %}\nThe `ios`, `android` and `web` option allows you to start the server on different platforms.\n\nOpens your app in Expo Go in a currently running iOS simulator on your computer:\n\n```json\n \"start\": {\n \"executor\": \"@nx/expo:start\",\n \"options\": {\n \"port\": 8081,\n \"ios\": true\n }\n }\n```\n\nOpens your app in Expo Go on a connected Android device\n\n```json\n \"start\": {\n \"executor\": \"@nx/expo:start\",\n \"options\": {\n \"port\": 8081,\n \"android\": true\n }\n }\n```\n\nOpens your app in a web browser:\n\n```json\n \"start\": {\n \"executor\": \"@nx/expo:start\",\n \"options\": {\n \"port\": 8081,\n \"web\": true\n }\n }\n```\n\n{% /tab %}\n{% tab label=\"Specify the host\" %}\nThe `host` option allows you to specify the type of host to use. `lan` uses the local network; `tunnel` ues any network by tunnel through ngrok; `localhost` connects to the dev server over localhost.\n\n```json\n \"start\": {\n \"executor\": \"@nx/expo:start\",\n \"options\": {\n \"port\": 8081,\n \"host\": \"localhost\"\n }\n }\n```\n\n{% /tab %}\n{% tab label=\"Starts the server with cache reset\" %}\n\nThe `clear` option allows you to remove Metro bundler cache.\n\n```json\n \"start\": {\n \"executor\": \"@nx/expo:start\",\n \"options\": {\n \"port\": 8081,\n \"clear\": true\n }\n }\n```\n\n{% /tab %}\n{% /tabs %}\n\n---\n",
"examplesFile": "`project.json`:\n\n```json\n{\n \"name\": \"mobile\",\n //...\n \"targets\": {\n //...\n \"start\": {\n \"executor\": \"@nx/expo:start\",\n \"options\": {\n \"port\": 8081\n }\n }\n //...\n }\n}\n```\n\n```shell\nnx run mobile:start\n```\n\n## Examples\n\n{% tabs %}\n{% tab label=\"Specify starting on platform\" %}\nThe `ios`, `android` and `web` option allows you to start the server on different platforms.\n\nOpens your app in Expo Go in a currently running iOS simulator on your computer:\n\n```json\n \"start\": {\n \"executor\": \"@nx/expo:start\",\n \"options\": {\n \"port\": 8081,\n \"ios\": true\n }\n }\n```\n\nor run command `nx start <your app name> --ios`.\n\nOpens your app in Expo Go on a connected Android device\n\n```json\n \"start\": {\n \"executor\": \"@nx/expo:start\",\n \"options\": {\n \"port\": 8081,\n \"android\": true\n }\n }\n```\n\nor run command `nx start <your app name> --android`.\n\nOpens your app in a web browser:\n\n```json\n \"start\": {\n \"executor\": \"@nx/expo:start\",\n \"options\": {\n \"port\": 8081,\n \"web\": true\n }\n }\n```\n\nor run command `nx start <your app name> --web`.\n\n{% /tab %}\n{% tab label=\"Specify the host\" %}\nThe `host` option allows you to specify the type of host to use. `lan` uses the local network; `tunnel` ues any network by tunnel through ngrok; `localhost` connects to the dev server over localhost.\n\n```json\n \"start\": {\n \"executor\": \"@nx/expo:start\",\n \"options\": {\n \"port\": 8081,\n \"host\": \"localhost\"\n }\n }\n```\n\n{% /tab %}\n{% tab label=\"Starts the server with cache reset\" %}\n\nThe `clear` option allows you to remove Metro bundler cache.\n\n```json\n \"start\": {\n \"executor\": \"@nx/expo:start\",\n \"options\": {\n \"port\": 8081,\n \"clear\": true\n }\n }\n```\n\n{% /tab %}\n{% /tabs %}\n\n---\n",
"presets": []
},
"description": "Start a local dev server for the app or start a Webpack dev server for the web app",

View File

@ -48,7 +48,7 @@
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint"],
"enum": ["eslint", "none"],
"default": "eslint"
},
"unitTestRunner": {
@ -75,8 +75,8 @@
"e2eTestRunner": {
"description": "Adds the specified e2e test runner",
"type": "string",
"enum": ["detox", "none"],
"default": "detox"
"enum": ["cypress", "playwright", "detox", "none"],
"default": "cypress"
},
"standaloneConfig": {
"description": "Split the project configuration into `<projectRoot>/project.json` rather than including it inside `workspace.json`.",

View File

@ -34,7 +34,7 @@
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint"],
"enum": ["eslint", "none"],
"default": "eslint"
},
"unitTestRunner": {

View File

@ -0,0 +1,21 @@
{
"name": "upgrade",
"implementation": "/packages/react-native/src/executors/upgrade/upgrade.impl.ts",
"schema": {
"$schema": "http://json-schema.org/schema",
"version": 2,
"cli": "nx",
"$id": "NxReactNativeUpgrade",
"title": "React Native Upgrade Executor",
"description": "Upgrade React Native code for project.",
"type": "object",
"properties": {},
"required": [],
"presets": []
},
"description": "upgrade executor",
"aliases": [],
"hidden": false,
"path": "/packages/react-native/src/executors/upgrade/schema.json",
"type": "executor"
}

View File

@ -48,7 +48,7 @@
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint"],
"enum": ["eslint", "none"],
"default": "eslint"
},
"unitTestRunner": {
@ -75,20 +75,28 @@
"e2eTestRunner": {
"description": "Adds the specified e2e test runner.",
"type": "string",
"enum": ["detox", "none"],
"default": "detox"
"enum": ["cypress", "playwright", "detox", "none"],
"default": "cypress"
},
"install": {
"type": "boolean",
"description": "Runs `pod install` for native modules before building iOS app.",
"default": true,
"x-priority": "internal"
"x-prompt": "Run 'pod install' for native modules?",
"default": true
},
"skipPackageJson": {
"description": "Do not add dependencies to `package.json`.",
"type": "boolean",
"default": false,
"x-priority": "internal"
},
"bundler": {
"description": "The bundler to use.",
"type": "string",
"enum": ["vite", "webpack"],
"x-prompt": "Which bundler do you want to use to build the application?",
"default": "webpack",
"x-priority": "important"
}
},
"required": [],

View File

@ -15,28 +15,36 @@
"description": "The project where to add the components.",
"examples": ["shared-ui-component"],
"$default": { "$source": "projectName", "index": 0 },
"x-prompt": "What's the name of the project where the component lives?"
"x-prompt": "What's name of the project where the component lives?",
"x-priority": "important"
},
"componentPath": {
"type": "string",
"description": "Relative path to the component file from the library root.",
"examples": ["lib/components"],
"x-prompt": "What's path of the component relative to the project's lib root?"
"x-prompt": "What's path of the component relative to the project's lib root?",
"x-priority": "important"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false,
"x-priority": "internal"
},
"interactionTests": {
"type": "boolean",
"description": "Set up Storybook interaction tests.",
"default": true,
"x-priority": "important"
}
},
"required": ["project", "componentPath"],
"presets": []
},
"description": "Generate Storybook story for a React-native component.",
"hidden": false,
"description": "Generate Storybook story for a React Native component.",
"implementation": "/packages/react-native/src/generators/component-story/component-story#componentStoryGenerator.ts",
"aliases": [],
"hidden": false,
"path": "/packages/react-native/src/generators/component-story/schema.json",
"type": "generator"
}

View File

@ -37,7 +37,7 @@
"required": [],
"presets": []
},
"description": "Initialize the `@nrwl/react-native` plugin.",
"description": "Initialize the `@nx/react-native` plugin.",
"hidden": true,
"implementation": "/packages/react-native/src/generators/init/init#reactNativeInitGenerator.ts",
"aliases": [],

View File

@ -36,7 +36,7 @@
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint"],
"enum": ["eslint", "none"],
"default": "eslint"
},
"unitTestRunner": {

View File

@ -14,7 +14,20 @@
"aliases": ["name", "projectName"],
"description": "Project for which to generate stories.",
"$default": { "$source": "projectName", "index": 0 },
"x-prompt": "For which project do you want to generate stories?"
"x-prompt": "For which project do you want to generate stories?",
"x-priority": "important"
},
"interactionTests": {
"type": "boolean",
"description": "Set up Storybook interaction tests.",
"x-prompt": "Do you want to set up Storybook interaction tests?",
"x-priority": "important",
"default": true
},
"js": {
"type": "boolean",
"description": "Generate JavaScript files rather than TypeScript files.",
"default": false
},
"ignorePaths": {
"type": "array",
@ -41,10 +54,10 @@
"required": ["project"],
"presets": []
},
"description": "Create stories/specs for all components declared in an application or library.",
"hidden": false,
"description": "Create stories for all components declared in an application or library.",
"implementation": "/packages/react-native/src/generators/stories/stories#storiesGenerator.ts",
"aliases": [],
"hidden": false,
"path": "/packages/react-native/src/generators/stories/schema.json",
"type": "generator"
}

View File

@ -9,21 +9,37 @@
"description": "Set up Storybook for a React-Native app or library.",
"type": "object",
"properties": {
"name": {
"project": {
"type": "string",
"aliases": ["project", "projectName"],
"aliases": ["name", "projectName"],
"description": "Project for which to generate Storybook configuration.",
"$default": { "$source": "argv", "index": 0 },
"x-prompt": "For which project do you want to generate Storybook configuration?",
"x-dropdown": "projects"
"x-dropdown": "projects",
"x-priority": "important"
},
"interactionTests": {
"type": "boolean",
"description": "Set up Storybook interaction tests.",
"x-prompt": "Do you want to set up Storybook interaction tests?",
"x-priority": "important",
"alias": ["configureTestRunner"],
"default": true
},
"generateStories": {
"type": "boolean",
"description": "Automatically generate *.stories.ts files for components declared in this project?",
"description": "Automatically generate `*.stories.ts` files for components declared in this project?",
"x-prompt": "Automatically generate *.stories.ts files for components declared in this project?",
"default": true,
"x-priority": "important"
},
"configureStaticServe": {
"type": "boolean",
"description": "Specifies whether to configure a static file server target for serving storybook. Helpful for speeding up CI build/test times.",
"x-prompt": "Configure a static file server for the storybook instance?",
"default": true,
"x-priority": "important"
},
"js": {
"type": "boolean",
"description": "Generate JavaScript story files rather than TypeScript story files.",
@ -40,12 +56,6 @@
"enum": ["eslint"],
"default": "eslint"
},
"standaloneConfig": {
"description": "Split the project configuration into `<projectRoot>/project.json` rather than including it inside `workspace.json`.",
"type": "boolean",
"default": true,
"x-deprecated": "Nx only supports standaloneConfig"
},
"ignorePaths": {
"type": "array",
"description": "Paths to ignore when looking for components.",
@ -62,13 +72,13 @@
]
}
},
"required": ["name"],
"required": ["project"],
"presets": []
},
"description": "Set up Storybook for a React-native application or library.",
"hidden": false,
"description": "Set up Storybook for a React Native application or library.",
"implementation": "/packages/react-native/src/generators/storybook-configuration/configuration#storybookConfigurationGenerator.ts",
"aliases": [],
"hidden": false,
"path": "/packages/react-native/src/generators/storybook-configuration/schema.json",
"type": "generator"
}

View File

@ -27,8 +27,8 @@
"e2eTestRunner": {
"description": "Adds the specified e2e test runner.",
"type": "string",
"enum": ["detox", "none"],
"default": "detox"
"enum": ["cypress", "playwright", "detox", "none"],
"default": "cypress"
},
"install": {
"type": "boolean",
@ -41,9 +41,9 @@
"presets": []
},
"description": "Destructive command to upgrade native iOS and Android code to latest.",
"hidden": false,
"implementation": "/packages/react-native/src/generators/upgrade-native/upgrade-native#reactNativeUpgradeNativeGenerator.ts",
"aliases": [],
"hidden": false,
"path": "/packages/react-native/src/generators/upgrade-native/schema.json",
"type": "generator"
}

View File

@ -0,0 +1,50 @@
{
"name": "web-configuration",
"factory": "./src/generators/web-configuration/web-configuration#webConfigurationGenerator",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "NxReactNativeWebConfiguration",
"description": "Setup web configuration to React Native apps using react-native-web.",
"title": "Nx React Native Web configuration",
"type": "object",
"properties": {
"project": {
"type": "string",
"aliases": ["name", "projectName"],
"description": "Project for which to generate web configuration.",
"$default": { "$source": "argv", "index": 0 },
"x-prompt": "For which project do you want to generate web configuration?",
"x-dropdown": "projects",
"x-priority": "important"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false,
"x-priority": "internal"
},
"skipPackageJson": {
"description": "Do not add dependencies to `package.json`.",
"type": "boolean",
"default": false,
"x-priority": "internal"
},
"bundler": {
"description": "The bundler to use.",
"type": "string",
"enum": ["vite", "webpack"],
"x-prompt": "Which bundler do you want to use to build the application?",
"default": "webpack",
"x-priority": "important"
}
},
"required": ["project", "bundler"],
"presets": []
},
"description": "Set up web configuration for a React Native app",
"implementation": "/packages/react-native/src/generators/web-configuration/web-configuration#webConfigurationGenerator.ts",
"aliases": [],
"hidden": false,
"path": "/packages/react-native/src/generators/web-configuration/schema.json",
"type": "generator"
}

View File

@ -63,13 +63,6 @@
"description": "Add a static-storybook to serve the static storybook built files.",
"default": false
},
"bundler": {
"description": "The Storybook builder to use.",
"type": "string",
"enum": ["vite", "webpack"],
"default": "webpack",
"x-priority": "important"
},
"uiFramework": {
"type": "string",
"description": "Storybook UI Framework to use.",

View File

@ -433,6 +433,7 @@
- [install](/nx-api/expo/executors/install)
- [export](/nx-api/expo/executors/export)
- [submit](/nx-api/expo/executors/submit)
- [serve](/nx-api/expo/executors/serve)
- [generators](/nx-api/expo/generators)
- [init](/nx-api/expo/generators/init)
- [application](/nx-api/expo/generators/application)
@ -612,6 +613,7 @@
- [ensure-symlink](/nx-api/react-native/executors/ensure-symlink)
- [storybook](/nx-api/react-native/executors/storybook)
- [pod-install](/nx-api/react-native/executors/pod-install)
- [upgrade](/nx-api/react-native/executors/upgrade)
- [generators](/nx-api/react-native/generators)
- [init](/nx-api/react-native/generators/init)
- [application](/nx-api/react-native/generators/application)
@ -621,6 +623,7 @@
- [component-story](/nx-api/react-native/generators/component-story)
- [stories](/nx-api/react-native/generators/stories)
- [upgrade-native](/nx-api/react-native/generators/upgrade-native)
- [web-configuration](/nx-api/react-native/generators/web-configuration)
- [remix](/nx-api/remix)
- [documents](/nx-api/remix/documents)
- [Overview](/nx-api/remix/documents/overview)

View File

@ -10,15 +10,16 @@ import {
checkFilesExist,
updateFile,
runCLIAsync,
runE2ETests,
killPorts,
} from 'e2e/utils';
import { join } from 'path';
describe('@nx/expo/plugin', () => {
let project: string;
let appName: string;
beforeAll(() => {
project = newProject();
newProject();
appName = uniq('app');
runCLI(
`generate @nx/expo:app ${appName} --project-name-and-root-format=as-provided --no-interactive`,
@ -107,6 +108,32 @@ describe('@nx/expo/plugin', () => {
const prebuildResult = await runCLIAsync(
`prebuild ${appName} --no-interactive --install=false`
);
expect(prebuildResult.combinedOutput).toContain('Config synced');
expect(prebuildResult.combinedOutput).toContain(
'Successfully ran target prebuild for project'
);
});
it('should run e2e for cypress', async () => {
if (runE2ETests()) {
const results = runCLI(`e2e ${appName}-e2e`);
expect(results).toContain('Successfully ran target e2e');
// port and process cleanup
try {
await killPorts(4200);
} catch (err) {
expect(err).toBeFalsy();
}
}
});
it('should create storybook with application', async () => {
runCLI(
`generate @nx/react:storybook-configuration ${appName} --generateStories --no-interactive`
);
checkFilesExist(
`${appName}/.storybook/main.ts`,
`${appName}/src/app/App.stories.tsx`
);
});
});

View File

@ -11,10 +11,12 @@ import {
runCLIAsync,
runCommand,
runCommandUntil,
runE2ETests,
uniq,
updateFile,
updateJson,
} from '@nx/e2e/utils';
import { ChildProcess } from 'child_process';
import { join } from 'path';
describe('expo', () => {
@ -34,7 +36,9 @@ describe('expo', () => {
nxJson.targetDefaults.build.inputs = ['production', '^production'];
return nxJson;
});
runCLI(`generate @nx/expo:application ${appName} --no-interactive`);
runCLI(
`generate @nx/expo:application ${appName} --e2eTestRunner=cypress --no-interactive`
);
runCLI(
`generate @nx/expo:library ${libName} --buildable --publishable --importPath=${proj}/${libName}`
);
@ -63,22 +67,46 @@ describe('expo', () => {
expect(libLintResults.combinedOutput).toContain('All files pass linting.');
});
it('should serve with metro', async () => {
let process: ChildProcess;
const port = 8081;
try {
process = await runCommandUntil(
`serve ${appName} --interactive=false --port=${port}`,
(output) => {
return (
output.includes(`http://localhost::${port}`) ||
output.includes('Starting JS server...')
);
}
);
} catch (err) {
console.error(err);
}
// port and process cleanup
try {
if (process && process.pid) {
await promisifiedTreeKill(process.pid, 'SIGKILL');
await killPorts(port);
}
} catch (err) {
expect(err).toBeFalsy();
}
});
it('should export', async () => {
const exportResults = await runCLIAsync(
`export ${appName} --no-interactive`
);
expect(exportResults.combinedOutput).toContain(
'Export was successful. Your exported files can be found'
'Successfully ran target export for project'
);
checkFilesExist(
`dist/apps/${appName}/index.html`,
`dist/apps/${appName}/metadata.json`
);
checkFilesExist(`dist/apps/${appName}/metadata.json`);
});
it('should export-web', async () => {
expect(() => {
runCLI(`export-web ${appName}`);
checkFilesExist(`apps/${appName}/dist/index.html`);
checkFilesExist(`apps/${appName}/dist/metadata.json`);
}).not.toThrow();
});
it('should prebuild', async () => {
@ -100,10 +128,14 @@ describe('expo', () => {
const prebuildResult = await runCLIAsync(
`prebuild ${appName} --no-interactive --install=false`
);
expect(prebuildResult.combinedOutput).toContain('Config synced');
expect(prebuildResult.combinedOutput).toContain(
'Successfully ran target prebuild for project'
);
});
it('should install', async () => {
// TODO (@xiongemi): this test is disabled due to expo requires typescript ^5.3.0
// re-enable it when typescript is updated
xit('should install', async () => {
// run install command
const installResults = await runCLIAsync(
`install ${appName} --no-interactive`
@ -189,4 +221,60 @@ describe('expo', () => {
`Successfully ran target test for project ${libName}`
);
});
it('should create storybook with application', async () => {
runCLI(
`generate @nx/react:storybook-configuration ${appName} --generateStories --no-interactive`
);
checkFilesExist(
`apps/${appName}/.storybook/main.ts`,
`apps/${appName}/src/app/App.stories.tsx`
);
});
it('should run e2e for cypress', async () => {
if (runE2ETests()) {
const results = runCLI(`e2e ${appName}-e2e`);
expect(results).toContain('Successfully ran target e2e');
// port and process cleanup
try {
await killPorts(4200);
} catch (err) {
expect(err).toBeFalsy();
}
}
});
it('should run e2e for cypress with configuration ci', async () => {
if (runE2ETests()) {
const results = runCLI(`e2e ${appName}-e2e --configuration=ci`);
expect(results).toContain('Successfully ran target e2e');
// port and process cleanup
try {
await killPorts(4200);
} catch (err) {
expect(err).toBeFalsy();
}
}
});
it('should run e2e for playwright', async () => {
const appName2 = uniq('my-app');
runCLI(
`generate @nx/expo:application ${appName2} --e2eTestRunner=playwright --no-interactive`
);
if (runE2ETests()) {
const results = runCLI(`e2e ${appName2}-e2e`, { verbose: true });
expect(results).toContain('Successfully ran target e2e');
// port and process cleanup
try {
await killPorts(4200);
} catch (err) {
expect(err).toBeFalsy();
}
}
});
});

View File

@ -8,6 +8,8 @@ import {
runCommandUntil,
killProcessAndPorts,
fileExists,
checkFilesExist,
runE2ETests,
} from 'e2e/utils';
describe('@nx/react-native/plugin', () => {
@ -70,4 +72,49 @@ describe('@nx/react-native/plugin', () => {
await killProcessAndPorts(process.pid, port);
}
});
it('should serve', async () => {
let process: ChildProcess;
const port = 8081;
try {
process = await runCommandUntil(
`serve ${appName} --interactive=false --port=${port}`,
(output) => {
return output.includes(`http://localhost:${port}`);
}
);
} catch (err) {
console.error(err);
}
// port and process cleanup
try {
if (process && process.pid) {
await killProcessAndPorts(process.pid, port);
}
} catch (err) {
expect(err).toBeFalsy();
}
});
it('should run e2e for cypress', async () => {
if (runE2ETests()) {
let results = runCLI(`e2e ${appName}-e2e`);
expect(results).toContain('Successfully ran target e2e');
results = runCLI(`e2e ${appName}-e2e --configuration=ci`);
expect(results).toContain('Successfully ran target e2e');
}
});
it('should create storybook with application', async () => {
runCLI(
`generate @nx/react:storybook-configuration ${appName} --generateStories --no-interactive`
);
checkFilesExist(
`${appName}/.storybook/main.ts`,
`${appName}/src/app/App.stories.tsx`
);
});
});

View File

@ -4,14 +4,14 @@ import {
expectTestsPass,
getPackageManagerCommand,
isOSX,
killPorts,
killProcessAndPorts,
newProject,
promisifiedTreeKill,
readJson,
runCLI,
runCLIAsync,
runCommand,
runCommandUntil,
runE2ETests,
uniq,
updateFile,
updateJson,
@ -37,7 +37,7 @@ describe('react native', () => {
return nxJson;
});
runCLI(
`generate @nx/react-native:application ${appName} --install=false --no-interactive`
`generate @nx/react-native:application ${appName} --bunlder=webpack --e2eTestRunner=cypress --install=false --no-interactive`
);
runCLI(
`generate @nx/react-native:library ${libName} --buildable --publishable --importPath=${proj}/${libName} --no-interactive`
@ -45,6 +45,11 @@ describe('react native', () => {
});
afterAll(() => cleanupProject());
it('should build for web', async () => {
const results = runCLI(`build ${appName}`);
expect(results).toContain('Successfully ran target build');
});
it('should test and lint', async () => {
const componentName = uniq('Component');
runCLI(
@ -66,6 +71,16 @@ describe('react native', () => {
expect(libLintResults.combinedOutput).toContain('All files pass linting.');
});
it('should run e2e for cypress', async () => {
if (runE2ETests()) {
let results = runCLI(`e2e ${appName}-e2e`);
expect(results).toContain('Successfully ran target e2e');
results = runCLI(`e2e ${appName}-e2e --configuration=ci`);
expect(results).toContain('Successfully ran target e2e');
}
});
it('should bundle-ios', async () => {
const iosBundleResult = await runCLIAsync(
`bundle-ios ${appName} --sourcemapOutput=../../dist/apps/${appName}/ios/main.map`
@ -114,8 +129,32 @@ describe('react native', () => {
// port and process cleanup
try {
if (process && process.pid) {
await promisifiedTreeKill(process.pid, 'SIGKILL');
await killPorts(port);
await killProcessAndPorts(process.pid, port);
}
} catch (err) {
expect(err).toBeFalsy();
}
});
it('should serve', async () => {
let process: ChildProcess;
const port = 8081;
try {
process = await runCommandUntil(
`serve ${appName} --interactive=false --port=${port}`,
(output) => {
return output.includes(`http://localhost:${port}`);
}
);
} catch (err) {
console.error(err);
}
// port and process cleanup
try {
if (process && process.pid) {
await killProcessAndPorts(process.pid, port);
}
} catch (err) {
expect(err).toBeFalsy();
@ -136,33 +175,14 @@ describe('react native', () => {
runCLI(
`generate @nx/react-native:storybook-configuration ${appName} --generateStories --no-interactive`
);
expect(() =>
checkFilesExist(
`.storybook/story-loader.ts`,
`apps/${appName}/src/storybook/storybook.ts`,
`apps/${appName}/src/storybook/toggle-storybook.tsx`,
`apps/${appName}/src/app/App.stories.tsx`
)
).not.toThrow();
await runCLIAsync(`storybook ${appName}`);
const result = readJson(join('apps', appName, 'package.json'));
expect(result).toMatchObject({
dependencies: {
'@storybook/addon-ondevice-actions': '*',
'@storybook/addon-ondevice-backgrounds': '*',
'@storybook/addon-ondevice-controls': '*',
'@storybook/addon-ondevice-notes': '*',
},
});
checkFilesExist(
`apps/${appName}/.storybook/main.ts`,
`apps/${appName}/src/app/App.stories.tsx`
);
});
it('should upgrade native for application', async () => {
expect(() =>
runCLI(
`generate @nx/react-native:upgrade-native ${appName} --install=false`
)
).not.toThrow();
expect(() => runCLI(`upgrade ${appName}`)).not.toThrow();
});
it('should build publishable library', async () => {
@ -180,13 +200,11 @@ describe('react native', () => {
it('sync npm dependencies for autolink', async () => {
// Add npm package with native modules
updateFile(join('package.json'), (content) => {
const json = JSON.parse(content);
json.dependencies['react-native-image-picker'] = '5.3.1';
json.dependencies['@react-native-async-storage/async-storage'] = '1.18.1';
return JSON.stringify(json, null, 2);
});
runCommand(`${getPackageManagerCommand().install}`);
runCommand(
`${
getPackageManagerCommand().addDev
} react-native-image-picker @react-native-async-storage/async-storage`
);
// Add import for Nx to pick up
updateFile(join('apps', appName, 'src/app/App.tsx'), (content) => {
@ -202,6 +220,8 @@ describe('react native', () => {
dependencies: {
'react-native-image-picker': '*',
'react-native': '*',
},
devDependencies: {
'@react-native-async-storage/async-storage': '*',
},
});
@ -261,4 +281,25 @@ describe('react native', () => {
`Successfully ran target test for project ${libName}`
);
});
it('should run build with vite bundler and e2e with playwright', async () => {
const appName2 = uniq('my-app');
runCLI(
`generate @nx/react-native:application ${appName2} --bundler=vite --e2eTestRunner=playwright --install=false --no-interactive`
);
const buildResults = runCLI(`build ${appName2}`);
expect(buildResults).toContain('Successfully ran target build');
if (runE2ETests()) {
const e2eResults = runCLI(`e2e ${appName2}-e2e`);
expect(e2eResults).toContain('Successfully ran target e2e');
}
runCLI(
`generate @nx/react-native:storybook-configuration ${appName2} --generateStories --no-interactive`
);
checkFilesExist(
`apps/${appName2}/.storybook/main.ts`,
`apps/${appName2}/src/app/App.stories.tsx`
);
});
});

View File

@ -224,8 +224,8 @@
"mdast-util-to-markdown": "^1.5.0",
"mdast-util-to-string": "^3.2.0",
"memfs": "^3.0.1",
"metro-config": "0.76.8",
"metro-resolver": "0.76.8",
"metro-config": "~0.80.4",
"metro-resolver": "~0.80.4",
"mini-css-extract-plugin": "~2.4.7",
"minimatch": "9.0.3",
"next-sitemap": "^3.1.10",

View File

@ -99,6 +99,15 @@
"alwaysAddToPackageJson": false
}
}
},
"18.0.0": {
"version": "18.0.0-beta.0",
"packages": {
"detox": {
"version": "^20.16.0",
"alwaysAddToPackageJson": false
}
}
}
}
}

View File

@ -1,5 +1,5 @@
export const nxVersion = require('../../package.json').version;
export const detoxVersion = '^20.11.1';
export const testingLibraryJestDom = '5.16.5';
export const detoxVersion = '^20.16.0';
export const testingLibraryJestDom = '6.2.0';
export const configPluginsDetoxVersion = '~6.0.0'; // only required for expo

View File

@ -27,6 +27,9 @@
"nx",
"@nx/rollup",
"@nx/webpack",
"@nx/cypress",
"@nx/playwright",
"@nx/detox",
"typescript",
"eslint",
"expo",

View File

@ -0,0 +1,112 @@
`project.json`:
```json
{
"name": "mobile",
//...
"targets": {
//...
"export": {
"executor": "@nx/expo:export",
"options": {
"outputs": ["{options.outputDir}"],
"platform": "all",
"outputDir": "dist/apps/mobile"
},
"dependsOn": ["sync-deps"]
}
//...
}
}
```
```shell
nx run mobile:export
```
## Examples
{% tabs %}
{% tab label="Specify outputDir" %}
The `outputDir` option allows you to specify the output directory of your bundle:
```json
"export": {
"executor": "@nx/expo:export",
"outputs": ["{options.outputDir}"],
"options": {
"platform": "all",
"bundler": "metro",
"outputDir": "dist/apps/mobile"
},
"dependsOn": ["sync-deps"]
},
```
or run command: `nx run mobile:export --outputDir=dist/apps/mobile`.
{% /tab %}
{% tab label="Specify the platform" %}
The `platform` option allows you to specify the platform to compile with metro bundler: "ios", "android", "all", and "web".
For example, to bundle for web:
```json
"export": {
"executor": "@nx/expo:export",
"outputs": ["{options.outputDir}"],
"options": {
"platform": "web",
"bundler": "metro",
"outputDir": "dist/apps/dogs"
},
"dependsOn": ["sync-deps"]
},
```
or run command `nx export mobile --platform=web`.
{% /tab %}
{% tab label="Bundle for development" %}
The `dev` option allows you to bundle for development environments.
```json
"export": {
"executor": "@nx/expo:export",
"outputs": ["{options.outputDir}"],
"options": {
"platform": "web",
"bundler": "metro",
"outputDir": "dist/apps/dogs",
"dev": true
},
"dependsOn": ["sync-deps"]
},
```
or run command `nx export mobile --dev`.
{% /tab %}
{% tab label="Clear bundle cache" %}
The `clear` option allows you to clear bundle cache.
```json
"export": {
"executor": "@nx/expo:export",
"outputs": ["{options.outputDir}"],
"options": {
"platform": "web",
"bundler": "metro",
"outputDir": "dist/apps/dogs",
"clear": true
},
"dependsOn": ["sync-deps"]
},
```
or run command `nx export mobile --clear`.
{% /tab %}
{% /tabs %}

View File

@ -39,6 +39,8 @@ Opens your app in Expo Go in a currently running iOS simulator on your computer:
}
```
or run command `nx start <your app name> --ios`.
Opens your app in Expo Go on a connected Android device
```json
@ -51,6 +53,8 @@ Opens your app in Expo Go on a connected Android device
}
```
or run command `nx start <your app name> --android`.
Opens your app in a web browser:
```json
@ -63,6 +67,8 @@ Opens your app in a web browser:
}
```
or run command `nx start <your app name> --web`.
{% /tab %}
{% tab label="Specify the host" %}
The `host` option allows you to specify the type of host to use. `lan` uses the local network; `tunnel` ues any network by tunnel through ngrok; `localhost` connects to the dev server over localhost.

View File

@ -54,6 +54,11 @@
"implementation": "./src/executors/submit/submit.impl",
"schema": "./src/executors/submit/schema.json",
"description": "Submit app binary to App Store and/or Play Store"
},
"serve": {
"implementation": "./src/executors/serve/serve.impl",
"schema": "./src/executors/serve/schema.json",
"description": "Serve up the Expo web app locally"
}
}
}

View File

@ -36,12 +36,6 @@
"description": "Update package.json eas build lifecycle scripts",
"implementation": "./src/migrations/update-16-1-4/update-eas-scripts"
},
"add-detox-app-json-16-1-4": {
"version": "16.1.4-beta.0",
"cli": "nx",
"description": "Add app.json for detox",
"factory": "./src/migrations/update-16-1-4/add-detox-app-json"
},
"update-16-6-0-add-dependsOn": {
"cli": "nx",
"version": "16.6.0-beta.0",
@ -65,6 +59,30 @@
"version": "16.9.0-beta.1",
"description": "Update eas.json cli version",
"implementation": "./src/migrations/update-16-9-0/update-eas-cli-version"
},
"update-18-0-0-remove-block-list": {
"cli": "nx",
"version": "18.0.0-beta.0",
"description": "Remove blockList in metro.config.js",
"implementation": "./src/migrations/update-18-0-0/remove-block-list"
},
"update-18-0-0-remove-symlink-target": {
"cli": "nx",
"version": "18.0.0-beta.0",
"description": "Remove symlink target in project.json",
"implementation": "./src/migrations/update-18-0-0/remove-symlink-target"
},
"update-18-0-0-remove-eas-cli": {
"cli": "nx",
"version": "18.0.0-beta.0",
"description": "Remove eas-cli from package.json",
"implementation": "./src/migrations/update-18-0-0/remove-eas-cli"
},
"update-18-0-0-remove-offset-export-outputDir": {
"cli": "nx",
"version": "18.0.0-beta.0",
"description": "Remove the offset from the outputDir of the export target",
"implementation": "./src/migrations/update-18-0-0/change-outputDir-export-target"
}
},
"packageJsonUpdates": {
@ -605,6 +623,67 @@
"alwaysAddToPackageJson": false
}
}
},
"18.0.0": {
"version": "18.0.0-beta.0",
"packages": {
"expo": {
"version": "50.0.1",
"alwaysAddToPackageJson": false
},
"expo-splash-screen": {
"version": "~0.26.1",
"alwaysAddToPackageJson": false
},
"expo-status-bar": {
"version": "~1.11.1",
"alwaysAddToPackageJson": false
},
"@expo/cli": {
"version": "~0.16.5",
"alwaysAddToPackageJson": false
},
"babel-preset-expo": {
"version": "~10.0.0",
"alwaysAddToPackageJson": false
},
"@types/react": {
"version": "~18.2.45",
"alwaysAddToPackageJson": false
},
"react-native": {
"version": "~0.73.2",
"alwaysAddToPackageJson": false
},
"react-native-web": {
"version": "~0.19.9",
"alwaysAddToPackageJson": false
},
"@expo/metro-config": {
"version": "~0.17.3",
"alwaysAddToPackageJson": false
},
"@expo/metro-runtime": {
"version": "~3.1.1",
"addToPackageJson": "devDependencies"
},
"react-native-svg-transformer": {
"version": "1.2.0",
"alwaysAddToPackageJson": false
},
"react-native-svg": {
"version": "14.1.0",
"alwaysAddToPackageJson": false
},
"@testing-library/react-native": {
"version": "~12.4.2",
"alwaysAddToPackageJson": false
},
"jest-expo": {
"version": "~50.0.1",
"alwaysAddToPackageJson": false
}
}
}
}
}

View File

@ -26,27 +26,23 @@
"main": "./index",
"types": "index.d.ts",
"dependencies": {
"@nx/devkit": "file:../devkit",
"chalk": "^4.1.0",
"enhanced-resolve": "^5.8.3",
"fs-extra": "^11.1.0",
"metro-config": "~0.76.8",
"metro-resolver": "~0.76.8",
"metro-config": "~0.80.4",
"metro-resolver": "~0.80.4",
"node-fetch": "^2.6.7",
"tslib": "^2.3.0",
"tsconfig-paths": "^4.1.2",
"tsconfig-paths-webpack-plugin": "^4.0.0",
"@nx/detox": "file:../detox",
"@nx/devkit": "file:../devkit",
"@nx/jest": "file:../jest",
"@nx/js": "file:../js",
"@nx/eslint": "file:../eslint",
"@nx/react": "file:../react",
"@nx/web": "file:../web",
"@nx/webpack": "file:../webpack"
},
"peerDependencies": {
"expo": ">= 49.0.0",
"@expo/cli": ">= 0.10.0",
"eas-cli": ">= 3.15.0"
},
"executors": "./executors.json",
"ng-update": {
"requirements": {},

View File

@ -45,6 +45,11 @@ export function getResolveRequest(extensions: string[]) {
if (resolvedPath) {
return resolvedPath;
}
if (debug) {
console.log(
chalk.red(`[Nx] Unable to resolve with any resolver: ${realModuleName}`)
);
}
throw new Error(`Cannot resolve ${chalk.bold(realModuleName)}`);
};
}
@ -53,7 +58,7 @@ function resolveRequestFromContext(
resolveRequest: Function,
context: any,
realModuleName: string,
platform: string,
platform: string | null,
debug: boolean
) {
try {
@ -75,7 +80,7 @@ function resolveRequestFromContext(
function defaultMetroResolver(
context: any,
realModuleName: string,
platform: string,
platform: string | null,
debug: boolean
) {
try {
@ -130,7 +135,7 @@ function tsconfigPathsResolver(
context: any,
extensions: string[],
realModuleName: string,
platform: string,
platform: string | null,
debug: boolean
) {
try {
@ -145,7 +150,7 @@ function tsconfigPathsResolver(
} catch {
if (debug) {
console.log(
chalk.red(`[Nx] Failed to resolve ${chalk.bold(realModuleName)}`)
chalk.cyan(`[Nx] Failed to resolve ${chalk.bold(realModuleName)}`)
);
console.log(
chalk.cyan(

View File

@ -1,7 +1,7 @@
import { workspaceRoot } from '@nx/devkit';
import { joinPathFragments, workspaceRoot } from '@nx/devkit';
import { mergeConfig } from 'metro-config';
import type { MetroConfig } from 'metro-config';
import { existsSync } from 'fs-extra';
import { existsSync, readdirSync, statSync } from 'fs-extra';
import { getResolveRequest } from './metro-resolver';
@ -19,16 +19,26 @@ export async function withNxMetro(
if (opts.debug) process.env.NX_REACT_NATIVE_DEBUG = 'true';
if (opts.extensions) extensions.push(...opts.extensions);
let watchFolders = [workspaceRoot];
let watchFolders = readdirSync(workspaceRoot)
.filter(
(fileName) =>
!['dist', 'e2e'].includes(fileName) && !fileName.startsWith('.')
)
.map((fileName) => joinPathFragments(workspaceRoot, fileName))
.filter((filePath) => statSync(filePath).isDirectory());
if (opts.watchFolders?.length) {
watchFolders = watchFolders.concat(opts.watchFolders);
}
watchFolders = watchFolders.filter((folder) => existsSync(folder));
watchFolders = [...new Set(watchFolders)].filter((folder) =>
existsSync(folder)
);
const nxConfig: MetroConfig = {
resolver: {
resolveRequest: getResolveRequest(extensions),
nodeModulesPaths: [joinPathFragments(workspaceRoot, 'node_modules')],
},
watchFolders,
};

View File

@ -2,12 +2,14 @@ import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';
import { resolve } from 'path';
/**
* @deprecated TODO(v17) use bundler: 'metro' instead, will be removed in 16.0.0
* @deprecated TODO(v19) use bundler: 'metro' instead, will be removed in v19
* This function add additional rules to expo's webpack config to make expo web working
*/
export async function withNxWebpack(config) {
// add additional rule to load files under libs
const rules = config.module.rules[1]?.oneOf;
const rules = config.module.rules.find((rule) =>
Array.isArray(rule.oneOf)
)?.oneOf;
if (rules) {
rules.push({
test: /\.(mjs|[jt]sx?)$/,
@ -34,17 +36,19 @@ export async function withNxWebpack(config) {
if (!config.resolve.plugins) {
config.resolve.plugins = [];
}
const extensions = ['.ts', '.tsx', '.mjs', '.js', '.jsx'];
const tsConfigPath = resolve('tsconfig.json');
const tsConfigPath = resolve(__dirname, 'tsconfig.json');
config.resolve.plugins.push(
new TsconfigPathsPlugin({
configFile: tsConfigPath,
extensions,
})
);
config.resolve.fallback = {
...config.resolve.fallback,
crypto: require.resolve('crypto-browserify'),
stream: require.resolve('stream-browserify'),
};
config.resolve.symlinks = true;
return config;
}

View File

@ -2,7 +2,7 @@ import { ExecutorContext, logger, names } from '@nx/devkit';
import { resolve as pathResolve } from 'path';
import { ChildProcess, fork } from 'child_process';
import { ensureNodeModulesSymlink } from '../../utils/ensure-node-modules-symlink';
import { resolveEas } from '../../utils/resolve-eas';
import { ExpoEasBuildListOptions } from './schema';
@ -18,7 +18,6 @@ export default async function* buildListExecutor(
): AsyncGenerator<ReactNativeBuildListOutput> {
const projectRoot =
context.projectsConfigurations.projects[context.projectName].root;
ensureNodeModulesSymlink(context.root, projectRoot);
try {
if (options.json) {
@ -42,7 +41,7 @@ export function runCliBuildList(
): Promise<string> {
return new Promise((resolve, reject) => {
childProcess = fork(
require.resolve('eas-cli/bin/run'),
resolveEas(workspaceRoot),
['build:list', ...createBuildListOptions(options)],
{
cwd: pathResolve(workspaceRoot, projectRoot),

View File

@ -2,6 +2,8 @@ import { ExecutorContext, names } from '@nx/devkit';
import { resolve as pathResolve } from 'path';
import { ChildProcess, fork } from 'child_process';
import { resolveEas } from '../../utils/resolve-eas';
import { ExpoEasBuildOptions } from './schema';
export interface ReactNativeBuildOutput {
@ -34,7 +36,7 @@ function runCliBuild(
) {
return new Promise((resolve, reject) => {
childProcess = fork(
require.resolve('eas-cli/bin/run'),
resolveEas(workspaceRoot),
['build', ...createBuildOptions(options)],
{
cwd: pathResolve(workspaceRoot, projectRoot),

View File

@ -6,6 +6,10 @@ export interface ExpoEnsureSymlinkOutput {
success: boolean;
}
/**
* TODO (@xiongemi): remove this function in v19.
* @deprecated It is no longer needed for react native 73.
*/
export default async function* ensureSymlinkExecutor(
_,
context: ExecutorContext

View File

@ -0,0 +1,18 @@
import { createExportOptions } from './export.impl';
describe('createExportOptions', () => {
it('should not pass --dev if dev is false', () => {
const options = createExportOptions({ dev: false }, '');
expect(options).toEqual([]);
});
it('should pass --dev if dev is true', () => {
const options = createExportOptions({ dev: true }, '');
expect(options).toEqual(['--dev']);
});
it('should pass --output-dir with offset to workspace root', () => {
const options = createExportOptions({ outputDir: 'dist' }, 'apps/app');
expect(options).toEqual(['--output-dir', '../../dist']);
});
});

View File

@ -1,4 +1,9 @@
import { ExecutorContext, names } from '@nx/devkit';
import {
ExecutorContext,
joinPathFragments,
names,
offsetFromRoot,
} from '@nx/devkit';
import { ChildProcess, fork } from 'child_process';
import { resolve as pathResolve } from 'path';
@ -19,7 +24,6 @@ export default async function* exportExecutor(
try {
await exportAsync(context.root, projectRoot, options);
yield {
success: true,
};
@ -38,11 +42,7 @@ function exportAsync(
return new Promise((resolve, reject) => {
childProcess = fork(
require.resolve('@expo/cli/build/bin/cli'),
[
`export${options.bundler === 'webpack' ? ':web' : ''}`,
'.',
...createExportOptions(options),
],
[`export`, ...createExportOptions(options, projectRoot)],
{ cwd: pathResolve(workspaceRoot, projectRoot), env: process.env }
);
@ -63,19 +63,34 @@ function exportAsync(
});
}
const nxOptions = ['bundler'];
const nxOptions = ['bundler', 'interactive']; // interactive is passed in by e2e tests
// options from https://github.com/expo/expo/blob/main/packages/@expo/cli/src/export/index.ts
function createExportOptions(options: ExportExecutorSchema) {
export function createExportOptions(
options: ExportExecutorSchema,
projectRoot: string
) {
return Object.keys(options).reduce((acc, k) => {
if (!nxOptions.includes(k)) {
const v = options[k];
if (typeof v === 'boolean') {
if (v === true) {
// when true, does not need to pass the value true, just need to pass the flag in kebob case
acc.push(`--${names(k).fileName}`);
}
} else {
acc.push(`--${names(k).fileName}`, v);
switch (k) {
case 'outputDir':
const path = joinPathFragments(offsetFromRoot(projectRoot), v); // need to add offset for the outputDir
acc.push('--output-dir', path);
break;
case 'minify':
if (v === false) {
acc.push('--no-minify'); // cli only accpets --no-minify
}
break;
default:
if (typeof v === 'boolean') {
if (v === true) {
// when true, does not need to pass the value true, just need to pass the flag in kebob case
acc.push(`--${names(k).fileName}`);
}
} else {
acc.push(`--${names(k).fileName}`, v);
}
}
}
return acc;

View File

@ -1,11 +1,13 @@
// options form https://github.com/expo/expo/blob/main/packages/%40expo/cli/src/export/resolveOptions.ts
// https://github.com/expo/expo/blob/main/packages/%40expo/cli/src/export/index.ts
export interface ExportExecutorSchema {
platform?: 'ios' | 'android' | 'all' | 'web'; // default is 'all'
outputDir?: string;
clear?: boolean;
dev?: boolean;
minify?: boolean;
maxWorkers?: number;
dumpAssetmap?: boolean;
dumpSourcemap?: boolean;
bundler: 'metro' | 'webpack';
sourcemap?: boolean;
}

View File

@ -11,38 +11,39 @@
"platform": {
"description": "Choose the platform to compile for",
"enum": ["ios", "android", "all", "web"],
"default": "all",
"alias": "p",
"x-priority": "important"
},
"dev": {
"type": "boolean",
"description": "Bundle for development environments without minifying code or stripping the __DEV__ boolean. Configure static files for developing locally using a non-https server."
"description": "Configure static files for developing locally using a non-https server"
},
"clear": {
"type": "boolean",
"description": "Clear the bundler cache before exporting"
},
"minify": {
"type": "boolean",
"description": "Minify source"
},
"outputDir": {
"type": "string",
"description": "The directory to export the static files to. Default: dist"
"description": "Relative to workspace root, the directory to export the static files to. Default: dist"
},
"maxWorkers": {
"type": "number",
"description": "Maximum number of tasks to allow Metro to spawn"
"description": "When bundler is metro, the maximum number of tasks to allow the bundler to spawn"
},
"dumpAssetmap": {
"type": "boolean",
"description": "Dump the asset map for further processing"
"description": "When bundler is metro, whether to dump the asset map for further processing"
},
"dumpSourcemap": {
"sourceMaps": {
"type": "boolean",
"description": "Dump the source map for debugging the JS bundle"
},
"bundler": {
"enum": ["metro", "webpack"],
"description": "Choose the bundler to compile for",
"default": "metro"
"description": "When bundler is metro, whether to emit JavaScript source maps"
}
},
"required": []
"required": ["platform"],
"examplesFile": "../../../docs/export-examples.md"
}

View File

@ -1,7 +1,6 @@
import { ExecutorContext, names } from '@nx/devkit';
import { ChildProcess, fork } from 'child_process';
import { ensureNodeModulesSymlink } from '../../utils/ensure-node-modules-symlink';
import { ExpoInstallOptions } from './schema';
export interface ExpoInstallOutput {
@ -19,7 +18,6 @@ export default async function* installExecutor(
try {
await installAsync(context.root, options);
ensureNodeModulesSymlink(context.root, projectRoot);
yield {
success: true,

View File

@ -1,8 +1,7 @@
import { ExecutorContext, names } from '@nx/devkit';
import { ExecutorContext, names, workspaceRoot } from '@nx/devkit';
import { ChildProcess, fork } from 'child_process';
import { join } from 'path';
import { ensureNodeModulesSymlink } from '../../utils/ensure-node-modules-symlink';
import { podInstall } from '../../utils/pod-install-task';
import { installAsync } from '../install/install.impl';
import { ExpoPrebuildOptions } from './schema';
@ -19,13 +18,12 @@ export default async function* prebuildExecutor(
): AsyncGenerator<ExpoPrebuildOutput> {
const projectRoot =
context.projectsConfigurations.projects[context.projectName].root;
ensureNodeModulesSymlink(context.root, projectRoot);
try {
await prebuildAsync(context.root, projectRoot, options);
if (options.install) {
await installAsync(context.root, {});
await installAsync(workspaceRoot, {});
if (options.platform === 'ios') {
podInstall(join(context.root, projectRoot, 'ios'));
}
@ -70,13 +68,20 @@ export function prebuildAsync(
});
}
const nxOptions = ['install', 'interactive'];
const nxOptions = ['install', 'interactive']; // interactive is passed in by e2e tests
// options from https://github.com/expo/expo/blob/main/packages/%40expo/cli/src/prebuild/index.ts
function createPrebuildOptions(options: ExpoPrebuildOptions) {
return Object.keys(options).reduce((acc, k) => {
if (!nxOptions.includes(k)) {
const v = options[k];
acc.push(`--${names(k).fileName}=${v}`);
if (typeof v === 'boolean') {
if (v === true) {
// when true, does not need to pass the value true, just need to pass the flag in kebob case
acc.push(`--${names(k).fileName}`);
}
} else {
acc.push(`--${names(k).fileName}`, v);
}
}
return acc;
}, []);

View File

@ -31,5 +31,6 @@
"description": "Project template to clone from. File path pointing to a local tar file or a github repo"
}
},
"required": ["platform"],
"examplesFile": "../../../docs/prebuild-examples.md"
}

View File

@ -4,11 +4,6 @@ import { ChildProcess, fork } from 'child_process';
import { platform } from 'os';
import { existsSync } from 'fs-extra';
import { ensureNodeModulesSymlink } from '../../utils/ensure-node-modules-symlink';
import {
displayNewlyAddedDepsMessage,
syncDeps,
} from '../sync-deps/sync-deps.impl';
import { ExpoRunOptions } from './schema';
import { prebuildAsync } from '../prebuild/prebuild.impl';
import { podInstall } from '../../utils/pod-install-task';

View File

@ -0,0 +1,13 @@
import fetch from 'node-fetch';
export async function isPackagerRunning(
packagerPort: number
): Promise<'running' | 'not_running' | 'unrecognized'> {
try {
const resp = await fetch(`http://localhost:${packagerPort}/status`);
const data = await resp.text();
return data === 'packager-status:running' ? 'running' : 'unrecognized';
} catch {
return 'not_running';
}
}

View File

@ -0,0 +1,7 @@
export interface ExpoServeExecutorSchema {
port: number;
dev?: boolean;
minify?: boolean;
https?: boolean;
clear?: boolean;
}

View File

@ -0,0 +1,35 @@
{
"version": 2,
"outputCapture": "direct-nodejs",
"cli": "nx",
"$id": "NxExpoServe",
"$schema": "http://json-schema.org/schema",
"title": "Serve web app for Expo",
"description": "Packager Server target options.",
"type": "object",
"properties": {
"port": {
"type": "number",
"description": "Port to start the native Metro bundler on (does not apply to web or tunnel)",
"default": 19000,
"alias": "p"
},
"clear": {
"type": "boolean",
"description": "Clear the Metro bundler cache",
"alias": "c"
},
"maxWorkers": {
"type": "number",
"description": "Maximum number of tasks to allow Metro to spawn"
},
"dev": {
"type": "boolean",
"description": "Turn development mode on or off"
},
"minify": {
"type": "boolean",
"description": "Whether or not to minify code"
}
}
}

View File

@ -0,0 +1,127 @@
import { ExecutorContext, logger, names } from '@nx/devkit';
import { ChildProcess, fork } from 'child_process';
import { resolve as pathResolve } from 'path';
import { isPackagerRunning } from './lib/is-packager-running';
import { ExpoServeExecutorSchema } from './schema';
export interface ExpoServeOutput {
port?: number;
baseUrl?: string;
success: boolean;
}
export default async function* serveExecutor(
options: ExpoServeExecutorSchema,
context: ExecutorContext
): AsyncGenerator<ExpoServeOutput> {
const projectRoot =
context.projectsConfigurations.projects[context.projectName].root;
const serveProcess = await runCliServe(context.root, projectRoot, options);
yield {
port: options.port,
baseUrl: `http://localhost:${options.port}`,
success: true,
};
if (!serveProcess) {
return;
}
await new Promise<void>((resolve) => {
const processExitListener = (signal?: number | NodeJS.Signals) => () => {
serveProcess.kill(signal);
resolve();
process.exit();
};
process.once('exit', (signal) => serveProcess.kill(signal));
process.once('SIGTERM', processExitListener);
process.once('SIGINT', processExitListener);
process.once('SIGQUIT', processExitListener);
});
}
export async function runCliServe(
workspaceRoot: string,
projectRoot: string,
options: ExpoServeExecutorSchema
): Promise<ChildProcess> {
const result = await isPackagerRunning(options.port);
if (result === 'running') {
logger.info(`JS server already running on port ${options.port}.`);
} else if (result === 'unrecognized') {
logger.warn('JS server not recognized.');
} else {
// result === 'not_running'
logger.info('Starting JS server...');
try {
return await serveAsync(workspaceRoot, projectRoot, options);
} catch (error) {
logger.error(
`Failed to serve the packager server. Error details: ${error.message}`
);
throw error;
}
}
}
function serveAsync(
workspaceRoot: string,
projectRoot: string,
options: ExpoServeExecutorSchema
): Promise<ChildProcess> {
return new Promise<ChildProcess>((resolve, reject) => {
const childProcess = fork(
require.resolve('@expo/cli/build/bin/cli'),
['start', '--web', ...createServeOptions(options)],
{
cwd: pathResolve(workspaceRoot, projectRoot),
env: process.env,
stdio: ['inherit', 'pipe', 'pipe', 'ipc'],
}
);
childProcess.stdout.on('data', (data) => {
process.stdout.write(data);
if (data.toString().includes('Bundling complete')) {
resolve(childProcess);
}
});
childProcess.stderr.on('data', (data) => {
process.stderr.write(data);
});
childProcess.on('error', (err) => {
reject(err);
});
childProcess.on('exit', (code) => {
if (code === 0) {
resolve(childProcess);
} else {
reject(code);
}
});
});
}
function createServeOptions(options: ExpoServeExecutorSchema): string[] {
return Object.keys(options).reduce((acc, k) => {
const v = options[k];
if (k === 'dev') {
if (v === false) {
acc.push(`--no-dev`); // only no-dev flag is supported
}
} else {
if (typeof v === 'boolean') {
if (v === true) {
// when true, does not need to pass the value true, just need to pass the flag in kebob case
acc.push(`--${names(k).fileName}`);
}
} else {
acc.push(`--${names(k).fileName}`, v);
}
}
return acc;
}, []);
}

View File

@ -45,7 +45,13 @@ function startAsync(
childProcess = fork(
require.resolve('@expo/cli/build/bin/cli'),
['start', ...createStartOptions(options)],
{ cwd: pathResolve(workspaceRoot, projectRoot), env: process.env }
{
cwd: pathResolve(workspaceRoot, projectRoot),
env: {
RCT_METRO_PORT: options.port.toString(),
...process.env,
},
}
);
// Ensure the child process is killed when the parent exits

View File

@ -2,6 +2,8 @@ import { ExecutorContext, names } from '@nx/devkit';
import { resolve as pathResolve } from 'path';
import { ChildProcess, fork } from 'child_process';
import { resolveEas } from '../../utils/resolve-eas';
import { SubmitExecutorSchema } from './schema';
export interface ReactNativeSubmitOutput {
@ -35,7 +37,7 @@ function runCliSubmit(
) {
return new Promise((resolve, reject) => {
childProcess = fork(
require.resolve('eas-cli/bin/run'),
resolveEas(workspaceRoot),
['submit', ...createSubmitOptions(options)],
{
cwd: pathResolve(workspaceRoot, projectRoot),

View File

@ -3,32 +3,27 @@ import * as chalk from 'chalk';
import {
ExecutorContext,
logger,
ProjectGraph,
readJsonFile,
writeJsonFile,
} from '@nx/devkit';
import { findAllNpmDependencies } from '../../utils/find-all-npm-dependencies';
import { ExpoSyncDepsOptions } from './schema';
export interface ExpoSyncDepsOutput {
export interface ReactNativeSyncDepsOutput {
success: boolean;
}
export default async function* syncDepsExecutor(
options: ExpoSyncDepsOptions,
context: ExecutorContext
): AsyncGenerator<ExpoSyncDepsOutput> {
): AsyncGenerator<ReactNativeSyncDepsOutput> {
const projectRoot =
context.projectsConfigurations.projects[context.projectName].root;
displayNewlyAddedDepsMessage(
context.projectName,
await syncDeps(
context.projectName,
projectRoot,
context.root,
context.projectGraph,
typeof options.include === 'string'
? options.include.split(',')
: options.include,
@ -42,14 +37,16 @@ export default async function* syncDepsExecutor(
}
export async function syncDeps(
projectName: string,
projectRoot: string,
workspaceRoot: string,
projectGraph: ProjectGraph,
include: string[] = [],
exclude: string[] = []
): Promise<string[]> {
let npmDeps = findAllNpmDependencies(projectGraph, projectName);
const workspacePackageJsonPath = join(workspaceRoot, 'package.json');
const workspacePackageJson = readJsonFile(workspacePackageJsonPath);
let npmDeps = Object.keys(workspacePackageJson.dependencies || {});
let npmDevdeps = Object.keys(workspacePackageJson.devDependencies || {});
const packageJsonPath = join(workspaceRoot, projectRoot, 'package.json');
const packageJson = readJsonFile(packageJsonPath);
const newDeps = [];
@ -67,13 +64,27 @@ export async function syncDeps(
npmDeps = npmDeps.filter((dep) => !exclude.includes(dep));
}
if (!packageJson.devDependencies) {
packageJson.devDependencies = {};
}
if (!packageJson.dependencies) {
packageJson.dependencies = {};
}
npmDeps.forEach((dep) => {
if (!packageJson.dependencies[dep]) {
if (!packageJson.dependencies[dep] && !packageJson.devDependencies[dep]) {
packageJson.dependencies[dep] = '*';
newDeps.push(dep);
updated = true;
}
});
npmDevdeps.forEach((dep) => {
if (!packageJson.dependencies[dep] && !packageJson.devDependencies[dep]) {
packageJson.devDependencies[dep] = '*';
newDeps.push(dep);
updated = true;
}
});
if (updated) {
writeJsonFile(packageJsonPath, packageJson);

View File

@ -2,15 +2,15 @@ import { ExecutorContext, names } from '@nx/devkit';
import { resolve as pathResolve } from 'path';
import { ChildProcess, fork } from 'child_process';
import { ensureNodeModulesSymlink } from '../../utils/ensure-node-modules-symlink';
import { ExpoEasUpdateOptions } from './schema';
import { resolveEas } from '../../utils/resolve-eas';
import {
displayNewlyAddedDepsMessage,
syncDeps,
} from '../sync-deps/sync-deps.impl';
import { installAsync } from '../install/install.impl';
import { ExpoEasUpdateOptions } from './schema';
export interface ReactNativeUpdateOutput {
success: boolean;
}
@ -26,15 +26,8 @@ export default async function* buildExecutor(
await installAsync(context.root, { packages: ['expo-updates'] });
displayNewlyAddedDepsMessage(
context.projectName,
await syncDeps(
context.projectName,
projectRoot,
context.root,
context.projectGraph,
['expo-updates']
)
await syncDeps(projectRoot, context.root, ['expo-updates'])
);
ensureNodeModulesSymlink(context.root, projectRoot);
try {
await runCliUpdate(context.root, projectRoot, options);
@ -53,7 +46,7 @@ function runCliUpdate(
) {
return new Promise((resolve, reject) => {
childProcess = fork(
require.resolve('eas-cli/bin/run'),
resolveEas(workspaceRoot),
['update', ...createUpdateOptions(options)],
{ cwd: pathResolve(workspaceRoot, projectRoot), env: process.env }
);

View File

@ -7,7 +7,6 @@ import {
} from '@nx/devkit';
import { initGenerator as jsInitGenerator } from '@nx/js';
import { runSymlink } from '../../utils/symlink-task';
import { addLinting } from '../../utils/add-linting';
import { addJest } from '../../utils/add-jest';
@ -16,7 +15,7 @@ import initGenerator from '../init/init';
import { addProject } from './lib/add-project';
import { createApplicationFiles } from './lib/create-application-files';
import { addEasScripts } from './lib/add-eas-scripts';
import { addDetox } from './lib/add-detox';
import { addE2e } from './lib/add-e2e';
import { Schema } from './schema';
import { ensureDependencies } from '../../utils/ensure-dependencies';
import { initRootBabelConfig } from '../../utils/init-root-babel-config';
@ -72,10 +71,8 @@ export async function expoApplicationGeneratorInternal(
options.skipPackageJson
);
tasks.push(jestTask);
const detoxTask = await addDetox(host, options);
tasks.push(detoxTask);
const symlinkTask = runSymlink(host.root, options.appProjectRoot);
tasks.push(symlinkTask);
const e2eTask = await addE2e(host, options);
tasks.push(e2eTask);
addEasScripts(host);
if (!options.skipFormat) {

View File

@ -30,6 +30,7 @@
"bundler": "metro"
},
"plugins": [
<% if (e2eTestRunner === 'detox') { %>
[
"@config-plugins/detox",
{
@ -37,6 +38,7 @@
"subdomains": ["10.0.2.2", "localhost"]
}
]
<% } %>
]
}
}

View File

@ -21,8 +21,5 @@
},
"submit": {
"production": {}
},
"cli": {
"version": ">= <%= easCliVersion.replace('~', '') %>"
}
}

View File

@ -1,7 +1,6 @@
const { withNxMetro } = require('@nx/expo');
const { getDefaultConfig } = require('@expo/metro-config');
const { mergeConfig } = require('metro-config');
const exclusionList = require('metro-config/src/defaults/exclusionList');
const defaultConfig = getDefaultConfig(__dirname);
const { assetExts, sourceExts } = defaultConfig.resolver;
@ -19,9 +18,6 @@ const customConfig = {
resolver: {
assetExts: assetExts.filter((ext) => ext !== 'svg'),
sourceExts: [...sourceExts, 'svg'],
blockList: exclusionList([/^(?!.*node_modules).*\/dist\/.*/]),
// unstable_enableSymlinks: true,
// unstable_enablePackageExports: true,
},
};

View File

@ -19,7 +19,10 @@ export const App = () => {
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<SafeAreaView
style={{
flex: 1,
}}>
<ScrollView
ref={(ref) => {
scrollViewRef.current = ref;
@ -29,7 +32,7 @@ export const App = () => {
>
<View style={styles.section}>
<Text style={styles.textLg}>Hello there,</Text>
<Text style={[styles.textXL, styles.appTitleText]} testID="heading">
<Text style={[styles.textXL, styles.appTitleText]} testID="heading" role="heading">
Welcome <%= displayName %> 👋
</Text>
</View>

View File

@ -0,0 +1,57 @@
const createExpoWebpackConfigAsync = require('@expo/webpack-config');
const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin');
const { resolve } = require('path');
/**
* @deprecated use bundler: 'metro' instead
*/
module.exports = async function (env, argv) {
const config = await createExpoWebpackConfigAsync(env, argv);
// Customize the config before returning it.
// add additional rule to load files under libs
const rules = config.module.rules.find((rule) =>
Array.isArray(rule.oneOf)
)?.oneOf;
if (rules) {
rules.push({
test: /\.(mjs|[jt]sx?)$/,
exclude: /node_modules/,
use: {
loader: require.resolve('@nx/webpack/src/utils/web-babel-loader.js'),
options: {
presets: [
[
'@nx/react/babel',
{
runtime: 'automatic',
},
],
],
},
},
});
}
if (!config.resolve) {
config.resolve = {};
}
if (!config.resolve.plugins) {
config.resolve.plugins = [];
}
const extensions = ['.ts', '.tsx', '.mjs', '.js', '.jsx'];
const tsConfigPath = resolve(__dirname, 'tsconfig.json');
config.resolve.plugins.push(
new TsconfigPathsPlugin({
configFile: tsConfigPath,
extensions,
})
);
config.resolve.fallback = {
...config.resolve.fallback,
crypto: require.resolve('crypto-browserify'),
stream: require.resolve('stream-browserify'),
};
return config;
};

View File

@ -1,24 +0,0 @@
import { detoxApplicationGenerator } from '@nx/detox';
import { Tree } from '@nx/devkit';
import { NormalizedSchema } from './normalize-options';
import { Linter } from '@nx/eslint';
export async function addDetox(host: Tree, options: NormalizedSchema) {
if (options?.e2eTestRunner !== 'detox') {
return () => {};
}
return detoxApplicationGenerator(host, {
...options,
linter: Linter.EsLint,
e2eName: `${options.projectName}-e2e`,
e2eDirectory: `${options.appProjectRoot}-e2e`,
projectNameAndRootFormat: 'as-provided',
appProject: options.projectName,
appDisplayName: options.displayName,
appName: options.name,
framework: 'expo',
setParserOptionsProject: options.setParserOptionsProject,
skipFormat: true,
});
}

View File

@ -0,0 +1,104 @@
import type { GeneratorCallback, Tree } from '@nx/devkit';
import {
addProjectConfiguration,
ensurePackage,
getPackageManagerCommand,
joinPathFragments,
} from '@nx/devkit';
import { webStaticServeGenerator } from '@nx/web';
import { nxVersion } from '../../../utils/versions';
import { hasExpoPlugin } from '../../../utils/has-expo-plugin';
import { NormalizedSchema } from './normalize-options';
export async function addE2e(
tree: Tree,
options: NormalizedSchema
): Promise<GeneratorCallback> {
switch (options.e2eTestRunner) {
case 'cypress': {
const hasNxExportPlugin = hasExpoPlugin(tree);
if (!hasNxExportPlugin) {
webStaticServeGenerator(tree, {
buildTarget: `${options.projectName}:export`,
targetName: 'serve-static',
});
}
const { configurationGenerator } = ensurePackage<
typeof import('@nx/cypress')
>('@nx/cypress', nxVersion);
addProjectConfiguration(tree, options.e2eProjectName, {
projectType: 'application',
root: options.e2eProjectRoot,
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
targets: {},
implicitDependencies: [options.projectName],
tags: [],
});
return await configurationGenerator(tree, {
...options,
project: options.e2eProjectName,
directory: 'src',
// the name and root are already normalized, instruct the generator to use them as is
bundler: 'none',
skipFormat: true,
devServerTarget: `${options.projectName}:serve`,
port: 4200,
baseUrl: 'http://localhost:4200',
ciWebServerCommand: hasNxExportPlugin
? `nx run ${options.projectName}:serve-static`
: undefined,
jsx: true,
rootProject: options.rootProject,
});
}
case 'playwright': {
const { configurationGenerator } = ensurePackage<
typeof import('@nx/playwright')
>('@nx/playwright', nxVersion);
addProjectConfiguration(tree, options.e2eProjectName, {
projectType: 'application',
root: options.e2eProjectRoot,
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
targets: {},
implicitDependencies: [options.projectName],
});
return configurationGenerator(tree, {
project: options.e2eProjectName,
skipFormat: true,
skipPackageJson: options.skipPackageJson,
directory: 'src',
js: false,
linter: options.linter,
setParserOptionsProject: options.setParserOptionsProject,
webServerCommand: `${getPackageManagerCommand().exec} nx serve ${
options.name
}`,
webServerAddress: 'http://localhost:4200',
rootProject: options.rootProject,
});
}
case 'detox':
const { detoxApplicationGenerator } = ensurePackage<
typeof import('@nx/detox')
>('@nx/detox', nxVersion);
return detoxApplicationGenerator(tree, {
...options,
e2eName: options.e2eProjectName,
e2eDirectory: options.e2eProjectRoot,
projectNameAndRootFormat: 'as-provided',
appProject: options.projectName,
appDisplayName: options.displayName,
appName: options.name,
framework: 'expo',
setParserOptionsProject: options.setParserOptionsProject,
skipFormat: true,
});
case 'none':
default:
return () => {};
}
}

View File

@ -1,6 +1,5 @@
import {
addProjectConfiguration,
offsetFromRoot,
ProjectConfiguration,
readNxJson,
TargetConfiguration,
@ -40,22 +39,21 @@ function getTargets(options: NormalizedSchema) {
architect.start = {
executor: '@nx/expo:start',
dependsOn: ['ensure-symlink', 'sync-deps'],
options: {
port: 8081,
},
dependsOn: ['sync-deps'],
options: {},
};
architect.serve = {
executor: 'nx:run-commands',
executor: '@nx/expo:serve',
dependsOn: ['sync-deps'],
options: {
command: `nx start ${options.projectName}`,
port: 4200,
},
};
architect['run-ios'] = {
executor: '@nx/expo:run',
dependsOn: ['ensure-symlink', 'sync-deps'],
dependsOn: ['sync-deps'],
options: {
platform: 'ios',
},
@ -63,7 +61,7 @@ function getTargets(options: NormalizedSchema) {
architect['run-android'] = {
executor: '@nx/expo:run',
dependsOn: ['ensure-symlink', 'sync-deps'],
dependsOn: ['sync-deps'],
options: {
platform: 'android',
},
@ -71,6 +69,7 @@ function getTargets(options: NormalizedSchema) {
architect['build'] = {
executor: '@nx/expo:build',
dependsOn: ['sync-deps'],
options: {},
};
@ -89,14 +88,9 @@ function getTargets(options: NormalizedSchema) {
options: {},
};
architect['ensure-symlink'] = {
executor: '@nx/expo:ensure-symlink',
options: {},
};
architect['prebuild'] = {
executor: '@nx/expo:prebuild',
dependsOn: ['ensure-symlink', 'sync-deps'],
dependsOn: ['sync-deps'],
options: {},
};
@ -112,19 +106,11 @@ function getTargets(options: NormalizedSchema) {
architect['export'] = {
executor: '@nx/expo:export',
dependsOn: ['ensure-symlink', 'sync-deps'],
dependsOn: ['sync-deps'],
outputs: ['{options.outputDir}'],
options: {
platform: 'all',
outputDir: `${offsetFromRoot(options.appProjectRoot)}dist/${
options.appProjectRoot
}`,
},
};
architect['export-web'] = {
executor: '@nx/expo:export',
options: {
bundler: 'metro',
outputDir: `dist/${options.appProjectRoot}`,
},
};

View File

@ -7,7 +7,6 @@ import {
Tree,
} from '@nx/devkit';
import { join } from 'path';
import { easCliVersion } from '../../../utils/versions';
import { NormalizedSchema } from './normalize-options';
export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
@ -23,7 +22,6 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
offsetFromRoot: offsetFromRoot(options.appProjectRoot),
packageManager,
packageLockFile,
easCliVersion,
});
if (options.unitTestRunner === 'none') {
host.delete(join(options.appProjectRoot, `App.spec.tsx`));

View File

@ -2,7 +2,7 @@ import { Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Linter } from '@nx/eslint';
import { Schema } from '../schema';
import { normalizeOptions } from './normalize-options';
import { NormalizedSchema, normalizeOptions } from './normalize-options';
describe('Normalize Options', () => {
let appTree: Tree;
@ -36,7 +36,10 @@ describe('Normalize Options', () => {
unitTestRunner: 'jest',
skipFormat: false,
js: true,
});
rootProject: false,
e2eProjectName: 'my-app-e2e',
e2eProjectRoot: 'my-app-e2e',
} as NormalizedSchema);
});
it('should normalize options with name in camel case', async () => {
@ -64,7 +67,10 @@ describe('Normalize Options', () => {
skipFormat: false,
js: true,
unitTestRunner: 'jest',
});
rootProject: false,
e2eProjectName: 'myApp-e2e',
e2eProjectRoot: 'myApp-e2e',
} as NormalizedSchema);
});
it('should normalize options with directory', async () => {
@ -94,7 +100,10 @@ describe('Normalize Options', () => {
linter: Linter.EsLint,
skipFormat: false,
js: true,
});
rootProject: false,
e2eProjectName: 'my-app-e2e',
e2eProjectRoot: 'directory-e2e',
} as NormalizedSchema);
});
it('should normalize options that has directory in its name', async () => {
@ -122,7 +131,10 @@ describe('Normalize Options', () => {
linter: Linter.EsLint,
skipFormat: false,
js: true,
});
rootProject: false,
e2eProjectName: 'my-app-e2e',
e2eProjectRoot: 'directory/my-app-e2e',
} as NormalizedSchema);
});
it('should normalize options with display name', async () => {
@ -151,6 +163,9 @@ describe('Normalize Options', () => {
linter: Linter.EsLint,
skipFormat: false,
js: true,
});
rootProject: false,
e2eProjectName: 'my-app-e2e',
e2eProjectRoot: 'my-app-e2e',
} as NormalizedSchema);
});
});

View File

@ -8,6 +8,9 @@ export interface NormalizedSchema extends Schema {
appProjectRoot: string;
lowerCaseName: string;
parsedTags: string[];
rootProject: boolean;
e2eProjectName: string;
e2eProjectRoot: string;
}
export async function normalizeOptions(
@ -32,11 +35,14 @@ export async function normalizeOptions(
const parsedTags = options.tags
? options.tags.split(',').map((s) => s.trim())
: [];
const rootProject = appProjectRoot === '.';
const e2eProjectName = rootProject ? 'e2e' : `${appProjectName}-e2e`;
const e2eProjectRoot = rootProject ? 'e2e' : `${appProjectRoot}-e2e`;
return {
...options,
unitTestRunner: options.unitTestRunner || 'jest',
e2eTestRunner: options.e2eTestRunner || 'detox',
e2eTestRunner: options.e2eTestRunner,
name: projectNames.projectSimpleName,
className,
lowerCaseName: className.toLowerCase(),
@ -44,5 +50,8 @@ export async function normalizeOptions(
projectName: appProjectName,
appProjectRoot,
parsedTags,
rootProject,
e2eProjectName,
e2eProjectRoot,
};
}

View File

@ -15,7 +15,7 @@ export interface Schema {
js: boolean; // default is false
linter: Linter; // default is eslint
setParserOptionsProject?: boolean; // default is false
e2eTestRunner: 'detox' | 'none'; // default is detox
e2eTestRunner: 'cypress' | 'playwright' | 'detox' | 'none'; // default is cypress
standaloneConfig?: boolean;
skipPackageJson?: boolean; // default is false
}

View File

@ -48,7 +48,7 @@
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint"],
"enum": ["eslint", "none"],
"default": "eslint"
},
"unitTestRunner": {
@ -75,8 +75,8 @@
"e2eTestRunner": {
"description": "Adds the specified e2e test runner",
"type": "string",
"enum": ["detox", "none"],
"default": "detox"
"enum": ["cypress", "playwright", "detox", "none"],
"default": "cypress"
},
"standaloneConfig": {
"description": "Split the project configuration into `<projectRoot>/project.json` rather than including it inside `workspace.json`.",

View File

@ -11,7 +11,6 @@ import {
import { updatePackageScripts } from '@nx/devkit/src/utils/update-package-scripts';
import { createNodes, ExpoPluginOptions } from '../../../plugins/plugin';
import {
easCliVersion,
expoCliVersion,
expoVersion,
nxVersion,
@ -20,6 +19,7 @@ import {
reactVersion,
} from '../../utils/versions';
import { hasExpoPlugin } from '../../utils/has-expo-plugin';
import { addGitIgnoreEntry } from './lib/add-git-ignore-entry';
import { Schema } from './schema';
@ -59,7 +59,6 @@ export function updateDependencies(host: Tree, schema: Schema) {
{
'@nx/expo': nxVersion,
'@expo/cli': expoCliVersion,
'eas-cli': easCliVersion,
},
undefined,
schema.keepExistingVersions

View File

@ -0,0 +1,21 @@
module.exports = function (api) {
api.cache(true);
return {
presets: [
[
'@nx/react/babel',
{
runtime: 'automatic',
useBuiltIns: 'usage',
},
],
],
plugins: [],
env: {
test: {
presets: ['babel-preset-expo'],
},
},
};
};

View File

@ -1,17 +0,0 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": [],
"env": {
"test": {
"presets": ["babel-preset-expo"]
}
}
}

View File

@ -34,7 +34,7 @@
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint"],
"enum": ["eslint", "none"],
"default": "eslint"
},
"unitTestRunner": {

View File

@ -1,40 +0,0 @@
import { addProjectConfiguration, getProjects, Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import update from './add-detox-app-json';
describe('add-eas-update-target', () => {
let tree: Tree;
beforeEach(async () => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
addProjectConfiguration(tree, 'product', {
root: 'apps/product',
sourceRoot: 'apps/product/src',
targets: {
start: {
executor: '@nrwl/expo:start',
},
},
});
tree.write('apps/product/app.json', '{"expo": {}}');
});
it(`should update app.json with plugin detox`, async () => {
await update(tree);
const appJson = JSON.parse(tree.read('apps/product/app.json').toString());
expect(appJson).toEqual({
expo: {
plugins: [
[
'@config-plugins/detox',
{
skipProguard: false,
subdomains: ['10.0.2.2', 'localhost'],
},
],
],
},
});
});
});

View File

@ -1,31 +0,0 @@
import { Tree, formatFiles, getProjects, updateJson } from '@nx/devkit';
/**
* Add detox plugin to app.json for expo
*/
export default async function update(tree: Tree) {
const projects = getProjects(tree);
projects.forEach((config) => {
if (
config.targets?.['start']?.executor === '@nrwl/expo:start' ||
config.targets?.['start']?.executor === '@nx/expo:start'
) {
updateJson(tree, `${config.root}/app.json`, (json) => {
if (!json.expo.plugins) {
json.expo.plugins = [];
}
json.expo.plugins.push([
'@config-plugins/detox',
{
skipProguard: false,
subdomains: ['10.0.2.2', 'localhost'],
},
]);
return json;
});
}
});
await formatFiles(tree);
}

View File

@ -36,7 +36,6 @@ const content = `
const { withNxMetro } = require('@nx/expo');
const { getDefaultConfig } = require('@expo/metro-config');
const { mergeConfig } = require('metro-config');
const exclusionList = require('metro-config/src/defaults/exclusionList');
const defaultConfig = getDefaultConfig(__dirname);
const { assetExts, sourceExts } = defaultConfig.resolver;
@ -54,9 +53,6 @@ const customConfig = {
resolver: {
assetExts: assetExts.filter((ext) => ext !== 'svg'),
sourceExts: [...sourceExts, 'svg'],
blockList: exclusionList([/^(?!.*node_modules).*\\/dist\\/.*/]),
unstable_enableSymlinks: true,
unstable_enablePackageExports: true,
},
};

View File

@ -1,4 +1,8 @@
import { readJson, Tree, updateJson } from '@nx/devkit';
import {
formatFiles,
removeDependenciesFromPackageJson,
Tree,
} from '@nx/devkit';
/**
* Remove @types/react-native package since it is no longer required. It would be a part of react native package.
@ -6,17 +10,6 @@ import { readJson, Tree, updateJson } from '@nx/devkit';
* @returns
*/
export default async function update(tree: Tree) {
const packageJson = readJson(tree, 'package.json');
if (
!packageJson.devDependencies['@types/react-native'] ||
!packageJson.dependencies['react-native']
) {
return;
}
updateJson(tree, 'package.json', (packageJson) => {
delete packageJson.devDependencies['@types/react-native'];
return packageJson;
});
removeDependenciesFromPackageJson(tree, [], ['@types/react-native']);
await formatFiles(tree);
}

View File

@ -1,11 +1,4 @@
import {
Tree,
formatFiles,
getProjects,
updateProjectConfiguration,
updateJson,
} from '@nx/devkit';
import { easCliVersion } from '../../utils/versions';
import { Tree, getProjects, updateJson } from '@nx/devkit';
export default async function update(tree: Tree) {
const projects = getProjects(tree);
@ -14,7 +7,7 @@ export default async function update(tree: Tree) {
if (config.targets?.['start']?.executor === '@nx/expo:start') {
updateJson(tree, `${config.root}/eas.json`, (easJson) => {
if (easJson?.cli?.version) {
easJson.cli.version = `>= ${easCliVersion.replace('~', '')}`;
easJson.cli.version = `>= 5`;
}
return easJson;
});

View File

@ -0,0 +1,41 @@
import { addProjectConfiguration, getProjects, Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import update from './change-outputDir-export-target';
describe('change-outputDir-export-target', () => {
let tree: Tree;
beforeEach(async () => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
addProjectConfiguration(tree, 'product', {
root: 'apps/product',
sourceRoot: 'apps/product/src',
targets: {
export: {
executor: '@nx/expo:export',
options: {
platform: 'all',
outputDir: '../../dist/apps/dogs',
},
dependsOn: ['sync-deps'],
},
},
});
});
it(`should remove offset from outputDir`, async () => {
await update(tree);
getProjects(tree).forEach((project) => {
expect(project.targets['export']).toEqual({
dependsOn: ['sync-deps'],
executor: '@nx/expo:export',
options: {
outputDir: 'dist/apps/dogs',
platform: 'all',
},
outputs: ['{options.outputDir}'],
});
});
});
});

View File

@ -0,0 +1,29 @@
import {
Tree,
getProjects,
offsetFromRoot,
updateProjectConfiguration,
} from '@nx/devkit';
/**
* Remove the offset from the outputDir of the export target
*/
export default async function update(tree: Tree) {
const projects = getProjects(tree);
for (const [projectName, config] of projects.entries()) {
if (config.targets?.['export']?.executor === '@nx/expo:export') {
const target = config.targets['export'];
if (target.options?.outputDir) {
const offset = offsetFromRoot(config.root);
target.options.outputDir = target.options.outputDir.replace(offset, '');
target.outputs = ['{options.outputDir}'];
updateProjectConfiguration(tree, projectName, config);
}
}
if (config.targets?.['export-web']?.executor === '@nx/expo:export') {
delete config.targets['export-web'];
updateProjectConfiguration(tree, projectName, config);
}
}
}

View File

@ -0,0 +1,68 @@
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Tree, addProjectConfiguration } from '@nx/devkit';
import update from './remove-block-list';
describe('remove-block-list', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
addProjectConfiguration(tree, 'product', {
root: 'apps/product',
targets: {
start: {
executor: '@nx/expo:start',
},
},
});
tree.write(
'apps/product/metro.config.js',
`
const { withNxMetro } = require('@nx/expo');
const { getDefaultConfig } = require('@expo/metro-config');
const { mergeConfig } = require('metro-config');
const exclusionList = require('metro-config/src/defaults/exclusionList');
const defaultConfig = getDefaultConfig(__dirname);
const { assetExts, sourceExts } = defaultConfig.resolver;
/**
* Metro configuration
* https://facebook.github.io/metro/docs/configuration
*
* @type {import('metro-config').MetroConfig}
*/
const customConfig = {
transformer: {
babelTransformerPath: require.resolve('react-native-svg-transformer'),
},
resolver: {
assetExts: assetExts.filter((ext) => ext !== 'svg'),
sourceExts: [...sourceExts, 'svg'],
blockList: exclusionList([/^(?!.*node_modules).*\/dist\/.*/]),
unstable_enableSymlinks: true,
unstable_enablePackageExports: true,
},
};
module.exports = withNxMetro(mergeConfig(defaultConfig, customConfig), {
// Change this to true to see debugging info.
// Useful if you have issues resolving modules
debug: false,
// all the file extensions used for imports other than 'ts', 'tsx', 'js', 'jsx', 'json'
extensions: [],
// Specify folders to watch, in addition to Nx defaults (workspace libraries and node_modules)
watchFolders: [],
});`
);
});
it('should remove blockList', async () => {
await update(tree);
const metroConfig = tree.read(`apps/product/metro.config.js`).toString();
expect(metroConfig).not.toContain('blockList');
expect(metroConfig).not.toContain('unstable_enableSymlinks');
expect(metroConfig).not.toContain('unstable_enablePackageExports');
});
});

View File

@ -0,0 +1,32 @@
import { Tree, formatFiles, getProjects, joinPathFragments } from '@nx/devkit';
/**
* This migration remove blockList in metro.config.js.
* It is now excluding dist folder in watchFolders in withNxMetro.
*/
export default async function update(tree: Tree) {
const projects = getProjects(tree);
for (const [_, config] of projects.entries()) {
if (config.targets?.['start']?.executor === '@nx/expo:start') {
if (tree.exists(joinPathFragments(config.root, 'metro.config.js'))) {
let content = tree
.read(joinPathFragments(config.root, 'metro.config.js'))
.toString();
content = content.replace(
`blockList: exclusionList([/^(?!.*node_modules).*/dist/.*/]),`,
''
);
content = content.replace('unstable_enableSymlinks: true,', '');
content = content.replace('unstable_enablePackageExports: true,', '');
content = content.replace(
`const exclusionList = require('metro-config/src/defaults/exclusionList');`,
''
);
tree.write(joinPathFragments(config.root, 'metro.config.js'), content);
await formatFiles(tree);
}
}
}
}

View File

@ -0,0 +1,24 @@
import {
formatFiles,
removeDependenciesFromPackageJson,
Tree,
} from '@nx/devkit';
/**
* Remove eas-cli from dev dependencies.
* Use globally eas-cli.
*
* Remove metro and metro-resolver from dev dependencies.
* react-native has dependency of @react-native/community-cli-plugin
* @react-native/community-cli-plugin has dependency of metro
* @param tree
* @returns
*/
export default async function update(tree: Tree) {
removeDependenciesFromPackageJson(
tree,
[],
['eas-cli', 'metro', 'metro-resolver']
);
await formatFiles(tree);
}

View File

@ -0,0 +1,38 @@
import { addProjectConfiguration, getProjects, Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import update from './remove-symlink-target';
describe('remove-symlink-target', () => {
let tree: Tree;
beforeEach(async () => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
addProjectConfiguration(tree, 'product', {
root: 'apps/product',
sourceRoot: 'apps/product/src',
targets: {
'ensure-symlink': {
executor: '@nx/expo:ensure-symlink',
options: {},
},
export: {
executor: '@nx/expo:export',
options: {
platform: 'all',
outputDir: '../../dist/apps/dogs',
},
dependsOn: ['ensure-symlink', 'sync-deps'],
},
},
});
});
it(`should remove ensure-symlink target from project.json`, async () => {
await update(tree);
getProjects(tree).forEach((project) => {
expect(project.targets['ensure-symlink']).toBeUndefined();
expect(project.targets['export'].dependsOn).toEqual(['sync-deps']);
});
});
});

View File

@ -0,0 +1,43 @@
import {
TargetConfiguration,
Tree,
getProjects,
updateProjectConfiguration,
} from '@nx/devkit';
import { removeSync } from 'fs-extra';
/**
* Remove ensure-symlink target.
* It is going to be supported by react-native version 0.73 by default.
*/
export default async function update(tree: Tree) {
const projects = getProjects(tree);
for (const [projectName, config] of projects.entries()) {
if (
config.targets?.['ensure-symlink']?.executor === '@nx/expo:ensure-symlink'
) {
removeTargets(config.targets, 'ensure-symlink');
updateProjectConfiguration(tree, projectName, config);
removeSync(`${config.root}/node_modules`);
}
}
}
function removeTargets(
targets: {
[targetName: string]: TargetConfiguration<any>;
},
targetNameToRemove: string
) {
for (const targetName in targets) {
if (targetName === targetNameToRemove) {
delete targets[targetName];
}
if (targets[targetName]?.dependsOn?.length) {
targets[targetName].dependsOn = targets[targetName].dependsOn.filter(
(dependsOn) => dependsOn !== targetNameToRemove
);
}
}
}

View File

@ -25,19 +25,28 @@ export async function addJest(
});
// overwrite the jest.config.ts file because react native needs to have special transform property
// use preset from https://github.com/expo/expo/blob/main/packages/jest-expo/jest-preset.js
const configPath = `${appProjectRoot}/jest.config.${js ? 'js' : 'ts'}`;
const content = `module.exports = {
displayName: '${projectName}',
resolver: '@nx/jest/plugins/resolver',
preset: 'jest-expo',
transformIgnorePatterns: [
'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)',
],
moduleFileExtensions: ['ts', 'js', 'html', 'tsx', 'jsx'],
setupFilesAfterEnv: ['<rootDir>/test-setup.${js ? 'js' : 'ts'}'],
moduleNameMapper: {
'\\\\.svg$': '@nx/expo/plugins/jest/svg-mock'
}
},
transform: {
'\\.[jt]sx?$': [
'babel-jest',
{
configFile: __dirname + '/.babelrc.js',
},
],
'^.+\\.(bmp|gif|jpg|jpeg|mp4|png|psd|svg|webp|ttf|otf|m4v|mov|mp4|mpeg|mpg|webm|aac|aiff|caf|m4a|mp3|wav|html|pdf|obj)$': require.resolve(
'jest-expo/src/preset/assetFileTransformer.js'
),
},
};`;
host.write(configPath, content);

View File

@ -6,10 +6,10 @@ import {
import {
babelPresetExpoVersion,
expoMetroConfigVersion,
expoMetroRuntimeVersion,
expoSplashScreenVersion,
expoStatusBarVersion,
jestExpoVersion,
metroVersion,
reactNativeSvgTransformerVersion,
reactNativeSvgVersion,
reactNativeWebVersion,
@ -27,13 +27,12 @@ export function ensureDependencies(host: Tree): GeneratorCallback {
'expo-status-bar': expoStatusBarVersion,
'react-native-web': reactNativeWebVersion,
'@expo/metro-config': expoMetroConfigVersion,
'@expo/metro-runtime': expoMetroRuntimeVersion,
'react-native-svg-transformer': reactNativeSvgTransformerVersion,
'react-native-svg': reactNativeSvgVersion,
},
{
'@types/react': typesReactVersion,
metro: metroVersion,
'metro-resolver': metroVersion,
'react-test-renderer': reactTestRendererVersion,
'@testing-library/react-native': testingLibraryReactNativeVersion,
'@testing-library/jest-native': testingLibraryJestNativeVersion,

View File

@ -1,103 +0,0 @@
import { findAllNpmDependencies } from './find-all-npm-dependencies';
import { DependencyType, ProjectGraph } from '@nx/devkit';
test('findAllNpmDependencies', () => {
const graph: ProjectGraph = {
nodes: {
myapp: {
type: 'app',
name: 'myapp',
data: { files: [] },
},
lib1: {
type: 'lib',
name: 'lib1',
data: { files: [] },
},
lib2: {
type: 'lib',
name: 'lib2',
data: { files: [] },
},
lib3: {
type: 'lib',
name: 'lib3',
data: { files: [] },
},
} as any,
externalNodes: {
'npm:react-native-image-picker': {
type: 'npm',
name: 'npm:react-native-image-picker',
data: {
version: '1',
packageName: 'react-native-image-picker',
},
},
'npm:react-native-dialog': {
type: 'npm',
name: 'npm:react-native-dialog',
data: {
version: '1',
packageName: 'react-native-dialog',
},
},
'npm:react-native-snackbar': {
type: 'npm',
name: 'npm:react-native-snackbar',
data: {
version: '1',
packageName: 'react-native-snackbar',
},
},
'npm:@nx/react-native': {
type: 'npm',
name: 'npm:@nx/react-native',
data: {
version: '1',
packageName: '@nx/react-native',
},
},
},
dependencies: {
myapp: [
{ type: DependencyType.static, source: 'myapp', target: 'lib1' },
{ type: DependencyType.static, source: 'myapp', target: 'lib2' },
{
type: DependencyType.static,
source: 'myapp',
target: 'npm:react-native-image-picker',
},
{
type: DependencyType.static,
source: 'myapp',
target: 'npm:@nx/react-native',
},
],
lib1: [
{ type: DependencyType.static, source: 'lib1', target: 'lib2' },
{
type: DependencyType.static,
source: 'lib3',
target: 'npm:react-native-snackbar',
},
],
lib2: [{ type: DependencyType.static, source: 'lib2', target: 'lib3' }],
lib3: [
{
type: DependencyType.static,
source: 'lib3',
target: 'npm:react-native-dialog',
},
],
},
};
const result = findAllNpmDependencies(graph, 'myapp');
expect(result).toEqual([
'react-native-dialog',
'react-native-snackbar',
'react-native-image-picker',
]);
});

View File

@ -1,33 +0,0 @@
import { ProjectGraph } from '@nx/devkit';
export function findAllNpmDependencies(
graph: ProjectGraph,
projectName: string,
list: string[] = [],
seen = new Set<string>()
) {
// In case of bad circular dependencies
if (seen.has(projectName)) {
return list;
}
seen.add(projectName);
const node = graph.externalNodes[projectName];
// Don't want to include '@nx/react-native' because React Native
// autolink will warn that the package has no podspec file for iOS.
if (node) {
if (
node.name !== `npm:@nx/react-native` &&
node.name !== `npm:@nrwl/react-native`
) {
list.push(node.data.packageName);
}
} else {
// it's workspace project, search for it's dependencies
graph.dependencies[projectName]?.forEach((dep) =>
findAllNpmDependencies(graph, dep.target, list, seen)
);
}
return list;
}

View File

@ -0,0 +1,37 @@
import { execSync } from 'child_process';
export function resolveEas(workspaceRoot: string): string {
try {
execSync('eas --version');
} catch {
throw new Error(
'EAS is not installed. Please run `npm install --global eas-cli` or `yarn global add eas-cli`.'
);
}
let npmGlobalPath: string, yarnGlobalPath: string;
try {
npmGlobalPath = execSync('npm root -g')
?.toString()
?.trim()
?.replace('\u001b[2K\u001b[1G', ''); // strip out ansi codes
} catch {}
try {
yarnGlobalPath = execSync('yarn global dir')
?.toString()
?.trim()
?.replace('\u001b[2K\u001b[1G', ''); // strip out ansi codes
} catch {}
try {
return require.resolve('eas-cli/bin/run', {
paths: [npmGlobalPath, yarnGlobalPath, workspaceRoot].filter(
Boolean
) as string[],
});
} catch {
throw new Error(
'Can not resolve EAS. Please run `npm install --global eas-cli` or `yarn global add eas-cli`.'
);
}
}

View File

@ -1,19 +0,0 @@
import { ensureNodeModulesSymlink } from './ensure-node-modules-symlink';
import * as chalk from 'chalk';
import { GeneratorCallback, logger } from '@nx/devkit';
export function runSymlink(
workspaceRoot: string,
projectRoot: string
): GeneratorCallback {
return () => {
logger.info(`creating symlinks for ${chalk.bold(projectRoot)}`);
try {
ensureNodeModulesSymlink(workspaceRoot, projectRoot);
} catch {
throw new Error(
`Failed to create symlinks for ${chalk.bold(projectRoot)}`
);
}
};
}

View File

@ -1,26 +1,25 @@
export const nxVersion = require('../../package.json').version;
export const expoVersion = '49.0.16';
export const expoMetroConfigVersion = '~0.10.7';
export const expoSplashScreenVersion = '~0.20.5';
export const expoStatusBarVersion = '~1.6.0';
export const expoCliVersion = '~0.10.13'; // @expo/cli
export const easCliVersion = '~5.2.0';
export const babelPresetExpoVersion = '~9.5.2';
export const expoVersion = '~50.0.3';
export const expoSplashScreenVersion = '~0.26.1';
export const expoStatusBarVersion = '~1.11.1';
export const expoCliVersion = '~0.16.5'; // @expo/cli
export const babelPresetExpoVersion = '~10.0.0';
export const reactVersion = '18.2.0';
export const reactDomVersion = '18.2.0';
export const reactTestRendererVersion = '18.2.0';
export const typesReactVersion = '18.0.28';
export const typesReactVersion = '~18.2.45';
export const reactNativeVersion = '0.72.6';
export const reactNativeVersion = '0.73.2';
export const reactNativeWebVersion = '~0.19.9';
export const reactNativeSvgTransformerVersion = '1.0.0';
export const reactNativeSvgVersion = '13.9.0';
export const expoMetroConfigVersion = '~0.17.3';
export const expoMetroRuntimeVersion = '~3.1.1';
export const metroVersion = '0.76.8';
export const reactNativeSvgTransformerVersion = '1.2.0';
export const reactNativeSvgVersion = '14.1.0';
export const testingLibraryReactNativeVersion = '~12.3.0';
export const testingLibraryReactNativeVersion = '~12.4.2';
export const testingLibraryJestNativeVersion = '~5.4.3';
export const jestExpoVersion = '~49.0.0';
export const jestExpoVersion = '~50.0.1';

View File

@ -32,7 +32,12 @@
"react-native",
// These are in ensurePackage
"@nx/rollup",
"@nx/storybook"
"@nx/storybook",
"@nx/vite",
"@nx/webpack",
"@nx/detox",
"@nx/cypress",
"@nx/playwright"
]
}
]

View File

@ -49,6 +49,11 @@
"implementation": "./src/executors/pod-install/pod-install.impl",
"schema": "./src/executors/pod-install/schema.json",
"description": "Run `pod install` in the `ios` directory."
},
"upgrade": {
"implementation": "./src/executors/upgrade/upgrade.impl",
"schema": "./src/executors/upgrade/schema.json",
"description": "upgrade executor"
}
}
}

View File

@ -6,7 +6,7 @@
"init": {
"factory": "./src/generators/init/init#reactNativeInitGenerator",
"schema": "./src/generators/init/schema.json",
"description": "Initialize the `@nrwl/react-native` plugin.",
"description": "Initialize the `@nx/react-native` plugin.",
"hidden": true
},
"application": {
@ -32,26 +32,27 @@
"storybook-configuration": {
"factory": "./src/generators/storybook-configuration/configuration#storybookConfigurationGenerator",
"schema": "./src/generators/storybook-configuration/schema.json",
"description": "Set up Storybook for a React-native application or library.",
"hidden": false
"description": "Set up Storybook for a React Native application or library."
},
"component-story": {
"factory": "./src/generators/component-story/component-story#componentStoryGenerator",
"schema": "./src/generators/component-story/schema.json",
"description": "Generate Storybook story for a React-native component.",
"hidden": false
"description": "Generate Storybook story for a React Native component."
},
"stories": {
"factory": "./src/generators/stories/stories#storiesGenerator",
"schema": "./src/generators/stories/schema.json",
"description": "Create stories/specs for all components declared in an application or library.",
"hidden": false
"description": "Create stories for all components declared in an application or library."
},
"upgrade-native": {
"factory": "./src/generators/upgrade-native/upgrade-native#reactNativeUpgradeNativeGenerator",
"schema": "./src/generators/upgrade-native/schema.json",
"description": "Destructive command to upgrade native iOS and Android code to latest.",
"hidden": false
"description": "Destructive command to upgrade native iOS and Android code to latest."
},
"web-configuration": {
"factory": "./src/generators/web-configuration/web-configuration#webConfigurationGenerator",
"schema": "./src/generators/web-configuration/schema.json",
"description": "Set up web configuration for a React Native app"
}
}
}

View File

@ -41,6 +41,42 @@
"version": "16.9.0-beta.1",
"description": "Remove @types/react-native from package.json",
"implementation": "./src/migrations/update-16-9-0/remove-types-react-native"
},
"update-18-0-0-add-web-configuration": {
"cli": "nx",
"version": "18.0.0-beta.0",
"description": "Add web configuration to react native projects",
"implementation": "./src/migrations/update-18-0-0/add-web-configuration"
},
"update-18-0-0-change-storybook-targets": {
"cli": "nx",
"version": "18.0.0-beta.0",
"description": "Upgrade react native storybook target to use web",
"implementation": "./src/migrations/update-18-0-0/change-storybook-targets"
},
"update-18-0-0-remove-block-list": {
"cli": "nx",
"version": "18.0.0-beta.0",
"description": "Remove blockList in metro.config.js.",
"implementation": "./src/migrations/update-18-0-0/remove-block-list"
},
"update-18-0-0-remove-metro": {
"cli": "nx",
"version": "18.0.0-beta.0",
"description": "Remove metro-* and @react-native-community/cli-* from package.json devDependencies",
"implementation": "./src/migrations/update-18-0-0/remove-metro"
},
"update-18-0-0-remove-symlink-target": {
"cli": "nx",
"version": "18.0.0-beta.0",
"description": "Remove ensure-symlink target",
"implementation": "./src/migrations/update-18-0-0/remove-symlink-target"
},
"update-18-0-0-add-upgrade-target": {
"cli": "nx",
"version": "18.0.0-beta.0",
"description": "Add upgrade target to react native projects",
"implementation": "./src/migrations/update-18-0-0/add-upgrade-target"
}
},
"packageJsonUpdates": {
@ -551,7 +587,43 @@
"version": "17.3.0-beta.3",
"packages": {
"@types/node": {
"version": "18.16.9",
"version": "18.16.9"
}
}
},
"18.0.0": {
"version": "18.0.0-beta.0",
"packages": {
"react-native": {
"version": "0.73.2",
"alwaysAddToPackageJson": false
},
"@react-native/babel-preset": {
"version": "^0.73.18",
"addToPackageJson": "devDependencies"
},
"@react-native/metro-config": {
"version": "^0.73.2",
"addToPackageJson": "devDependencies"
},
"@types/react": {
"version": "~18.2.45",
"alwaysAddToPackageJson": false
},
"@testing-library/react-native": {
"version": "~12.4.2",
"alwaysAddToPackageJson": false
},
"react-native-svg-transformer": {
"version": "1.2.0",
"alwaysAddToPackageJson": false
},
"react-native-svg": {
"version": "14.1.0",
"alwaysAddToPackageJson": false
},
"@react-native-community/cli-platform-android": {
"version": "12.3.0",
"alwaysAddToPackageJson": false
}
}

View File

@ -30,13 +30,11 @@
"fs-extra": "^11.1.0",
"glob": "7.1.4",
"ignore": "^5.0.4",
"metro-config": "~0.76.8",
"metro-resolver": "~0.76.8",
"minimatch": "9.0.3",
"metro-config": "~0.80.4",
"metro-resolver": "~0.80.4",
"node-fetch": "^2.6.7",
"tsconfig-paths": "^4.1.2",
"tslib": "^2.3.0",
"@nx/detox": "file:../detox",
"@nx/devkit": "file:../devkit",
"@nx/jest": "file:../jest",
"@nx/js": "file:../js",
@ -44,9 +42,6 @@
"@nx/react": "file:../react",
"@nx/workspace": "file:../workspace"
},
"peerDependencies": {
"react-native": ">= 0.72.0 < 0.73.0"
},
"executors": "./executors.json",
"ng-update": {
"requirements": {},

View File

@ -45,6 +45,11 @@ export function getResolveRequest(extensions: string[]) {
if (resolvedPath) {
return resolvedPath;
}
if (debug) {
console.log(
chalk.red(`[Nx] Unable to resolve with any resolver: ${realModuleName}`)
);
}
throw new Error(`Cannot resolve ${chalk.bold(realModuleName)}`);
};
}
@ -53,7 +58,7 @@ function resolveRequestFromContext(
resolveRequest: Function,
context: any,
realModuleName: string,
platform: string,
platform: string | null,
debug: boolean
) {
try {
@ -75,7 +80,7 @@ function resolveRequestFromContext(
function defaultMetroResolver(
context: any,
realModuleName: string,
platform: string,
platform: string | null,
debug: boolean
) {
try {
@ -130,7 +135,7 @@ function tsconfigPathsResolver(
context: any,
extensions: string[],
realModuleName: string,
platform: string,
platform: string | null,
debug: boolean
) {
try {
@ -145,7 +150,7 @@ function tsconfigPathsResolver(
} catch {
if (debug) {
console.log(
chalk.red(`[Nx] Failed to resolve ${chalk.bold(realModuleName)}`)
chalk.cyan(`[Nx] Failed to resolve ${chalk.bold(realModuleName)}`)
);
console.log(
chalk.cyan(

View File

@ -1,7 +1,7 @@
import { workspaceRoot } from '@nx/devkit';
import { joinPathFragments, workspaceRoot } from '@nx/devkit';
import { mergeConfig } from 'metro-config';
import type { MetroConfig } from 'metro-config';
import { existsSync } from 'fs-extra';
import { existsSync, readdirSync, statSync } from 'fs-extra';
import { getResolveRequest } from './metro-resolver';
@ -19,16 +19,26 @@ export async function withNxMetro(
if (opts.debug) process.env.NX_REACT_NATIVE_DEBUG = 'true';
if (opts.extensions) extensions.push(...opts.extensions);
let watchFolders = [workspaceRoot];
let watchFolders = readdirSync(workspaceRoot)
.filter(
(fileName) =>
!['dist', 'e2e'].includes(fileName) && !fileName.startsWith('.')
)
.map((fileName) => joinPathFragments(workspaceRoot, fileName))
.filter((filePath) => statSync(filePath).isDirectory());
if (opts.watchFolders?.length) {
watchFolders = watchFolders.concat(opts.watchFolders);
}
watchFolders = watchFolders.filter((folder) => existsSync(folder));
watchFolders = [...new Set(watchFolders)].filter((folder) =>
existsSync(folder)
);
const nxConfig: MetroConfig = {
resolver: {
resolveRequest: getResolveRequest(extensions),
nodeModulesPaths: [joinPathFragments(workspaceRoot, 'node_modules')],
},
watchFolders,
};

Some files were not shown because too many files have changed in this diff Show More