From 35b4e1292ac2c3f8a52e89a4b25654bd2df319b7 Mon Sep 17 00:00:00 2001 From: Emily Xiong Date: Sun, 28 Nov 2021 13:21:13 -0500 Subject: [PATCH] feat(repo): add support for expo in repo --- .../api-detox/generators/application.md | 10 + .../api-expo/executors/build-android.md | 67 ++++ docs/angular/api-expo/executors/build-ios.md | 131 +++++++ .../api-expo/executors/build-status.md | 13 + docs/angular/api-expo/executors/build-web.md | 27 ++ .../api-expo/executors/ensure-symlink.md | 5 + docs/angular/api-expo/executors/run.md | 73 ++++ docs/angular/api-expo/executors/start.md | 123 ++++++ docs/angular/api-expo/executors/sync-deps.md | 13 + .../api-expo/generators/application.md | 125 +++++++ docs/angular/api-expo/generators/component.md | 127 +++++++ docs/angular/api-expo/generators/library.md | 157 ++++++++ docs/angular/api-nx-devkit/index.md | 2 +- .../api-react-native/executors/run-ios.md | 2 +- .../generators/application.md | 12 +- docs/angular/executors.json | 1 + docs/angular/generators.json | 1 + docs/map.json | 183 +++++++++ docs/node/api-detox/generators/application.md | 10 + docs/node/api-expo/executors/build-android.md | 67 ++++ docs/node/api-expo/executors/build-ios.md | 131 +++++++ docs/node/api-expo/executors/build-status.md | 13 + docs/node/api-expo/executors/build-web.md | 27 ++ .../node/api-expo/executors/ensure-symlink.md | 5 + docs/node/api-expo/executors/run.md | 73 ++++ docs/node/api-expo/executors/start.md | 123 ++++++ docs/node/api-expo/executors/sync-deps.md | 13 + docs/node/api-expo/generators/application.md | 125 +++++++ docs/node/api-expo/generators/component.md | 127 +++++++ docs/node/api-expo/generators/library.md | 157 ++++++++ docs/node/api-nx-devkit/index.md | 2 +- .../api-react-native/executors/run-ios.md | 2 +- .../generators/application.md | 12 +- docs/node/executors.json | 1 + docs/node/generators.json | 1 + .../react/api-detox/generators/application.md | 10 + .../react/api-expo/executors/build-android.md | 67 ++++ docs/react/api-expo/executors/build-ios.md | 131 +++++++ docs/react/api-expo/executors/build-status.md | 13 + docs/react/api-expo/executors/build-web.md | 27 ++ .../api-expo/executors/ensure-symlink.md | 5 + docs/react/api-expo/executors/run.md | 73 ++++ docs/react/api-expo/executors/start.md | 123 ++++++ docs/react/api-expo/executors/sync-deps.md | 13 + docs/react/api-expo/generators/application.md | 125 +++++++ docs/react/api-expo/generators/component.md | 127 +++++++ docs/react/api-expo/generators/library.md | 157 ++++++++ docs/react/api-nx-devkit/index.md | 2 +- .../api-react-native/executors/run-ios.md | 2 +- .../generators/application.md | 12 +- docs/react/executors.json | 1 + docs/react/generators.json | 1 + e2e/expo/jest.config.js | 11 + e2e/expo/project.json | 34 ++ e2e/expo/tests/expo.test.ts | 40 ++ e2e/expo/tsconfig.json | 13 + e2e/expo/tsconfig.spec.json | 19 + e2e/utils/index.ts | 1 + .../bin/create-nx-workspace.ts | 8 +- .../application/application.spec.ts | 13 + .../src/generators/application/application.ts | 2 +- .../files/app/test-setup.ts.template | 1 - .../application/lib/add-linting.spec.ts | 16 + .../application/lib/add-project.spec.ts | 8 + .../application/lib/create-files.spec.ts | 4 + .../application/lib/normalize-options.spec.ts | 38 ++ .../src/generators/application/schema.d.ts | 9 +- .../src/generators/application/schema.json | 6 + packages/detox/src/generators/init/init.ts | 4 +- .../detox/src/generators/init/schema.d.ts | 2 +- packages/devkit/src/utils/names.ts | 2 +- packages/expo/.babelrc | 3 + packages/expo/.eslintrc.json | 5 + packages/expo/README.md | 7 + packages/expo/collection.json | 0 packages/expo/executors.json | 86 +++++ packages/expo/generators.json | 61 +++ packages/expo/index.ts | 3 + packages/expo/jest.config.js | 12 + packages/expo/package.json | 48 +++ packages/expo/plugins/metro-resolver.ts | 194 ++++++++++ packages/expo/plugins/with-nx-metro.ts | 32 ++ packages/expo/project.json | 83 +++++ .../build-android/build-android.impl.ts | 73 ++++ .../src/executors/build-android/compat.ts | 5 + .../src/executors/build-android/schema.d.ts | 12 + .../src/executors/build-android/schema.json | 49 +++ .../src/executors/build-ios/build-ios.impl.ts | 87 +++++ .../expo/src/executors/build-ios/compat.ts | 5 + .../expo/src/executors/build-ios/schema.d.ts | 23 ++ .../expo/src/executors/build-ios/schema.json | 91 +++++ .../build-status/build-status.impl.ts | 73 ++++ .../expo/src/executors/build-status/compat.ts | 5 + .../src/executors/build-status/schema.d.ts | 4 + .../src/executors/build-status/schema.json | 15 + .../src/executors/build-web/build-web.impl.ts | 73 ++++ .../expo/src/executors/build-web/compat.ts | 5 + .../expo/src/executors/build-web/schema.d.ts | 6 + .../expo/src/executors/build-web/schema.json | 24 ++ .../src/executors/ensure-symlink/compat.ts | 5 + .../ensure-symlink/ensure-symlink.impl.ts | 17 + .../src/executors/ensure-symlink/schema.json | 9 + packages/expo/src/executors/run/compat.ts | 5 + packages/expo/src/executors/run/run.impl.ts | 108 ++++++ packages/expo/src/executors/run/schema.d.ts | 11 + packages/expo/src/executors/run/schema.json | 51 +++ packages/expo/src/executors/start/compat.ts | 5 + packages/expo/src/executors/start/schema.d.ts | 22 ++ packages/expo/src/executors/start/schema.json | 85 +++++ .../expo/src/executors/start/start.impl.ts | 90 +++++ .../expo/src/executors/sync-deps/compat.ts | 5 + .../expo/src/executors/sync-deps/schema.d.ts | 3 + .../expo/src/executors/sync-deps/schema.json | 14 + .../src/executors/sync-deps/sync-deps.impl.ts | 84 +++++ .../application/application.spec.ts | 93 +++++ .../src/generators/application/application.ts | 59 +++ .../application/files/.babelrc.template | 3 + .../application/files/app.json.template | 32 ++ .../files/assets/adaptive-icon.png | Bin 0 -> 17547 bytes .../application/files/assets/favicon.png | Bin 0 -> 1466 bytes .../application/files/assets/icon.png | Bin 0 -> 22380 bytes .../application/files/assets/logo.png | Bin 0 -> 28693 bytes .../application/files/assets/splash.png | Bin 0 -> 47346 bytes .../application/files/assets/star.svg | 11 + .../src/generators/application/files/eas.json | 18 + .../application/files/index.js.template | 9 + .../files/metro.config.js.template | 14 + .../application/files/package.json.template | 28 ++ .../files/src/app/App.spec.tsx.template | 9 + .../files/src/app/App.tsx.template | 131 +++++++ .../application/files/test-setup.ts.template | 1 + .../files/tsconfig.app.json.template | 9 + .../application/files/tsconfig.json.template | 23 ++ .../generators/application/lib/add-detox.ts | 20 + .../generators/application/lib/add-project.ts | 103 +++++ .../lib/create-application-files.ts | 16 + .../application/lib/nomalize-options.spec.ts | 138 +++++++ .../application/lib/normalize-options.ts | 48 +++ .../src/generators/application/schema.d.ts | 17 + .../src/generators/application/schema.json | 76 ++++ .../generators/component/component.spec.ts | 159 ++++++++ .../src/generators/component/component.ts | 87 +++++ .../files/__fileName__.spec.tsx.template | 11 + .../component/files/__fileName__.tsx.template | 32 ++ .../generators/component/lib/add-import.ts | 28 ++ .../component/lib/normalize-options.ts | 83 +++++ .../expo/src/generators/component/schema.d.ts | 15 + .../expo/src/generators/component/schema.json | 82 ++++ .../expo/src/generators/init/init.spec.ts | 59 +++ packages/expo/src/generators/init/init.ts | 98 +++++ .../init/lib/add-git-ignore-entry.ts | 17 + .../generators/init/lib/gitignore-entries.ts | 14 + packages/expo/src/generators/init/schema.d.ts | 5 + packages/expo/src/generators/init/schema.json | 27 ++ .../library/files/lib/.babelrc.template | 3 + .../generators/library/files/lib/README.md | 7 + .../library/files/lib/package.json.template | 4 + .../library/files/lib/src/index.ts.template | 0 .../library/files/lib/test-setup.ts.template | 1 + .../library/files/lib/tsconfig.json.template | 16 + .../files/lib/tsconfig.lib.json.template | 9 + .../library/lib/normalize-options.ts | 52 +++ .../src/generators/library/library.spec.ts | 351 ++++++++++++++++++ .../expo/src/generators/library/library.ts | 197 ++++++++++ .../expo/src/generators/library/schema.d.ts | 22 ++ .../expo/src/generators/library/schema.json | 97 +++++ packages/expo/src/utils/add-jest.ts | 44 +++ packages/expo/src/utils/add-linting.spec.ts | 60 +++ packages/expo/src/utils/add-linting.ts | 68 ++++ .../utils/ensure-node-modules-symlink.spec.ts | 110 ++++++ .../src/utils/ensure-node-modules-symlink.ts | 31 ++ .../utils/find-all-npm-dependencies.spec.ts | 95 +++++ .../src/utils/find-all-npm-dependencies.ts | 30 ++ packages/expo/src/utils/symlink-task.ts | 19 + packages/expo/src/utils/versions.ts | 22 ++ packages/expo/tsconfig.json | 13 + packages/expo/tsconfig.lib.json | 11 + packages/expo/tsconfig.spec.json | 19 + .../src/executors/run-ios/schema.json | 2 +- .../generators/application/lib/add-detox.ts | 5 +- .../src/generators/application/schema.json | 2 +- .../src/generators/init/init.spec.ts | 1 - .../react-native/src/generators/init/init.ts | 2 +- .../init/lib/add-git-ignore-entry.ts | 7 +- .../generators/init/lib/gitignore-entries.ts | 3 + .../library/files/lib/tsconfig.json__tmpl__ | 2 +- packages/react-native/src/utils/add-jest.ts | 3 + packages/workspace/src/generators/new/new.ts | 8 +- .../workspace/src/generators/preset/preset.ts | 11 +- .../workspace/src/generators/utils/presets.ts | 1 + scripts/e2e-build-package-publish.ts | 2 + scripts/nx-release.js | 1 + scripts/package.sh | 8 +- tsconfig.base.json | 2 + workspace.json | 2 + 195 files changed, 7543 insertions(+), 54 deletions(-) create mode 100644 docs/angular/api-expo/executors/build-android.md create mode 100644 docs/angular/api-expo/executors/build-ios.md create mode 100644 docs/angular/api-expo/executors/build-status.md create mode 100644 docs/angular/api-expo/executors/build-web.md create mode 100644 docs/angular/api-expo/executors/ensure-symlink.md create mode 100644 docs/angular/api-expo/executors/run.md create mode 100644 docs/angular/api-expo/executors/start.md create mode 100644 docs/angular/api-expo/executors/sync-deps.md create mode 100644 docs/angular/api-expo/generators/application.md create mode 100644 docs/angular/api-expo/generators/component.md create mode 100644 docs/angular/api-expo/generators/library.md create mode 100644 docs/node/api-expo/executors/build-android.md create mode 100644 docs/node/api-expo/executors/build-ios.md create mode 100644 docs/node/api-expo/executors/build-status.md create mode 100644 docs/node/api-expo/executors/build-web.md create mode 100644 docs/node/api-expo/executors/ensure-symlink.md create mode 100644 docs/node/api-expo/executors/run.md create mode 100644 docs/node/api-expo/executors/start.md create mode 100644 docs/node/api-expo/executors/sync-deps.md create mode 100644 docs/node/api-expo/generators/application.md create mode 100644 docs/node/api-expo/generators/component.md create mode 100644 docs/node/api-expo/generators/library.md create mode 100644 docs/react/api-expo/executors/build-android.md create mode 100644 docs/react/api-expo/executors/build-ios.md create mode 100644 docs/react/api-expo/executors/build-status.md create mode 100644 docs/react/api-expo/executors/build-web.md create mode 100644 docs/react/api-expo/executors/ensure-symlink.md create mode 100644 docs/react/api-expo/executors/run.md create mode 100644 docs/react/api-expo/executors/start.md create mode 100644 docs/react/api-expo/executors/sync-deps.md create mode 100644 docs/react/api-expo/generators/application.md create mode 100644 docs/react/api-expo/generators/component.md create mode 100644 docs/react/api-expo/generators/library.md create mode 100644 e2e/expo/jest.config.js create mode 100644 e2e/expo/project.json create mode 100644 e2e/expo/tests/expo.test.ts create mode 100644 e2e/expo/tsconfig.json create mode 100644 e2e/expo/tsconfig.spec.json create mode 100644 packages/expo/.babelrc create mode 100644 packages/expo/.eslintrc.json create mode 100644 packages/expo/README.md create mode 100644 packages/expo/collection.json create mode 100644 packages/expo/executors.json create mode 100644 packages/expo/generators.json create mode 100644 packages/expo/index.ts create mode 100644 packages/expo/jest.config.js create mode 100644 packages/expo/package.json create mode 100644 packages/expo/plugins/metro-resolver.ts create mode 100644 packages/expo/plugins/with-nx-metro.ts create mode 100644 packages/expo/project.json create mode 100644 packages/expo/src/executors/build-android/build-android.impl.ts create mode 100644 packages/expo/src/executors/build-android/compat.ts create mode 100644 packages/expo/src/executors/build-android/schema.d.ts create mode 100644 packages/expo/src/executors/build-android/schema.json create mode 100644 packages/expo/src/executors/build-ios/build-ios.impl.ts create mode 100644 packages/expo/src/executors/build-ios/compat.ts create mode 100644 packages/expo/src/executors/build-ios/schema.d.ts create mode 100644 packages/expo/src/executors/build-ios/schema.json create mode 100644 packages/expo/src/executors/build-status/build-status.impl.ts create mode 100644 packages/expo/src/executors/build-status/compat.ts create mode 100644 packages/expo/src/executors/build-status/schema.d.ts create mode 100644 packages/expo/src/executors/build-status/schema.json create mode 100644 packages/expo/src/executors/build-web/build-web.impl.ts create mode 100644 packages/expo/src/executors/build-web/compat.ts create mode 100644 packages/expo/src/executors/build-web/schema.d.ts create mode 100644 packages/expo/src/executors/build-web/schema.json create mode 100644 packages/expo/src/executors/ensure-symlink/compat.ts create mode 100644 packages/expo/src/executors/ensure-symlink/ensure-symlink.impl.ts create mode 100644 packages/expo/src/executors/ensure-symlink/schema.json create mode 100644 packages/expo/src/executors/run/compat.ts create mode 100644 packages/expo/src/executors/run/run.impl.ts create mode 100644 packages/expo/src/executors/run/schema.d.ts create mode 100644 packages/expo/src/executors/run/schema.json create mode 100644 packages/expo/src/executors/start/compat.ts create mode 100644 packages/expo/src/executors/start/schema.d.ts create mode 100644 packages/expo/src/executors/start/schema.json create mode 100644 packages/expo/src/executors/start/start.impl.ts create mode 100644 packages/expo/src/executors/sync-deps/compat.ts create mode 100644 packages/expo/src/executors/sync-deps/schema.d.ts create mode 100644 packages/expo/src/executors/sync-deps/schema.json create mode 100644 packages/expo/src/executors/sync-deps/sync-deps.impl.ts create mode 100644 packages/expo/src/generators/application/application.spec.ts create mode 100644 packages/expo/src/generators/application/application.ts create mode 100644 packages/expo/src/generators/application/files/.babelrc.template create mode 100644 packages/expo/src/generators/application/files/app.json.template create mode 100644 packages/expo/src/generators/application/files/assets/adaptive-icon.png create mode 100644 packages/expo/src/generators/application/files/assets/favicon.png create mode 100644 packages/expo/src/generators/application/files/assets/icon.png create mode 100644 packages/expo/src/generators/application/files/assets/logo.png create mode 100644 packages/expo/src/generators/application/files/assets/splash.png create mode 100644 packages/expo/src/generators/application/files/assets/star.svg create mode 100644 packages/expo/src/generators/application/files/eas.json create mode 100644 packages/expo/src/generators/application/files/index.js.template create mode 100644 packages/expo/src/generators/application/files/metro.config.js.template create mode 100644 packages/expo/src/generators/application/files/package.json.template create mode 100644 packages/expo/src/generators/application/files/src/app/App.spec.tsx.template create mode 100644 packages/expo/src/generators/application/files/src/app/App.tsx.template create mode 100644 packages/expo/src/generators/application/files/test-setup.ts.template create mode 100644 packages/expo/src/generators/application/files/tsconfig.app.json.template create mode 100644 packages/expo/src/generators/application/files/tsconfig.json.template create mode 100644 packages/expo/src/generators/application/lib/add-detox.ts create mode 100644 packages/expo/src/generators/application/lib/add-project.ts create mode 100644 packages/expo/src/generators/application/lib/create-application-files.ts create mode 100644 packages/expo/src/generators/application/lib/nomalize-options.spec.ts create mode 100644 packages/expo/src/generators/application/lib/normalize-options.ts create mode 100644 packages/expo/src/generators/application/schema.d.ts create mode 100644 packages/expo/src/generators/application/schema.json create mode 100644 packages/expo/src/generators/component/component.spec.ts create mode 100644 packages/expo/src/generators/component/component.ts create mode 100644 packages/expo/src/generators/component/files/__fileName__.spec.tsx.template create mode 100644 packages/expo/src/generators/component/files/__fileName__.tsx.template create mode 100644 packages/expo/src/generators/component/lib/add-import.ts create mode 100644 packages/expo/src/generators/component/lib/normalize-options.ts create mode 100644 packages/expo/src/generators/component/schema.d.ts create mode 100644 packages/expo/src/generators/component/schema.json create mode 100644 packages/expo/src/generators/init/init.spec.ts create mode 100644 packages/expo/src/generators/init/init.ts create mode 100644 packages/expo/src/generators/init/lib/add-git-ignore-entry.ts create mode 100644 packages/expo/src/generators/init/lib/gitignore-entries.ts create mode 100644 packages/expo/src/generators/init/schema.d.ts create mode 100644 packages/expo/src/generators/init/schema.json create mode 100644 packages/expo/src/generators/library/files/lib/.babelrc.template create mode 100644 packages/expo/src/generators/library/files/lib/README.md create mode 100644 packages/expo/src/generators/library/files/lib/package.json.template create mode 100644 packages/expo/src/generators/library/files/lib/src/index.ts.template create mode 100644 packages/expo/src/generators/library/files/lib/test-setup.ts.template create mode 100644 packages/expo/src/generators/library/files/lib/tsconfig.json.template create mode 100644 packages/expo/src/generators/library/files/lib/tsconfig.lib.json.template create mode 100644 packages/expo/src/generators/library/lib/normalize-options.ts create mode 100644 packages/expo/src/generators/library/library.spec.ts create mode 100644 packages/expo/src/generators/library/library.ts create mode 100644 packages/expo/src/generators/library/schema.d.ts create mode 100644 packages/expo/src/generators/library/schema.json create mode 100644 packages/expo/src/utils/add-jest.ts create mode 100644 packages/expo/src/utils/add-linting.spec.ts create mode 100644 packages/expo/src/utils/add-linting.ts create mode 100644 packages/expo/src/utils/ensure-node-modules-symlink.spec.ts create mode 100644 packages/expo/src/utils/ensure-node-modules-symlink.ts create mode 100644 packages/expo/src/utils/find-all-npm-dependencies.spec.ts create mode 100644 packages/expo/src/utils/find-all-npm-dependencies.ts create mode 100644 packages/expo/src/utils/symlink-task.ts create mode 100644 packages/expo/src/utils/versions.ts create mode 100644 packages/expo/tsconfig.json create mode 100644 packages/expo/tsconfig.lib.json create mode 100644 packages/expo/tsconfig.spec.json 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 0000000000000000000000000000000000000000..03d6f6b6c6727954aec1d8206222769afd178d8d GIT binary patch literal 17547 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a0b1526fc7b78680fd8d733dbc6113e1af695487 GIT binary patch literal 22380 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e9b9b6eb620ffe5c4ee48dec55e9a6392a136cf9 GIT binary patch literal 28693 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 LH03MgEJOYWSW^Zd literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0e89705a9436743e42954d3744a0e7ff0d3d4701 GIT binary patch literal 47346 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 literal 0 HcmV?d00001 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",