diff --git a/docs/angular/api-detox/generators/application.md b/docs/angular/api-detox/generators/application.md index c99ef6852f99f..8dc9c46e12da4 100644 --- a/docs/angular/api-detox/generators/application.md +++ b/docs/angular/api-detox/generators/application.md @@ -79,3 +79,13 @@ Default: `false` Type: `boolean` Skip formatting files + +### type + +Default: `react-native` + +Type: `string` + +Possible values: `react-native`, `expo` + +The type of project to generate detox e2e for diff --git a/docs/angular/api-expo/executors/build-android.md b/docs/angular/api-expo/executors/build-android.md new file mode 100644 index 0000000000000..fc05d61ccb2c5 --- /dev/null +++ b/docs/angular/api-expo/executors/build-android.md @@ -0,0 +1,67 @@ +# @nrwl/expo:build-android + +Build and sign a standalone APK or App Bundle for the Google Play Store + +Options can be configured in `angular.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets. + +## Options + +### clearCredentials + +Alias(es): c + +Type: `boolean` + +Clear all credentials stored on Expo servers. + +### keystoreAlias + +Type: `string` + +Keystore Alias + +### keystorePath + +Type: `string` + +Path to your Keystore: \*.jks. + +### noPublish + +Type: `boolean` + +Disable automatic publishing before building. + +### noWait + +Type: `boolean` + +Exit immediately after scheduling build. + +### publicUrl + +Type: `string` + +The URL of an externally hosted manifest (for self-hosted apps). + +### releaseChannel + +Type: `string` + +Pull from specified release channel. + +### skipWorkflowCheck + +Type: `boolean` + +Skip warning about build service bare workflow limitations. + +### type + +Alias(es): t + +Type: `string` + +Possible values: `app-bundle`, `apk` + +Type of build: [app-bundle⎮apk]. diff --git a/docs/angular/api-expo/executors/build-ios.md b/docs/angular/api-expo/executors/build-ios.md new file mode 100644 index 0000000000000..cf761628b23d1 --- /dev/null +++ b/docs/angular/api-expo/executors/build-ios.md @@ -0,0 +1,131 @@ +# @nrwl/expo:build-ios + +Build and sign a standalone IPA for the Apple App Store + +Options can be configured in `angular.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets. + +## Options + +### appleId + +Type: `string` + +Apple ID username (please also set the Apple ID password as EXPO_APPLE_PASSWORD environment variable). + +### clearCredentials + +Alias(es): c + +Type: `boolean` + +Clear all credentials stored on Expo servers. + +### clearDistCert + +Type: `boolean` + +Remove Distribution Certificate stored on Expo servers. + +### clearProvisioningProfile + +Type: `boolean` + +Remove Provisioning Profile stored on Expo servers. + +### clearPushCert + +Type: `boolean` + +Remove Push Notifications Certificate stored on Expo servers. Use of Push Notifications Certificates is deprecated. + +### clearPushKey + +Type: `boolean` + +Remove Push Notifications Key stored on Expo servers. + +### distP12Path + +Type: `string` + +Path to your Distribution Certificate P12 (set password as EXPO_IOS_DIST_P12_PASSWORD environment variable). + +### noPublish + +Type: `boolean` + +Disable automatic publishing before building. + +### noWait + +Type: `boolean` + +Exit immediately after scheduling build. + +### provisioningProfilePath + +Type: `string` + +Path to your Provisioning Profile. + +### publicUrl + +Type: `string` + +The URL of an externally hosted manifest (for self-hosted apps). + +### pushP8Path + +Type: `string` + +Path to your Push Key .p8 file. + +### releaseChannel + +Type: `string` + +Pull from specified release channel. + +### revokeCredentials + +Alias(es): r + +Type: `boolean` + +Revoke credentials on developer.apple.com, select appropriate using --clear-\* options. + +### skipCredentialsCheck + +Type: `boolean` + +Skip checking credentials. + +### skipWorkflowCheck + +Type: `boolean` + +Skip warning about build service bare workflow limitations. + +### sync + +Default: `true` + +Type: `boolean` + +Syncs npm dependencies to package.json (for React Native autolink). Always true when --install is used. + +### teamId + +Type: `string` + +Apple Team ID. + +### type + +Alias(es): t + +Type: `string` + +Possible values: `archive`, `simulator` + +Type of build: [archive⎮simulator]. diff --git a/docs/angular/api-expo/executors/build-status.md b/docs/angular/api-expo/executors/build-status.md new file mode 100644 index 0000000000000..3123bb38037f8 --- /dev/null +++ b/docs/angular/api-expo/executors/build-status.md @@ -0,0 +1,13 @@ +# @nrwl/expo:build-status + +Get the status of the latest build for the project + +Options can be configured in `angular.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets. + +## Options + +### publicUrl + +Type: `string` + +The URL of an externally hosted manifest (for self-hosted apps). diff --git a/docs/angular/api-expo/executors/build-web.md b/docs/angular/api-expo/executors/build-web.md new file mode 100644 index 0000000000000..8518cdaab2cfa --- /dev/null +++ b/docs/angular/api-expo/executors/build-web.md @@ -0,0 +1,27 @@ +# @nrwl/expo:build-web + +Build the web app for production + +Options can be configured in `angular.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets. + +## Options + +### clear + +Alias(es): c + +Type: `boolean` + +Clear all cached build files and assets. + +### dev + +Type: `boolean` + +Turns dev flag on before bundling + +### noPwa + +Type: `boolean` + +Prevent webpack from generating the manifest.json and injecting meta into the index.html head. diff --git a/docs/angular/api-expo/executors/ensure-symlink.md b/docs/angular/api-expo/executors/ensure-symlink.md new file mode 100644 index 0000000000000..e758fdcb991d8 --- /dev/null +++ b/docs/angular/api-expo/executors/ensure-symlink.md @@ -0,0 +1,5 @@ +# @nrwl/expo:ensure-symlink + +Ensure workspace node_modules is symlink under app's node_modules folder. + +Options can be configured in `angular.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets. diff --git a/docs/angular/api-expo/executors/run.md b/docs/angular/api-expo/executors/run.md new file mode 100644 index 0000000000000..b74734aee3283 --- /dev/null +++ b/docs/angular/api-expo/executors/run.md @@ -0,0 +1,73 @@ +# @nrwl/expo:run + +Run the Android app binary locally or run the iOS app binary locally + +Options can be configured in `angular.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets. + +## Options + +### platform (_**required**_) + +Default: `ios` + +Type: `string` + +Possible values: `ios`, `android` + +Platform to run for (ios, android). + +### bundler + +Default: `true` + +Type: `boolean` + +Whether to skip starting the Metro bundler. True to start it, false to skip it. + +### device + +Alias(es): d + +Type: `string` + +Device name or UDID to build the app on. The value is not required if you have a single device connected. + +### port + +Alias(es): p + +Default: `8081` + +Type: `number` + +Port to start the Metro bundler on + +### scheme + +Type: `string` + +(iOS) Explicitly set the Xcode scheme to use + +### sync + +Default: `true` + +Type: `boolean` + +Syncs npm dependencies to package.json (for React Native autolink). Always true when --install is used. + +### variant + +Default: `debug` + +Type: `string` + +(Android) Specify your app's build variant (e.g. debug, release). + +### xcodeConfiguration + +Default: `Debug` + +Type: `string` + +(iOS) Xcode configuration to use. Debug or Release diff --git a/docs/angular/api-expo/executors/start.md b/docs/angular/api-expo/executors/start.md new file mode 100644 index 0000000000000..c472404fa2e1c --- /dev/null +++ b/docs/angular/api-expo/executors/start.md @@ -0,0 +1,123 @@ +# @nrwl/expo:start + +Start a local dev server for the app or start a Webpack dev server for the web app + +Options can be configured in `angular.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets. + +## Options + +### android + +Alias(es): a + +Type: `boolean` + +Opens your app in Expo Go on a connected Android device + +### clear + +Alias(es): c + +Type: `boolean` + +Clear the Metro bundler cache + +### dev + +Type: `boolean` + +Turn development mode on or off + +### devClient + +Type: `boolean` + +Experimental: Starts the bundler for use with the expo-development-client + +### host + +Alias(es): m + +Type: `string` + +lan (default), tunnel, localhost. Type of host to use. "tunnel" allows you to view your link on other networks + +### https + +Type: `boolean` + +To start webpack with https or http protocol + +### ios + +Alias(es): i + +Type: `boolean` + +Opens your app in Expo Go in a currently running iOS simulator on your computer + +### lan + +Type: `boolean` + +Same as --host lan + +### localhost + +Type: `boolean` + +Same as --host localhost + +### maxWorkers + +Type: `number` + +Maximum number of tasks to allow Metro to spawn + +### minify + +Type: `boolean` + +Whether or not to minify code + +### offline + +Type: `boolean` + +Allows this command to run while offline + +### port + +Alias(es): p + +Default: `19000` + +Type: `number` + +Port to start the native Metro bundler on (does not apply to web or tunnel) + +### scheme + +Type: `string` + +Custom URI protocol to use with a development build + +### sentTo + +Alias(es): s + +Type: `string` + +An email address to send a link to + +### tunnel + +Type: `boolean` + +Same as --host tunnel + +### webpack + +Type: `boolean` + +Start a Webpack dev server for the web app. diff --git a/docs/angular/api-expo/executors/sync-deps.md b/docs/angular/api-expo/executors/sync-deps.md new file mode 100644 index 0000000000000..3fc42313c9790 --- /dev/null +++ b/docs/angular/api-expo/executors/sync-deps.md @@ -0,0 +1,13 @@ +# @nrwl/expo:sync-deps + +Syncs dependencies to package.json (required for autolinking). + +Options can be configured in `angular.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets. + +## Options + +### include + +Type: `string` + +A comma-separated list of additional npm packages to include. e.g. 'nx sync-deps --include=react-native-gesture-handler,react-native-safe-area-context' diff --git a/docs/angular/api-expo/generators/application.md b/docs/angular/api-expo/generators/application.md new file mode 100644 index 0000000000000..265f1987939be --- /dev/null +++ b/docs/angular/api-expo/generators/application.md @@ -0,0 +1,125 @@ +# @nrwl/expo:application + +Create an application + +## Usage + +```bash +nx generate application ... +``` + +```bash +nx g app ... # same +``` + +By default, Nx will search for `application` in the default collection provisioned in `angular.json`. + +You can specify the collection explicitly as follows: + +```bash +nx g @nrwl/expo:application ... +``` + +Show what will be generated without writing to disk: + +```bash +nx g application ... --dry-run +``` + +### Examples + +Generate apps/nested/myapp: + +```bash +nx g app myapp --directory=nested +``` + +Use class components instead of functional components: + +```bash +nx g app myapp --classComponent +``` + +## Options + +### name (_**required**_) + +Type: `string` + +The name of the application. + +### directory + +Alias(es): d + +Type: `string` + +The directory of the new application. + +### displayName + +Type: `string` + +The display name to show in the application. Defaults to name. + +### e2eTestRunner + +Default: `detox` + +Type: `string` + +Possible values: `detox`, `none` + +Adds the specified e2e test runner + +### js + +Default: `false` + +Type: `boolean` + +Generate JavaScript files rather than TypeScript files + +### linter + +Default: `eslint` + +Type: `string` + +Possible values: `eslint`, `tslint` + +The tool to use for running lint checks. + +### setParserOptionsProject + +Default: `false` + +Type: `boolean` + +Whether or not to configure the ESLint "parserOptions.project" option. We do not do this by default for lint performance reasons. + +### skipFormat + +Default: `false` + +Type: `boolean` + +Skip formatting files + +### tags + +Alias(es): t + +Type: `string` + +Add tags to the application (used for linting) + +### unitTestRunner + +Default: `jest` + +Type: `string` + +Possible values: `jest`, `none` + +Test runner to use for unit tests diff --git a/docs/angular/api-expo/generators/component.md b/docs/angular/api-expo/generators/component.md new file mode 100644 index 0000000000000..a7627f269b762 --- /dev/null +++ b/docs/angular/api-expo/generators/component.md @@ -0,0 +1,127 @@ +# @nrwl/expo:component + +Create a component + +## Usage + +```bash +nx generate component ... +``` + +```bash +nx g c ... # same +``` + +By default, Nx will search for `component` in the default collection provisioned in `angular.json`. + +You can specify the collection explicitly as follows: + +```bash +nx g @nrwl/expo:component ... +``` + +Show what will be generated without writing to disk: + +```bash +nx g component ... --dry-run +``` + +### Examples + +Generate a component in the mylib library: + +```bash +nx g component my-component --project=mylib +``` + +Generate a class component in the mylib library: + +```bash +nx g component my-component --project=mylib --classComponent +``` + +## Options + +### name (_**required**_) + +Type: `string` + +The name of the component. + +### project (_**required**_) + +Alias(es): p + +Type: `string` + +The name of the project. + +### classComponent + +Alias(es): C + +Default: `false` + +Type: `boolean` + +Use class components instead of functional component. + +### directory + +Alias(es): d + +Type: `string` + +Create the component under this directory (can be nested). + +### export + +Alias(es): e + +Default: `false` + +Type: `boolean` + +When true, the component is exported from the project index.ts (if it exists). + +### flat + +Default: `false` + +Type: `boolean` + +Create component at the source root rather than its own directory. + +### js + +Default: `false` + +Type: `boolean` + +Generate JavaScript files rather than TypeScript files. + +### pascalCaseFiles + +Alias(es): P + +Default: `false` + +Type: `boolean` + +Use pascal case component file name (e.g. App.tsx). + +### skipFormat + +Default: `false` + +Type: `boolean` + +Skip formatting files. + +### skipTests + +Default: `false` + +Type: `boolean` + +When true, does not create "spec.ts" test files for the new component. diff --git a/docs/angular/api-expo/generators/library.md b/docs/angular/api-expo/generators/library.md new file mode 100644 index 0000000000000..dfe531b19f1db --- /dev/null +++ b/docs/angular/api-expo/generators/library.md @@ -0,0 +1,157 @@ +# @nrwl/expo:library + +Create a library + +## Usage + +```bash +nx generate library ... +``` + +```bash +nx g lib ... # same +``` + +By default, Nx will search for `library` in the default collection provisioned in `angular.json`. + +You can specify the collection explicitly as follows: + +```bash +nx g @nrwl/expo:library ... +``` + +Show what will be generated without writing to disk: + +```bash +nx g library ... --dry-run +``` + +### Examples + +Generate libs/myapp/mylib: + +```bash +nx g lib mylib --directory=myapp +``` + +## Options + +### name (_**required**_) + +Type: `string` + +Library name + +### buildable + +Default: `false` + +Type: `boolean` + +Generate a buildable library. + +### directory + +Alias(es): d + +Type: `string` + +A directory where the lib is placed. + +### globalCss + +Default: `false` + +Type: `boolean` + +When true, the stylesheet is generated using global CSS instead of CSS modules (e.g. file is '_.css' rather than '_.module.css'). + +### importPath + +Type: `string` + +The library name used to import it, like @myorg/my-awesome-lib + +### js + +Default: `false` + +Type: `boolean` + +Generate JavaScript files rather than TypeScript files. + +### linter + +Default: `eslint` + +Type: `string` + +Possible values: `eslint`, `tslint` + +The tool to use for running lint checks. + +### pascalCaseFiles + +Alias(es): P + +Default: `false` + +Type: `boolean` + +Use pascal case component file name (e.g. App.tsx). + +### publishable + +Type: `boolean` + +Create a publishable library. + +### setParserOptionsProject + +Default: `false` + +Type: `boolean` + +Whether or not to configure the ESLint "parserOptions.project" option. We do not do this by default for lint performance reasons. + +### skipFormat + +Default: `false` + +Type: `boolean` + +Skip formatting files. + +### skipTsConfig + +Default: `false` + +Type: `boolean` + +Do not update tsconfig.json for development experience. + +### strict + +Default: `true` + +Type: `boolean` + +Whether to enable tsconfig strict mode or not. + +### tags + +Alias(es): t + +Type: `string` + +Add tags to the library (used for linting). + +### unitTestRunner + +Default: `jest` + +Type: `string` + +Possible values: `jest`, `none` + +Test runner to use for unit tests. diff --git a/docs/angular/api-nx-devkit/index.md b/docs/angular/api-nx-devkit/index.md index 943e346aac39c..50ff4c3869799 100644 --- a/docs/angular/api-nx-devkit/index.md +++ b/docs/angular/api-nx-devkit/index.md @@ -986,7 +986,7 @@ Examples: ```typescript names('my-name'); // {name: 'my-name', className: 'MyName', propertyName: 'myName', constantName: 'MY_NAME', fileName: 'my-name'} -names('myName'); // {name: 'my-name', className: 'MyName', propertyName: 'myName', constantName: 'MY_NAME', fileName: 'my-name'} +names('myName'); // {name: 'myName', className: 'MyName', propertyName: 'myName', constantName: 'MY_NAME', fileName: 'my-name'} ``` #### Parameters diff --git a/docs/angular/api-react-native/executors/run-ios.md b/docs/angular/api-react-native/executors/run-ios.md index 020452d5bdf45..9d4fcb6fcf8a9 100644 --- a/docs/angular/api-react-native/executors/run-ios.md +++ b/docs/angular/api-react-native/executors/run-ios.md @@ -78,4 +78,4 @@ Default: `Debug` Type: `string` -Explicitly set the Xcode configuration to use +Explicitly set the Xcode configuration to use. Debug or Release. diff --git a/docs/angular/api-react-native/generators/application.md b/docs/angular/api-react-native/generators/application.md index df252a83c6397..38e0b62845d67 100644 --- a/docs/angular/api-react-native/generators/application.md +++ b/docs/angular/api-react-native/generators/application.md @@ -42,6 +42,12 @@ nx g app myapp --classComponent ## Options +### name (_**required**_) + +Type: `string` + +The name of the application. + ### directory Alias(es): d @@ -84,12 +90,6 @@ Possible values: `eslint`, `tslint` The tool to use for running lint checks. -### name - -Type: `string` - -The name of the application. - ### setParserOptionsProject Default: `false` diff --git a/docs/angular/executors.json b/docs/angular/executors.json index cb26dd84b8573..4cd764be39293 100644 --- a/docs/angular/executors.json +++ b/docs/angular/executors.json @@ -2,6 +2,7 @@ "angular", "cypress", "detox", + "expo", "gatsby", "jest", "js", diff --git a/docs/angular/generators.json b/docs/angular/generators.json index 4bc621d6ae35a..6a9e601e07d8c 100644 --- a/docs/angular/generators.json +++ b/docs/angular/generators.json @@ -2,6 +2,7 @@ "angular", "cypress", "detox", + "expo", "express", "gatsby", "jest", diff --git a/docs/map.json b/docs/map.json index c30e9df39b8b4..81fe9b38e8617 100644 --- a/docs/map.json +++ b/docs/map.json @@ -1011,6 +1011,67 @@ } ] }, + { + "name": "expo", + "id": "expo", + "itemList": [ + { + "name": "application generator", + "id": "application", + "file": "angular/api-expo/generators/application" + }, + { + "name": "component generator", + "id": "component", + "file": "angular/api-expo/generators/component" + }, + { + "name": "library generator", + "id": "library", + "file": "angular/api-expo/generators/library" + }, + { + "name": "build android executor", + "id": "build-android", + "file": "angular/api-expo/executors/build-android" + }, + { + "name": "build ios executor", + "id": "build-ios", + "file": "angular/api-expo/executors/build-ios" + }, + { + "name": "build status executor", + "id": "build-status", + "file": "angular/api-expo/executors/build-status" + }, + { + "name": "build web executor", + "id": "build-web", + "file": "angular/api-expo/executors/build-web" + }, + { + "name": "ensure symlink executor", + "id": "ensure-symlink", + "file": "angular/api-expo/executors/ensure-symlink" + }, + { + "name": "run executor", + "id": "run", + "file": "angular/api-expo/executors/run" + }, + { + "name": "start executor", + "id": "start", + "file": "angular/api-expo/executors/start" + }, + { + "name": "sync deps executor", + "id": "sync-deps", + "file": "angular/api-expo/executors/sync-deps" + } + ] + }, { "name": "Nx Plugin", "id": "nx-plugin", @@ -2346,6 +2407,67 @@ } ] }, + { + "name": "expo", + "id": "expo", + "itemList": [ + { + "name": "application generator", + "id": "application", + "file": "react/api-expo/generators/application" + }, + { + "name": "component generator", + "id": "component", + "file": "react/api-expo/generators/component" + }, + { + "name": "library generator", + "id": "library", + "file": "react/api-expo/generators/library" + }, + { + "name": "build android executor", + "id": "build-android", + "file": "react/api-expo/executors/build-android" + }, + { + "name": "build ios executor", + "id": "build-ios", + "file": "react/api-expo/executors/build-ios" + }, + { + "name": "build status executor", + "id": "build-status", + "file": "react/api-expo/executors/build-status" + }, + { + "name": "build web executor", + "id": "build-web", + "file": "react/api-expo/executors/build-web" + }, + { + "name": "ensure symlink executor", + "id": "ensure-symlink", + "file": "react/api-expo/executors/ensure-symlink" + }, + { + "name": "run executor", + "id": "run", + "file": "react/api-expo/executors/run" + }, + { + "name": "start executor", + "id": "start", + "file": "react/api-expo/executors/start" + }, + { + "name": "sync deps executor", + "id": "sync-deps", + "file": "react/api-expo/executors/sync-deps" + } + ] + }, { "name": "Nx Plugin", "id": "nx-plugin", @@ -3622,6 +3744,67 @@ } ] }, + { + "name": "expo", + "id": "expo", + "itemList": [ + { + "name": "application generator", + "id": "application", + "file": "node/api-expo/generators/application" + }, + { + "name": "component generator", + "id": "component", + "file": "node/api-expo/generators/component" + }, + { + "name": "library generator", + "id": "library", + "file": "node/api-expo/generators/library" + }, + { + "name": "build android executor", + "id": "build-android", + "file": "node/api-expo/executors/build-android" + }, + { + "name": "build ios executor", + "id": "build-ios", + "file": "node/api-expo/executors/build-ios" + }, + { + "name": "build status executor", + "id": "build-status", + "file": "node/api-expo/executors/build-status" + }, + { + "name": "build web executor", + "id": "build-web", + "file": "node/api-expo/executors/build-web" + }, + { + "name": "ensure symlink executor", + "id": "ensure-symlink", + "file": "node/api-expo/executors/ensure-symlink" + }, + { + "name": "run executor", + "id": "run", + "file": "node/api-expo/executors/run" + }, + { + "name": "start executor", + "id": "start", + "file": "node/api-expo/executors/start" + }, + { + "name": "sync deps executor", + "id": "sync-deps", + "file": "node/api-expo/executors/sync-deps" + } + ] + }, { "name": "Nx Plugin", "id": "nx-plugin", diff --git a/docs/node/api-detox/generators/application.md b/docs/node/api-detox/generators/application.md index d4623cead290e..e42e23d236c5e 100644 --- a/docs/node/api-detox/generators/application.md +++ b/docs/node/api-detox/generators/application.md @@ -79,3 +79,13 @@ Default: `false` Type: `boolean` Skip formatting files + +### type + +Default: `react-native` + +Type: `string` + +Possible values: `react-native`, `expo` + +The type of project to generate detox e2e for diff --git a/docs/node/api-expo/executors/build-android.md b/docs/node/api-expo/executors/build-android.md new file mode 100644 index 0000000000000..34ea4ea01f2aa --- /dev/null +++ b/docs/node/api-expo/executors/build-android.md @@ -0,0 +1,67 @@ +# @nrwl/expo:build-android + +Build and sign a standalone APK or App Bundle for the Google Play Store + +Options can be configured in `workspace.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets. + +## Options + +### clearCredentials + +Alias(es): c + +Type: `boolean` + +Clear all credentials stored on Expo servers. + +### keystoreAlias + +Type: `string` + +Keystore Alias + +### keystorePath + +Type: `string` + +Path to your Keystore: \*.jks. + +### noPublish + +Type: `boolean` + +Disable automatic publishing before building. + +### noWait + +Type: `boolean` + +Exit immediately after scheduling build. + +### publicUrl + +Type: `string` + +The URL of an externally hosted manifest (for self-hosted apps). + +### releaseChannel + +Type: `string` + +Pull from specified release channel. + +### skipWorkflowCheck + +Type: `boolean` + +Skip warning about build service bare workflow limitations. + +### type + +Alias(es): t + +Type: `string` + +Possible values: `app-bundle`, `apk` + +Type of build: [app-bundle⎮apk]. diff --git a/docs/node/api-expo/executors/build-ios.md b/docs/node/api-expo/executors/build-ios.md new file mode 100644 index 0000000000000..26abcbd4dcec2 --- /dev/null +++ b/docs/node/api-expo/executors/build-ios.md @@ -0,0 +1,131 @@ +# @nrwl/expo:build-ios + +Build and sign a standalone IPA for the Apple App Store + +Options can be configured in `workspace.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets. + +## Options + +### appleId + +Type: `string` + +Apple ID username (please also set the Apple ID password as EXPO_APPLE_PASSWORD environment variable). + +### clearCredentials + +Alias(es): c + +Type: `boolean` + +Clear all credentials stored on Expo servers. + +### clearDistCert + +Type: `boolean` + +Remove Distribution Certificate stored on Expo servers. + +### clearProvisioningProfile + +Type: `boolean` + +Remove Provisioning Profile stored on Expo servers. + +### clearPushCert + +Type: `boolean` + +Remove Push Notifications Certificate stored on Expo servers. Use of Push Notifications Certificates is deprecated. + +### clearPushKey + +Type: `boolean` + +Remove Push Notifications Key stored on Expo servers. + +### distP12Path + +Type: `string` + +Path to your Distribution Certificate P12 (set password as EXPO_IOS_DIST_P12_PASSWORD environment variable). + +### noPublish + +Type: `boolean` + +Disable automatic publishing before building. + +### noWait + +Type: `boolean` + +Exit immediately after scheduling build. + +### provisioningProfilePath + +Type: `string` + +Path to your Provisioning Profile. + +### publicUrl + +Type: `string` + +The URL of an externally hosted manifest (for self-hosted apps). + +### pushP8Path + +Type: `string` + +Path to your Push Key .p8 file. + +### releaseChannel + +Type: `string` + +Pull from specified release channel. + +### revokeCredentials + +Alias(es): r + +Type: `boolean` + +Revoke credentials on developer.apple.com, select appropriate using --clear-\* options. + +### skipCredentialsCheck + +Type: `boolean` + +Skip checking credentials. + +### skipWorkflowCheck + +Type: `boolean` + +Skip warning about build service bare workflow limitations. + +### sync + +Default: `true` + +Type: `boolean` + +Syncs npm dependencies to package.json (for React Native autolink). Always true when --install is used. + +### teamId + +Type: `string` + +Apple Team ID. + +### type + +Alias(es): t + +Type: `string` + +Possible values: `archive`, `simulator` + +Type of build: [archive⎮simulator]. diff --git a/docs/node/api-expo/executors/build-status.md b/docs/node/api-expo/executors/build-status.md new file mode 100644 index 0000000000000..38c8d31edd5ce --- /dev/null +++ b/docs/node/api-expo/executors/build-status.md @@ -0,0 +1,13 @@ +# @nrwl/expo:build-status + +Get the status of the latest build for the project + +Options can be configured in `workspace.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets. + +## Options + +### publicUrl + +Type: `string` + +The URL of an externally hosted manifest (for self-hosted apps). diff --git a/docs/node/api-expo/executors/build-web.md b/docs/node/api-expo/executors/build-web.md new file mode 100644 index 0000000000000..53b756ac89245 --- /dev/null +++ b/docs/node/api-expo/executors/build-web.md @@ -0,0 +1,27 @@ +# @nrwl/expo:build-web + +Build the web app for production + +Options can be configured in `workspace.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets. + +## Options + +### clear + +Alias(es): c + +Type: `boolean` + +Clear all cached build files and assets. + +### dev + +Type: `boolean` + +Turns dev flag on before bundling + +### noPwa + +Type: `boolean` + +Prevent webpack from generating the manifest.json and injecting meta into the index.html head. diff --git a/docs/node/api-expo/executors/ensure-symlink.md b/docs/node/api-expo/executors/ensure-symlink.md new file mode 100644 index 0000000000000..7f35d6525400e --- /dev/null +++ b/docs/node/api-expo/executors/ensure-symlink.md @@ -0,0 +1,5 @@ +# @nrwl/expo:ensure-symlink + +Ensure workspace node_modules is symlink under app's node_modules folder. + +Options can be configured in `workspace.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets. diff --git a/docs/node/api-expo/executors/run.md b/docs/node/api-expo/executors/run.md new file mode 100644 index 0000000000000..2eed2e3c239d3 --- /dev/null +++ b/docs/node/api-expo/executors/run.md @@ -0,0 +1,73 @@ +# @nrwl/expo:run + +Run the Android app binary locally or run the iOS app binary locally + +Options can be configured in `workspace.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets. + +## Options + +### platform (_**required**_) + +Default: `ios` + +Type: `string` + +Possible values: `ios`, `android` + +Platform to run for (ios, android). + +### bundler + +Default: `true` + +Type: `boolean` + +Whether to skip starting the Metro bundler. True to start it, false to skip it. + +### device + +Alias(es): d + +Type: `string` + +Device name or UDID to build the app on. The value is not required if you have a single device connected. + +### port + +Alias(es): p + +Default: `8081` + +Type: `number` + +Port to start the Metro bundler on + +### scheme + +Type: `string` + +(iOS) Explicitly set the Xcode scheme to use + +### sync + +Default: `true` + +Type: `boolean` + +Syncs npm dependencies to package.json (for React Native autolink). Always true when --install is used. + +### variant + +Default: `debug` + +Type: `string` + +(Android) Specify your app's build variant (e.g. debug, release). + +### xcodeConfiguration + +Default: `Debug` + +Type: `string` + +(iOS) Xcode configuration to use. Debug or Release diff --git a/docs/node/api-expo/executors/start.md b/docs/node/api-expo/executors/start.md new file mode 100644 index 0000000000000..8808882993112 --- /dev/null +++ b/docs/node/api-expo/executors/start.md @@ -0,0 +1,123 @@ +# @nrwl/expo:start + +Start a local dev server for the app or start a Webpack dev server for the web app + +Options can be configured in `workspace.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets. + +## Options + +### android + +Alias(es): a + +Type: `boolean` + +Opens your app in Expo Go on a connected Android device + +### clear + +Alias(es): c + +Type: `boolean` + +Clear the Metro bundler cache + +### dev + +Type: `boolean` + +Turn development mode on or off + +### devClient + +Type: `boolean` + +Experimental: Starts the bundler for use with the expo-development-client + +### host + +Alias(es): m + +Type: `string` + +lan (default), tunnel, localhost. Type of host to use. "tunnel" allows you to view your link on other networks + +### https + +Type: `boolean` + +To start webpack with https or http protocol + +### ios + +Alias(es): i + +Type: `boolean` + +Opens your app in Expo Go in a currently running iOS simulator on your computer + +### lan + +Type: `boolean` + +Same as --host lan + +### localhost + +Type: `boolean` + +Same as --host localhost + +### maxWorkers + +Type: `number` + +Maximum number of tasks to allow Metro to spawn + +### minify + +Type: `boolean` + +Whether or not to minify code + +### offline + +Type: `boolean` + +Allows this command to run while offline + +### port + +Alias(es): p + +Default: `19000` + +Type: `number` + +Port to start the native Metro bundler on (does not apply to web or tunnel) + +### scheme + +Type: `string` + +Custom URI protocol to use with a development build + +### sentTo + +Alias(es): s + +Type: `string` + +An email address to send a link to + +### tunnel + +Type: `boolean` + +Same as --host tunnel + +### webpack + +Type: `boolean` + +Start a Webpack dev server for the web app. diff --git a/docs/node/api-expo/executors/sync-deps.md b/docs/node/api-expo/executors/sync-deps.md new file mode 100644 index 0000000000000..6d39b840b383c --- /dev/null +++ b/docs/node/api-expo/executors/sync-deps.md @@ -0,0 +1,13 @@ +# @nrwl/expo:sync-deps + +Syncs dependencies to package.json (required for autolinking). + +Options can be configured in `workspace.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets. + +## Options + +### include + +Type: `string` + +A comma-separated list of additional npm packages to include. e.g. 'nx sync-deps --include=react-native-gesture-handler,react-native-safe-area-context' diff --git a/docs/node/api-expo/generators/application.md b/docs/node/api-expo/generators/application.md new file mode 100644 index 0000000000000..3f0fbb717e970 --- /dev/null +++ b/docs/node/api-expo/generators/application.md @@ -0,0 +1,125 @@ +# @nrwl/expo:application + +Create an application + +## Usage + +```bash +nx generate application ... +``` + +```bash +nx g app ... # same +``` + +By default, Nx will search for `application` in the default collection provisioned in `workspace.json`. + +You can specify the collection explicitly as follows: + +```bash +nx g @nrwl/expo:application ... +``` + +Show what will be generated without writing to disk: + +```bash +nx g application ... --dry-run +``` + +### Examples + +Generate apps/nested/myapp: + +```bash +nx g app myapp --directory=nested +``` + +Use class components instead of functional components: + +```bash +nx g app myapp --classComponent +``` + +## Options + +### name (_**required**_) + +Type: `string` + +The name of the application. + +### directory + +Alias(es): d + +Type: `string` + +The directory of the new application. + +### displayName + +Type: `string` + +The display name to show in the application. Defaults to name. + +### e2eTestRunner + +Default: `detox` + +Type: `string` + +Possible values: `detox`, `none` + +Adds the specified e2e test runner + +### js + +Default: `false` + +Type: `boolean` + +Generate JavaScript files rather than TypeScript files + +### linter + +Default: `eslint` + +Type: `string` + +Possible values: `eslint`, `tslint` + +The tool to use for running lint checks. + +### setParserOptionsProject + +Default: `false` + +Type: `boolean` + +Whether or not to configure the ESLint "parserOptions.project" option. We do not do this by default for lint performance reasons. + +### skipFormat + +Default: `false` + +Type: `boolean` + +Skip formatting files + +### tags + +Alias(es): t + +Type: `string` + +Add tags to the application (used for linting) + +### unitTestRunner + +Default: `jest` + +Type: `string` + +Possible values: `jest`, `none` + +Test runner to use for unit tests diff --git a/docs/node/api-expo/generators/component.md b/docs/node/api-expo/generators/component.md new file mode 100644 index 0000000000000..cefa24002e6e5 --- /dev/null +++ b/docs/node/api-expo/generators/component.md @@ -0,0 +1,127 @@ +# @nrwl/expo:component + +Create a component + +## Usage + +```bash +nx generate component ... +``` + +```bash +nx g c ... # same +``` + +By default, Nx will search for `component` in the default collection provisioned in `workspace.json`. + +You can specify the collection explicitly as follows: + +```bash +nx g @nrwl/expo:component ... +``` + +Show what will be generated without writing to disk: + +```bash +nx g component ... --dry-run +``` + +### Examples + +Generate a component in the mylib library: + +```bash +nx g component my-component --project=mylib +``` + +Generate a class component in the mylib library: + +```bash +nx g component my-component --project=mylib --classComponent +``` + +## Options + +### name (_**required**_) + +Type: `string` + +The name of the component. + +### project (_**required**_) + +Alias(es): p + +Type: `string` + +The name of the project. + +### classComponent + +Alias(es): C + +Default: `false` + +Type: `boolean` + +Use class components instead of functional component. + +### directory + +Alias(es): d + +Type: `string` + +Create the component under this directory (can be nested). + +### export + +Alias(es): e + +Default: `false` + +Type: `boolean` + +When true, the component is exported from the project index.ts (if it exists). + +### flat + +Default: `false` + +Type: `boolean` + +Create component at the source root rather than its own directory. + +### js + +Default: `false` + +Type: `boolean` + +Generate JavaScript files rather than TypeScript files. + +### pascalCaseFiles + +Alias(es): P + +Default: `false` + +Type: `boolean` + +Use pascal case component file name (e.g. App.tsx). + +### skipFormat + +Default: `false` + +Type: `boolean` + +Skip formatting files. + +### skipTests + +Default: `false` + +Type: `boolean` + +When true, does not create "spec.ts" test files for the new component. diff --git a/docs/node/api-expo/generators/library.md b/docs/node/api-expo/generators/library.md new file mode 100644 index 0000000000000..c9a4045dcfddb --- /dev/null +++ b/docs/node/api-expo/generators/library.md @@ -0,0 +1,157 @@ +# @nrwl/expo:library + +Create a library + +## Usage + +```bash +nx generate library ... +``` + +```bash +nx g lib ... # same +``` + +By default, Nx will search for `library` in the default collection provisioned in `workspace.json`. + +You can specify the collection explicitly as follows: + +```bash +nx g @nrwl/expo:library ... +``` + +Show what will be generated without writing to disk: + +```bash +nx g library ... --dry-run +``` + +### Examples + +Generate libs/myapp/mylib: + +```bash +nx g lib mylib --directory=myapp +``` + +## Options + +### name (_**required**_) + +Type: `string` + +Library name + +### buildable + +Default: `false` + +Type: `boolean` + +Generate a buildable library. + +### directory + +Alias(es): d + +Type: `string` + +A directory where the lib is placed. + +### globalCss + +Default: `false` + +Type: `boolean` + +When true, the stylesheet is generated using global CSS instead of CSS modules (e.g. file is '_.css' rather than '_.module.css'). + +### importPath + +Type: `string` + +The library name used to import it, like @myorg/my-awesome-lib + +### js + +Default: `false` + +Type: `boolean` + +Generate JavaScript files rather than TypeScript files. + +### linter + +Default: `eslint` + +Type: `string` + +Possible values: `eslint`, `tslint` + +The tool to use for running lint checks. + +### pascalCaseFiles + +Alias(es): P + +Default: `false` + +Type: `boolean` + +Use pascal case component file name (e.g. App.tsx). + +### publishable + +Type: `boolean` + +Create a publishable library. + +### setParserOptionsProject + +Default: `false` + +Type: `boolean` + +Whether or not to configure the ESLint "parserOptions.project" option. We do not do this by default for lint performance reasons. + +### skipFormat + +Default: `false` + +Type: `boolean` + +Skip formatting files. + +### skipTsConfig + +Default: `false` + +Type: `boolean` + +Do not update tsconfig.json for development experience. + +### strict + +Default: `true` + +Type: `boolean` + +Whether to enable tsconfig strict mode or not. + +### tags + +Alias(es): t + +Type: `string` + +Add tags to the library (used for linting). + +### unitTestRunner + +Default: `jest` + +Type: `string` + +Possible values: `jest`, `none` + +Test runner to use for unit tests. diff --git a/docs/node/api-nx-devkit/index.md b/docs/node/api-nx-devkit/index.md index 85fe3b63872ca..db5110b86d66b 100644 --- a/docs/node/api-nx-devkit/index.md +++ b/docs/node/api-nx-devkit/index.md @@ -986,7 +986,7 @@ Examples: ```typescript names('my-name'); // {name: 'my-name', className: 'MyName', propertyName: 'myName', constantName: 'MY_NAME', fileName: 'my-name'} -names('myName'); // {name: 'my-name', className: 'MyName', propertyName: 'myName', constantName: 'MY_NAME', fileName: 'my-name'} +names('myName'); // {name: 'myName', className: 'MyName', propertyName: 'myName', constantName: 'MY_NAME', fileName: 'my-name'} ``` #### Parameters diff --git a/docs/node/api-react-native/executors/run-ios.md b/docs/node/api-react-native/executors/run-ios.md index 0c7c27a757966..5c8408b9a4066 100644 --- a/docs/node/api-react-native/executors/run-ios.md +++ b/docs/node/api-react-native/executors/run-ios.md @@ -78,4 +78,4 @@ Default: `Debug` Type: `string` -Explicitly set the Xcode configuration to use +Explicitly set the Xcode configuration to use. Debug or Release. diff --git a/docs/node/api-react-native/generators/application.md b/docs/node/api-react-native/generators/application.md index 9d531048d8c34..d6b3da69dc99c 100644 --- a/docs/node/api-react-native/generators/application.md +++ b/docs/node/api-react-native/generators/application.md @@ -42,6 +42,12 @@ nx g app myapp --classComponent ## Options +### name (_**required**_) + +Type: `string` + +The name of the application. + ### directory Alias(es): d @@ -84,12 +90,6 @@ Possible values: `eslint`, `tslint` The tool to use for running lint checks. -### name - -Type: `string` - -The name of the application. - ### setParserOptionsProject Default: `false` diff --git a/docs/node/executors.json b/docs/node/executors.json index cb26dd84b8573..4cd764be39293 100644 --- a/docs/node/executors.json +++ b/docs/node/executors.json @@ -2,6 +2,7 @@ "angular", "cypress", "detox", + "expo", "gatsby", "jest", "js", diff --git a/docs/node/generators.json b/docs/node/generators.json index 4bc621d6ae35a..6a9e601e07d8c 100644 --- a/docs/node/generators.json +++ b/docs/node/generators.json @@ -2,6 +2,7 @@ "angular", "cypress", "detox", + "expo", "express", "gatsby", "jest", diff --git a/docs/react/api-detox/generators/application.md b/docs/react/api-detox/generators/application.md index d4623cead290e..e42e23d236c5e 100644 --- a/docs/react/api-detox/generators/application.md +++ b/docs/react/api-detox/generators/application.md @@ -79,3 +79,13 @@ Default: `false` Type: `boolean` Skip formatting files + +### type + +Default: `react-native` + +Type: `string` + +Possible values: `react-native`, `expo` + +The type of project to generate detox e2e for diff --git a/docs/react/api-expo/executors/build-android.md b/docs/react/api-expo/executors/build-android.md new file mode 100644 index 0000000000000..34ea4ea01f2aa --- /dev/null +++ b/docs/react/api-expo/executors/build-android.md @@ -0,0 +1,67 @@ +# @nrwl/expo:build-android + +Build and sign a standalone APK or App Bundle for the Google Play Store + +Options can be configured in `workspace.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets. + +## Options + +### clearCredentials + +Alias(es): c + +Type: `boolean` + +Clear all credentials stored on Expo servers. + +### keystoreAlias + +Type: `string` + +Keystore Alias + +### keystorePath + +Type: `string` + +Path to your Keystore: \*.jks. + +### noPublish + +Type: `boolean` + +Disable automatic publishing before building. + +### noWait + +Type: `boolean` + +Exit immediately after scheduling build. + +### publicUrl + +Type: `string` + +The URL of an externally hosted manifest (for self-hosted apps). + +### releaseChannel + +Type: `string` + +Pull from specified release channel. + +### skipWorkflowCheck + +Type: `boolean` + +Skip warning about build service bare workflow limitations. + +### type + +Alias(es): t + +Type: `string` + +Possible values: `app-bundle`, `apk` + +Type of build: [app-bundle⎮apk]. diff --git a/docs/react/api-expo/executors/build-ios.md b/docs/react/api-expo/executors/build-ios.md new file mode 100644 index 0000000000000..26abcbd4dcec2 --- /dev/null +++ b/docs/react/api-expo/executors/build-ios.md @@ -0,0 +1,131 @@ +# @nrwl/expo:build-ios + +Build and sign a standalone IPA for the Apple App Store + +Options can be configured in `workspace.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets. + +## Options + +### appleId + +Type: `string` + +Apple ID username (please also set the Apple ID password as EXPO_APPLE_PASSWORD environment variable). + +### clearCredentials + +Alias(es): c + +Type: `boolean` + +Clear all credentials stored on Expo servers. + +### clearDistCert + +Type: `boolean` + +Remove Distribution Certificate stored on Expo servers. + +### clearProvisioningProfile + +Type: `boolean` + +Remove Provisioning Profile stored on Expo servers. + +### clearPushCert + +Type: `boolean` + +Remove Push Notifications Certificate stored on Expo servers. Use of Push Notifications Certificates is deprecated. + +### clearPushKey + +Type: `boolean` + +Remove Push Notifications Key stored on Expo servers. + +### distP12Path + +Type: `string` + +Path to your Distribution Certificate P12 (set password as EXPO_IOS_DIST_P12_PASSWORD environment variable). + +### noPublish + +Type: `boolean` + +Disable automatic publishing before building. + +### noWait + +Type: `boolean` + +Exit immediately after scheduling build. + +### provisioningProfilePath + +Type: `string` + +Path to your Provisioning Profile. + +### publicUrl + +Type: `string` + +The URL of an externally hosted manifest (for self-hosted apps). + +### pushP8Path + +Type: `string` + +Path to your Push Key .p8 file. + +### releaseChannel + +Type: `string` + +Pull from specified release channel. + +### revokeCredentials + +Alias(es): r + +Type: `boolean` + +Revoke credentials on developer.apple.com, select appropriate using --clear-\* options. + +### skipCredentialsCheck + +Type: `boolean` + +Skip checking credentials. + +### skipWorkflowCheck + +Type: `boolean` + +Skip warning about build service bare workflow limitations. + +### sync + +Default: `true` + +Type: `boolean` + +Syncs npm dependencies to package.json (for React Native autolink). Always true when --install is used. + +### teamId + +Type: `string` + +Apple Team ID. + +### type + +Alias(es): t + +Type: `string` + +Possible values: `archive`, `simulator` + +Type of build: [archive⎮simulator]. diff --git a/docs/react/api-expo/executors/build-status.md b/docs/react/api-expo/executors/build-status.md new file mode 100644 index 0000000000000..38c8d31edd5ce --- /dev/null +++ b/docs/react/api-expo/executors/build-status.md @@ -0,0 +1,13 @@ +# @nrwl/expo:build-status + +Get the status of the latest build for the project + +Options can be configured in `workspace.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets. + +## Options + +### publicUrl + +Type: `string` + +The URL of an externally hosted manifest (for self-hosted apps). diff --git a/docs/react/api-expo/executors/build-web.md b/docs/react/api-expo/executors/build-web.md new file mode 100644 index 0000000000000..53b756ac89245 --- /dev/null +++ b/docs/react/api-expo/executors/build-web.md @@ -0,0 +1,27 @@ +# @nrwl/expo:build-web + +Build the web app for production + +Options can be configured in `workspace.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets. + +## Options + +### clear + +Alias(es): c + +Type: `boolean` + +Clear all cached build files and assets. + +### dev + +Type: `boolean` + +Turns dev flag on before bundling + +### noPwa + +Type: `boolean` + +Prevent webpack from generating the manifest.json and injecting meta into the index.html head. diff --git a/docs/react/api-expo/executors/ensure-symlink.md b/docs/react/api-expo/executors/ensure-symlink.md new file mode 100644 index 0000000000000..7f35d6525400e --- /dev/null +++ b/docs/react/api-expo/executors/ensure-symlink.md @@ -0,0 +1,5 @@ +# @nrwl/expo:ensure-symlink + +Ensure workspace node_modules is symlink under app's node_modules folder. + +Options can be configured in `workspace.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets. diff --git a/docs/react/api-expo/executors/run.md b/docs/react/api-expo/executors/run.md new file mode 100644 index 0000000000000..2eed2e3c239d3 --- /dev/null +++ b/docs/react/api-expo/executors/run.md @@ -0,0 +1,73 @@ +# @nrwl/expo:run + +Run the Android app binary locally or run the iOS app binary locally + +Options can be configured in `workspace.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets. + +## Options + +### platform (_**required**_) + +Default: `ios` + +Type: `string` + +Possible values: `ios`, `android` + +Platform to run for (ios, android). + +### bundler + +Default: `true` + +Type: `boolean` + +Whether to skip starting the Metro bundler. True to start it, false to skip it. + +### device + +Alias(es): d + +Type: `string` + +Device name or UDID to build the app on. The value is not required if you have a single device connected. + +### port + +Alias(es): p + +Default: `8081` + +Type: `number` + +Port to start the Metro bundler on + +### scheme + +Type: `string` + +(iOS) Explicitly set the Xcode scheme to use + +### sync + +Default: `true` + +Type: `boolean` + +Syncs npm dependencies to package.json (for React Native autolink). Always true when --install is used. + +### variant + +Default: `debug` + +Type: `string` + +(Android) Specify your app's build variant (e.g. debug, release). + +### xcodeConfiguration + +Default: `Debug` + +Type: `string` + +(iOS) Xcode configuration to use. Debug or Release diff --git a/docs/react/api-expo/executors/start.md b/docs/react/api-expo/executors/start.md new file mode 100644 index 0000000000000..8808882993112 --- /dev/null +++ b/docs/react/api-expo/executors/start.md @@ -0,0 +1,123 @@ +# @nrwl/expo:start + +Start a local dev server for the app or start a Webpack dev server for the web app + +Options can be configured in `workspace.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets. + +## Options + +### android + +Alias(es): a + +Type: `boolean` + +Opens your app in Expo Go on a connected Android device + +### clear + +Alias(es): c + +Type: `boolean` + +Clear the Metro bundler cache + +### dev + +Type: `boolean` + +Turn development mode on or off + +### devClient + +Type: `boolean` + +Experimental: Starts the bundler for use with the expo-development-client + +### host + +Alias(es): m + +Type: `string` + +lan (default), tunnel, localhost. Type of host to use. "tunnel" allows you to view your link on other networks + +### https + +Type: `boolean` + +To start webpack with https or http protocol + +### ios + +Alias(es): i + +Type: `boolean` + +Opens your app in Expo Go in a currently running iOS simulator on your computer + +### lan + +Type: `boolean` + +Same as --host lan + +### localhost + +Type: `boolean` + +Same as --host localhost + +### maxWorkers + +Type: `number` + +Maximum number of tasks to allow Metro to spawn + +### minify + +Type: `boolean` + +Whether or not to minify code + +### offline + +Type: `boolean` + +Allows this command to run while offline + +### port + +Alias(es): p + +Default: `19000` + +Type: `number` + +Port to start the native Metro bundler on (does not apply to web or tunnel) + +### scheme + +Type: `string` + +Custom URI protocol to use with a development build + +### sentTo + +Alias(es): s + +Type: `string` + +An email address to send a link to + +### tunnel + +Type: `boolean` + +Same as --host tunnel + +### webpack + +Type: `boolean` + +Start a Webpack dev server for the web app. diff --git a/docs/react/api-expo/executors/sync-deps.md b/docs/react/api-expo/executors/sync-deps.md new file mode 100644 index 0000000000000..6d39b840b383c --- /dev/null +++ b/docs/react/api-expo/executors/sync-deps.md @@ -0,0 +1,13 @@ +# @nrwl/expo:sync-deps + +Syncs dependencies to package.json (required for autolinking). + +Options can be configured in `workspace.json` when defining the executor, or when invoking it. Read more about how to configure targets and executors here: https://nx.dev/core-concepts/configuration#targets. + +## Options + +### include + +Type: `string` + +A comma-separated list of additional npm packages to include. e.g. 'nx sync-deps --include=react-native-gesture-handler,react-native-safe-area-context' diff --git a/docs/react/api-expo/generators/application.md b/docs/react/api-expo/generators/application.md new file mode 100644 index 0000000000000..3f0fbb717e970 --- /dev/null +++ b/docs/react/api-expo/generators/application.md @@ -0,0 +1,125 @@ +# @nrwl/expo:application + +Create an application + +## Usage + +```bash +nx generate application ... +``` + +```bash +nx g app ... # same +``` + +By default, Nx will search for `application` in the default collection provisioned in `workspace.json`. + +You can specify the collection explicitly as follows: + +```bash +nx g @nrwl/expo:application ... +``` + +Show what will be generated without writing to disk: + +```bash +nx g application ... --dry-run +``` + +### Examples + +Generate apps/nested/myapp: + +```bash +nx g app myapp --directory=nested +``` + +Use class components instead of functional components: + +```bash +nx g app myapp --classComponent +``` + +## Options + +### name (_**required**_) + +Type: `string` + +The name of the application. + +### directory + +Alias(es): d + +Type: `string` + +The directory of the new application. + +### displayName + +Type: `string` + +The display name to show in the application. Defaults to name. + +### e2eTestRunner + +Default: `detox` + +Type: `string` + +Possible values: `detox`, `none` + +Adds the specified e2e test runner + +### js + +Default: `false` + +Type: `boolean` + +Generate JavaScript files rather than TypeScript files + +### linter + +Default: `eslint` + +Type: `string` + +Possible values: `eslint`, `tslint` + +The tool to use for running lint checks. + +### setParserOptionsProject + +Default: `false` + +Type: `boolean` + +Whether or not to configure the ESLint "parserOptions.project" option. We do not do this by default for lint performance reasons. + +### skipFormat + +Default: `false` + +Type: `boolean` + +Skip formatting files + +### tags + +Alias(es): t + +Type: `string` + +Add tags to the application (used for linting) + +### unitTestRunner + +Default: `jest` + +Type: `string` + +Possible values: `jest`, `none` + +Test runner to use for unit tests diff --git a/docs/react/api-expo/generators/component.md b/docs/react/api-expo/generators/component.md new file mode 100644 index 0000000000000..cefa24002e6e5 --- /dev/null +++ b/docs/react/api-expo/generators/component.md @@ -0,0 +1,127 @@ +# @nrwl/expo:component + +Create a component + +## Usage + +```bash +nx generate component ... +``` + +```bash +nx g c ... # same +``` + +By default, Nx will search for `component` in the default collection provisioned in `workspace.json`. + +You can specify the collection explicitly as follows: + +```bash +nx g @nrwl/expo:component ... +``` + +Show what will be generated without writing to disk: + +```bash +nx g component ... --dry-run +``` + +### Examples + +Generate a component in the mylib library: + +```bash +nx g component my-component --project=mylib +``` + +Generate a class component in the mylib library: + +```bash +nx g component my-component --project=mylib --classComponent +``` + +## Options + +### name (_**required**_) + +Type: `string` + +The name of the component. + +### project (_**required**_) + +Alias(es): p + +Type: `string` + +The name of the project. + +### classComponent + +Alias(es): C + +Default: `false` + +Type: `boolean` + +Use class components instead of functional component. + +### directory + +Alias(es): d + +Type: `string` + +Create the component under this directory (can be nested). + +### export + +Alias(es): e + +Default: `false` + +Type: `boolean` + +When true, the component is exported from the project index.ts (if it exists). + +### flat + +Default: `false` + +Type: `boolean` + +Create component at the source root rather than its own directory. + +### js + +Default: `false` + +Type: `boolean` + +Generate JavaScript files rather than TypeScript files. + +### pascalCaseFiles + +Alias(es): P + +Default: `false` + +Type: `boolean` + +Use pascal case component file name (e.g. App.tsx). + +### skipFormat + +Default: `false` + +Type: `boolean` + +Skip formatting files. + +### skipTests + +Default: `false` + +Type: `boolean` + +When true, does not create "spec.ts" test files for the new component. diff --git a/docs/react/api-expo/generators/library.md b/docs/react/api-expo/generators/library.md new file mode 100644 index 0000000000000..c9a4045dcfddb --- /dev/null +++ b/docs/react/api-expo/generators/library.md @@ -0,0 +1,157 @@ +# @nrwl/expo:library + +Create a library + +## Usage + +```bash +nx generate library ... +``` + +```bash +nx g lib ... # same +``` + +By default, Nx will search for `library` in the default collection provisioned in `workspace.json`. + +You can specify the collection explicitly as follows: + +```bash +nx g @nrwl/expo:library ... +``` + +Show what will be generated without writing to disk: + +```bash +nx g library ... --dry-run +``` + +### Examples + +Generate libs/myapp/mylib: + +```bash +nx g lib mylib --directory=myapp +``` + +## Options + +### name (_**required**_) + +Type: `string` + +Library name + +### buildable + +Default: `false` + +Type: `boolean` + +Generate a buildable library. + +### directory + +Alias(es): d + +Type: `string` + +A directory where the lib is placed. + +### globalCss + +Default: `false` + +Type: `boolean` + +When true, the stylesheet is generated using global CSS instead of CSS modules (e.g. file is '_.css' rather than '_.module.css'). + +### importPath + +Type: `string` + +The library name used to import it, like @myorg/my-awesome-lib + +### js + +Default: `false` + +Type: `boolean` + +Generate JavaScript files rather than TypeScript files. + +### linter + +Default: `eslint` + +Type: `string` + +Possible values: `eslint`, `tslint` + +The tool to use for running lint checks. + +### pascalCaseFiles + +Alias(es): P + +Default: `false` + +Type: `boolean` + +Use pascal case component file name (e.g. App.tsx). + +### publishable + +Type: `boolean` + +Create a publishable library. + +### setParserOptionsProject + +Default: `false` + +Type: `boolean` + +Whether or not to configure the ESLint "parserOptions.project" option. We do not do this by default for lint performance reasons. + +### skipFormat + +Default: `false` + +Type: `boolean` + +Skip formatting files. + +### skipTsConfig + +Default: `false` + +Type: `boolean` + +Do not update tsconfig.json for development experience. + +### strict + +Default: `true` + +Type: `boolean` + +Whether to enable tsconfig strict mode or not. + +### tags + +Alias(es): t + +Type: `string` + +Add tags to the library (used for linting). + +### unitTestRunner + +Default: `jest` + +Type: `string` + +Possible values: `jest`, `none` + +Test runner to use for unit tests. diff --git a/docs/react/api-nx-devkit/index.md b/docs/react/api-nx-devkit/index.md index 35bd6ee9cb871..24cbcf73d9ae6 100644 --- a/docs/react/api-nx-devkit/index.md +++ b/docs/react/api-nx-devkit/index.md @@ -986,7 +986,7 @@ Examples: ```typescript names('my-name'); // {name: 'my-name', className: 'MyName', propertyName: 'myName', constantName: 'MY_NAME', fileName: 'my-name'} -names('myName'); // {name: 'my-name', className: 'MyName', propertyName: 'myName', constantName: 'MY_NAME', fileName: 'my-name'} +names('myName'); // {name: 'myName', className: 'MyName', propertyName: 'myName', constantName: 'MY_NAME', fileName: 'my-name'} ``` #### Parameters diff --git a/docs/react/api-react-native/executors/run-ios.md b/docs/react/api-react-native/executors/run-ios.md index 0c7c27a757966..5c8408b9a4066 100644 --- a/docs/react/api-react-native/executors/run-ios.md +++ b/docs/react/api-react-native/executors/run-ios.md @@ -78,4 +78,4 @@ Default: `Debug` Type: `string` -Explicitly set the Xcode configuration to use +Explicitly set the Xcode configuration to use. Debug or Release. diff --git a/docs/react/api-react-native/generators/application.md b/docs/react/api-react-native/generators/application.md index 9d531048d8c34..d6b3da69dc99c 100644 --- a/docs/react/api-react-native/generators/application.md +++ b/docs/react/api-react-native/generators/application.md @@ -42,6 +42,12 @@ nx g app myapp --classComponent ## Options +### name (_**required**_) + +Type: `string` + +The name of the application. + ### directory Alias(es): d @@ -84,12 +90,6 @@ Possible values: `eslint`, `tslint` The tool to use for running lint checks. -### name - -Type: `string` - -The name of the application. - ### setParserOptionsProject Default: `false` diff --git a/docs/react/executors.json b/docs/react/executors.json index cb26dd84b8573..4cd764be39293 100644 --- a/docs/react/executors.json +++ b/docs/react/executors.json @@ -2,6 +2,7 @@ "angular", "cypress", "detox", + "expo", "gatsby", "jest", "js", diff --git a/docs/react/generators.json b/docs/react/generators.json index 4bc621d6ae35a..6a9e601e07d8c 100644 --- a/docs/react/generators.json +++ b/docs/react/generators.json @@ -2,6 +2,7 @@ "angular", "cypress", "detox", + "expo", "express", "gatsby", "jest", diff --git a/e2e/expo/jest.config.js b/e2e/expo/jest.config.js new file mode 100644 index 0000000000000..2be6d98610584 --- /dev/null +++ b/e2e/expo/jest.config.js @@ -0,0 +1,11 @@ +module.exports = { + preset: '../../jest.preset.js', + transform: { + '^.+\\.[tj]sx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], + maxWorkers: 1, + globals: { 'ts-jest': { tsconfig: '/tsconfig.spec.json' } }, + displayName: 'e2e-expo', + testTimeout: 600000, +}; diff --git a/e2e/expo/project.json b/e2e/expo/project.json new file mode 100644 index 0000000000000..09c5e0c312e28 --- /dev/null +++ b/e2e/expo/project.json @@ -0,0 +1,34 @@ +{ + "root": "e2e/expo", + "sourceRoot": "e2e/expo", + "projectType": "application", + "targets": { + "e2e": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "yarn e2e-start-local-registry" + }, + { + "command": "yarn e2e-build-package-publish" + }, + { + "command": "nx run-e2e-tests e2e-expo" + } + ], + "parallel": false + } + }, + "run-e2e-tests": { + "executor": "@nrwl/jest:jest", + "options": { + "jestConfig": "e2e/expo/jest.config.js", + "passWithNoTests": true, + "runInBand": true + }, + "outputs": ["coverage/e2e/expo"] + } + }, + "implicitDependencies": ["expo"] +} diff --git a/e2e/expo/tests/expo.test.ts b/e2e/expo/tests/expo.test.ts new file mode 100644 index 0000000000000..e3867b7b64491 --- /dev/null +++ b/e2e/expo/tests/expo.test.ts @@ -0,0 +1,40 @@ +import { + newProject, + runCLI, + runCLIAsync, + uniq, + updateFile, +} from '@nrwl/e2e/utils'; + +describe('Expo', () => { + let proj: string; + + beforeEach(() => (proj = newProject())); + + it('should create files and run lint command', async () => { + const appName = uniq('my-app'); + const libName = uniq('lib'); + const componentName = uniq('component'); + + runCLI(`generate @nrwl/expo:application ${appName}`); + runCLI(`generate @nrwl/expo:library ${libName}`); + runCLI( + `generate @nrwl/expo:component ${componentName} --project=${libName} --export` + ); + + updateFile(`apps/${appName}/src/app/App.tsx`, (content) => { + let updated = `import ${componentName} from '${proj}/${libName}';\n${content}`; + return updated; + }); + + // testing does not work due to issue https://github.com/callstack/react-native-testing-library/issues/743 + // react-native 0.64.3 is using @jest/create-cache-key-function 26.5.0 that is incompatible with jest 27. + // expectTestsPass(await runCLIAsync(`test ${appName}`)); + // expectTestsPass(await runCLIAsync(`test ${libName}`)); + + const appLintResults = await runCLIAsync(`lint ${appName}`); + expect(appLintResults.combinedOutput).toContain('All files pass linting.'); + const libLintResults = await runCLIAsync(`lint ${libName}`); + expect(libLintResults.combinedOutput).toContain('All files pass linting.'); + }); +}); diff --git a/e2e/expo/tsconfig.json b/e2e/expo/tsconfig.json new file mode 100644 index 0000000000000..6d5abf8483200 --- /dev/null +++ b/e2e/expo/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["node", "jest"] + }, + "include": [], + "files": [], + "references": [ + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/e2e/expo/tsconfig.spec.json b/e2e/expo/tsconfig.spec.json new file mode 100644 index 0000000000000..1ad559c708973 --- /dev/null +++ b/e2e/expo/tsconfig.spec.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx", + "**/*.d.ts" + ] +} diff --git a/e2e/utils/index.ts b/e2e/utils/index.ts index 1b8ea11a09e95..162c33cc050fa 100644 --- a/e2e/utils/index.ts +++ b/e2e/utils/index.ts @@ -223,6 +223,7 @@ export function newProject({ `@nrwl/storybook`, `@nrwl/web`, `@nrwl/react-native`, + `@nrwl/expo`, ]; packageInstall(packages.join(` `), projScope); diff --git a/packages/create-nx-workspace/bin/create-nx-workspace.ts b/packages/create-nx-workspace/bin/create-nx-workspace.ts index 98fd55f407813..76c9907ec3c1d 100644 --- a/packages/create-nx-workspace/bin/create-nx-workspace.ts +++ b/packages/create-nx-workspace/bin/create-nx-workspace.ts @@ -26,6 +26,7 @@ export enum Preset { React = 'react', ReactWithExpress = 'react-express', ReactNative = 'react-native', + Expo = 'expo', NextJs = 'next', Gatsby = 'gatsby', Nest = 'nest', @@ -80,6 +81,10 @@ const presetOptions: { name: Preset; message: string }[] = [ message: 'react-native [a workspace with a single React Native application]', }, + { + name: Preset.Expo, + message: 'expo [a workspace with a single Expo application]', + }, { name: Preset.ReactWithExpress, message: @@ -347,7 +352,8 @@ function determineStyle(preset: Preset, parsedArgs: any) { preset === Preset.NPM || preset === Preset.Nest || preset === Preset.Express || - preset === Preset.ReactNative + preset === Preset.ReactNative || + preset === Preset.Expo ) { return Promise.resolve(null); } diff --git a/packages/detox/src/generators/application/application.spec.ts b/packages/detox/src/generators/application/application.spec.ts index fbd07a58c3257..c15539961ab73 100644 --- a/packages/detox/src/generators/application/application.spec.ts +++ b/packages/detox/src/generators/application/application.spec.ts @@ -14,6 +14,7 @@ describe('detox application generator', () => { beforeEach(() => { tree = createTreeWithEmptyWorkspace(); + tree.write('.gitignore', ''); }); describe('app at root', () => { @@ -26,6 +27,10 @@ describe('detox application generator', () => { name: 'my-app-e2e', project: 'my-app', linter: Linter.None, + js: false, + type: 'react-native', + skipFormat: false, + setParserOptionsProject: false, }); }); @@ -59,6 +64,10 @@ describe('detox application generator', () => { directory: 'my-dir', project: 'my-dir-my-app', linter: Linter.None, + js: false, + type: 'react-native', + skipFormat: false, + setParserOptionsProject: false, }); }); @@ -93,6 +102,10 @@ describe('detox application generator', () => { name: 'my-dir/my-app-e2e', project: 'my-dir-my-app', linter: Linter.None, + js: false, + type: 'react-native', + skipFormat: false, + setParserOptionsProject: false, }); }); diff --git a/packages/detox/src/generators/application/application.ts b/packages/detox/src/generators/application/application.ts index cc1c60a318332..60b9c47b601d9 100644 --- a/packages/detox/src/generators/application/application.ts +++ b/packages/detox/src/generators/application/application.ts @@ -1,6 +1,6 @@ import { convertNxGenerator, formatFiles, Tree } from '@nrwl/devkit'; - import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial'; + import detoxInitGenerator from '../init/init'; import { addGitIgnoreEntry } from './lib/add-git-ignore-entry'; import { addLinting } from './lib/add-linting'; diff --git a/packages/detox/src/generators/application/files/app/test-setup.ts.template b/packages/detox/src/generators/application/files/app/test-setup.ts.template index 004486f6bf0af..a4e12aa418f88 100644 --- a/packages/detox/src/generators/application/files/app/test-setup.ts.template +++ b/packages/detox/src/generators/application/files/app/test-setup.ts.template @@ -2,5 +2,4 @@ import { device } from 'detox'; beforeAll(async () => { await device.launchApp(); - await device.disableSynchronization(); }); diff --git a/packages/detox/src/generators/application/lib/add-linting.spec.ts b/packages/detox/src/generators/application/lib/add-linting.spec.ts index dede28035e1be..3a216b69338fb 100644 --- a/packages/detox/src/generators/application/lib/add-linting.spec.ts +++ b/packages/detox/src/generators/application/lib/add-linting.spec.ts @@ -17,6 +17,10 @@ describe('Add Linting', () => { appFileName: 'my-app', appClassName: 'MyApp', linter: Linter.EsLint, + js: false, + type: 'react-native', + skipFormat: false, + setParserOptionsProject: false, }); }); @@ -29,6 +33,10 @@ describe('Add Linting', () => { appFileName: 'my-app', appClassName: 'MyApp', linter: Linter.EsLint, + js: false, + type: 'react-native', + skipFormat: false, + setParserOptionsProject: false, }); const project = readProjectConfiguration(tree, 'my-app-e2e'); @@ -45,6 +53,10 @@ describe('Add Linting', () => { appFileName: 'my-app', appClassName: 'MyApp', linter: Linter.TsLint, + js: false, + type: 'react-native', + skipFormat: false, + setParserOptionsProject: false, }); const project = readProjectConfiguration(tree, 'my-app-e2e'); @@ -63,6 +75,10 @@ describe('Add Linting', () => { appFileName: 'my-app', appClassName: 'MyApp', linter: Linter.None, + js: false, + type: 'react-native', + skipFormat: false, + setParserOptionsProject: false, }); const project = readProjectConfiguration(tree, 'my-app-e2e'); diff --git a/packages/detox/src/generators/application/lib/add-project.spec.ts b/packages/detox/src/generators/application/lib/add-project.spec.ts index 41e3ad45ca871..7554e0be8c1d8 100644 --- a/packages/detox/src/generators/application/lib/add-project.spec.ts +++ b/packages/detox/src/generators/application/lib/add-project.spec.ts @@ -36,6 +36,10 @@ describe('Add Project', () => { appFileName: 'my-app', appClassName: 'MyApp', linter: Linter.EsLint, + js: false, + type: 'react-native', + skipFormat: false, + setParserOptionsProject: false, }); }); @@ -63,6 +67,10 @@ describe('Add Project', () => { appFileName: 'my-app', appClassName: 'MyApp', linter: Linter.EsLint, + js: false, + type: 'react-native', + skipFormat: false, + setParserOptionsProject: false, }); }); diff --git a/packages/detox/src/generators/application/lib/create-files.spec.ts b/packages/detox/src/generators/application/lib/create-files.spec.ts index e945319f380fc..9c51b0aae5ea9 100644 --- a/packages/detox/src/generators/application/lib/create-files.spec.ts +++ b/packages/detox/src/generators/application/lib/create-files.spec.ts @@ -19,6 +19,10 @@ describe('Create Files', () => { appFileName: 'my-app', appClassName: 'MyApp', linter: Linter.EsLint, + js: false, + type: 'react-native', + skipFormat: false, + setParserOptionsProject: false, }); expect(tree.exists('apps/my-app-e2e/.detoxrc.json')).toBeTruthy(); diff --git a/packages/detox/src/generators/application/lib/normalize-options.spec.ts b/packages/detox/src/generators/application/lib/normalize-options.spec.ts index 636e40ab1a661..b97aa662ef112 100644 --- a/packages/detox/src/generators/application/lib/normalize-options.spec.ts +++ b/packages/detox/src/generators/application/lib/normalize-options.spec.ts @@ -20,6 +20,10 @@ describe('Normalize Options', () => { name: 'my-app-e2e', project: 'my-app', linter: Linter.EsLint, + js: false, + type: 'react-native', + skipFormat: false, + setParserOptionsProject: false, }; const options = normalizeOptions(appTree, schema); expect(options).toEqual({ @@ -30,6 +34,10 @@ describe('Normalize Options', () => { appFileName: 'my-app', appClassName: 'MyApp', linter: Linter.EsLint, + js: false, + type: 'react-native', + skipFormat: false, + setParserOptionsProject: false, }); }); @@ -41,6 +49,11 @@ describe('Normalize Options', () => { const schema: Schema = { name: 'myAppE2e', project: 'myApp', + linter: Linter.None, + js: false, + type: 'react-native', + skipFormat: false, + setParserOptionsProject: false, }; const options = normalizeOptions(appTree, schema); expect(options).toEqual({ @@ -50,6 +63,11 @@ describe('Normalize Options', () => { project: 'myApp', projectName: 'my-app-e2e', projectRoot: 'apps/my-app-e2e', + linter: Linter.None, + js: false, + type: 'react-native', + skipFormat: false, + setParserOptionsProject: false, }); }); @@ -62,6 +80,11 @@ describe('Normalize Options', () => { name: 'my-app-e2e', project: 'my-app', directory: 'directory', + linter: Linter.None, + js: false, + type: 'react-native', + skipFormat: false, + setParserOptionsProject: false, }; const options = normalizeOptions(appTree, schema); expect(options).toEqual({ @@ -72,6 +95,11 @@ describe('Normalize Options', () => { name: 'my-app-e2e', directory: 'directory', projectName: 'directory-my-app-e2e', + linter: Linter.None, + js: false, + type: 'react-native', + skipFormat: false, + setParserOptionsProject: false, }); }); @@ -83,6 +111,11 @@ describe('Normalize Options', () => { const schema: Schema = { name: 'directory/my-app-e2e', project: 'my-app', + linter: Linter.None, + js: false, + type: 'react-native', + skipFormat: false, + setParserOptionsProject: false, }; const options = normalizeOptions(appTree, schema); expect(options).toEqual({ @@ -92,6 +125,11 @@ describe('Normalize Options', () => { projectRoot: 'apps/directory/my-app-e2e', name: 'directory/my-app-e2e', projectName: 'directory-my-app-e2e', + linter: Linter.None, + js: false, + type: 'react-native', + skipFormat: false, + setParserOptionsProject: false, }); }); }); diff --git a/packages/detox/src/generators/application/schema.d.ts b/packages/detox/src/generators/application/schema.d.ts index c8f9019d2683d..c39db93452de7 100644 --- a/packages/detox/src/generators/application/schema.d.ts +++ b/packages/detox/src/generators/application/schema.d.ts @@ -4,8 +4,9 @@ export interface Schema { project: string; name: string; directory?: string; - linter?: Linter; - js?: boolean; - skipFormat?: boolean; - setParserOptionsProject?: boolean; + linter: Linter; // default is eslint + js: boolean; // default is false + skipFormat: boolean; // default is false + setParserOptionsProject: boolean; // default is false + type: 'expo' | 'react-native'; // default is react-native } diff --git a/packages/detox/src/generators/application/schema.json b/packages/detox/src/generators/application/schema.json index 2fb2512be7f97..8a41f4e1cbbe1 100644 --- a/packages/detox/src/generators/application/schema.json +++ b/packages/detox/src/generators/application/schema.json @@ -44,6 +44,12 @@ "type": "boolean", "description": "Whether or not to configure the ESLint \"parserOptions.project\" option. We do not do this by default for lint performance reasons.", "default": false + }, + "type": { + "description": "The type of project to generate detox e2e for", + "type": "string", + "enum": ["react-native", "expo"], + "default": "react-native" } }, "required": ["name", "project"] diff --git a/packages/detox/src/generators/init/init.ts b/packages/detox/src/generators/init/init.ts index 8460d76b45e41..217bcf37c68f2 100644 --- a/packages/detox/src/generators/init/init.ts +++ b/packages/detox/src/generators/init/init.ts @@ -15,10 +15,10 @@ import { typesDetoxVersion, } from '../../utils/versions'; -export async function detoxInitGenerator(host: Tree, schema: Schema) { +export async function detoxInitGenerator(host: Tree, schema?: Schema) { const tasks = [moveDependency(host), updateDependencies(host)]; - if (!schema.skipFormat) { + if (!schema?.skipFormat) { await formatFiles(host); } diff --git a/packages/detox/src/generators/init/schema.d.ts b/packages/detox/src/generators/init/schema.d.ts index e5fe924e01d2f..67c68d4bfab8d 100644 --- a/packages/detox/src/generators/init/schema.d.ts +++ b/packages/detox/src/generators/init/schema.d.ts @@ -1,3 +1,3 @@ export interface Schema { - skipFormat?: boolean; + skipFormat?: boolean; // default is false } diff --git a/packages/devkit/src/utils/names.ts b/packages/devkit/src/utils/names.ts index 2648f5db45f96..b8131fda91b88 100644 --- a/packages/devkit/src/utils/names.ts +++ b/packages/devkit/src/utils/names.ts @@ -5,7 +5,7 @@ * * ```typescript * names("my-name") // {name: 'my-name', className: 'MyName', propertyName: 'myName', constantName: 'MY_NAME', fileName: 'my-name'} - * names("myName") // {name: 'my-name', className: 'MyName', propertyName: 'myName', constantName: 'MY_NAME', fileName: 'my-name'} + * names("myName") // {name: 'myName', className: 'MyName', propertyName: 'myName', constantName: 'MY_NAME', fileName: 'my-name'} * ``` * @param name */ diff --git a/packages/expo/.babelrc b/packages/expo/.babelrc new file mode 100644 index 0000000000000..cf7ddd99c615a --- /dev/null +++ b/packages/expo/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]] +} diff --git a/packages/expo/.eslintrc.json b/packages/expo/.eslintrc.json new file mode 100644 index 0000000000000..9afd5d63f09f7 --- /dev/null +++ b/packages/expo/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "extends": "../../.eslintrc", + "rules": {}, + "ignorePatterns": ["!**/*"] +} diff --git a/packages/expo/README.md b/packages/expo/README.md new file mode 100644 index 0000000000000..463f67ca8edf1 --- /dev/null +++ b/packages/expo/README.md @@ -0,0 +1,7 @@ +# expo + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test expo` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/packages/expo/collection.json b/packages/expo/collection.json new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/expo/executors.json b/packages/expo/executors.json new file mode 100644 index 0000000000000..654eeade0abf9 --- /dev/null +++ b/packages/expo/executors.json @@ -0,0 +1,86 @@ +{ + "executors": { + "build-ios": { + "implementation": "./src/executors/build-ios/build-ios.impl", + "schema": "./src/executors/build-ios/schema.json", + "description": "Build and sign a standalone IPA for the Apple App Store" + }, + "build-android": { + "implementation": "./src/executors/build-android/build-android.impl", + "schema": "./src/executors/build-android/schema.json", + "description": "Build and sign a standalone APK or App Bundle for the Google Play Store" + }, + "build-web": { + "implementation": "./src/executors/build-web/build-web.impl", + "schema": "./src/executors/build-web/schema.json", + "description": "Build the web app for production" + }, + "build-status": { + "implementation": "./src/executors/build-status/build-status.impl", + "schema": "./src/executors/build-status/schema.json", + "description": "Get the status of the latest build for the project" + }, + "run": { + "implementation": "./src/executors/run/run.impl", + "schema": "./src/executors/run/schema.json", + "description": "Run the Android app binary locally or run the iOS app binary locally" + }, + "start": { + "implementation": "./src/executors/start/start.impl", + "schema": "./src/executors/start/schema.json", + "description": "Start a local dev server for the app or start a Webpack dev server for the web app" + }, + "sync-deps": { + "implementation": "./src/executors/sync-deps/sync-deps.impl", + "schema": "./src/executors/sync-deps/schema.json", + "description": "Syncs dependencies to package.json (required for autolinking)." + }, + "ensure-symlink": { + "implementation": "./src/executors/ensure-symlink/ensure-symlink.impl", + "schema": "./src/executors/ensure-symlink//schema.json", + "description": "Ensure workspace node_modules is symlink under app's node_modules folder." + } + }, + "builders": { + "build-ios": { + "implementation": "./src/executors/build-ios/compat", + "schema": "./src/executors/build-ios/schema.json", + "description": "Build and sign a standalone IPA for the Apple App Store" + }, + "build-android": { + "implementation": "./src/executors/build-android/compat", + "schema": "./src/executors/build-android/schema.json", + "description": "Build and sign a standalone APK or App Bundle for the Google Play Store" + }, + "build-web": { + "implementation": "./src/executors/build-web/compat", + "schema": "./src/executors/build-web/schema.json", + "description": "Build the web app for production" + }, + "build-status": { + "implementation": "./src/executors/build-status/compat", + "schema": "./src/executors/build-status/schema.json", + "description": "Get the status of the latest build for the project" + }, + "run": { + "implementation": "./src/executors/run/compat", + "schema": "./src/executors/run/schema.json", + "description": "Run the Android app binary locally or run the iOS app binary locally" + }, + "start": { + "implementation": "./src/executors/start/compat", + "schema": "./src/executors/start/schema.json", + "description": "Start a local dev server for the app or start a Webpack dev server for the web app" + }, + "sync-deps": { + "implementation": "./src/executors/sync-deps/compat", + "schema": "./src/executors/sync-deps/schema.json", + "description": "Syncs dependencies to package.json (required for autolinking)." + }, + "ensure-symlink": { + "implementation": "./src/executors/ensure-symlink/compat", + "schema": "./src/executors/ensure-symlink//schema.json", + "description": "Ensure workspace node_modules is symlink under app's node_modules folder." + } + } +} diff --git a/packages/expo/generators.json b/packages/expo/generators.json new file mode 100644 index 0000000000000..904f40a533e03 --- /dev/null +++ b/packages/expo/generators.json @@ -0,0 +1,61 @@ +{ + "name": "Nx Expo", + "version": "0.1", + "extends": ["@nrwl/workspace"], + "schematics": { + "init": { + "factory": "./src/generators/init/init#expoInitSchematic", + "schema": "./src/generators/init/schema.json", + "description": "Initialize the @nrwl/expo plugin", + "hidden": true + }, + "application": { + "factory": "./src/generators/application/application#expoApplicationSchematic", + "schema": "./src/generators/application/schema.json", + "aliases": ["app"], + "x-type": "application", + "description": "Create an application" + }, + "library": { + "factory": "./src/generators/library/library#expoLibrarySchematic", + "schema": "./src/generators/library/schema.json", + "aliases": ["lib"], + "x-type": "library", + "description": "Create a library" + }, + "component": { + "factory": "./src/generators/component/component#expoComponentSchematic", + "schema": "./src/generators/component/schema.json", + "description": "Create a component", + "aliases": ["c"] + } + }, + "generators": { + "init": { + "factory": "./src/generators/init/init#expoInitGenerator", + "schema": "./src/generators/init/schema.json", + "description": "Initialize the @nrwl/expo plugin", + "hidden": true + }, + "application": { + "factory": "./src/generators/application/application#expoApplicationGenerator", + "schema": "./src/generators/application/schema.json", + "aliases": ["app"], + "x-type": "application", + "description": "Create an application" + }, + "library": { + "factory": "./src/generators/library/library#expoLibraryGenerator", + "schema": "./src/generators/library/schema.json", + "aliases": ["lib"], + "x-type": "library", + "description": "Create a library" + }, + "component": { + "factory": "./src/generators/component/component#expoComponentGenerator", + "schema": "./src/generators/component/schema.json", + "description": "Create a component", + "aliases": ["c"] + } + } +} diff --git a/packages/expo/index.ts b/packages/expo/index.ts new file mode 100644 index 0000000000000..d296681d43aa6 --- /dev/null +++ b/packages/expo/index.ts @@ -0,0 +1,3 @@ +export { expoInitGenerator } from './src/generators/init/init'; +export { expoApplicationGenerator } from './src/generators/application/application'; +export { withNxMetro } from './plugins/with-nx-metro'; diff --git a/packages/expo/jest.config.js b/packages/expo/jest.config.js new file mode 100644 index 0000000000000..4bf1110724403 --- /dev/null +++ b/packages/expo/jest.config.js @@ -0,0 +1,12 @@ +module.exports = { + preset: '../../jest.preset.js', + transform: { + '^.+\\.[tj]sx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html', 'json'], + globals: { + 'ts-jest': { tsconfig: '/tsconfig.spec.json' }, + }, + displayName: 'expo', + testEnvironment: 'node', +}; diff --git a/packages/expo/package.json b/packages/expo/package.json new file mode 100644 index 0000000000000..3f82320489cc8 --- /dev/null +++ b/packages/expo/package.json @@ -0,0 +1,48 @@ +{ + "name": "@nrwl/expo", + "version": "0.0.1", + "description": "React Native Plugin for Nx", + "keywords": [ + "Monorepo", + "React", + "Web", + "Jest", + "Native", + "CLI" + ], + "homepage": "https://nx.dev", + "bugs": { + "url": "https://github.com/nrwl/nx/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/nrwl/nx.git", + "directory": "packages/expo" + }, + "license": "MIT", + "author": "Victor Savkin", + "main": "index.js", + "types": "index.d.ts", + "dependencies": { + "@nrwl/detox": "*", + "@nrwl/devkit": "*", + "@nrwl/jest": "*", + "@nrwl/linter": "*", + "@nrwl/react": "*", + "@nrwl/workspace": "*", + "chalk": "4.1.0", + "enhanced-resolve": "^5.8.3", + "expo-cli": "^4.13.0", + "metro-resolver": "^0.66.2", + "tsconfig-paths": "^3.9.0" + }, + "peerDependencies": { + "expo": "43.0.3" + }, + "builders": "./executors.json", + "ng-update": { + "requirements": {}, + "migrations": "./migrations.json" + }, + "schematics": "./generators.json" +} diff --git a/packages/expo/plugins/metro-resolver.ts b/packages/expo/plugins/metro-resolver.ts new file mode 100644 index 0000000000000..4bf52eb9db8c2 --- /dev/null +++ b/packages/expo/plugins/metro-resolver.ts @@ -0,0 +1,194 @@ +import * as metroResolver from 'metro-resolver'; +import type { MatchPath } from 'tsconfig-paths'; +import { createMatchPath, loadConfig } from 'tsconfig-paths'; +import * as chalk from 'chalk'; +import { detectPackageManager } from '@nrwl/devkit'; +import { CachedInputFileSystem, ResolverFactory } from 'enhanced-resolve'; +import { dirname, join } from 'path'; +import * as fs from 'fs'; +import { appRootPath } from '@nrwl/tao/src/utils/app-root'; + +/* + * Use tsconfig to resolve additional workspace libs. + * + * This resolve function requires projectRoot to be set to + * workspace root in order modules and assets to be registered and watched. + */ +export function getResolveRequest(extensions: string[]) { + return function ( + _context: any, + realModuleName: string, + platform: string | null, + moduleName: string + ) { + const DEBUG = process.env.NX_REACT_NATIVE_DEBUG === 'true'; + + if (DEBUG) console.log(chalk.cyan(`[Nx] Resolving: ${moduleName}`)); + + const { resolveRequest, ...context } = _context; + + let resolvedPath = defaultMetroResolver(context, moduleName, platform); + if (resolvedPath) { + return resolvedPath; + } + + if (detectPackageManager(appRootPath) === 'pnpm') { + resolvedPath = pnpmResolver( + extensions, + context, + realModuleName, + moduleName + ); + if (resolvedPath) { + return resolvedPath; + } + } + + return tsconfigPathsResolver(extensions, realModuleName, moduleName); + }; +} + +/** + * This function try to resolve path using metro's default resolver + * @returns path if resolved, else undefined + */ +function defaultMetroResolver( + context: string, + moduleName: string, + platform: string +) { + const DEBUG = process.env.NX_REACT_NATIVE_DEBUG === 'true'; + try { + return metroResolver.resolve(context, moduleName, platform); + } catch { + if (DEBUG) + console.log( + chalk.cyan( + `[Nx] Unable to resolve with default Metro resolver: ${moduleName}` + ) + ); + } +} + +/** + * This resolver try to resolve module for pnpm. + * @returns path if resolved, else undefined + * This pnpm resolver is inspired from https://github.com/vjpr/pnpm-react-native-example/blob/main/packages/pnpm-expo-helper/util/make-resolver.js + */ +function pnpmResolver(extensions, context, realModuleName, moduleName) { + const DEBUG = process.env.NX_REACT_NATIVE_DEBUG === 'true'; + try { + const pnpmResolver = getPnpmResolver(appRootPath, extensions); + const lookupStartPath = dirname(context.originModulePath); + const filePath = pnpmResolver.resolveSync( + {}, + lookupStartPath, + realModuleName + ); + if (filePath) { + return { type: 'sourceFile', filePath }; + } + } catch { + if (DEBUG) + console.log( + chalk.cyan( + `[Nx] Unable to resolve with default PNPM resolver: ${moduleName}` + ) + ); + } +} + +/** + * This function try to resolve files that are specified in tsconfig's paths + * @returns path if resolved, else undefined + */ +function tsconfigPathsResolver( + extensions: string[], + realModuleName: string, + moduleName: string +) { + const DEBUG = process.env.NX_REACT_NATIVE_DEBUG === 'true'; + const matcher = getMatcher(); + let match; + + // find out the file extension + const matchExtension = extensions.find((extension) => { + match = matcher(realModuleName, undefined, undefined, ['.' + extension]); + return !!match; + }); + + if (match) { + return { + type: 'sourceFile', + filePath: + !matchExtension || match.endsWith(`.${matchExtension}`) + ? match + : `${match}.${matchExtension}`, + }; + } else { + if (DEBUG) { + console.log( + chalk.red(`[Nx] Failed to resolve ${chalk.bold(moduleName)}`) + ); + console.log( + chalk.cyan( + `[Nx] The following tsconfig paths was used:\n:${chalk.bold( + JSON.stringify(paths, null, 2) + )}` + ) + ); + } + throw new Error(`Cannot resolve ${chalk.bold(moduleName)}`); + } +} + +let matcher: MatchPath; +let absoluteBaseUrl: string; +let paths: Record; + +function getMatcher() { + const DEBUG = process.env.NX_REACT_NATIVE_DEBUG === 'true'; + + if (!matcher) { + const result = loadConfig(); + if (result.resultType === 'success') { + absoluteBaseUrl = result.absoluteBaseUrl; + paths = result.paths; + if (DEBUG) { + console.log( + chalk.cyan(`[Nx] Located tsconfig at ${chalk.bold(absoluteBaseUrl)}`) + ); + console.log( + chalk.cyan( + `[Nx] Found the following paths:\n:${chalk.bold( + JSON.stringify(paths, null, 2) + )}` + ) + ); + } + matcher = createMatchPath(absoluteBaseUrl, paths); + } else { + console.log(chalk.cyan(`[Nx] Failed to locate tsconfig}`)); + throw new Error(`Could not load tsconfig for project`); + } + } + return matcher; +} + +/** + * This function returns resolver for pnpm. + * It is inspired form https://github.com/vjpr/pnpm-expo-example/blob/main/packages/pnpm-expo-helper/util/make-resolver.js. + */ +let resolver; +function getPnpmResolver(appRootPath: string, extensions: string[]) { + if (!resolver) { + const fileSystem = new CachedInputFileSystem(fs, 4000); + resolver = ResolverFactory.createResolver({ + fileSystem, + extensions: extensions.map((extension) => '.' + extension), + useSyncFileSystemCalls: true, + modules: [join(appRootPath, 'node_modules'), 'node_modules'], + }); + } + return resolver; +} diff --git a/packages/expo/plugins/with-nx-metro.ts b/packages/expo/plugins/with-nx-metro.ts new file mode 100644 index 0000000000000..7e829784e5bfe --- /dev/null +++ b/packages/expo/plugins/with-nx-metro.ts @@ -0,0 +1,32 @@ +import { workspaceLayout } from '@nrwl/workspace/src/core/file-utils'; +import { appRootPath } from '@nrwl/workspace/src/utils/app-root'; +import { join } from 'path'; +import { getResolveRequest } from './metro-resolver'; + +interface WithNxOptions { + debug?: boolean; + extensions?: string[]; +} + +export function withNxMetro(config: any, opts: WithNxOptions = {}) { + const extensions = ['', 'ts', 'tsx', 'js', 'jsx', 'json']; + if (opts.debug) process.env.NX_REACT_NATIVE_DEBUG = 'true'; + if (opts.extensions) extensions.push(...opts.extensions); + + // Set the root to workspace root so we can resolve modules and assets + config.projectRoot = appRootPath; + + const watchFolders = config.watchFolders || []; + config.watchFolders = watchFolders.concat([ + join(appRootPath, 'node_modules'), + join(appRootPath, workspaceLayout().libsDir), + ]); + + // Add support for paths specified by tsconfig + config.resolver = { + ...config.resolver, + resolveRequest: getResolveRequest(extensions), + }; + + return config; +} diff --git a/packages/expo/project.json b/packages/expo/project.json new file mode 100644 index 0000000000000..2ab32055fdfd2 --- /dev/null +++ b/packages/expo/project.json @@ -0,0 +1,83 @@ +{ + "root": "packages/expo", + "sourceRoot": "packages/expo/src", + "projectType": "library", + "targets": { + "lint": { + "executor": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": [ + "packages/expo/**/*.ts", + "packages/expo/**/*.spec.ts", + "packages/expo/**/*.spec.tsx", + "packages/expo/**/*.spec.js", + "packages/expo/**/*.spec.jsx", + "packages/expo/**/*.d.ts" + ] + }, + "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nrwl/jest:jest", + "options": { + "jestConfig": "packages/expo/jest.config.js", + "passWithNoTests": true + }, + "outputs": ["coverage/packages/expo"] + }, + "build-base": { + "executor": "@nrwl/node:package", + "options": { + "outputPath": "build/packages/expo", + "tsConfig": "packages/expo/tsconfig.lib.json", + "packageJson": "packages/expo/package.json", + "main": "packages/expo/index.ts", + "updateBuildableProjectDepsInPackageJson": false, + "assets": [ + "packages/expo/*.md", + { + "input": "packages/expo", + "glob": "**/!(*.ts)", + "output": "/" + }, + { + "input": "packages/expo", + "glob": "**/*.d.ts", + "output": "/" + }, + { + "input": "packages/expo", + "glob": "**/files/**", + "output": "/" + }, + { + "input": "packages/expo", + "glob": "**/files/**/.gitkeep", + "output": "/" + }, + { + "input": "packages/expo", + "glob": "**/files/**/.babelrc.template", + "output": "/" + }, + { + "input": "./packages/expo", + "glob": "**/*.json", + "ignore": ["**/tsconfig*.json"], + "output": "/" + }, + "LICENSE" + ] + }, + "outputs": ["{options.outputPath}"] + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["build/packages/expo"], + "options": { + "command": "node ./scripts/copy-readme.js expo" + } + } + }, + "tags": [] +} diff --git a/packages/expo/src/executors/build-android/build-android.impl.ts b/packages/expo/src/executors/build-android/build-android.impl.ts new file mode 100644 index 0000000000000..9d485814aaefd --- /dev/null +++ b/packages/expo/src/executors/build-android/build-android.impl.ts @@ -0,0 +1,73 @@ +import { ExecutorContext } from '@nrwl/devkit'; +import { join } from 'path'; +import { toFileName } from '@nrwl/workspace/src/devkit-reexport'; +import { ChildProcess, fork } from 'child_process'; + +import { ensureNodeModulesSymlink } from '../../utils/ensure-node-modules-symlink'; + +import { ExpoBuildAndroidOptions } from './schema'; + +export interface ReactNativeBuildOutput { + success: boolean; +} + +let childProcess: ChildProcess; + +export default async function* buildAndroidExecutor( + options: ExpoBuildAndroidOptions, + context: ExecutorContext +): AsyncGenerator { + const projectRoot = context.workspace.projects[context.projectName].root; + ensureNodeModulesSymlink(context.root, projectRoot); + + try { + await runCliBuild(context.root, projectRoot, options); + yield { success: true }; + } finally { + if (childProcess) { + childProcess.kill(); + } + } +} + +function runCliBuild( + workspaceRoot: string, + projectRoot: string, + options: ExpoBuildAndroidOptions +) { + return new Promise((resolve, reject) => { + childProcess = fork( + join(workspaceRoot, './node_modules/expo/bin/cli.js'), + ['build:android', ...createRunOptions(options)], + { cwd: join(workspaceRoot, projectRoot) } + ); + + // Ensure the child process is killed when the parent exits + process.on('exit', () => childProcess.kill()); + process.on('SIGTERM', () => childProcess.kill()); + + childProcess.on('error', (err) => { + reject(err); + }); + childProcess.on('exit', (code) => { + if (code === 0) { + resolve(code); + } else { + reject(code); + } + }); + }); +} + +function createRunOptions(options) { + return Object.keys(options).reduce((acc, k) => { + const v = options[k]; + if (v === true) { + // when true, does not need to pass the value true, just need to pass the flag in kebob case + acc.push(`--${toFileName(k)}`); + } else { + acc.push(`--${toFileName(k)}`, v); + } + return acc; + }, []); +} diff --git a/packages/expo/src/executors/build-android/compat.ts b/packages/expo/src/executors/build-android/compat.ts new file mode 100644 index 0000000000000..12465b9fe895c --- /dev/null +++ b/packages/expo/src/executors/build-android/compat.ts @@ -0,0 +1,5 @@ +import { convertNxExecutor } from '@nrwl/devkit'; + +import buildAndroidExecutor from './build-android.impl'; + +export default convertNxExecutor(buildAndroidExecutor); diff --git a/packages/expo/src/executors/build-android/schema.d.ts b/packages/expo/src/executors/build-android/schema.d.ts new file mode 100644 index 0000000000000..602fa4c95b7b1 --- /dev/null +++ b/packages/expo/src/executors/build-android/schema.d.ts @@ -0,0 +1,12 @@ +// options from https://docs.expo.dev/workflow/expo-cli/#expo-buildandroid +export interface ExpoBuildAndroidOptions { + clearCredentials?: boolean; + type?: 'app-bundle' | 'apk'; + releaseChannel?: string; + noPublish?: boolean; + noWait?: boolean; + keystorePath?: string; + keystoreAlias?: string; + publicUrl?: string; + skipWorkflowCheck?: boolean; +} diff --git a/packages/expo/src/executors/build-android/schema.json b/packages/expo/src/executors/build-android/schema.json new file mode 100644 index 0000000000000..b3a497551ddcf --- /dev/null +++ b/packages/expo/src/executors/build-android/schema.json @@ -0,0 +1,49 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "NxExpoBuildAndroid", + "cli": "nx", + "title": "Expo Android Build executor", + "description": "Build and sign a standalone APK or App Bundle for the Google Play Store", + "type": "object", + "properties": { + "clearCredentials": { + "type": "boolean", + "description": "Clear all credentials stored on Expo servers.", + "alias": "c" + }, + "type": { + "enum": ["app-bundle", "apk"], + "description": "Type of build: [app-bundle⎮apk].", + "alias": "t" + }, + "releaseChannel": { + "type": "string", + "description": "Pull from specified release channel." + }, + "noPublish": { + "type": "boolean", + "description": "Disable automatic publishing before building." + }, + "noWait": { + "type": "boolean", + "description": "Exit immediately after scheduling build." + }, + "keystorePath": { + "type": "string", + "description": "Path to your Keystore: *.jks." + }, + "keystoreAlias": { + "type": "string", + "description": "Keystore Alias" + }, + "publicUrl": { + "type": "string", + "description": "The URL of an externally hosted manifest (for self-hosted apps)." + }, + "skipWorkflowCheck": { + "type": "boolean", + "description": "Skip warning about build service bare workflow limitations." + } + }, + "required": [] +} diff --git a/packages/expo/src/executors/build-ios/build-ios.impl.ts b/packages/expo/src/executors/build-ios/build-ios.impl.ts new file mode 100644 index 0000000000000..fb9d6e3ddfaf2 --- /dev/null +++ b/packages/expo/src/executors/build-ios/build-ios.impl.ts @@ -0,0 +1,87 @@ +import { ExecutorContext } from '@nrwl/devkit'; +import { join } from 'path'; +import { ChildProcess, fork } from 'child_process'; +import { toFileName } from '@nrwl/workspace/src/devkit-reexport'; + +import { ensureNodeModulesSymlink } from '../../utils/ensure-node-modules-symlink'; +import { + displayNewlyAddedDepsMessage, + syncDeps, +} from '../sync-deps/sync-deps.impl'; +import { ExpoBuildIOSOptions } from './schema'; + +export interface ExpoRunOutput { + success: boolean; +} + +let childProcess: ChildProcess; + +export default async function* buildIosExecutor( + options: ExpoBuildIOSOptions, + context: ExecutorContext +): AsyncGenerator { + const projectRoot = context.workspace.projects[context.projectName].root; + ensureNodeModulesSymlink(context.root, projectRoot); + if (options.sync) { + displayNewlyAddedDepsMessage( + context.projectName, + await syncDeps(context.projectName, projectRoot) + ); + } + + try { + await runCliBuildIOS(context.root, projectRoot, options); + + yield { success: true }; + } finally { + if (childProcess) { + childProcess.kill(); + } + } +} + +function runCliBuildIOS( + workspaceRoot: string, + projectRoot: string, + options: ExpoBuildIOSOptions +) { + return new Promise((resolve, reject) => { + childProcess = fork( + join(workspaceRoot, './node_modules/expo/bin/cli.js'), + ['build:ios', ...createRunOptions(options)], + { cwd: join(workspaceRoot, projectRoot) } + ); + + // Ensure the child process is killed when the parent exits + process.on('exit', () => childProcess.kill()); + process.on('SIGTERM', () => childProcess.kill()); + + childProcess.on('error', (err) => { + reject(err); + }); + childProcess.on('exit', (code) => { + if (code === 0) { + resolve(code); + } else { + reject(code); + } + }); + }); +} + +const nxOptions = ['sync']; + +function createRunOptions(options) { + return Object.keys(options).reduce((acc, k) => { + const v = options[k]; + if (!nxOptions.includes(k)) { + if (v === true) { + // when true, does not need to pass the value true, just need to pass the flag in kebob case + acc.push(`--${toFileName(k)}`); + } else { + acc.push(`--${toFileName(k)}`, v); + } + } + return acc; + }, []); +} diff --git a/packages/expo/src/executors/build-ios/compat.ts b/packages/expo/src/executors/build-ios/compat.ts new file mode 100644 index 0000000000000..3803011250b70 --- /dev/null +++ b/packages/expo/src/executors/build-ios/compat.ts @@ -0,0 +1,5 @@ +import { convertNxExecutor } from '@nrwl/devkit'; + +import buildIosExecutor from './build-ios.impl'; + +export default convertNxExecutor(buildIosExecutor); diff --git a/packages/expo/src/executors/build-ios/schema.d.ts b/packages/expo/src/executors/build-ios/schema.d.ts new file mode 100644 index 0000000000000..67d959081103f --- /dev/null +++ b/packages/expo/src/executors/build-ios/schema.d.ts @@ -0,0 +1,23 @@ +// options from https://docs.expo.dev/workflow/expo-cli/#expo-buildios +export interface ExpoBuildIOSOptions { + clearCredentials?: boolean; + clearDistCert?: boolean; + clearPushKey?: boolean; + clearnPushCert?: boolean; + clearProvisioningProfile?: boolean; + revokeCredentials?: boolean; + appleId?: string; + type: 'archive' | 'simulator'; + releaseChannel?: string; + noPublish?: boolean; + noWait?: boolean; + teamId?: string; + dishP12Path?: string; + pushId?: string; + pushP8Path?: string; + provisioningProfile?: string; + publicUrl?: string; + skipCredentialsCheck?: booolean; + skipWorkflowCheck?: boolean; + sync: boolean; // default is true +} diff --git a/packages/expo/src/executors/build-ios/schema.json b/packages/expo/src/executors/build-ios/schema.json new file mode 100644 index 0000000000000..8c945459a7c76 --- /dev/null +++ b/packages/expo/src/executors/build-ios/schema.json @@ -0,0 +1,91 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "NxExpoBuildIOS", + "cli": "nx", + "title": "Expo iOS Build executor", + "description": "Build and sign a standalone IPA for the Apple App Store", + "type": "object", + "properties": { + "clearCredentials": { + "type": "boolean", + "description": "Clear all credentials stored on Expo servers.", + "alias": "c" + }, + "clearDistCert": { + "type": "boolean", + "description": "Remove Distribution Certificate stored on Expo servers." + }, + "clearPushKey": { + "type": "boolean", + "description": "Remove Push Notifications Key stored on Expo servers." + }, + "clearPushCert": { + "type": "boolean", + "description": "Remove Push Notifications Certificate stored on Expo servers. Use of Push Notifications Certificates is deprecated." + }, + "clearProvisioningProfile": { + "type": "boolean", + "description": "Remove Provisioning Profile stored on Expo servers." + }, + "revokeCredentials": { + "type": "boolean", + "description": "Revoke credentials on developer.apple.com, select appropriate using --clear-* options.", + "alias": "r" + }, + "appleId": { + "type": "string", + "description": "Apple ID username (please also set the Apple ID password as EXPO_APPLE_PASSWORD environment variable)." + }, + "type": { + "enum": ["archive", "simulator"], + "description": "Type of build: [archive⎮simulator].", + "alias": "t" + }, + "releaseChannel": { + "type": "string", + "description": "Pull from specified release channel." + }, + "noPublish": { + "type": "boolean", + "description": "Disable automatic publishing before building." + }, + "noWait": { + "type": "boolean", + "description": "Exit immediately after scheduling build." + }, + "teamId": { + "type": "string", + "description": "Apple Team ID." + }, + "distP12Path": { + "type": "string", + "description": "Path to your Distribution Certificate P12 (set password as EXPO_IOS_DIST_P12_PASSWORD environment variable)." + }, + "pushP8Path": { + "type": "string", + "description": "Path to your Push Key .p8 file." + }, + "provisioningProfilePath": { + "type": "string", + "description": "Path to your Provisioning Profile." + }, + "publicUrl": { + "type": "string", + "description": "The URL of an externally hosted manifest (for self-hosted apps)." + }, + "skipCredentialsCheck": { + "type": "boolean", + "description": "Skip checking credentials." + }, + "skipWorkflowCheck": { + "type": "boolean", + "description": "Skip warning about build service bare workflow limitations." + }, + "sync": { + "type": "boolean", + "description": "Syncs npm dependencies to package.json (for React Native autolink). Always true when --install is used.", + "default": true + } + }, + "required": [] +} diff --git a/packages/expo/src/executors/build-status/build-status.impl.ts b/packages/expo/src/executors/build-status/build-status.impl.ts new file mode 100644 index 0000000000000..164c5b7978cba --- /dev/null +++ b/packages/expo/src/executors/build-status/build-status.impl.ts @@ -0,0 +1,73 @@ +import { ExecutorContext } from '@nrwl/devkit'; +import { join } from 'path'; +import { toFileName } from '@nrwl/workspace/src/devkit-reexport'; +import { ChildProcess, fork } from 'child_process'; + +import { ensureNodeModulesSymlink } from '../../utils/ensure-node-modules-symlink'; + +import { ExpoBuildStatusOptions } from './schema'; + +export interface ReactNativeBuildOutput { + success: boolean; +} + +let childProcess: ChildProcess; + +export default async function* buildStatusExecutor( + options: ExpoBuildStatusOptions, + context: ExecutorContext +): AsyncGenerator { + const projectRoot = context.workspace.projects[context.projectName].root; + ensureNodeModulesSymlink(context.root, projectRoot); + + try { + await runCliBuild(context.root, projectRoot, options); + yield { success: true }; + } finally { + if (childProcess) { + childProcess.kill(); + } + } +} + +function runCliBuild( + workspaceRoot: string, + projectRoot: string, + options: ExpoBuildStatusOptions +) { + return new Promise((resolve, reject) => { + childProcess = fork( + join(workspaceRoot, './node_modules/expo/bin/cli.js'), + ['build:status', ...createRunOptions(options)], + { cwd: join(workspaceRoot, projectRoot) } + ); + + // Ensure the child process is killed when the parent exits + process.on('exit', () => childProcess.kill()); + process.on('SIGTERM', () => childProcess.kill()); + + childProcess.on('error', (err) => { + reject(err); + }); + childProcess.on('exit', (code) => { + if (code === 0) { + resolve(code); + } else { + reject(code); + } + }); + }); +} + +function createRunOptions(options) { + return Object.keys(options).reduce((acc, k) => { + const v = options[k]; + if (v === true) { + // when true, does not need to pass the value true, just need to pass the flag in kebob case + acc.push(`--${toFileName(k)}`); + } else { + acc.push(`--${toFileName(k)}`, v); + } + return acc; + }, []); +} diff --git a/packages/expo/src/executors/build-status/compat.ts b/packages/expo/src/executors/build-status/compat.ts new file mode 100644 index 0000000000000..b2d2934f1507c --- /dev/null +++ b/packages/expo/src/executors/build-status/compat.ts @@ -0,0 +1,5 @@ +import { convertNxExecutor } from '@nrwl/devkit'; + +import buildStatusExecutor from './build-status.impl'; + +export default convertNxExecutor(buildStatusExecutor); diff --git a/packages/expo/src/executors/build-status/schema.d.ts b/packages/expo/src/executors/build-status/schema.d.ts new file mode 100644 index 0000000000000..241aae41160e7 --- /dev/null +++ b/packages/expo/src/executors/build-status/schema.d.ts @@ -0,0 +1,4 @@ +// options from https://docs.expo.dev/workflow/expo-cli/#expo-buildweb +export interface ExpoBuildStatusOptions { + publicUrl: string; +} diff --git a/packages/expo/src/executors/build-status/schema.json b/packages/expo/src/executors/build-status/schema.json new file mode 100644 index 0000000000000..d99c1a90f93d3 --- /dev/null +++ b/packages/expo/src/executors/build-status/schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "NxExpoBuildStatus", + "cli": "nx", + "title": "Expo web Build executor", + "description": "Get the status of the latest build for the project", + "type": "object", + "properties": { + "publicUrl": { + "type": "string", + "description": "The URL of an externally hosted manifest (for self-hosted apps)." + } + }, + "required": [] +} diff --git a/packages/expo/src/executors/build-web/build-web.impl.ts b/packages/expo/src/executors/build-web/build-web.impl.ts new file mode 100644 index 0000000000000..f17dcd8abb43e --- /dev/null +++ b/packages/expo/src/executors/build-web/build-web.impl.ts @@ -0,0 +1,73 @@ +import { ExecutorContext } from '@nrwl/devkit'; +import { join } from 'path'; +import { toFileName } from '@nrwl/workspace/src/devkit-reexport'; +import { ChildProcess, fork } from 'child_process'; + +import { ensureNodeModulesSymlink } from '../../utils/ensure-node-modules-symlink'; + +import { ExpoBuildWebOptions } from './schema'; + +export interface ReactNativeBuildOutput { + success: boolean; +} + +let childProcess: ChildProcess; + +export default async function* buildWebExecutor( + options: ExpoBuildWebOptions, + context: ExecutorContext +): AsyncGenerator { + const projectRoot = context.workspace.projects[context.projectName].root; + ensureNodeModulesSymlink(context.root, projectRoot); + + try { + await runCliBuild(context.root, projectRoot, options); + yield { success: true }; + } finally { + if (childProcess) { + childProcess.kill(); + } + } +} + +function runCliBuild( + workspaceRoot: string, + projectRoot: string, + options: ExpoBuildWebOptions +) { + return new Promise((resolve, reject) => { + childProcess = fork( + join(workspaceRoot, './node_modules/expo/bin/cli.js'), + ['build:web', ...createRunOptions(options)], + { cwd: join(workspaceRoot, projectRoot) } + ); + + // Ensure the child process is killed when the parent exits + process.on('exit', () => childProcess.kill()); + process.on('SIGTERM', () => childProcess.kill()); + + childProcess.on('error', (err) => { + reject(err); + }); + childProcess.on('exit', (code) => { + if (code === 0) { + resolve(code); + } else { + reject(code); + } + }); + }); +} + +function createRunOptions(options) { + return Object.keys(options).reduce((acc, k) => { + const v = options[k]; + if (v === true) { + // when true, does not need to pass the value true, just need to pass the flag in kebob case + acc.push(`--${toFileName(k)}`); + } else { + acc.push(`--${toFileName(k)}`, v); + } + return acc; + }, []); +} diff --git a/packages/expo/src/executors/build-web/compat.ts b/packages/expo/src/executors/build-web/compat.ts new file mode 100644 index 0000000000000..9596d592125dd --- /dev/null +++ b/packages/expo/src/executors/build-web/compat.ts @@ -0,0 +1,5 @@ +import { convertNxExecutor } from '@nrwl/devkit'; + +import buildWebExecutor from './build-web.impl'; + +export default convertNxExecutor(buildWebExecutor); diff --git a/packages/expo/src/executors/build-web/schema.d.ts b/packages/expo/src/executors/build-web/schema.d.ts new file mode 100644 index 0000000000000..e7e55d18e4f89 --- /dev/null +++ b/packages/expo/src/executors/build-web/schema.d.ts @@ -0,0 +1,6 @@ +// options from https://docs.expo.dev/workflow/expo-cli/#expo-buildweb +export interface ExpoBuildWebOptions { + clear?: boolean; + noPwa?: boolean; + dev?: boolean; +} diff --git a/packages/expo/src/executors/build-web/schema.json b/packages/expo/src/executors/build-web/schema.json new file mode 100644 index 0000000000000..062a0a76b147d --- /dev/null +++ b/packages/expo/src/executors/build-web/schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "NxExpoBuildWeb", + "cli": "nx", + "title": "Expo web Build executor", + "description": "Build the web app for production", + "type": "object", + "properties": { + "clear": { + "type": "boolean", + "description": "Clear all cached build files and assets.", + "alias": "c" + }, + "noPwa": { + "type": "boolean", + "description": "Prevent webpack from generating the manifest.json and injecting meta into the index.html head." + }, + "dev": { + "type": "boolean", + "description": "Turns dev flag on before bundling" + } + }, + "required": [] +} diff --git a/packages/expo/src/executors/ensure-symlink/compat.ts b/packages/expo/src/executors/ensure-symlink/compat.ts new file mode 100644 index 0000000000000..94c2df8239aa6 --- /dev/null +++ b/packages/expo/src/executors/ensure-symlink/compat.ts @@ -0,0 +1,5 @@ +import { convertNxExecutor } from '@nrwl/devkit'; + +import ensureSymlinkExecutor from './ensure-symlink.impl'; + +export default convertNxExecutor(ensureSymlinkExecutor); diff --git a/packages/expo/src/executors/ensure-symlink/ensure-symlink.impl.ts b/packages/expo/src/executors/ensure-symlink/ensure-symlink.impl.ts new file mode 100644 index 0000000000000..f56f348664e92 --- /dev/null +++ b/packages/expo/src/executors/ensure-symlink/ensure-symlink.impl.ts @@ -0,0 +1,17 @@ +import { ExecutorContext } from '@nrwl/devkit'; +import { ensureNodeModulesSymlink } from '../../utils/ensure-node-modules-symlink'; + +export interface ExpoEnsureSymlinkOutput { + success: boolean; +} + +export default async function* ensureSymlinkExecutor( + _, + context: ExecutorContext +): AsyncGenerator { + const projectRoot = context.workspace.projects[context.projectName].root; + + ensureNodeModulesSymlink(context.root, projectRoot); + + yield { success: true }; +} diff --git a/packages/expo/src/executors/ensure-symlink/schema.json b/packages/expo/src/executors/ensure-symlink/schema.json new file mode 100644 index 0000000000000..b50d9a40f7732 --- /dev/null +++ b/packages/expo/src/executors/ensure-symlink/schema.json @@ -0,0 +1,9 @@ +{ + "cli": "nx", + "$id": "NxExpoEnsureSymlink", + "$schema": "http://json-schema.org/schema", + "title": "Ensure Symlink for React Native", + "description": "Ensure workspace node_modules is symlink under app's node_modules folder.", + "type": "object", + "properties": {} +} diff --git a/packages/expo/src/executors/run/compat.ts b/packages/expo/src/executors/run/compat.ts new file mode 100644 index 0000000000000..54c6c1a9704f8 --- /dev/null +++ b/packages/expo/src/executors/run/compat.ts @@ -0,0 +1,5 @@ +import { convertNxExecutor } from '@nrwl/devkit'; + +import runExecutor from './run.impl'; + +export default convertNxExecutor(runExecutor); diff --git a/packages/expo/src/executors/run/run.impl.ts b/packages/expo/src/executors/run/run.impl.ts new file mode 100644 index 0000000000000..74f0463fa1567 --- /dev/null +++ b/packages/expo/src/executors/run/run.impl.ts @@ -0,0 +1,108 @@ +import { ExecutorContext } from '@nrwl/devkit'; +import { join } from 'path'; +import { ChildProcess, fork } from 'child_process'; +import { platform } from 'os'; +import { toFileName } from '@nrwl/workspace/src/devkit-reexport'; + +import { ensureNodeModulesSymlink } from '../../utils/ensure-node-modules-symlink'; +import { + displayNewlyAddedDepsMessage, + syncDeps, +} from '../sync-deps/sync-deps.impl'; +import { ExpoRunOptions } from './schema'; + +export interface ExpoRunOutput { + success: boolean; +} + +let childProcess: ChildProcess; + +export default async function* runExecutor( + options: ExpoRunOptions, + context: ExecutorContext +): AsyncGenerator { + if (platform() !== 'darwin' && options.platform === 'ios') { + throw new Error(`The run-ios build requires Mac to run`); + } + const projectRoot = context.workspace.projects[context.projectName].root; + ensureNodeModulesSymlink(context.root, projectRoot); + if (options.sync) { + displayNewlyAddedDepsMessage( + context.projectName, + await syncDeps(context.projectName, projectRoot) + ); + } + + try { + await runCliRun(context.root, projectRoot, options); + + yield { success: true }; + } finally { + if (childProcess) { + childProcess.kill(); + } + } +} + +function runCliRun( + workspaceRoot: string, + projectRoot: string, + options: ExpoRunOptions +) { + return new Promise((resolve, reject) => { + childProcess = fork( + join(workspaceRoot, './node_modules/expo/bin/cli.js'), + ['run:' + options.platform, ...createRunOptions(options)], + { + cwd: projectRoot, + } + ); + + // Ensure the child process is killed when the parent exits + process.on('exit', () => childProcess.kill()); + process.on('SIGTERM', () => childProcess.kill()); + + childProcess.on('error', (err) => { + reject(err); + }); + childProcess.on('exit', (code) => { + if (code === 0) { + resolve(code); + } else { + reject(code); + } + }); + }); +} + +const nxOptions = ['sync', 'platform']; +const iOSOptions = ['xcodeConfiguration', 'schema']; +const androidOptions = ['variant']; + +function createRunOptions(options: ExpoRunOptions) { + return Object.keys(options).reduce((acc, k) => { + if ( + nxOptions.includes(k) || + (options.platform === 'ios' && androidOptions.includes(k)) || + (options.platform === 'android' && iOSOptions.includes(k)) + ) { + return acc; + } + const v = options[k]; + { + if (k === 'xcodeConfiguration') { + acc.push('--configuration', v); + } else if (k === 'bundler') { + if (v === false) { + acc.push('--no-bundler'); + } + } else if (v === true) { + // when true, does not need to pass the value true, just need to pass the flag in kebob case + acc.push(`--${toFileName(k)}`); + } else { + acc.push(`--${toFileName(k)}`, v); + } + } + return acc; + }, []); +} diff --git a/packages/expo/src/executors/run/schema.d.ts b/packages/expo/src/executors/run/schema.d.ts new file mode 100644 index 0000000000000..b67a428ad9b52 --- /dev/null +++ b/packages/expo/src/executors/run/schema.d.ts @@ -0,0 +1,11 @@ +// options from https://docs.expo.dev/workflow/expo-cli/#expo-runios and https://docs.expo.dev/workflow/expo-cli/#expo-runandroid +export interface ExpoRunOptions { + platform: 'ios' | 'android'; + xcodeConfiguration: string; // iOS only, default is Debug + scheme?: string; // iOS only + variant: string; // android only, default is debug + port: number; // default is 8081 + bundler: boolean; // default is true + sync: boolean; // default is true + device?: string; +} diff --git a/packages/expo/src/executors/run/schema.json b/packages/expo/src/executors/run/schema.json new file mode 100644 index 0000000000000..1e53e7676b159 --- /dev/null +++ b/packages/expo/src/executors/run/schema.json @@ -0,0 +1,51 @@ +{ + "cli": "nx", + "$id": "NxExpoRun", + "$schema": "http://json-schema.org/schema", + "title": "Run iOS or Android application", + "description": "Run Expo target options", + "type": "object", + "properties": { + "platform": { + "description": "Platform to run for (ios, android).", + "enum": ["ios", "android"], + "default": "ios" + }, + "xcodeConfiguration": { + "type": "string", + "description": "(iOS) Xcode configuration to use. Debug or Release", + "default": "Debug" + }, + "scheme": { + "type": "string", + "description": "(iOS) Explicitly set the Xcode scheme to use" + }, + "variant": { + "type": "string", + "description": "(Android) Specify your app's build variant (e.g. debug, release).", + "default": "debug" + }, + "device": { + "type": "string", + "description": "Device name or UDID to build the app on. The value is not required if you have a single device connected.", + "alias": "d" + }, + "sync": { + "type": "boolean", + "description": "Syncs npm dependencies to package.json (for React Native autolink). Always true when --install is used.", + "default": true + }, + "port": { + "type": "number", + "description": "Port to start the Metro bundler on", + "default": 8081, + "alias": "p" + }, + "bundler": { + "type": "boolean", + "description": "Whether to skip starting the Metro bundler. True to start it, false to skip it.", + "default": true + } + }, + "required": ["platform"] +} diff --git a/packages/expo/src/executors/start/compat.ts b/packages/expo/src/executors/start/compat.ts new file mode 100644 index 0000000000000..dfc3aa7e71e14 --- /dev/null +++ b/packages/expo/src/executors/start/compat.ts @@ -0,0 +1,5 @@ +import { convertNxExecutor } from '@nrwl/devkit'; + +import startExecutor from './start.impl'; + +export default convertNxExecutor(startExecutor); diff --git a/packages/expo/src/executors/start/schema.d.ts b/packages/expo/src/executors/start/schema.d.ts new file mode 100644 index 0000000000000..b6f2d6233544e --- /dev/null +++ b/packages/expo/src/executors/start/schema.d.ts @@ -0,0 +1,22 @@ +// options from https://docs.expo.dev/workflow/expo-cli/ + +export interface ExpoStartOptions { + port: number; + dev?: boolean; + devClient?: boolean; + minify?: boolean; + https?: boolean; + clear?: boolean; + maxWorkers?: number; + scheme?: string; + sendTo?: string; + ios?: boolean; + android?: boolean; + web?: boolean; + host?: string; + lan?: boolean; + localhost?: boolean; + tunnel?: boolean; + offline?: boolean; + webpack?: boolean; +} diff --git a/packages/expo/src/executors/start/schema.json b/packages/expo/src/executors/start/schema.json new file mode 100644 index 0000000000000..a013b65bb512a --- /dev/null +++ b/packages/expo/src/executors/start/schema.json @@ -0,0 +1,85 @@ +{ + "cli": "nx", + "$id": "NxExpoStart", + "$schema": "http://json-schema.org/schema", + "title": "Packager Server 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" + }, + "devClient": { + "type": "boolean", + "description": "Experimental: Starts the bundler for use with the expo-development-client" + }, + "minify": { + "type": "boolean", + "description": "Whether or not to minify code" + }, + "https": { + "type": "boolean", + "description": "To start webpack with https or http protocol" + }, + "scheme": { + "type": "string", + "description": "Custom URI protocol to use with a development build" + }, + "sentTo": { + "type": "string", + "description": "An email address to send a link to", + "alias": "s" + }, + "android": { + "type": "boolean", + "description": "Opens your app in Expo Go on a connected Android device", + "alias": "a" + }, + "ios": { + "type": "boolean", + "description": "Opens your app in Expo Go in a currently running iOS simulator on your computer", + "alias": "i" + }, + "host": { + "type": "string", + "description": "lan (default), tunnel, localhost. Type of host to use. \"tunnel\" allows you to view your link on other networks", + "alias": "m" + }, + "tunnel": { + "type": "boolean", + "description": "Same as --host tunnel" + }, + "lan": { + "type": "boolean", + "description": "Same as --host lan" + }, + "localhost": { + "type": "boolean", + "description": "Same as --host localhost" + }, + "offline": { + "type": "boolean", + "description": "Allows this command to run while offline" + }, + "webpack": { + "type": "boolean", + "description": "Start a Webpack dev server for the web app." + } + } +} diff --git a/packages/expo/src/executors/start/start.impl.ts b/packages/expo/src/executors/start/start.impl.ts new file mode 100644 index 0000000000000..5444455c67943 --- /dev/null +++ b/packages/expo/src/executors/start/start.impl.ts @@ -0,0 +1,90 @@ +import * as chalk from 'chalk'; +import { ExecutorContext, logger } from '@nrwl/devkit'; +import { toFileName } from '@nrwl/workspace/src/devkit-reexport'; +import { ChildProcess, fork } from 'child_process'; +import { join } from 'path'; + +import { ensureNodeModulesSymlink } from '../../utils/ensure-node-modules-symlink'; +import { ExpoStartOptions } from './schema'; + +export interface ExpoStartOutput { + baseUrl?: string; + success: boolean; +} + +let childProcess: ChildProcess; + +export default async function* startExecutor( + options: ExpoStartOptions, + context: ExecutorContext +): AsyncGenerator { + const projectRoot = context.workspace.projects[context.projectName].root; + ensureNodeModulesSymlink(context.root, projectRoot); + + try { + const baseUrl = `http://localhost:${options.port}`; + logger.info(chalk.cyan(`Packager is ready at ${baseUrl}`)); + + await startAsync(context.root, projectRoot, options); + + yield { + baseUrl, + success: true, + }; + } finally { + if (childProcess) { + childProcess.kill(); + } + } +} + +function startAsync( + workspaceRoot: string, + projectRoot: string, + options: ExpoStartOptions +): Promise { + return new Promise((resolve, reject) => { + childProcess = fork( + join(workspaceRoot, './node_modules/expo/bin/cli.js'), + [options.webpack ? 'web' : 'start', ...createStartOptions(options)], + { cwd: join(workspaceRoot, projectRoot) } + ); + + // Ensure the child process is killed when the parent exits + process.on('exit', () => childProcess.kill()); + process.on('SIGTERM', () => childProcess.kill()); + + childProcess.on('error', (err) => { + reject(err); + }); + childProcess.on('exit', (code) => { + if (code === 0) { + resolve(code); + } else { + reject(code); + } + }); + }); +} + +const nxOptions = ['webpack']; +function createStartOptions(options: ExpoStartOptions) { + return Object.keys(options).reduce((acc, k) => { + const v = options[k]; + if (k === 'dev' && v === false) { + acc.push(`--no-dev`); + } else if (k === 'minify' && v === false) { + acc.push(`--no-minify`); + } else if (k === 'https' && v === false) { + acc.push(`--no-https`); + } else if (!nxOptions.includes(k)) { + if (v === true) { + // when true, does not need to pass the value true, just need to pass the flag in kebob case + acc.push(`--${toFileName(k)}`); + } else { + acc.push(`--${toFileName(k)}`, v); + } + } + return acc; + }, []); +} diff --git a/packages/expo/src/executors/sync-deps/compat.ts b/packages/expo/src/executors/sync-deps/compat.ts new file mode 100644 index 0000000000000..91634e4ee6cba --- /dev/null +++ b/packages/expo/src/executors/sync-deps/compat.ts @@ -0,0 +1,5 @@ +import { convertNxExecutor } from '@nrwl/devkit'; + +import syncDepsExecutor from './sync-deps.impl'; + +export default convertNxExecutor(syncDepsExecutor); diff --git a/packages/expo/src/executors/sync-deps/schema.d.ts b/packages/expo/src/executors/sync-deps/schema.d.ts new file mode 100644 index 0000000000000..e8043201c433d --- /dev/null +++ b/packages/expo/src/executors/sync-deps/schema.d.ts @@ -0,0 +1,3 @@ +export interface ExpoSyncDepsOptions { + include: string; +} diff --git a/packages/expo/src/executors/sync-deps/schema.json b/packages/expo/src/executors/sync-deps/schema.json new file mode 100644 index 0000000000000..1ff20dd647b04 --- /dev/null +++ b/packages/expo/src/executors/sync-deps/schema.json @@ -0,0 +1,14 @@ +{ + "cli": "nx", + "$id": "NxExpoSyncDeps", + "$schema": "http://json-schema.org/schema", + "title": "Sync Deps for React Native", + "description": "Updates package.json with project dependencies", + "type": "object", + "properties": { + "include": { + "type": "string", + "description": "A comma-separated list of additional npm packages to include. e.g. 'nx sync-deps --include=react-native-gesture-handler,react-native-safe-area-context'" + } + } +} diff --git a/packages/expo/src/executors/sync-deps/sync-deps.impl.ts b/packages/expo/src/executors/sync-deps/sync-deps.impl.ts new file mode 100644 index 0000000000000..5818c036813ab --- /dev/null +++ b/packages/expo/src/executors/sync-deps/sync-deps.impl.ts @@ -0,0 +1,84 @@ +import { join } from 'path'; +import { + readJsonFile, + writeJsonFile, +} from '@nrwl/workspace/src/utilities/fileutils'; +import * as chalk from 'chalk'; +import { ExecutorContext, logger } from '@nrwl/devkit'; +import { createProjectGraphAsync } from '@nrwl/workspace/src/core/project-graph'; + +import { findAllNpmDependencies } from '../../utils/find-all-npm-dependencies'; + +import { ExpoSyncDepsOptions } from './schema'; + +export interface ExpoSyncDepsOutput { + success: boolean; +} + +export default async function* syncDepsExecutor( + options: ExpoSyncDepsOptions, + context: ExecutorContext +): AsyncGenerator { + const projectRoot = context.workspace.projects[context.projectName].root; + displayNewlyAddedDepsMessage( + context.projectName, + await syncDeps(context.projectName, projectRoot, options.include) + ); + + yield { success: true }; +} + +export async function syncDeps( + projectName: string, + projectRoot: string, + include?: string +): Promise { + const graph = await createProjectGraphAsync(); + const npmDeps = findAllNpmDependencies(graph, projectName); + const packageJsonPath = join(projectRoot, 'package.json'); + const packageJson = readJsonFile(packageJsonPath); + const newDeps = []; + const includeDeps = include?.split(','); + let updated = false; + + if (!packageJson.dependencies) { + packageJson.dependencies = {}; + updated = true; + } + + if (includeDeps) { + npmDeps.push(...includeDeps); + } + + npmDeps.forEach((dep) => { + if (!packageJson.dependencies[dep]) { + packageJson.dependencies[dep] = '*'; + newDeps.push(dep); + updated = true; + } + }); + + if (updated) { + writeJsonFile(packageJsonPath, packageJson); + } + + return newDeps; +} + +export function displayNewlyAddedDepsMessage( + projectName: string, + deps: string[] +) { + if (deps.length > 0) { + logger.info(`${chalk.bold.cyan( + 'info' + )} Added entries to 'package.json' for '${projectName}' (for autolink): + ${deps.map((d) => chalk.bold.cyan(`"${d}": "*"`)).join('\n ')}`); + } else { + logger.info( + `${chalk.bold.cyan( + 'info' + )} Dependencies for '${projectName}' are up to date! No changes made.` + ); + } +} diff --git a/packages/expo/src/generators/application/application.spec.ts b/packages/expo/src/generators/application/application.spec.ts new file mode 100644 index 0000000000000..13286dfa639ad --- /dev/null +++ b/packages/expo/src/generators/application/application.spec.ts @@ -0,0 +1,93 @@ +import { + Tree, + readWorkspaceConfiguration, + getProjects, + readJson, +} from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { Linter } from '@nrwl/linter'; +import { expoApplicationGenerator } from './application'; + +describe('app', () => { + let appTree: Tree; + + beforeEach(() => { + appTree = createTreeWithEmptyWorkspace(); + appTree.write('.gitignore', ''); + }); + + it('should update workspace.json', async () => { + await expoApplicationGenerator(appTree, { + name: 'myApp', + displayName: 'myApp', + linter: Linter.EsLint, + e2eTestRunner: 'none', + skipFormat: false, + js: false, + unitTestRunner: 'none', + }); + const workspaceJson = readWorkspaceConfiguration(appTree); + const projects = getProjects(appTree); + + expect(projects.get('my-app').root).toEqual('apps/my-app'); + expect(workspaceJson.defaultProject).toEqual('my-app'); + }); + + it('should update nx.json', async () => { + await expoApplicationGenerator(appTree, { + name: 'myApp', + displayName: 'myApp', + tags: 'one,two', + linter: Linter.EsLint, + e2eTestRunner: 'none', + skipFormat: false, + js: false, + unitTestRunner: 'none', + }); + + const { projects } = readJson(appTree, '/workspace.json'); + expect(projects).toMatchObject({ + 'my-app': { + tags: ['one', 'two'], + }, + }); + }); + + it('should generate files', async () => { + await expoApplicationGenerator(appTree, { + name: 'myApp', + displayName: 'myApp', + linter: Linter.EsLint, + e2eTestRunner: 'none', + skipFormat: false, + js: false, + unitTestRunner: 'jest', + }); + expect(appTree.exists('apps/my-app/src/app/App.tsx')).toBeTruthy(); + expect(appTree.exists('apps/my-app/src/app/App.spec.tsx')).toBeTruthy(); + + const tsconfig = readJson(appTree, 'apps/my-app/tsconfig.json'); + expect(tsconfig.extends).toEqual('../../tsconfig.base.json'); + + expect(appTree.exists('apps/my-app/.eslintrc.json')).toBe(true); + }); + + it('should generate js files', async () => { + await expoApplicationGenerator(appTree, { + name: 'myApp', + displayName: 'myApp', + linter: Linter.EsLint, + e2eTestRunner: 'none', + skipFormat: false, + js: true, + unitTestRunner: 'jest', + }); + expect(appTree.exists('apps/my-app/src/app/App.js')).toBeTruthy(); + expect(appTree.exists('apps/my-app/src/app/App.spec.js')).toBeTruthy(); + + const tsconfig = readJson(appTree, 'apps/my-app/tsconfig.json'); + expect(tsconfig.extends).toEqual('../../tsconfig.base.json'); + + expect(appTree.exists('apps/my-app/.eslintrc.json')).toBe(true); + }); +}); diff --git a/packages/expo/src/generators/application/application.ts b/packages/expo/src/generators/application/application.ts new file mode 100644 index 0000000000000..046bb45cc4071 --- /dev/null +++ b/packages/expo/src/generators/application/application.ts @@ -0,0 +1,59 @@ +import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial'; +import { + convertNxGenerator, + Tree, + formatFiles, + joinPathFragments, + GeneratorCallback, +} from '@nrwl/devkit'; + +import { addLinting } from '../../utils/add-linting'; +import { addJest } from '../../utils/add-jest'; +import { runSymlink } from '../../utils/symlink-task'; + +import { normalizeOptions } from './lib/normalize-options'; +import initGenerator from '../init/init'; +import { addProject } from './lib/add-project'; +import { addDetox } from './lib/add-detox'; +import { createApplicationFiles } from './lib/create-application-files'; +import { Schema } from './schema'; + +export async function expoApplicationGenerator( + host: Tree, + schema: Schema +): Promise { + const options = normalizeOptions(schema); + + createApplicationFiles(host, options); + addProject(host, options); + + const initTask = await initGenerator(host, { ...options, skipFormat: true }); + const lintTask = await addLinting( + host, + options.projectName, + options.appProjectRoot, + [joinPathFragments(options.appProjectRoot, 'tsconfig.app.json')], + options.linter, + options.setParserOptionsProject + ); + const jestTask = await addJest( + host, + options.unitTestRunner, + options.projectName, + options.appProjectRoot, + options.js + ); + const detoxTask = await addDetox(host, options); + const symlinkTask = runSymlink(host.root, options.appProjectRoot); + + if (!options.skipFormat) { + await formatFiles(host); + } + + return runTasksInSerial(initTask, lintTask, jestTask, detoxTask, symlinkTask); +} + +export default expoApplicationGenerator; +export const expoApplicationSchematic = convertNxGenerator( + expoApplicationGenerator +); diff --git a/packages/expo/src/generators/application/files/.babelrc.template b/packages/expo/src/generators/application/files/.babelrc.template new file mode 100644 index 0000000000000..7d30f8bf0669a --- /dev/null +++ b/packages/expo/src/generators/application/files/.babelrc.template @@ -0,0 +1,3 @@ +{ + "presets": ["babel-preset-expo"] +} diff --git a/packages/expo/src/generators/application/files/app.json.template b/packages/expo/src/generators/application/files/app.json.template new file mode 100644 index 0000000000000..7900abb4afe2c --- /dev/null +++ b/packages/expo/src/generators/application/files/app.json.template @@ -0,0 +1,32 @@ +{ + "expo": { + "name": "<%= projectName %>", + "slug": "<%= projectName %>", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/icon.png", + "splash": { + "image": "./assets/splash.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "updates": { + "fallbackToCacheTimeout": 0 + }, + "assetBundlePatterns": [ + "**/*" + ], + "ios": { + "supportsTablet": true + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/adaptive-icon.png", + "backgroundColor": "#FFFFFF" + } + }, + "web": { + "favicon": "./assets/favicon.png" + } + } +} diff --git a/packages/expo/src/generators/application/files/assets/adaptive-icon.png b/packages/expo/src/generators/application/files/assets/adaptive-icon.png new file mode 100644 index 0000000000000..03d6f6b6c6727 Binary files /dev/null and b/packages/expo/src/generators/application/files/assets/adaptive-icon.png differ diff --git a/packages/expo/src/generators/application/files/assets/favicon.png b/packages/expo/src/generators/application/files/assets/favicon.png new file mode 100644 index 0000000000000..e75f697b18018 Binary files /dev/null and b/packages/expo/src/generators/application/files/assets/favicon.png differ diff --git a/packages/expo/src/generators/application/files/assets/icon.png b/packages/expo/src/generators/application/files/assets/icon.png new file mode 100644 index 0000000000000..a0b1526fc7b78 Binary files /dev/null and b/packages/expo/src/generators/application/files/assets/icon.png differ diff --git a/packages/expo/src/generators/application/files/assets/logo.png b/packages/expo/src/generators/application/files/assets/logo.png new file mode 100644 index 0000000000000..e9b9b6eb620ff Binary files /dev/null and b/packages/expo/src/generators/application/files/assets/logo.png differ diff --git a/packages/expo/src/generators/application/files/assets/splash.png b/packages/expo/src/generators/application/files/assets/splash.png new file mode 100644 index 0000000000000..0e89705a94367 Binary files /dev/null and b/packages/expo/src/generators/application/files/assets/splash.png differ diff --git a/packages/expo/src/generators/application/files/assets/star.svg b/packages/expo/src/generators/application/files/assets/star.svg new file mode 100644 index 0000000000000..901053d385490 --- /dev/null +++ b/packages/expo/src/generators/application/files/assets/star.svg @@ -0,0 +1,11 @@ + + + + + diff --git a/packages/expo/src/generators/application/files/eas.json b/packages/expo/src/generators/application/files/eas.json new file mode 100644 index 0000000000000..c351cb58edb4c --- /dev/null +++ b/packages/expo/src/generators/application/files/eas.json @@ -0,0 +1,18 @@ +{ + "build": { + "release": { + "android": { + "buildType": "app-bundle" + } + }, + "development": { + "android": { + "developmentClient": true, + "distribution": "internal" + } + } + }, + "cli": { + "version": ">= 0.38.1" + } +} diff --git a/packages/expo/src/generators/application/files/index.js.template b/packages/expo/src/generators/application/files/index.js.template new file mode 100644 index 0000000000000..887550c4ee416 --- /dev/null +++ b/packages/expo/src/generators/application/files/index.js.template @@ -0,0 +1,9 @@ +import 'react-native-gesture-handler'; +import { registerRootComponent } from 'expo'; + +import App from './src/app/App'; + +// registerRootComponent calls AppRegistry.registerComponent('main', () => App); +// It also ensures that whether you load the app in Expo Go or in a native build, +// the environment is set up appropriately +registerRootComponent(App); diff --git a/packages/expo/src/generators/application/files/metro.config.js.template b/packages/expo/src/generators/application/files/metro.config.js.template new file mode 100644 index 0000000000000..8101ea9873f6d --- /dev/null +++ b/packages/expo/src/generators/application/files/metro.config.js.template @@ -0,0 +1,14 @@ +const { withNxMetro } = require('@nrwl/expo'); +const { getDefaultConfig } = require('@expo/metro-config'); + +const defaultConfig = getDefaultConfig(__dirname); +module.exports = withNxMetro( + defaultConfig, + { + // 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' + extensions: [], + } +); diff --git a/packages/expo/src/generators/application/files/package.json.template b/packages/expo/src/generators/application/files/package.json.template new file mode 100644 index 0000000000000..7f18f4fabc15e --- /dev/null +++ b/packages/expo/src/generators/application/files/package.json.template @@ -0,0 +1,28 @@ +{ + "name": "<%= projectName %>", + "version": "0.0.1", + "private": true, + "dependencies": { + "@testing-library/jest-native": "*", + "@testing-library/react-native": "*", + "@nrwl/expo": "*", + "react": "*", + "react-native": "*", + "expo": "*", + "expo-status-bar": "*", + "@expo/metro-config": "*", + "expo-splash-screen": "*", + "expo-structured-headers": "*", + "expo-updates": "*", + "react-dom": "*", + "react-native-gesture-handler": "*", + "react-native-reanimated": "*", + "react-native-safe-area-context": "*", + "react-native-screens": "*", + "react-native-web": "*", + "tslib": "*" + }, + "devDependencies": { + "typescript": "*" + } +} diff --git a/packages/expo/src/generators/application/files/src/app/App.spec.tsx.template b/packages/expo/src/generators/application/files/src/app/App.spec.tsx.template new file mode 100644 index 0000000000000..485394bdf3105 --- /dev/null +++ b/packages/expo/src/generators/application/files/src/app/App.spec.tsx.template @@ -0,0 +1,9 @@ +import * as React from 'react'; +import { render } from '@testing-library/react-native'; + +import App from './App'; + +test('renders correctly', () => { + const { getByTestId } = render(); + expect(getByTestId('heading')).toHaveTextContent('Welcome'); +}); diff --git a/packages/expo/src/generators/application/files/src/app/App.tsx.template b/packages/expo/src/generators/application/files/src/app/App.tsx.template new file mode 100644 index 0000000000000..653ce060fedde --- /dev/null +++ b/packages/expo/src/generators/application/files/src/app/App.tsx.template @@ -0,0 +1,131 @@ +import React from 'react'; +import { StatusBar } from 'expo-status-bar'; +import { + SafeAreaView, + StyleSheet, + ScrollView, + Image, + View, + Text, + TouchableOpacity, +} from 'react-native'; + +import { + Colors, + DebugInstructions, + ReloadInstructions, +} from 'react-native/Libraries/NewAppScreen'; +// @ts-ignore +import openURLInBrowser from 'react-native/Libraries/Core/Devtools/openURLInBrowser'; + +const App = () => { + return ( + <> + + + + + + Welcome to <%= displayName %> + + + + Step One + + Edit <%= appProjectRoot %>/App.tsx to change this + screen and then come back to see your edits. + + + + See Your Changes + + Alternatively, press{' '} + R in the bundler + terminal window. + + + + Debug + + + + + + Learn More + openURLInBrowser('https://nx.dev')} + testID="nx-link" + > + + Visit nx.dev for more info + about Nx. + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + scrollView: { + backgroundColor: Colors.lighter, + }, + header: { + backgroundColor: '#143055', + flex: 1, + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 24, + }, + logo: { + width: 200, + height: 180, + resizeMode: 'contain', + }, + heading: { + fontSize: 24, + fontWeight: '600', + color: Colors.lighter, + }, + body: { + backgroundColor: Colors.white, + }, + sectionContainer: { + marginTop: 32, + paddingHorizontal: 24, + }, + sectionTitle: { + fontSize: 24, + fontWeight: '600', + color: Colors.black, + }, + sectionDescription: { + marginTop: 8, + fontSize: 18, + fontWeight: '400', + color: Colors.dark, + }, + highlight: { + fontWeight: '700', + }, + footer: { + color: Colors.dark, + fontSize: 12, + fontWeight: '600', + padding: 4, + paddingRight: 12, + textAlign: 'right', + }, + link: { + color: '#45bc98', + } +}); + +export default App; diff --git a/packages/expo/src/generators/application/files/test-setup.ts.template b/packages/expo/src/generators/application/files/test-setup.ts.template new file mode 100644 index 0000000000000..9f28ad211b736 --- /dev/null +++ b/packages/expo/src/generators/application/files/test-setup.ts.template @@ -0,0 +1 @@ +import '@testing-library/jest-native/extend-expect'; diff --git a/packages/expo/src/generators/application/files/tsconfig.app.json.template b/packages/expo/src/generators/application/files/tsconfig.app.json.template new file mode 100644 index 0000000000000..f3ac07d2b4b9f --- /dev/null +++ b/packages/expo/src/generators/application/files/tsconfig.app.json.template @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "<%= offsetFromRoot %>dist/out-tsc", + "types": ["node"] + }, + "exclude": ["**/*.spec.ts", "**/*.spec.tsx"], + "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"] +} diff --git a/packages/expo/src/generators/application/files/tsconfig.json.template b/packages/expo/src/generators/application/files/tsconfig.json.template new file mode 100644 index 0000000000000..e92f4f5483ba6 --- /dev/null +++ b/packages/expo/src/generators/application/files/tsconfig.json.template @@ -0,0 +1,23 @@ +{ + "extends": "<%= offsetFromRoot %>tsconfig.base.json", + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "jsx": "react-native", + "lib": ["dom", "esnext"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "strict": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + } + ], + "exclude": [ + "node_modules" + ] +} diff --git a/packages/expo/src/generators/application/lib/add-detox.ts b/packages/expo/src/generators/application/lib/add-detox.ts new file mode 100644 index 0000000000000..b1312fde39acc --- /dev/null +++ b/packages/expo/src/generators/application/lib/add-detox.ts @@ -0,0 +1,20 @@ +import { detoxApplicationGenerator } from '@nrwl/detox'; +import { Tree } from '@nrwl/devkit'; +import { NormalizedSchema } from './normalize-options'; +import { Linter } from '@nrwl/linter'; + +export async function addDetox(host: Tree, options: NormalizedSchema) { + if (options?.e2eTestRunner !== 'detox') { + return () => {}; + } + + return detoxApplicationGenerator(host, { + ...options, + linter: Linter.EsLint, + name: `${options.name}-e2e`, + directory: options.directory, + project: options.projectName, + type: 'expo', + setParserOptionsProject: options.setParserOptionsProject, + }); +} diff --git a/packages/expo/src/generators/application/lib/add-project.ts b/packages/expo/src/generators/application/lib/add-project.ts new file mode 100644 index 0000000000000..48232aa01ff35 --- /dev/null +++ b/packages/expo/src/generators/application/lib/add-project.ts @@ -0,0 +1,103 @@ +import { + addProjectConfiguration, + ProjectConfiguration, + readWorkspaceConfiguration, + TargetConfiguration, + Tree, + updateWorkspaceConfiguration, +} from '@nrwl/devkit'; +import { NormalizedSchema } from './normalize-options'; + +export function addProject(host: Tree, options: NormalizedSchema) { + const project: ProjectConfiguration = { + root: options.appProjectRoot, + sourceRoot: `${options.appProjectRoot}/src`, + projectType: 'application', + targets: { ...getTargets(options) }, + tags: options.parsedTags, + }; + + addProjectConfiguration(host, options.projectName, { + ...project, + }); + + const workspace = readWorkspaceConfiguration(host); + + if (!workspace.defaultProject) { + workspace.defaultProject = options.projectName; + + updateWorkspaceConfiguration(host, workspace); + } +} + +function getTargets(options: NormalizedSchema) { + const architect: { [key: string]: TargetConfiguration } = {}; + + architect.start = { + executor: '@nrwl/expo:start', + options: { + port: 8081, + }, + }; + + architect.web = { + executor: '@nrwl/expo:start', + options: { + port: 8081, + webpack: true, + }, + }; + + architect.serve = { + executor: '@nrwl/workspace:run-commands', + options: { + command: `nx start ${options.name}`, + }, + }; + + architect['run-ios'] = { + executor: '@nrwl/expo:run', + options: { + platform: 'ios', + }, + }; + + architect['run-android'] = { + executor: '@nrwl/expo:run', + options: { + platform: 'android', + }, + }; + + architect['build-ios'] = { + executor: '@nrwl/expo:build-ios', + options: {}, + }; + + architect['build-android'] = { + executor: '@nrwl/expo:build-android', + options: {}, + }; + + architect['build-web'] = { + executor: '@nrwl/expo:build-web', + options: {}, + }; + + architect['build-status'] = { + executor: '@nrwl/expo:build-web', + options: {}, + }; + + architect['sync-deps'] = { + executor: '@nrwl/expo:sync-deps', + options: {}, + }; + + architect['ensure-symlink'] = { + executor: '@nrwl/expo:ensure-symlink', + options: {}, + }; + + return architect; +} diff --git a/packages/expo/src/generators/application/lib/create-application-files.ts b/packages/expo/src/generators/application/lib/create-application-files.ts new file mode 100644 index 0000000000000..7fc4b76da6474 --- /dev/null +++ b/packages/expo/src/generators/application/lib/create-application-files.ts @@ -0,0 +1,16 @@ +import { generateFiles, offsetFromRoot, toJS, Tree } from '@nrwl/devkit'; +import { join } from 'path'; +import { NormalizedSchema } from './normalize-options'; + +export function createApplicationFiles(host: Tree, options: NormalizedSchema) { + generateFiles(host, join(__dirname, '../files'), options.appProjectRoot, { + ...options, + offsetFromRoot: offsetFromRoot(options.appProjectRoot), + }); + if (options.unitTestRunner === 'none') { + host.delete(join(options.appProjectRoot, `App.spec.tsx`)); + } + if (options.js) { + toJS(host); + } +} diff --git a/packages/expo/src/generators/application/lib/nomalize-options.spec.ts b/packages/expo/src/generators/application/lib/nomalize-options.spec.ts new file mode 100644 index 0000000000000..994b0fbb400cc --- /dev/null +++ b/packages/expo/src/generators/application/lib/nomalize-options.spec.ts @@ -0,0 +1,138 @@ +import { Linter } from '@nrwl/linter'; +import { Schema } from '../schema'; +import { normalizeOptions } from './normalize-options'; + +describe('Normalize Options', () => { + it('should normalize options with name in kebab case', () => { + const schema: Schema = { + name: 'my-app', + linter: Linter.EsLint, + e2eTestRunner: 'none', + skipFormat: false, + js: true, + unitTestRunner: 'jest', + }; + const options = normalizeOptions(schema); + expect(options).toEqual({ + appProjectRoot: 'apps/my-app', + className: 'MyApp', + displayName: 'MyApp', + lowerCaseName: 'myapp', + name: 'my-app', + parsedTags: [], + projectName: 'my-app', + linter: Linter.EsLint, + e2eTestRunner: 'none', + unitTestRunner: 'jest', + skipFormat: false, + js: true, + }); + }); + + it('should normalize options with name in camel case', () => { + const schema: Schema = { + name: 'myApp', + linter: Linter.EsLint, + e2eTestRunner: 'none', + skipFormat: false, + js: true, + unitTestRunner: 'jest', + }; + const options = normalizeOptions(schema); + expect(options).toEqual({ + appProjectRoot: 'apps/my-app', + className: 'MyApp', + displayName: 'MyApp', + lowerCaseName: 'myapp', + name: 'my-app', + parsedTags: [], + projectName: 'my-app', + linter: Linter.EsLint, + e2eTestRunner: 'none', + skipFormat: false, + js: true, + unitTestRunner: 'jest', + }); + }); + + it('should normalize options with directory', () => { + const schema: Schema = { + name: 'my-app', + directory: 'directory', + linter: Linter.EsLint, + e2eTestRunner: 'none', + skipFormat: false, + js: true, + unitTestRunner: 'jest', + }; + const options = normalizeOptions(schema); + expect(options).toEqual({ + appProjectRoot: 'apps/directory/my-app', + className: 'MyApp', + displayName: 'MyApp', + lowerCaseName: 'myapp', + name: 'my-app', + directory: 'directory', + parsedTags: [], + projectName: 'directory-my-app', + e2eTestRunner: 'none', + unitTestRunner: 'jest', + linter: Linter.EsLint, + skipFormat: false, + js: true, + }); + }); + + it('should normalize options that has directory in its name', () => { + const schema: Schema = { + name: 'directory/my-app', + linter: Linter.EsLint, + e2eTestRunner: 'none', + skipFormat: false, + js: true, + unitTestRunner: 'jest', + }; + const options = normalizeOptions(schema); + expect(options).toEqual({ + appProjectRoot: 'apps/directory/my-app', + className: 'DirectoryMyApp', + displayName: 'DirectoryMyApp', + lowerCaseName: 'directorymyapp', + name: 'directory/my-app', + parsedTags: [], + projectName: 'directory-my-app', + e2eTestRunner: 'none', + unitTestRunner: 'jest', + linter: Linter.EsLint, + skipFormat: false, + js: true, + }); + }); + + it('should normalize options with display name', () => { + const schema: Schema = { + name: 'my-app', + displayName: 'My App', + linter: Linter.EsLint, + e2eTestRunner: 'none', + skipFormat: false, + js: true, + unitTestRunner: 'jest', + }; + const options = normalizeOptions(schema); + expect(options).toEqual({ + appProjectRoot: 'apps/my-app', + className: 'MyApp', + displayName: 'My App', + lowerCaseName: 'myapp', + name: 'my-app', + parsedTags: [], + projectName: 'my-app', + e2eTestRunner: 'none', + unitTestRunner: 'jest', + linter: Linter.EsLint, + skipFormat: false, + js: true, + }); + }); +}); diff --git a/packages/expo/src/generators/application/lib/normalize-options.ts b/packages/expo/src/generators/application/lib/normalize-options.ts new file mode 100644 index 0000000000000..8e64b86308435 --- /dev/null +++ b/packages/expo/src/generators/application/lib/normalize-options.ts @@ -0,0 +1,48 @@ +import { names, Tree } from '@nrwl/devkit'; +import { Schema } from '../schema'; + +export interface NormalizedSchema extends Schema { + className: string; + projectName: string; + appProjectRoot: string; + lowerCaseName: string; + parsedTags: string[]; +} + +export function normalizeOptions(options: Schema): NormalizedSchema { + const { fileName, className } = names(options.name); + + const directoryName = options.directory + ? names(options.directory).fileName + : ''; + const projectDirectory = directoryName + ? `${directoryName}/${fileName}` + : fileName; + + const appProjectName = projectDirectory.replace(new RegExp('/', 'g'), '-'); + + const appProjectRoot = `apps/${projectDirectory}`; + + const parsedTags = options.tags + ? options.tags.split(',').map((s) => s.trim()) + : []; + + /** + * if options.name is "my-app" + * name: "my-app", className: 'MyApp', lowerCaseName: 'myapp', displayName: 'MyApp', projectName: 'my-app', appProjectRoot: 'apps/my-app', androidProjectRoot: 'apps/my-app/android', iosProjectRoot: 'apps/my-app/ios' + * if options.name is "myApp" + * name: "my-app", className: 'MyApp', lowerCaseName: 'myapp', displayName: 'MyApp', projectName: 'my-app', appProjectRoot: 'apps/my-app', androidProjectRoot: 'apps/my-app/android', iosProjectRoot: 'apps/my-app/ios' + */ + return { + ...options, + unitTestRunner: options.unitTestRunner || 'jest', + e2eTestRunner: options.e2eTestRunner || 'detox', + name: fileName, + className, + lowerCaseName: className.toLowerCase(), + displayName: options.displayName || className, + projectName: appProjectName, + appProjectRoot, + parsedTags, + }; +} diff --git a/packages/expo/src/generators/application/schema.d.ts b/packages/expo/src/generators/application/schema.d.ts new file mode 100644 index 0000000000000..0d64acd0dd0df --- /dev/null +++ b/packages/expo/src/generators/application/schema.d.ts @@ -0,0 +1,17 @@ +import { Linter } from '@nrwl/linter'; + +export interface Schema { + name: string; + displayName?: string; + style?: string; + skipFormat: boolean; // default is false + directory?: string; + tags?: string; + unitTestRunner: 'jest' | 'none'; // default is jest + pascalCaseFiles?: boolean; + classComponent?: boolean; + js: boolean; // default is false + linter: Linter; // default is eslint + setParserOptionsProject?: boolean; // default is false + e2eTestRunner: 'detox' | 'none'; // default is detox +} diff --git a/packages/expo/src/generators/application/schema.json b/packages/expo/src/generators/application/schema.json new file mode 100644 index 0000000000000..45df8be24b16d --- /dev/null +++ b/packages/expo/src/generators/application/schema.json @@ -0,0 +1,76 @@ +{ + "cli": "nx", + "$id": "NxExpoApplication", + "$schema": "http://json-schema.org/schema", + "title": "Create an Expo Application for Nx", + "examples": [ + { + "command": "g app myapp --directory=nested", + "description": "Generate apps/nested/myapp" + }, + { + "command": "g app myapp --classComponent", + "description": "Use class components instead of functional components" + } + ], + "type": "object", + "properties": { + "name": { + "description": "The name of the application.", + "type": "string", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What name would you like to use for the application?" + }, + "displayName": { + "description": "The display name to show in the application. Defaults to name.", + "type": "string" + }, + "directory": { + "description": "The directory of the new application.", + "type": "string", + "alias": "d" + }, + "skipFormat": { + "description": "Skip formatting files", + "type": "boolean", + "default": false + }, + "linter": { + "description": "The tool to use for running lint checks.", + "type": "string", + "enum": ["eslint", "tslint"], + "default": "eslint" + }, + "unitTestRunner": { + "type": "string", + "enum": ["jest", "none"], + "description": "Test runner to use for unit tests", + "default": "jest" + }, + "tags": { + "type": "string", + "description": "Add tags to the application (used for linting)", + "alias": "t" + }, + "js": { + "type": "boolean", + "description": "Generate JavaScript files rather than TypeScript files", + "default": false + }, + "setParserOptionsProject": { + "type": "boolean", + "description": "Whether or not to configure the ESLint \"parserOptions.project\" option. We do not do this by default for lint performance reasons.", + "default": false + }, + "e2eTestRunner": { + "description": "Adds the specified e2e test runner", + "type": "string", + "enum": ["detox", "none"], + "default": "detox" + } + }, + "required": ["name"] +} diff --git a/packages/expo/src/generators/component/component.spec.ts b/packages/expo/src/generators/component/component.spec.ts new file mode 100644 index 0000000000000..0341f659a42ad --- /dev/null +++ b/packages/expo/src/generators/component/component.spec.ts @@ -0,0 +1,159 @@ +import { logger, Tree } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { Linter } from '@nrwl/linter'; +import expoApplicationGenerator from '../application/application'; +import expoLibraryGenerator from '../library/library'; +import { expoComponentGenerator } from './component'; +import { Schema } from './schema'; + +describe('component', () => { + let appTree: Tree; + let projectName: string; + + let defaultSchema: Schema; + + beforeEach(async () => { + projectName = 'my-lib'; + appTree = createTreeWithEmptyWorkspace(); + appTree.write('.gitignore', ''); + defaultSchema = { + name: 'hello', + project: projectName, + skipTests: false, + export: false, + pascalCaseFiles: false, + classComponent: false, + js: false, + flat: false, + skipFormat: true, + }; + + expoApplicationGenerator(appTree, { + name: 'my-app', + linter: Linter.EsLint, + e2eTestRunner: 'none', + skipFormat: false, + js: true, + unitTestRunner: 'jest', + }); + expoLibraryGenerator(appTree, { + name: projectName, + linter: Linter.EsLint, + skipFormat: false, + skipTsConfig: false, + unitTestRunner: 'jest', + strict: true, + js: false, + }); + jest.spyOn(logger, 'warn').mockImplementation(() => {}); + jest.spyOn(logger, 'debug').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should generate files', async () => { + await expoComponentGenerator(appTree, defaultSchema); + + expect(appTree.exists('libs/my-lib/src/lib/hello/hello.tsx')).toBeTruthy(); + expect( + appTree.exists('libs/my-lib/src/lib/hello/hello.spec.tsx') + ).toBeTruthy(); + }); + + it('should generate files for an app', async () => { + await expoComponentGenerator(appTree, { + ...defaultSchema, + project: 'my-app', + }); + + expect(appTree.exists('apps/my-app/src/app/hello/hello.tsx')).toBeTruthy(); + expect( + appTree.exists('apps/my-app/src/app/hello/hello.spec.tsx') + ).toBeTruthy(); + }); + + describe('--export', () => { + it('should add to index.ts barrel', async () => { + await expoComponentGenerator(appTree, { + ...defaultSchema, + export: true, + }); + + const indexContent = appTree.read('libs/my-lib/src/index.ts', 'utf-8'); + + expect(indexContent).toMatch(/lib\/hello/); + }); + + it('should not export from an app', async () => { + await expoComponentGenerator(appTree, { + ...defaultSchema, + project: 'my-app', + export: true, + }); + + const indexContent = appTree.read('libs/my-lib/src/index.ts', 'utf-8'); + + expect(indexContent).not.toMatch(/lib\/hello/); + }); + }); + + describe('--pascalCaseFiles', () => { + it('should generate component files with upper case names', async () => { + await expoComponentGenerator(appTree, { + ...defaultSchema, + pascalCaseFiles: true, + }); + expect( + appTree.exists('libs/my-lib/src/lib/hello/Hello.tsx') + ).toBeTruthy(); + expect( + appTree.exists('libs/my-lib/src/lib/hello/Hello.spec.tsx') + ).toBeTruthy(); + }); + }); + + describe('--directory', () => { + it('should create component under the directory', async () => { + await expoComponentGenerator(appTree, { + ...defaultSchema, + directory: 'components', + }); + + expect(appTree.exists('/libs/my-lib/src/components/hello/hello.tsx')); + }); + + it('should create with nested directories', async () => { + await expoComponentGenerator(appTree, { + ...defaultSchema, + name: 'helloWorld', + directory: 'lib/foo', + }); + + expect( + appTree.exists('/libs/my-lib/src/lib/foo/hello-world/hello-world.tsx') + ); + }); + }); + + describe('--flat', () => { + it('should create in project directory rather than in its own folder', async () => { + await expoComponentGenerator(appTree, { + ...defaultSchema, + flat: true, + }); + + expect(appTree.exists('/libs/my-lib/src/lib/hello.tsx')); + }); + it('should work with custom directory path', async () => { + await expoComponentGenerator(appTree, { + ...defaultSchema, + flat: true, + directory: 'components', + }); + + expect(appTree.exists('/libs/my-lib/src/components/hello.tsx')); + }); + }); +}); diff --git a/packages/expo/src/generators/component/component.ts b/packages/expo/src/generators/component/component.ts new file mode 100644 index 0000000000000..f1ccb40f7a089 --- /dev/null +++ b/packages/expo/src/generators/component/component.ts @@ -0,0 +1,87 @@ +import * as ts from 'typescript'; +import { Schema } from './schema'; +import { + applyChangesToString, + convertNxGenerator, + formatFiles, + generateFiles, + getProjects, + joinPathFragments, + toJS, + Tree, +} from '@nrwl/devkit'; +import { NormalizedSchema, normalizeOptions } from './lib/normalize-options'; +import { addImport } from './lib/add-import'; + +export async function expoComponentGenerator(host: Tree, schema: Schema) { + const options = await normalizeOptions(host, schema); + createComponentFiles(host, options); + + addExportsToBarrel(host, options); + + if (options.skipFormat) { + await formatFiles(host); + } +} + +function createComponentFiles(host: Tree, options: NormalizedSchema) { + const componentDir = joinPathFragments( + options.projectSourceRoot, + options.directory + ); + + generateFiles(host, joinPathFragments(__dirname, './files'), componentDir, { + ...options, + tmpl: '', + }); + + for (const c of host.listChanges()) { + let deleteFile = false; + + if (options.skipTests && /.*spec.tsx/.test(c.path)) { + deleteFile = true; + } + + if (deleteFile) { + host.delete(c.path); + } + } + + if (options.js) { + toJS(host); + } +} + +function addExportsToBarrel(host: Tree, options: NormalizedSchema) { + const workspace = getProjects(host); + const isApp = workspace.get(options.project).projectType === 'application'; + + if (options.export && !isApp) { + const indexFilePath = joinPathFragments( + options.projectSourceRoot, + options.js ? 'index.js' : 'index.ts' + ); + const indexSource = host.read(indexFilePath, 'utf-8'); + if (indexSource !== null) { + const indexSourceFile = ts.createSourceFile( + indexFilePath, + indexSource, + ts.ScriptTarget.Latest, + true + ); + const changes = applyChangesToString( + indexSource, + addImport( + indexSourceFile, + `export * from './${options.directory}/${options.fileName}';` + ) + ); + host.write(indexFilePath, changes); + } + } +} + +export default expoComponentGenerator; +export const expoComponentSchematic = convertNxGenerator( + expoComponentGenerator +); diff --git a/packages/expo/src/generators/component/files/__fileName__.spec.tsx.template b/packages/expo/src/generators/component/files/__fileName__.spec.tsx.template new file mode 100644 index 0000000000000..d42366c2ff40d --- /dev/null +++ b/packages/expo/src/generators/component/files/__fileName__.spec.tsx.template @@ -0,0 +1,11 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; + +import <%= className %> from './<%= fileName %>'; + +describe('<%= className %>', () => { + it('should render successfully', () => { + const { container } = render(< <%= className %> />); + expect(container).toBeTruthy(); + }); +}); diff --git a/packages/expo/src/generators/component/files/__fileName__.tsx.template b/packages/expo/src/generators/component/files/__fileName__.tsx.template new file mode 100644 index 0000000000000..130b2fe9266f6 --- /dev/null +++ b/packages/expo/src/generators/component/files/__fileName__.tsx.template @@ -0,0 +1,32 @@ +<% if (classComponent) { %> +import { Component } from 'react'; +<% } else { %> +import React from 'react'; +<% } %> +import { View, Text } from 'react-native'; + +/* eslint-disable-next-line */ +export interface <%= className %>Props { +} + +<% if (classComponent) { %> +export class <%= className %> extends Component<<%= className %>Props> { + render() { + return ( + + Welcome to <%= name %>! + + ); + } +} +<% } else { %> +export function <%= className %>(props: <%= className %>Props) { + return ( + + Welcome to <%= name %>! + + ); +}; +<% } %> + +export default <%= className %>; diff --git a/packages/expo/src/generators/component/lib/add-import.ts b/packages/expo/src/generators/component/lib/add-import.ts new file mode 100644 index 0000000000000..b151ee22261a0 --- /dev/null +++ b/packages/expo/src/generators/component/lib/add-import.ts @@ -0,0 +1,28 @@ +import { findNodes } from '@nrwl/workspace/src/utilities/typescript'; +import * as ts from 'typescript'; +import { ChangeType, StringChange } from '@nrwl/devkit'; + +export function addImport( + source: ts.SourceFile, + statement: string +): StringChange[] { + const allImports = findNodes(source, ts.SyntaxKind.ImportDeclaration); + if (allImports.length > 0) { + const lastImport = allImports[allImports.length - 1]; + return [ + { + type: ChangeType.Insert, + index: lastImport.end + 1, + text: `\n${statement}\n`, + }, + ]; + } else { + return [ + { + type: ChangeType.Insert, + index: 0, + text: `\n${statement}\n`, + }, + ]; + } +} diff --git a/packages/expo/src/generators/component/lib/normalize-options.ts b/packages/expo/src/generators/component/lib/normalize-options.ts new file mode 100644 index 0000000000000..23bc23c724f43 --- /dev/null +++ b/packages/expo/src/generators/component/lib/normalize-options.ts @@ -0,0 +1,83 @@ +import { + getProjects, + joinPathFragments, + logger, + names, + Tree, +} from '@nrwl/devkit'; +import { Schema } from '../schema'; + +export interface NormalizedSchema extends Schema { + projectSourceRoot: string; + fileName: string; + className: string; +} + +export async function normalizeOptions( + host: Tree, + options: Schema +): Promise { + assertValidOptions(options); + + const { className, fileName } = names(options.name); + const componentFileName = options.pascalCaseFiles ? className : fileName; + const project = getProjects(host).get(options.project); + + if (!project) { + logger.error( + `Cannot find the ${options.project} project. Please double check the project name.` + ); + throw new Error(); + } + + const { sourceRoot: projectSourceRoot, projectType } = project; + + const directory = await getDirectory(host, options); + + if (options.export && projectType === 'application') { + logger.warn( + `The "--export" option should not be used with applications and will do nothing.` + ); + } + + options.classComponent = options.classComponent ?? false; + + return { + ...options, + directory, + className, + fileName: componentFileName, + projectSourceRoot, + }; +} + +async function getDirectory(host: Tree, options: Schema) { + const fileName = names(options.name).fileName; + const workspace = getProjects(host); + let baseDir: string; + if (options.directory) { + baseDir = options.directory; + } else { + baseDir = + workspace.get(options.project).projectType === 'application' + ? 'app' + : 'lib'; + } + return options.flat ? baseDir : joinPathFragments(baseDir, fileName); +} + +function assertValidOptions(options: Schema) { + const slashes = ['/', '\\']; + slashes.forEach((s) => { + if (options.name.indexOf(s) !== -1) { + const [name, ...rest] = options.name.split(s).reverse(); + let suggestion = rest.map((x) => x.toLowerCase()).join(s); + if (options.directory) { + suggestion = `${options.directory}${s}${suggestion}`; + } + throw new Error( + `Found "${s}" in the component name. Did you mean to use the --directory option (e.g. \`nx g c ${name} --directory ${suggestion}\`)?` + ); + } + }); +} diff --git a/packages/expo/src/generators/component/schema.d.ts b/packages/expo/src/generators/component/schema.d.ts new file mode 100644 index 0000000000000..05440ac661a2d --- /dev/null +++ b/packages/expo/src/generators/component/schema.d.ts @@ -0,0 +1,15 @@ +/** + * Same as the @nrwl/react library schema, except it removes keys: style, routing, globalCss + */ +export interface Schema { + name: string; + project: string; + directory?: string; + skipFormat: boolean; // default is false + skipTests: boolean; // default is false + export: boolean; // default is false + pascalCaseFiles: boolean; // default is false + classComponent: boolean; // default is false + js: boolean; // default is false + flat: boolean; // default is false +} diff --git a/packages/expo/src/generators/component/schema.json b/packages/expo/src/generators/component/schema.json new file mode 100644 index 0000000000000..f449a76089808 --- /dev/null +++ b/packages/expo/src/generators/component/schema.json @@ -0,0 +1,82 @@ +{ + "cli": "nx", + "$id": "NxReactNativeApplication", + "$schema": "http://json-schema.org/schema", + "title": "Create a React Component for Nx", + "type": "object", + "examples": [ + { + "command": "g component my-component --project=mylib", + "description": "Generate a component in the mylib library" + }, + { + "command": "g component my-component --project=mylib --classComponent", + "description": "Generate a class component in the mylib library" + } + ], + "properties": { + "project": { + "type": "string", + "description": "The name of the project.", + "alias": "p", + "$default": { + "$source": "projectName" + }, + "x-prompt": "What is the name of the project for this component?" + }, + "name": { + "type": "string", + "description": "The name of the component.", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What name would you like to use for the component?" + }, + "js": { + "type": "boolean", + "description": "Generate JavaScript files rather than TypeScript files.", + "default": false + }, + "skipFormat": { + "description": "Skip formatting files.", + "type": "boolean", + "default": false + }, + "skipTests": { + "type": "boolean", + "description": "When true, does not create \"spec.ts\" test files for the new component.", + "default": false + }, + "directory": { + "type": "string", + "description": "Create the component under this directory (can be nested).", + "alias": "d" + }, + "flat": { + "type": "boolean", + "description": "Create component at the source root rather than its own directory.", + "default": false + }, + "export": { + "type": "boolean", + "description": "When true, the component is exported from the project index.ts (if it exists).", + "alias": "e", + "default": false, + "x-prompt": "Should this component be exported in the project?" + }, + "pascalCaseFiles": { + "type": "boolean", + "description": "Use pascal case component file name (e.g. App.tsx).", + "alias": "P", + "default": false + }, + "classComponent": { + "type": "boolean", + "alias": "C", + "description": "Use class components instead of functional component.", + "default": false + } + }, + "required": ["name", "project"] +} diff --git a/packages/expo/src/generators/init/init.spec.ts b/packages/expo/src/generators/init/init.spec.ts new file mode 100644 index 0000000000000..429db947486c5 --- /dev/null +++ b/packages/expo/src/generators/init/init.spec.ts @@ -0,0 +1,59 @@ +import { Tree, readJson, updateJson } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { expoInitGenerator } from './init'; + +describe('init', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + tree.write('.gitignore', ''); + }); + + it('should add react native dependencies', async () => { + await expoInitGenerator(tree, { e2eTestRunner: 'none' }); + const packageJson = readJson(tree, 'package.json'); + expect(packageJson.dependencies['react']).toBeDefined(); + expect(packageJson.dependencies['expo']).toBeDefined(); + expect(packageJson.dependencies['react-native']).toBeDefined(); + expect(packageJson.devDependencies['@types/react']).toBeDefined(); + expect(packageJson.devDependencies['@types/react-native']).toBeDefined(); + }); + + it('should add .gitignore entries for React native files and directories', async () => { + tree.write( + '/.gitignore', + ` +/node_modules +` + ); + await expoInitGenerator(tree, { e2eTestRunner: 'none' }); + + const content = tree.read('/.gitignore').toString(); + + expect(content).toMatch(/# Expo/); + }); + + describe('defaultCollection', () => { + it('should be set if none was set before', async () => { + await expoInitGenerator(tree, { e2eTestRunner: 'none' }); + const { cli } = readJson(tree, 'nx.json'); + expect(cli.defaultCollection).toEqual('@nrwl/expo'); + }); + + it('should not be set if something else was set before', async () => { + updateJson(tree, 'nx.json', (json) => { + json.cli = { + defaultCollection: '@nrwl/react', + }; + + json.targets = {}; + + return json; + }); + await expoInitGenerator(tree, { e2eTestRunner: 'none' }); + const { cli } = readJson(tree, 'nx.json'); + expect(cli.defaultCollection).toEqual('@nrwl/react'); + }); + }); +}); diff --git a/packages/expo/src/generators/init/init.ts b/packages/expo/src/generators/init/init.ts new file mode 100644 index 0000000000000..99fa6fc43de4c --- /dev/null +++ b/packages/expo/src/generators/init/init.ts @@ -0,0 +1,98 @@ +import { setDefaultCollection } from '@nrwl/workspace/src/utilities/set-default-collection'; +import { + addDependenciesToPackageJson, + convertNxGenerator, + formatFiles, + removeDependenciesFromPackageJson, + Tree, +} from '@nrwl/devkit'; +import { Schema } from './schema'; +import { + expoStatusBarVersion, + expoVersion, + nxVersion, + reactNativeVersion, + reactNativeWebVersion, + typesReactNativeVersion, + expoMetroConfigVersion, + metroVersion, + expoStructuredHeadersVersion, + expoSplashScreenVersion, + expoUpdatesVersion, + reactNativeGestureHandlerVersion, + reactNativeReanimatedVersion, + reactNativeSafeAreaContextVersion, + reactNativeScreensVersion, + testingLibraryReactNativeVersion, + testingLibraryJestNativeVersion, + jestExpoVersion, +} from '../../utils/versions'; +import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial'; +import { addGitIgnoreEntry } from './lib/add-git-ignore-entry'; +import { jestInitGenerator } from '@nrwl/jest'; +import { detoxInitGenerator } from '@nrwl/detox'; +import { + reactVersion, + typesReactVersion, +} from '@nrwl/react/src/utils/versions'; + +export async function expoInitGenerator(host: Tree, schema: Schema) { + setDefaultCollection(host, '@nrwl/expo'); + addGitIgnoreEntry(host); + + const tasks = [moveDependency(host), updateDependencies(host)]; + + if (!schema.unitTestRunner || schema.unitTestRunner === 'jest') { + const jestTask = jestInitGenerator(host, {}); + tasks.push(jestTask); + } + + if (!schema.e2eTestRunner || schema.e2eTestRunner === 'detox') { + const detoxTask = await detoxInitGenerator(host, { skipFormat: true }); + tasks.push(detoxTask); + } + + if (!schema.skipFormat) { + await formatFiles(host); + } + + return runTasksInSerial(...tasks); +} + +export function updateDependencies(host: Tree) { + return addDependenciesToPackageJson( + host, + { + react: reactVersion, + 'react-dom': reactVersion, + 'react-native': reactNativeVersion, + expo: expoVersion, + 'expo-status-bar': expoStatusBarVersion, + 'react-native-web': reactNativeWebVersion, + '@expo/metro-config': expoMetroConfigVersion, + 'expo-structured-headers': expoStructuredHeadersVersion, + 'expo-splash-screen': expoSplashScreenVersion, + 'expo-updates': expoUpdatesVersion, + 'react-native-gesture-handler': reactNativeGestureHandlerVersion, + 'react-native-reanimated': reactNativeReanimatedVersion, + 'react-native-safe-area-context': reactNativeSafeAreaContextVersion, + 'react-native-screens': reactNativeScreensVersion, + }, + { + '@nrwl/expo': nxVersion, + '@types/react': typesReactVersion, + '@types/react-native': typesReactNativeVersion, + 'metro-resolver': metroVersion, + '@testing-library/react-native': testingLibraryReactNativeVersion, + '@testing-library/jest-native': testingLibraryJestNativeVersion, + 'jest-expo': jestExpoVersion, + } + ); +} + +function moveDependency(host: Tree) { + return removeDependenciesFromPackageJson(host, ['@nrwl/react-native'], []); +} + +export default expoInitGenerator; +export const reactNativeInitSchematic = convertNxGenerator(expoInitGenerator); diff --git a/packages/expo/src/generators/init/lib/add-git-ignore-entry.ts b/packages/expo/src/generators/init/lib/add-git-ignore-entry.ts new file mode 100644 index 0000000000000..224b9d6a80f7a --- /dev/null +++ b/packages/expo/src/generators/init/lib/add-git-ignore-entry.ts @@ -0,0 +1,17 @@ +import { logger, Tree } from '@nrwl/devkit'; +import { gitIgnoreEntriesForExpo } from './gitignore-entries'; + +export function addGitIgnoreEntry(host: Tree) { + if (!host.exists('.gitignore')) { + logger.warn(`Couldn't find .gitignore file to update`); + return; + } + + let content = host.read('.gitignore')?.toString('utf-8').trimRight(); + + if (!/^\.expo$/gm.test(content)) { + content = `${content}\n${gitIgnoreEntriesForExpo}\n`; + } + + host.write('.gitignore', content); +} diff --git a/packages/expo/src/generators/init/lib/gitignore-entries.ts b/packages/expo/src/generators/init/lib/gitignore-entries.ts new file mode 100644 index 0000000000000..a0dec25ed867f --- /dev/null +++ b/packages/expo/src/generators/init/lib/gitignore-entries.ts @@ -0,0 +1,14 @@ +export const gitIgnoreEntriesForExpo = ` +# Expo +node_modules/ +.expo/ +dist/ +npm-debug.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision +*.orig.* +web-build/ +`; diff --git a/packages/expo/src/generators/init/schema.d.ts b/packages/expo/src/generators/init/schema.d.ts new file mode 100644 index 0000000000000..11ea540ee8778 --- /dev/null +++ b/packages/expo/src/generators/init/schema.d.ts @@ -0,0 +1,5 @@ +export interface Schema { + unitTestRunner?: 'jest' | 'none'; + skipFormat?: boolean; + e2eTestRunner?: 'detox' | 'none'; +} diff --git a/packages/expo/src/generators/init/schema.json b/packages/expo/src/generators/init/schema.json new file mode 100644 index 0000000000000..69ce55450463f --- /dev/null +++ b/packages/expo/src/generators/init/schema.json @@ -0,0 +1,27 @@ +{ + "cli": "nx", + "$id": "NxReactNativeInit", + "$schema": "http://json-schema.org/schema", + "title": "Add Nx React Schematics", + "type": "object", + "properties": { + "unitTestRunner": { + "description": "Adds the specified unit test runner", + "type": "string", + "enum": ["jest", "none"], + "default": "jest" + }, + "skipFormat": { + "description": "Skip formatting files", + "type": "boolean", + "default": false + }, + "e2eTestRunner": { + "description": "Adds the specified e2e test runner", + "type": "string", + "enum": ["detox", "none"], + "default": "detox" + } + }, + "required": [] +} diff --git a/packages/expo/src/generators/library/files/lib/.babelrc.template b/packages/expo/src/generators/library/files/lib/.babelrc.template new file mode 100644 index 0000000000000..7d30f8bf0669a --- /dev/null +++ b/packages/expo/src/generators/library/files/lib/.babelrc.template @@ -0,0 +1,3 @@ +{ + "presets": ["babel-preset-expo"] +} diff --git a/packages/expo/src/generators/library/files/lib/README.md b/packages/expo/src/generators/library/files/lib/README.md new file mode 100644 index 0000000000000..b74453ce2e839 --- /dev/null +++ b/packages/expo/src/generators/library/files/lib/README.md @@ -0,0 +1,7 @@ +# <%= name %> + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test <%= name %>` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/packages/expo/src/generators/library/files/lib/package.json.template b/packages/expo/src/generators/library/files/lib/package.json.template new file mode 100644 index 0000000000000..fa518765a372f --- /dev/null +++ b/packages/expo/src/generators/library/files/lib/package.json.template @@ -0,0 +1,4 @@ +{ + "name": "<%= name %>", + "version": "0.0.1" +} diff --git a/packages/expo/src/generators/library/files/lib/src/index.ts.template b/packages/expo/src/generators/library/files/lib/src/index.ts.template new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/expo/src/generators/library/files/lib/test-setup.ts.template b/packages/expo/src/generators/library/files/lib/test-setup.ts.template new file mode 100644 index 0000000000000..9f28ad211b736 --- /dev/null +++ b/packages/expo/src/generators/library/files/lib/test-setup.ts.template @@ -0,0 +1 @@ +import '@testing-library/jest-native/extend-expect'; diff --git a/packages/expo/src/generators/library/files/lib/tsconfig.json.template b/packages/expo/src/generators/library/files/lib/tsconfig.json.template new file mode 100644 index 0000000000000..441ee25621bd7 --- /dev/null +++ b/packages/expo/src/generators/library/files/lib/tsconfig.json.template @@ -0,0 +1,16 @@ +{ + "extends": "<%= offsetFromRoot %>tsconfig.base.json", + "compilerOptions": { + "jsx": "react-native", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/packages/expo/src/generators/library/files/lib/tsconfig.lib.json.template b/packages/expo/src/generators/library/files/lib/tsconfig.lib.json.template new file mode 100644 index 0000000000000..566fcd105dd27 --- /dev/null +++ b/packages/expo/src/generators/library/files/lib/tsconfig.lib.json.template @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "<%= offsetFromRoot %>dist/out-tsc", + "types": ["node"] + }, + "exclude": ["**/*.spec.ts", "**/*.spec.tsx"], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/packages/expo/src/generators/library/lib/normalize-options.ts b/packages/expo/src/generators/library/lib/normalize-options.ts new file mode 100644 index 0000000000000..dd5b9bd2ee09c --- /dev/null +++ b/packages/expo/src/generators/library/lib/normalize-options.ts @@ -0,0 +1,52 @@ +import { + getWorkspaceLayout, + joinPathFragments, + names, + Tree, +} from '@nrwl/devkit'; +import { Schema } from '../schema'; + +export interface NormalizedSchema extends Schema { + name: string; + fileName: string; + projectRoot: string; + routePath: string; + projectDirectory: string; + parsedTags: string[]; + appMain?: string; + appSourceRoot?: string; +} + +export function normalizeOptions( + host: Tree, + options: Schema +): NormalizedSchema { + const name = names(options.name).fileName; + const projectDirectory = options.directory + ? `${names(options.directory).fileName}/${name}` + : name; + + const projectName = projectDirectory.replace(new RegExp('/', 'g'), '-'); + const fileName = projectName; + const { libsDir, npmScope } = getWorkspaceLayout(host); + const projectRoot = joinPathFragments(libsDir, projectDirectory); + + const parsedTags = options.tags + ? options.tags.split(',').map((s) => s.trim()) + : []; + + const importPath = options.importPath || `@${npmScope}/${projectDirectory}`; + + const normalized: NormalizedSchema = { + ...options, + fileName, + routePath: `/${name}`, + name: projectName, + projectRoot, + projectDirectory, + parsedTags, + importPath, + }; + + return normalized; +} diff --git a/packages/expo/src/generators/library/library.spec.ts b/packages/expo/src/generators/library/library.spec.ts new file mode 100644 index 0000000000000..7bcfcd658c914 --- /dev/null +++ b/packages/expo/src/generators/library/library.spec.ts @@ -0,0 +1,351 @@ +import { getProjects, readJson, Tree, updateJson } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { Linter } from '@nrwl/linter'; + +import { expoLibraryGenerator } from './library'; +import { Schema } from './schema'; + +describe('lib', () => { + let appTree: Tree; + + const defaultSchema: Schema = { + name: 'myLib', + linter: Linter.EsLint, + skipFormat: false, + skipTsConfig: false, + unitTestRunner: 'jest', + strict: true, + js: false, + }; + + beforeEach(() => { + appTree = createTreeWithEmptyWorkspace(); + appTree.write('.gitignore', ''); + }); + + describe('not nested', () => { + it('should update workspace.json', async () => { + await expoLibraryGenerator(appTree, { + ...defaultSchema, + tags: 'one,two', + }); + const workspaceJson = readJson(appTree, '/workspace.json'); + expect(workspaceJson.projects['my-lib'].root).toEqual('libs/my-lib'); + expect(workspaceJson.projects['my-lib'].architect.build).toBeUndefined(); + expect(workspaceJson.projects['my-lib'].architect.lint).toEqual({ + builder: '@nrwl/linter:eslint', + outputs: ['{options.outputFile}'], + options: { + lintFilePatterns: ['libs/my-lib/**/*.{ts,tsx,js,jsx}'], + }, + }); + expect(workspaceJson.projects['my-lib'].tags).toEqual(['one', 'two']); + }); + + it('should update tsconfig.base.json', async () => { + await expoLibraryGenerator(appTree, defaultSchema); + const tsconfigJson = readJson(appTree, '/tsconfig.base.json'); + expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([ + 'libs/my-lib/src/index.ts', + ]); + }); + + it('should update root tsconfig.base.json (no existing path mappings)', async () => { + updateJson(appTree, 'tsconfig.base.json', (json) => { + json.compilerOptions.paths = undefined; + return json; + }); + + await expoLibraryGenerator(appTree, defaultSchema); + const tsconfigJson = readJson(appTree, '/tsconfig.base.json'); + expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([ + 'libs/my-lib/src/index.ts', + ]); + }); + + it('should create a local tsconfig.json', async () => { + await expoLibraryGenerator(appTree, defaultSchema); + const tsconfigJson = readJson(appTree, 'libs/my-lib/tsconfig.json'); + expect(tsconfigJson.references).toEqual([ + { + path: './tsconfig.lib.json', + }, + { + path: './tsconfig.spec.json', + }, + ]); + expect( + tsconfigJson.compilerOptions.forceConsistentCasingInFileNames + ).toEqual(true); + expect(tsconfigJson.compilerOptions.strict).toEqual(true); + expect(tsconfigJson.compilerOptions.noImplicitReturns).toEqual(true); + expect(tsconfigJson.compilerOptions.noFallthroughCasesInSwitch).toEqual( + true + ); + }); + + it('should extend the local tsconfig.json with tsconfig.spec.json', async () => { + await expoLibraryGenerator(appTree, defaultSchema); + const tsconfigJson = readJson(appTree, 'libs/my-lib/tsconfig.spec.json'); + expect(tsconfigJson.extends).toEqual('./tsconfig.json'); + }); + + it('should extend the local tsconfig.json with tsconfig.lib.json', async () => { + await expoLibraryGenerator(appTree, defaultSchema); + const tsconfigJson = readJson(appTree, 'libs/my-lib/tsconfig.lib.json'); + expect(tsconfigJson.extends).toEqual('./tsconfig.json'); + }); + }); + + describe('nested', () => { + it('should update nx.json', async () => { + await expoLibraryGenerator(appTree, { + ...defaultSchema, + directory: 'myDir', + tags: 'one', + }); + const workspaceJson = readJson(appTree, '/workspace.json'); + expect(workspaceJson.projects).toMatchObject({ + 'my-dir-my-lib': { + tags: ['one'], + }, + }); + + await expoLibraryGenerator(appTree, { + ...defaultSchema, + name: 'myLib2', + directory: 'myDir', + tags: 'one,two', + }); + + const workspaceJson2 = readJson(appTree, '/workspace.json'); + expect(workspaceJson2.projects).toMatchObject({ + 'my-dir-my-lib': { + tags: ['one'], + }, + 'my-dir-my-lib2': { + tags: ['one', 'two'], + }, + }); + }); + + it('should update workspace.json', async () => { + await expoLibraryGenerator(appTree, { + ...defaultSchema, + directory: 'myDir', + }); + const workspaceJson = readJson(appTree, '/workspace.json'); + + expect(workspaceJson.projects['my-dir-my-lib'].root).toEqual( + 'libs/my-dir/my-lib' + ); + expect(workspaceJson.projects['my-dir-my-lib'].architect.lint).toEqual({ + builder: '@nrwl/linter:eslint', + outputs: ['{options.outputFile}'], + options: { + lintFilePatterns: ['libs/my-dir/my-lib/**/*.{ts,tsx,js,jsx}'], + }, + }); + }); + + it('should update tsconfig.base.json', async () => { + await expoLibraryGenerator(appTree, { + ...defaultSchema, + directory: 'myDir', + }); + const tsconfigJson = readJson(appTree, '/tsconfig.base.json'); + expect(tsconfigJson.compilerOptions.paths['@proj/my-dir/my-lib']).toEqual( + ['libs/my-dir/my-lib/src/index.ts'] + ); + expect( + tsconfigJson.compilerOptions.paths['my-dir-my-lib/*'] + ).toBeUndefined(); + }); + + it('should create a local tsconfig.json', async () => { + await expoLibraryGenerator(appTree, { + ...defaultSchema, + directory: 'myDir', + }); + + const tsconfigJson = readJson( + appTree, + 'libs/my-dir/my-lib/tsconfig.json' + ); + expect(tsconfigJson.references).toEqual([ + { + path: './tsconfig.lib.json', + }, + { + path: './tsconfig.spec.json', + }, + ]); + }); + }); + + describe('--unit-test-runner none', () => { + it('should not generate test configuration', async () => { + await expoLibraryGenerator(appTree, { + ...defaultSchema, + unitTestRunner: 'none', + }); + + expect(appTree.exists('libs/my-lib/tsconfig.spec.json')).toBeFalsy(); + expect(appTree.exists('libs/my-lib/jest.config.js')).toBeFalsy(); + const workspaceJson = readJson(appTree, 'workspace.json'); + expect(workspaceJson.projects['my-lib'].architect.test).toBeUndefined(); + expect(workspaceJson.projects['my-lib'].architect.lint) + .toMatchInlineSnapshot(` + Object { + "builder": "@nrwl/linter:eslint", + "options": Object { + "lintFilePatterns": Array [ + "libs/my-lib/**/*.{ts,tsx,js,jsx}", + ], + }, + "outputs": Array [ + "{options.outputFile}", + ], + } + `); + }); + }); + + describe('--buildable', () => { + it('should have a builder defined', async () => { + await expoLibraryGenerator(appTree, { + ...defaultSchema, + buildable: true, + }); + + const workspaceJson = getProjects(appTree); + + expect(workspaceJson.get('my-lib').targets.build).toBeDefined(); + }); + }); + + describe('--publishable', () => { + it('should add build architect', async () => { + await expoLibraryGenerator(appTree, { + ...defaultSchema, + publishable: true, + importPath: '@proj/my-lib', + }); + + const workspaceJson = getProjects(appTree); + + expect(workspaceJson.get('my-lib').targets.build).toMatchObject({ + executor: '@nrwl/web:rollup', + outputs: ['{options.outputPath}'], + options: { + external: ['react/jsx-runtime'], + entryFile: 'libs/my-lib/src/index.ts', + outputPath: 'dist/libs/my-lib', + project: 'libs/my-lib/package.json', + tsConfig: 'libs/my-lib/tsconfig.lib.json', + rollupConfig: '@nrwl/react/plugins/bundle-rollup', + }, + }); + }); + + it('should fail if no importPath is provided with publishable', async () => { + expect.assertions(1); + + try { + await expoLibraryGenerator(appTree, { + ...defaultSchema, + directory: 'myDir', + publishable: true, + }); + } catch (e) { + expect(e.message).toContain( + 'For publishable libs you have to provide a proper "--importPath" which needs to be a valid npm package name (e.g. my-awesome-lib or @myorg/my-lib)' + ); + } + }); + + it('should add package.json and .babelrc', async () => { + await expoLibraryGenerator(appTree, { + ...defaultSchema, + publishable: true, + importPath: '@proj/my-lib', + }); + + const packageJson = readJson(appTree, '/libs/my-lib/package.json'); + expect(packageJson.name).toEqual('@proj/my-lib'); + expect(appTree.exists('/libs/my-lib/.babelrc')); + }); + }); + + describe('--js', () => { + it('should generate JS files', async () => { + await expoLibraryGenerator(appTree, { + ...defaultSchema, + js: true, + }); + + expect(appTree.exists('/libs/my-lib/src/index.js')).toBe(true); + }); + }); + + describe('--importPath', () => { + it('should update the package.json & tsconfig with the given import path', async () => { + await expoLibraryGenerator(appTree, { + ...defaultSchema, + publishable: true, + directory: 'myDir', + importPath: '@myorg/lib', + }); + const packageJson = readJson(appTree, 'libs/my-dir/my-lib/package.json'); + const tsconfigJson = readJson(appTree, '/tsconfig.base.json'); + + expect(packageJson.name).toBe('@myorg/lib'); + expect( + tsconfigJson.compilerOptions.paths[packageJson.name] + ).toBeDefined(); + }); + + it('should fail if the same importPath has already been used', async () => { + await expoLibraryGenerator(appTree, { + ...defaultSchema, + name: 'myLib1', + publishable: true, + importPath: '@myorg/lib', + }); + + try { + await expoLibraryGenerator(appTree, { + ...defaultSchema, + name: 'myLib2', + publishable: true, + importPath: '@myorg/lib', + }); + } catch (e) { + expect(e.message).toContain( + 'You already have a library using the import path' + ); + } + + expect.assertions(1); + }); + }); + + describe('--no-strict', () => { + it('should not add options for strict mode', async () => { + await expoLibraryGenerator(appTree, { + ...defaultSchema, + strict: false, + }); + const tsconfigJson = readJson(appTree, '/libs/my-lib/tsconfig.json'); + + expect( + tsconfigJson.compilerOptions.forceConsistentCasingInFileNames + ).not.toBeDefined(); + expect(tsconfigJson.compilerOptions.strict).not.toBeDefined(); + expect(tsconfigJson.compilerOptions.noImplicitReturns).not.toBeDefined(); + expect( + tsconfigJson.compilerOptions.noFallthroughCasesInSwitch + ).not.toBeDefined(); + }); + }); +}); diff --git a/packages/expo/src/generators/library/library.ts b/packages/expo/src/generators/library/library.ts new file mode 100644 index 0000000000000..bdd3ea5b4d2b5 --- /dev/null +++ b/packages/expo/src/generators/library/library.ts @@ -0,0 +1,197 @@ +import { + addProjectConfiguration, + convertNxGenerator, + formatFiles, + generateFiles, + GeneratorCallback, + getWorkspaceLayout, + joinPathFragments, + names, + offsetFromRoot, + TargetConfiguration, + toJS, + Tree, + updateJson, +} from '@nrwl/devkit'; +import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial'; + +import init from '../init/init'; +import { addLinting } from '../../utils/add-linting'; +import { addJest } from '../../utils/add-jest'; + +import { NormalizedSchema, normalizeOptions } from './lib/normalize-options'; +import { Schema } from './schema'; + +export async function expoLibraryGenerator( + host: Tree, + schema: Schema +): Promise { + const options = normalizeOptions(host, schema); + if (options.publishable === true && !schema.importPath) { + throw new Error( + `For publishable libs you have to provide a proper "--importPath" which needs to be a valid npm package name (e.g. my-awesome-lib or @myorg/my-lib)` + ); + } + + addProject(host, options); + createFiles(host, options); + + const initTask = await init(host, { + ...options, + skipFormat: true, + e2eTestRunner: 'none', + }); + + const lintTask = await addLinting( + host, + options.name, + options.projectRoot, + [joinPathFragments(options.projectRoot, 'tsconfig.lib.json')], + options.linter, + options.setParserOptionsProject + ); + + if (!options.skipTsConfig) { + updateBaseTsConfig(host, options); + } + + const jestTask = await addJest( + host, + options.unitTestRunner, + options.name, + options.projectRoot, + options.js + ); + + if (options.publishable || options.buildable) { + updateLibPackageNpmScope(host, options); + } + + if (!options.skipFormat) { + await formatFiles(host); + } + + return runTasksInSerial(initTask, lintTask, jestTask); +} + +function addProject(host: Tree, options: NormalizedSchema) { + const targets: { [key: string]: TargetConfiguration } = {}; + + if (options.publishable || options.buildable) { + const { libsDir } = getWorkspaceLayout(host); + const external = ['react/jsx-runtime']; + + targets.build = { + executor: '@nrwl/web:rollup', + outputs: ['{options.outputPath}'], + options: { + outputPath: `dist/${libsDir}/${options.projectDirectory}`, + tsConfig: `${options.projectRoot}/tsconfig.lib.json`, + project: `${options.projectRoot}/package.json`, + entryFile: maybeJs(options, `${options.projectRoot}/src/index.ts`), + external, + rollupConfig: `@nrwl/react/plugins/bundle-rollup`, + assets: [ + { + glob: `${options.projectRoot}/README.md`, + input: '.', + output: '.', + }, + ], + }, + }; + } + + addProjectConfiguration(host, options.name, { + root: options.projectRoot, + sourceRoot: joinPathFragments(options.projectRoot, 'src'), + projectType: 'library', + tags: options.parsedTags, + targets, + }); +} + +function updateTsConfig(tree: Tree, options: NormalizedSchema) { + updateJson( + tree, + joinPathFragments(options.projectRoot, 'tsconfig.json'), + (json) => { + if (options.strict) { + json.compilerOptions = { + ...json.compilerOptions, + forceConsistentCasingInFileNames: true, + strict: true, + noImplicitReturns: true, + noFallthroughCasesInSwitch: true, + }; + } + + return json; + } + ); +} + +function updateBaseTsConfig(host: Tree, options: NormalizedSchema) { + updateJson(host, 'tsconfig.base.json', (json) => { + const c = json.compilerOptions; + c.paths = c.paths || {}; + delete c.paths[options.name]; + + if (c.paths[options.importPath]) { + throw new Error( + `You already have a library using the import path "${options.importPath}". Make sure to specify a unique one.` + ); + } + + const { libsDir } = getWorkspaceLayout(host); + + c.paths[options.importPath] = [ + maybeJs( + options, + joinPathFragments(libsDir, `${options.projectDirectory}/src/index.ts`) + ), + ]; + + return json; + }); +} + +function createFiles(host: Tree, options: NormalizedSchema) { + generateFiles( + host, + joinPathFragments(__dirname, './files/lib'), + options.projectRoot, + { + ...options, + ...names(options.name), + tmpl: '', + offsetFromRoot: offsetFromRoot(options.projectRoot), + } + ); + + if (!options.publishable && !options.buildable) { + host.delete(`${options.projectRoot}/package.json`); + } + + if (options.js) { + toJS(host); + } + + updateTsConfig(host, options); +} + +function updateLibPackageNpmScope(host: Tree, options: NormalizedSchema) { + return updateJson(host, `${options.projectRoot}/package.json`, (json) => { + json.name = options.importPath; + return json; + }); +} + +function maybeJs(options: NormalizedSchema, path: string): string { + return options.js && (path.endsWith('.ts') || path.endsWith('.tsx')) + ? path.replace(/\.tsx?$/, '.js') + : path; +} + +export default expoLibraryGenerator; +export const expoLibrarySchematic = convertNxGenerator(expoLibraryGenerator); diff --git a/packages/expo/src/generators/library/schema.d.ts b/packages/expo/src/generators/library/schema.d.ts new file mode 100644 index 0000000000000..11cb3ecd70064 --- /dev/null +++ b/packages/expo/src/generators/library/schema.d.ts @@ -0,0 +1,22 @@ +import { Linter } from '@nrwl/linter'; + +/** + * Same as the @nrwl/react library schema, except it removes keys: style, component, routing, appProject + */ +export interface Schema { + name: string; + directory?: string; + skipTsConfig: boolean; // default is false + skipFormat: boolean; // default is false + tags?: string; + pascalCaseFiles?: boolean; + unitTestRunner: 'jest' | 'none'; + linter: Linter; // default is eslint + publishable?: boolean; + buildable?: boolean; + importPath?: string; + js: boolean; // default is false + globalCss?: boolean; + strict: boolean; // default is true + setParserOptionsProject?: boolean; +} diff --git a/packages/expo/src/generators/library/schema.json b/packages/expo/src/generators/library/schema.json new file mode 100644 index 0000000000000..5bed94ff0cf1d --- /dev/null +++ b/packages/expo/src/generators/library/schema.json @@ -0,0 +1,97 @@ +{ + "cli": "nx", + "$id": "NxReactNativeLibrary", + "$schema": "http://json-schema.org/schema", + "title": "Create a React Native Library for Nx", + "type": "object", + "examples": [ + { + "command": "g lib mylib --directory=myapp", + "description": "Generate libs/myapp/mylib" + } + ], + "properties": { + "name": { + "type": "string", + "description": "Library name", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What name would you like to use for the library?", + "pattern": "^[a-zA-Z].*$" + }, + "directory": { + "type": "string", + "description": "A directory where the lib is placed.", + "alias": "d" + }, + "linter": { + "description": "The tool to use for running lint checks.", + "type": "string", + "enum": ["eslint", "tslint"], + "default": "eslint" + }, + "unitTestRunner": { + "type": "string", + "enum": ["jest", "none"], + "description": "Test runner to use for unit tests.", + "default": "jest" + }, + "tags": { + "type": "string", + "description": "Add tags to the library (used for linting).", + "alias": "t" + }, + "skipFormat": { + "description": "Skip formatting files.", + "type": "boolean", + "default": false + }, + "skipTsConfig": { + "type": "boolean", + "default": false, + "description": "Do not update tsconfig.json for development experience." + }, + "pascalCaseFiles": { + "type": "boolean", + "description": "Use pascal case component file name (e.g. App.tsx).", + "alias": "P", + "default": false + }, + "publishable": { + "type": "boolean", + "description": "Create a publishable library." + }, + "buildable": { + "type": "boolean", + "default": false, + "description": "Generate a buildable library." + }, + "importPath": { + "type": "string", + "description": "The library name used to import it, like @myorg/my-awesome-lib" + }, + "js": { + "type": "boolean", + "description": "Generate JavaScript files rather than TypeScript files.", + "default": false + }, + "globalCss": { + "type": "boolean", + "description": "When true, the stylesheet is generated using global CSS instead of CSS modules (e.g. file is '*.css' rather than '*.module.css').", + "default": false + }, + "strict": { + "type": "boolean", + "description": "Whether to enable tsconfig strict mode or not.", + "default": true + }, + "setParserOptionsProject": { + "type": "boolean", + "description": "Whether or not to configure the ESLint \"parserOptions.project\" option. We do not do this by default for lint performance reasons.", + "default": false + } + }, + "required": ["name"] +} diff --git a/packages/expo/src/utils/add-jest.ts b/packages/expo/src/utils/add-jest.ts new file mode 100644 index 0000000000000..f7553cbb93f14 --- /dev/null +++ b/packages/expo/src/utils/add-jest.ts @@ -0,0 +1,44 @@ +import { Tree } from '@nrwl/devkit'; +import { jestProjectGenerator } from '@nrwl/jest'; + +export async function addJest( + host: Tree, + unitTestRunner: 'jest' | 'none', + projectName: string, + appProjectRoot: string, + js: boolean +) { + if (unitTestRunner !== 'jest') { + return () => {}; + } + + const jestTask = await jestProjectGenerator(host, { + project: projectName, + supportTsx: true, + skipSerializers: true, + setupFile: 'none', + babelJest: true, + }); + + // overwrite the jest.config.js file because react native needs to have special transform property + const configPath = `${appProjectRoot}/jest.config.js`; + const content = `module.exports = { + displayName: '${projectName}', + resolver: '@nrwl/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: ['/test-setup.${js ? 'js' : 'ts'}'], + transform: { + '\\\\.(js|ts|tsx)$': require.resolve('react-native/jest/preprocessor.js'), + '^.+\\\\.(bmp|gif|jpg|jpeg|mp4|png|psd|svg|webp)$': require.resolve( + 'react-native/jest/assetFileTransformer.js', + ), + } +};`; + host.write(configPath, content); + + return jestTask; +} diff --git a/packages/expo/src/utils/add-linting.spec.ts b/packages/expo/src/utils/add-linting.spec.ts new file mode 100644 index 0000000000000..c06e679b3260a --- /dev/null +++ b/packages/expo/src/utils/add-linting.spec.ts @@ -0,0 +1,60 @@ +import { readProjectConfiguration, Tree } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { Linter } from '@nrwl/linter'; +import { libraryGenerator } from '@nrwl/workspace/src/generators/library/library'; +import { addLinting } from './add-linting'; + +describe('Add Linting', () => { + let tree: Tree; + + beforeEach(async () => { + tree = createTreeWithEmptyWorkspace(); + await libraryGenerator(tree, { + name: 'my-lib', + linter: Linter.None, + }); + }); + + it('should add update `workspace.json` file properly when eslint is passed', () => { + addLinting( + tree, + 'my-lib', + 'libs/my-lib', + ['libs/my-lib/tsconfig.lib.json'], + Linter.EsLint + ); + const project = readProjectConfiguration(tree, 'my-lib'); + + expect(project.targets.lint).toBeDefined(); + expect(project.targets.lint.executor).toEqual('@nrwl/linter:eslint'); + }); + + it('should add update `workspace.json` file properly when tslint is passed', () => { + addLinting( + tree, + 'my-lib', + 'libs/my-lib', + ['libs/my-lib/tsconfig.lib.json'], + Linter.TsLint + ); + const project = readProjectConfiguration(tree, 'my-lib'); + + expect(project.targets.lint).toBeDefined(); + expect(project.targets.lint.executor).toEqual( + '@angular-devkit/build-angular:tslint' + ); + }); + + it('should not add lint target when "none" is passed', async () => { + addLinting( + tree, + 'my-lib', + 'libs/my-lib', + ['libs/my-lib/tsconfig.lib.json'], + Linter.None + ); + const project = readProjectConfiguration(tree, 'my-lib'); + + expect(project.targets.lint).toBeUndefined(); + }); +}); diff --git a/packages/expo/src/utils/add-linting.ts b/packages/expo/src/utils/add-linting.ts new file mode 100644 index 0000000000000..0daccfd4be42c --- /dev/null +++ b/packages/expo/src/utils/add-linting.ts @@ -0,0 +1,68 @@ +import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial'; +import { Linter, lintProjectGenerator } from '@nrwl/linter'; +import { + addDependenciesToPackageJson, + joinPathFragments, + updateJson, + Tree, +} from '@nrwl/devkit'; +import { extraEslintDependencies, createReactEslintJson } from '@nrwl/react'; +import type { Linter as ESLintLinter } from 'eslint'; + +export async function addLinting( + host: Tree, + projectName: string, + appProjectRoot: string, + tsConfigPaths: string[], + linter: Linter, + setParserOptionsProject?: boolean +) { + if (linter === Linter.None) { + return () => {}; + } + + const lintTask = await lintProjectGenerator(host, { + linter, + project: projectName, + tsConfigPaths, + eslintFilePatterns: [`${appProjectRoot}/**/*.{ts,tsx,js,jsx}`], + skipFormat: true, + }); + + if (linter === Linter.TsLint) { + return () => {}; + } + + const reactEslintJson = createReactEslintJson( + appProjectRoot, + setParserOptionsProject + ); + + updateJson( + host, + joinPathFragments(appProjectRoot, '.eslintrc.json'), + (json: ESLintLinter.Config) => { + json = reactEslintJson; + json.ignorePatterns = ['!**/*', '.expo', 'node_modules']; + + // Find the override that handles both TS and JS files. + const commonOverride = json.overrides?.find((o) => + ['*.ts', '*.tsx', '*.js', '*.jsx'].every((ext) => o.files.includes(ext)) + ); + if (commonOverride) { + commonOverride.rules = commonOverride.rules || {}; + commonOverride.rules['@typescript-eslint/ban-ts-comment'] = 'off'; + } + + return json; + } + ); + + const installTask = await addDependenciesToPackageJson( + host, + extraEslintDependencies.dependencies, + extraEslintDependencies.devDependencies + ); + + return runTasksInSerial(lintTask, installTask); +} diff --git a/packages/expo/src/utils/ensure-node-modules-symlink.spec.ts b/packages/expo/src/utils/ensure-node-modules-symlink.spec.ts new file mode 100644 index 0000000000000..e1f3ad39e4506 --- /dev/null +++ b/packages/expo/src/utils/ensure-node-modules-symlink.spec.ts @@ -0,0 +1,110 @@ +import { tmpdir } from 'os'; +import { join } from 'path'; +import * as fs from 'fs'; +import { ensureNodeModulesSymlink } from './ensure-node-modules-symlink'; + +const workspaceDir = join(tmpdir(), 'nx-react-native-test'); +const appDir = 'apps/myapp'; +const appDirAbsolutePath = join(workspaceDir, appDir); + +describe('ensureNodeModulesSymlink', () => { + beforeEach(() => { + if (fs.existsSync(workspaceDir)) + fs.rmdirSync(workspaceDir, { recursive: true }); + fs.mkdirSync(workspaceDir); + fs.mkdirSync(appDirAbsolutePath, { recursive: true }); + fs.mkdirSync(appDirAbsolutePath, { recursive: true }); + fs.writeFileSync( + join(appDirAbsolutePath, 'package.json'), + JSON.stringify({ + name: 'myapp', + dependencies: { 'react-native': '*' }, + }) + ); + fs.writeFileSync( + join(workspaceDir, 'package.json'), + JSON.stringify({ + name: 'workspace', + dependencies: { + '@nrwl/react-native': '9999.9.9', + '@react-native-community/cli-platform-ios': '7777.7.7', + '@react-native-community/cli-platform-android': '7777.7.7', + 'react-native': '0.9999.0', + }, + }) + ); + }); + + afterEach(() => { + if (fs.existsSync(workspaceDir)) + fs.rmdirSync(workspaceDir, { recursive: true }); + }); + + it('should create symlinks', () => { + createNpmDirectory('@nrwl/react-native', '9999.9.9'); + createNpmDirectory( + '@react-native-community/cli-platform-android', + '7777.7.7' + ); + createNpmDirectory('@react-native-community/cli-platform-ios', '7777.7.7'); + createNpmDirectory('hermes-engine', '3333.3.3'); + createNpmDirectory('react-native', '0.9999.0'); + createNpmDirectory('jsc-android', '888888.0.0'); + createNpmDirectory('@babel/runtime', '5555.0.0'); + + ensureNodeModulesSymlink(workspaceDir, appDir); + + expectSymlinkToExist('@nrwl/react-native'); + expectSymlinkToExist('react-native'); + expectSymlinkToExist('jsc-android'); + expectSymlinkToExist('hermes-engine'); + expectSymlinkToExist('@react-native-community/cli-platform-ios'); + expectSymlinkToExist('@react-native-community/cli-platform-android'); + expectSymlinkToExist('@babel/runtime'); + }); + + it('should add packages listed in workspace package.json', () => { + fs.writeFileSync( + join(workspaceDir, 'package.json'), + JSON.stringify({ + name: 'workspace', + dependencies: { + random: '9999.9.9', + }, + }) + ); + createNpmDirectory('@nrwl/react-native', '9999.9.9'); + createNpmDirectory( + '@react-native-community/cli-platform-android', + '7777.7.7' + ); + createNpmDirectory('@react-native-community/cli-platform-ios', '7777.7.7'); + createNpmDirectory('hermes-engine', '3333.3.3'); + createNpmDirectory('react-native', '0.9999.0'); + createNpmDirectory('jsc-android', '888888.0.0'); + createNpmDirectory('@babel/runtime', '5555.0.0'); + createNpmDirectory('random', '9999.9.9'); + + ensureNodeModulesSymlink(workspaceDir, appDir); + + expectSymlinkToExist('random'); + }); + + function createNpmDirectory(packageName, version) { + const dir = join(workspaceDir, `node_modules/${packageName}`); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync( + join(dir, 'package.json'), + JSON.stringify({ name: packageName, version: version }) + ); + return dir; + } + + function expectSymlinkToExist(packageName) { + expect( + fs.existsSync( + join(appDirAbsolutePath, `node_modules/${packageName}/package.json`) + ) + ).toBe(true); + } +}); diff --git a/packages/expo/src/utils/ensure-node-modules-symlink.ts b/packages/expo/src/utils/ensure-node-modules-symlink.ts new file mode 100644 index 0000000000000..7cb4aff161ea9 --- /dev/null +++ b/packages/expo/src/utils/ensure-node-modules-symlink.ts @@ -0,0 +1,31 @@ +import { join } from 'path'; +import { platform } from 'os'; +import * as fs from 'fs'; +import chalk = require('chalk'); + +/** + * This function symlink workspace node_modules folder with app project's node_mdules folder. + * For yarn and npm, it will symlink the entire node_modules folder. + * If app project's node_modules already exist, it will remove it first then symlink it. + * For pnpm, it will go through the package.json' dependencies and devDependencies, and also the required packages listed above. + * @param workspaceRoot path of the workspace root + * @param projectRoot path of app project root + */ +export function ensureNodeModulesSymlink( + workspaceRoot: string, + projectRoot: string +): void { + const worksapceNodeModulesPath = join(workspaceRoot, 'node_modules'); + if (!fs.existsSync(worksapceNodeModulesPath)) { + throw new Error(`Cannot find ${worksapceNodeModulesPath}`); + } + + const appNodeModulesPath = join(workspaceRoot, projectRoot, 'node_modules'); + // `mklink /D` requires admin privilege in Windows so we need to use junction + const symlinkType = platform() === 'win32' ? 'junction' : 'dir'; + + if (fs.existsSync(appNodeModulesPath)) { + fs.rmdirSync(appNodeModulesPath, { recursive: true }); + } + fs.symlinkSync(worksapceNodeModulesPath, appNodeModulesPath, symlinkType); +} diff --git a/packages/expo/src/utils/find-all-npm-dependencies.spec.ts b/packages/expo/src/utils/find-all-npm-dependencies.spec.ts new file mode 100644 index 0000000000000..3c0709ef2e6d6 --- /dev/null +++ b/packages/expo/src/utils/find-all-npm-dependencies.spec.ts @@ -0,0 +1,95 @@ +import { findAllNpmDependencies } from './find-all-npm-dependencies'; +import { ProjectGraph } from '@nrwl/workspace/src/core/project-graph'; + +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: [] }, + }, + }, + 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:@nrwl/react-native': { + type: 'npm', + name: 'npm:@nrwl/react-native', + data: { + version: '1', + packageName: '@nrwl/react-native', + }, + }, + }, + dependencies: { + myapp: [ + { type: 'static', source: 'myapp', target: 'lib1' }, + { type: 'static', source: 'myapp', target: 'lib2' }, + { + type: 'static', + source: 'myapp', + target: 'npm:react-native-image-picker', + }, + { + type: 'static', + source: 'myapp', + target: 'npm:@nrwl/react-native', + }, + ], + lib1: [ + { type: 'static', source: 'lib1', target: 'lib2' }, + { type: 'static', source: 'lib3', target: 'npm:react-native-snackbar' }, + ], + lib2: [{ type: 'static', source: 'lib2', target: 'lib3' }], + lib3: [ + { type: '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', + ]); +}); diff --git a/packages/expo/src/utils/find-all-npm-dependencies.ts b/packages/expo/src/utils/find-all-npm-dependencies.ts new file mode 100644 index 0000000000000..7e40b9273c249 --- /dev/null +++ b/packages/expo/src/utils/find-all-npm-dependencies.ts @@ -0,0 +1,30 @@ +import { ProjectGraph } from '@nrwl/workspace/src/core/project-graph'; + +export function findAllNpmDependencies( + graph: ProjectGraph, + projectName: string, + list: string[] = [], + seen = new Set() +) { + // 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 '@nrwl/react-native' because React Native + // autolink will warn that the package has no podspec file for iOS. + if (node) { + if (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; +} diff --git a/packages/expo/src/utils/symlink-task.ts b/packages/expo/src/utils/symlink-task.ts new file mode 100644 index 0000000000000..47662c59e55c9 --- /dev/null +++ b/packages/expo/src/utils/symlink-task.ts @@ -0,0 +1,19 @@ +import { ensureNodeModulesSymlink } from './ensure-node-modules-symlink'; +import * as chalk from 'chalk'; +import { GeneratorCallback, logger } from '@nrwl/devkit'; + +export function runSymlink( + worksapceRoot: string, + projectRoot: string +): GeneratorCallback { + return () => { + logger.info(`creating symlinks for ${chalk.bold(projectRoot)}`); + try { + ensureNodeModulesSymlink(worksapceRoot, projectRoot); + } catch { + throw new Error( + `Failed to create symlinks for ${chalk.bold(projectRoot)}` + ); + } + }; +} diff --git a/packages/expo/src/utils/versions.ts b/packages/expo/src/utils/versions.ts new file mode 100644 index 0000000000000..0abc1e479da96 --- /dev/null +++ b/packages/expo/src/utils/versions.ts @@ -0,0 +1,22 @@ +export const nxVersion = '*'; + +export const expoVersion = '43.0.3'; +export const expoStatusBarVersion = '1.1.0'; +export const expoMetroConfigVersion = '0.3.2'; +export const expoStructuredHeadersVersion = '2.0.0'; +export const expoSplashScreenVersion = '0.13.5'; +export const expoUpdatesVersion = '0.10.15'; +export const jestExpoVersion = '43.0.1'; + +export const reactNativeVersion = '0.64.3'; +export const typesReactNativeVersion = '0.64.19'; +export const reactNativeWebVersion = '0.17.5'; +export const reactNativeGestureHandlerVersion = '1.10.3'; +export const reactNativeReanimatedVersion = '2.2.4'; +export const reactNativeSafeAreaContextVersion = '3.3.2'; +export const reactNativeScreensVersion = '3.9.0'; + +export const metroVersion = '0.66.2'; + +export const testingLibraryReactNativeVersion = '8.0.0'; +export const testingLibraryJestNativeVersion = '4.0.4'; diff --git a/packages/expo/tsconfig.json b/packages/expo/tsconfig.json new file mode 100644 index 0000000000000..62ebbd946474c --- /dev/null +++ b/packages/expo/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/expo/tsconfig.lib.json b/packages/expo/tsconfig.lib.json new file mode 100644 index 0000000000000..6efdbeecb5481 --- /dev/null +++ b/packages/expo/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "exclude": ["**/*.spec.ts", "**/*.test.ts"], + "include": ["**/*.ts"] +} diff --git a/packages/expo/tsconfig.spec.json b/packages/expo/tsconfig.spec.json new file mode 100644 index 0000000000000..7175fb8305dc4 --- /dev/null +++ b/packages/expo/tsconfig.spec.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.test.tsx", + "**/*.spec.tsx", + "**/*.test.js", + "**/*.spec.js", + "**/*.test.jsx", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/packages/react-native/src/executors/run-ios/schema.json b/packages/react-native/src/executors/run-ios/schema.json index 84ea7aba439e5..3f4649890114d 100644 --- a/packages/react-native/src/executors/run-ios/schema.json +++ b/packages/react-native/src/executors/run-ios/schema.json @@ -8,7 +8,7 @@ "properties": { "xcodeConfiguration": { "type": "string", - "description": "Explicitly set the Xcode configuration to use", + "description": "Explicitly set the Xcode configuration to use. Debug or Release.", "default": "Debug" }, "scheme": { diff --git a/packages/react-native/src/generators/application/lib/add-detox.ts b/packages/react-native/src/generators/application/lib/add-detox.ts index eef3d4787716d..58615f12dab46 100644 --- a/packages/react-native/src/generators/application/lib/add-detox.ts +++ b/packages/react-native/src/generators/application/lib/add-detox.ts @@ -9,10 +9,13 @@ export async function addDetox(host: Tree, options: NormalizedSchema) { } return detoxApplicationGenerator(host, { - ...options, linter: Linter.EsLint, name: `${options.name}-e2e`, directory: options.directory, project: options.projectName, + type: 'react-native', + js: options.js, + skipFormat: options.skipFormat, + setParserOptionsProject: options.setParserOptionsProject, }); } diff --git a/packages/react-native/src/generators/application/schema.json b/packages/react-native/src/generators/application/schema.json index 4a6a4f4054454..625c4ef4bdb39 100644 --- a/packages/react-native/src/generators/application/schema.json +++ b/packages/react-native/src/generators/application/schema.json @@ -72,5 +72,5 @@ "default": "detox" } }, - "required": [] + "required": ["name"] } diff --git a/packages/react-native/src/generators/init/init.spec.ts b/packages/react-native/src/generators/init/init.spec.ts index 99afc69657ba1..2547726c8a86c 100644 --- a/packages/react-native/src/generators/init/init.spec.ts +++ b/packages/react-native/src/generators/init/init.spec.ts @@ -31,7 +31,6 @@ describe('init', () => { const content = tree.read('/.gitignore').toString(); expect(content).toMatch(/# React Native/); - expect(content).toMatch(/# Nested node_modules/); }); describe('defaultCollection', () => { diff --git a/packages/react-native/src/generators/init/init.ts b/packages/react-native/src/generators/init/init.ts index 9739cce2e6ade..ad758ffbc898f 100644 --- a/packages/react-native/src/generators/init/init.ts +++ b/packages/react-native/src/generators/init/init.ts @@ -46,7 +46,7 @@ export async function reactNativeInitGenerator(host: Tree, schema: Schema) { } if (!schema.e2eTestRunner || schema.e2eTestRunner === 'detox') { - const detoxTask = await detoxInitGenerator(host, {}); + const detoxTask = await detoxInitGenerator(host); tasks.push(detoxTask); } diff --git a/packages/react-native/src/generators/init/lib/add-git-ignore-entry.ts b/packages/react-native/src/generators/init/lib/add-git-ignore-entry.ts index d1fcae7717055..f12460deed13a 100644 --- a/packages/react-native/src/generators/init/lib/add-git-ignore-entry.ts +++ b/packages/react-native/src/generators/init/lib/add-git-ignore-entry.ts @@ -14,12 +14,7 @@ export function addGitIgnoreEntry(host: Tree) { ig.add(host.read('.gitignore').toString()); if (!ig.ignores('apps/example/ios/Pods/Folly')) { - content = `${content}\n${gitIgnoreEntriesForReactNative}/\n`; - } - - // also ignore nested node_modules folders due to symlink for React Native - if (!ig.ignores('apps/example/node_modules')) { - content = `${content}\n## Nested node_modules\n\nnode_modules/\n`; + content = `${content}\n${gitIgnoreEntriesForReactNative}\n`; } host.write('.gitignore', content); diff --git a/packages/react-native/src/generators/init/lib/gitignore-entries.ts b/packages/react-native/src/generators/init/lib/gitignore-entries.ts index 70dc2a827ae6a..d6983d6a06639 100644 --- a/packages/react-native/src/generators/init/lib/gitignore-entries.ts +++ b/packages/react-native/src/generators/init/lib/gitignore-entries.ts @@ -50,4 +50,7 @@ buck-out/ ## CocoaPods **/ios/Pods/ + +## Nested node_modules +**/node_modules/ `; diff --git a/packages/react-native/src/generators/library/files/lib/tsconfig.json__tmpl__ b/packages/react-native/src/generators/library/files/lib/tsconfig.json__tmpl__ index 514efa55d7104..441ee25621bd7 100644 --- a/packages/react-native/src/generators/library/files/lib/tsconfig.json__tmpl__ +++ b/packages/react-native/src/generators/library/files/lib/tsconfig.json__tmpl__ @@ -1,7 +1,7 @@ { "extends": "<%= offsetFromRoot %>tsconfig.base.json", "compilerOptions": { - "jsx": "react-jsx", + "jsx": "react-native", "allowJs": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true diff --git a/packages/react-native/src/utils/add-jest.ts b/packages/react-native/src/utils/add-jest.ts index 85d72ab9ba079..6dcf32e0dbadc 100644 --- a/packages/react-native/src/utils/add-jest.ts +++ b/packages/react-native/src/utils/add-jest.ts @@ -29,6 +29,9 @@ export async function addJest( resolver: '@nrwl/jest/plugins/resolver', moduleFileExtensions: ['ts', 'js', 'html', 'tsx', 'jsx'], setupFilesAfterEnv: ['/test-setup.${js ? 'js' : 'ts'}'], + 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)', + ], moduleNameMapper: { '\\.svg': '@nrwl/react-native/plugins/jest/svg-mock' }, diff --git a/packages/workspace/src/generators/new/new.ts b/packages/workspace/src/generators/new/new.ts index 6b05c4a362b8e..431df8aefe551 100644 --- a/packages/workspace/src/generators/new/new.ts +++ b/packages/workspace/src/generators/new/new.ts @@ -8,9 +8,7 @@ import { convertNxGenerator, names, getPackageManagerCommand, - WorkspaceJsonConfiguration, PackageManager, - NxJsonConfiguration, } from '@nrwl/devkit'; import { join } from 'path'; @@ -265,6 +263,12 @@ const presetDependencies: Omit< '@nrwl/react-native': nxVersion, }, }, + [Preset.Expo]: { + dependencies: {}, + dev: { + '@nrwl/expo': nxVersion, + }, + }, }; function addPresetDependencies(host: Tree, options: Schema) { diff --git a/packages/workspace/src/generators/preset/preset.ts b/packages/workspace/src/generators/preset/preset.ts index 0770c53bbadea..39fbaafcdc255 100644 --- a/packages/workspace/src/generators/preset/preset.ts +++ b/packages/workspace/src/generators/preset/preset.ts @@ -177,7 +177,7 @@ async function createPreset(tree: Tree, options: Schema) { standaloneConfig: options.standaloneConfig, }); setDefaultCollection(tree, '@nrwl/gatsby'); - } else if (options.preset === 'react-native') { + } else if (options.preset === Preset.ReactNative) { const { reactNativeApplicationGenerator } = require('@nrwl' + '/react-native'); await reactNativeApplicationGenerator(tree, { @@ -187,6 +187,15 @@ async function createPreset(tree: Tree, options: Schema) { e2eTestRunner: 'detox', }); setDefaultCollection(tree, '@nrwl/react-native'); + } else if (options.preset === Preset.Expo) { + const { expoApplicationGenerator } = require('@nrwl' + '/expo'); + await expoApplicationGenerator(tree, { + name: options.name, + linter: options.linter, + standaloneConfig: options.standaloneConfig, + e2eTestRunner: 'detox', + }); + setDefaultCollection(tree, '@nrwl/expo'); } else { throw new Error(`Invalid preset ${options.preset}`); } diff --git a/packages/workspace/src/generators/utils/presets.ts b/packages/workspace/src/generators/utils/presets.ts index 41f24cf961db5..cdae85fc14401 100644 --- a/packages/workspace/src/generators/utils/presets.ts +++ b/packages/workspace/src/generators/utils/presets.ts @@ -11,4 +11,5 @@ export enum Preset { Nest = 'nest', Express = 'express', ReactNative = 'react-native', + Expo = 'expo', } diff --git a/scripts/e2e-build-package-publish.ts b/scripts/e2e-build-package-publish.ts index d10fc2ceaad4a..95f3adddac784 100644 --- a/scripts/e2e-build-package-publish.ts +++ b/scripts/e2e-build-package-publish.ts @@ -133,6 +133,7 @@ function build(nxVersion: string) { 'angular', 'workspace', 'react-native', + 'expo', 'detox', 'js', ].map((f) => `${f}/src/utils/versions.js`), @@ -158,6 +159,7 @@ function build(nxVersion: string) { 'create-nx-plugin', 'nx-plugin', 'react-native', + 'expo', 'detox', 'js', ].map((f) => `${f}/package.json`), diff --git a/scripts/nx-release.js b/scripts/nx-release.js index 588217fdac572..1587032fabb60 100755 --- a/scripts/nx-release.js +++ b/scripts/nx-release.js @@ -74,6 +74,7 @@ function updatePackageJsonFiles(parsedVersion, isLocal) { 'build/npm/nx-plugin/package.json', 'build/npm/nx/package.json', 'build/npm/react-native/package.json', + 'build/npm/expo/package.json', 'build/npm/detox/package.json', 'build/npm/js/package.json', ]; diff --git a/scripts/package.sh b/scripts/package.sh index 50a736f890da4..f73098c48fdf0 100755 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -19,7 +19,7 @@ cd build/packages if [[ "$OSTYPE" == "darwin"* ]]; then sed -i "" "s|exports.nxVersion = '\*';|exports.nxVersion = '$NX_VERSION';|g" {react,next,gatsby,web,jest,node,linter,express,nest,cypress,storybook,angular,workspace,nx-plugin,react-native,detox,js}/src/utils/versions.js - sed -i "" "s|\*|$NX_VERSION|g" {nx,react,next,gatsby,web,jest,node,express,nest,cypress,storybook,angular,workspace,cli,linter,tao,devkit,eslint-plugin-nx,create-nx-workspace,create-nx-plugin,nx-plugin,react-native,detox,js}/package.json + sed -i "" "s|\*|$NX_VERSION|g" {nx,react,next,gatsby,web,jest,node,express,nest,cypress,storybook,angular,workspace,cli,linter,tao,devkit,eslint-plugin-nx,create-nx-workspace,create-nx-plugin,nx-plugin,react-native,expo,detox,js}/package.json sed -i "" "s|NX_VERSION|$NX_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js sed -i "" "s|ANGULAR_CLI_VERSION|$ANGULAR_CLI_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js sed -i "" "s|TYPESCRIPT_VERSION|$TYPESCRIPT_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js @@ -30,7 +30,7 @@ if [[ "$OSTYPE" == "darwin"* ]]; then sed -i "" "s|PRETTIER_VERSION|$PRETTIER_VERSION|g" create-nx-plugin/bin/create-nx-plugin.js else sed -i "s|exports.nxVersion = '\*';|exports.nxVersion = '$NX_VERSION';|g" {react,next,gatsby,web,jest,node,linter,express,nest,cypress,storybook,angular,workspace,nx-plugin,react-native,detox,js}/src/utils/versions.js - sed -i "s|\*|$NX_VERSION|g" {nx,react,next,gatsby,web,jest,node,express,nest,cypress,storybook,angular,workspace,cli,linter,tao,devkit,eslint-plugin-nx,create-nx-workspace,create-nx-plugin,nx-plugin,react-native,detox,js}/package.json + sed -i "s|\*|$NX_VERSION|g" {nx,react,next,gatsby,web,jest,node,express,nest,cypress,storybook,angular,workspace,cli,linter,tao,devkit,eslint-plugin-nx,create-nx-workspace,create-nx-plugin,nx-plugin,react-native,expo,detox,js}/package.json sed -i "s|NX_VERSION|$NX_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js sed -i "s|ANGULAR_CLI_VERSION|$ANGULAR_CLI_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js sed -i "s|TYPESCRIPT_VERSION|$TYPESCRIPT_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js @@ -43,9 +43,9 @@ fi if [[ $NX_VERSION == "*" ]]; then if [[ "$OSTYPE" == "darwin"* ]]; then - sed -E -i "" "s|\"@nrwl\/([^\"]+)\": \"\\*\"|\"@nrwl\/\1\": \"file:$PWD\/\1\"|" {nx,jest,web,react,next,gatsby,node,express,nest,cypress,storybook,angular,workspace,linter,cli,tao,devkit,eslint-plugin-nx,create-nx-workspace,create-nx-plugin,nx-plugin,react-native,detox}/package.json + sed -E -i "" "s|\"@nrwl\/([^\"]+)\": \"\\*\"|\"@nrwl\/\1\": \"file:$PWD\/\1\"|" {nx,jest,web,react,next,gatsby,node,express,nest,cypress,storybook,angular,workspace,linter,cli,tao,devkit,eslint-plugin-nx,create-nx-workspace,create-nx-plugin,nx-plugin,react-native,expo,detox}/package.json else echo $PWD - sed -E -i "s|\"@nrwl\/([^\"]+)\": \"\\*\"|\"@nrwl\/\1\": \"file:$PWD\/\1\"|" {nx,jest,web,react,next,gatsby,node,express,nest,cypress,storybook,angular,workspace,linter,cli,tao,devkit,eslint-plugin-nx,create-nx-workspace,create-nx-plugin,nx-plugin,react-native,detox}/package.json + sed -E -i "s|\"@nrwl\/([^\"]+)\": \"\\*\"|\"@nrwl\/\1\": \"file:$PWD\/\1\"|" {nx,jest,web,react,next,gatsby,node,express,nest,cypress,storybook,angular,workspace,linter,cli,tao,devkit,eslint-plugin-nx,create-nx-workspace,create-nx-plugin,nx-plugin,react-native,expo,detox}/package.json fi fi diff --git a/tsconfig.base.json b/tsconfig.base.json index 8ffae4965838a..3f74a9c924372 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -29,6 +29,7 @@ "@nrwl/e2e/cli": ["./e2e/cli"], "@nrwl/e2e/utils": ["./e2e/utils"], "@nrwl/eslint-plugin-nx": ["./packages/eslint-plugin-nx/src"], + "@nrwl/expo": ["./packages/expo"], "@nrwl/express": ["./packages/express"], "@nrwl/gatsby": ["./packages/gatsby"], "@nrwl/jest": ["./packages/jest"], @@ -64,6 +65,7 @@ "@nrwl/nx-dev/ui-sponsor-card": ["./nx-dev/ui-sponsor-card/src/index.ts"], "@nrwl/react": ["./packages/react"], "@nrwl/react-native": ["./packages/react-native"], + "@nrwl/react-native/*": ["./packages/react-native/*"], "@nrwl/react/*": ["./packages/react/*"], "@nrwl/storybook": ["./packages/storybook"], "@nrwl/tao": ["./packages/tao"], diff --git a/workspace.json b/workspace.json index 858842c6cbb0c..d839a905e3a82 100644 --- a/workspace.json +++ b/workspace.json @@ -16,6 +16,7 @@ "e2e-cli": "e2e/cli", "e2e-cypress": "e2e/cypress", "e2e-detox": "e2e/detox", + "e2e-expo": "e2e/expo", "e2e-gatsby": "e2e/gatsby", "e2e-jest": "e2e/jest", "e2e-js": "e2e/js", @@ -32,6 +33,7 @@ "e2e-workspace-create": "e2e/workspace-create", "e2e-workspace-integrations": "e2e/workspace-integrations", "eslint-plugin-nx": "packages/eslint-plugin-nx", + "expo": "packages/expo", "express": "packages/express", "gatsby": "packages/gatsby", "jest": "packages/jest",