diff --git a/.cz-config.js b/.cz-config.js index 51b4dbf1de..5dbebd5825 100644 --- a/.cz-config.js +++ b/.cz-config.js @@ -35,6 +35,7 @@ module.exports = { { name: 'nxdev', description: 'anything related to docs infrastructure' }, { name: 'react', description: 'anything React specific' }, { name: 'react-native', description: 'anything React Native specific' }, + { name: 'expo', description: 'anything Expo specific' }, { name: 'repo', description: 'anything related to managing the repo itself', diff --git a/docs/generated/cli/create-nx-workspace.md b/docs/generated/cli/create-nx-workspace.md index d4b68352ac..04f3cdfb71 100644 --- a/docs/generated/cli/create-nx-workspace.md +++ b/docs/generated/cli/create-nx-workspace.md @@ -113,7 +113,7 @@ Package manager to use Type: string -Customizes the initial content of your workspace. Default presets include: ["apps", "empty", "core", "npm", "ts", "web-components", "angular", "angular-nest", "react", "react-express", "react-native", "next", "nest", "express"]. To build your own see https://nx.dev/packages/nx-plugin#preset +Customizes the initial content of your workspace. Default presets include: ["apps", "empty", "core", "npm", "ts", "web-components", "angular", "angular-nest", "react", "react-express", "react-native", "expo", "next", "nest", "express"]. To build your own see https://nx.dev/packages/nx-plugin#preset ### skipGit diff --git a/docs/generated/packages/expo.json b/docs/generated/packages/expo.json new file mode 100644 index 0000000000..46df0750c6 --- /dev/null +++ b/docs/generated/packages/expo.json @@ -0,0 +1,1161 @@ +{ + "githubRoot": "https://github.com/nrwl/nx/blob/master", + "name": "expo", + "packageName": "@nrwl/expo", + "description": "Expo Plugin for Nx", + "root": "/packages/expo", + "source": "/packages/expo/src", + "documentation": [], + "generators": [ + { + "name": "init", + "factory": "./src/generators/init/init#expoInitGenerator", + "schema": { + "cli": "nx", + "$id": "NxExpoInit", + "$schema": "http://json-schema.org/schema", + "title": "Add Nx Expo 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": [], + "presets": [] + }, + "description": "Initialize the @nrwl/expo plugin", + "hidden": true, + "implementation": "/packages/expo/src/generators/init/init#expoInitGenerator.ts", + "aliases": [], + "path": "/packages/expo/src/generators/init/schema.json" + }, + { + "name": "application", + "factory": "./src/generators/application/application#expoApplicationGenerator", + "schema": { + "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" + }, + "standaloneConfig": { + "description": "Split the project configuration into `/project.json` rather than including it inside `workspace.json`.", + "type": "boolean" + } + }, + "required": ["name"], + "presets": [] + }, + "aliases": ["app"], + "x-type": "application", + "description": "Create an application", + "implementation": "/packages/expo/src/generators/application/application#expoApplicationGenerator.ts", + "hidden": false, + "path": "/packages/expo/src/generators/application/schema.json" + }, + { + "name": "library", + "factory": "./src/generators/library/library#expoLibraryGenerator", + "schema": { + "cli": "nx", + "$id": "NxExpoLibrary", + "$schema": "http://json-schema.org/schema", + "title": "Create a Expo 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"], + "presets": [] + }, + "aliases": ["lib"], + "x-type": "library", + "description": "Create a library", + "implementation": "/packages/expo/src/generators/library/library#expoLibraryGenerator.ts", + "hidden": false, + "path": "/packages/expo/src/generators/library/schema.json" + }, + { + "name": "component", + "factory": "./src/generators/component/component#expoComponentGenerator", + "schema": { + "cli": "nx", + "$id": "NxExpoComponent", + "$schema": "http://json-schema.org/schema", + "title": "Create a Expo 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"], + "presets": [] + }, + "description": "Create a component", + "aliases": ["c"], + "implementation": "/packages/expo/src/generators/component/component#expoComponentGenerator.ts", + "hidden": false, + "path": "/packages/expo/src/generators/component/schema.json" + } + ], + "executors": [ + { + "name": "update", + "implementation": "/packages/expo/src/executors/update/update.impl.ts", + "schema": { + "$schema": "http://json-schema.org/schema", + "$id": "NxExpoEasUpdate", + "cli": "nx", + "title": "Expo EAS Update executor", + "description": "Start an EAS update for your expo project", + "type": "object", + "properties": { + "branch": { + "type": "string", + "description": "Branch to publish the update group on" + }, + "message": { + "type": "string", + "description": "A short message describing the update" + }, + "republish": { + "type": "boolean", + "description": "Enable JSON output, non-JSON messages will be printed to stderr", + "default": false + }, + "group": { + "type": "string", + "description": "Update group to republish" + }, + "inputDir": { + "type": "string", + "description": "Location of the bundle" + }, + "skipBundler": { + "type": "boolean", + "description": "Skip running Expo CLI to bundle the app before publishing", + "default": false + }, + "platform": { + "enum": ["ios", "android", "all"], + "alias": "p", + "description": "The platform to build the app, example values: ios, android, all.", + "default": "all" + }, + "json": { + "type": "boolean", + "description": "Enable JSON output, non-JSON messages will be printed to stderr", + "default": false + }, + "auto": { + "type": "boolean", + "description": "Use the current git branch and commit message for the EAS branch and update message", + "default": false + }, + "privateKeyPath": { + "type": "string", + "description": "File containing the PEM-encoded private key corresponding to the certificate in expo-updates' configuration. Defaults to a file named \"private-key.pem\" in the certificate's directory." + }, + "nonInteractive": { + "type": "boolean", + "description": "Run command in non-interactive mode", + "default": false + } + }, + "required": [], + "presets": [] + }, + "description": "Start an EAS update for your expo project", + "aliases": [], + "hidden": false, + "path": "/packages/expo/src/executors/update/schema.json" + }, + { + "name": "build", + "implementation": "/packages/expo/src/executors/build/build.impl.ts", + "schema": { + "$schema": "http://json-schema.org/schema", + "$id": "NxExpoEasBuild", + "cli": "nx", + "title": "Expo EAS Build executor", + "description": "Start an EAS build for your expo project", + "type": "object", + "properties": { + "platform": { + "enum": ["ios", "android", "all"], + "alias": "p", + "description": "The platform to build the app, example values: ios, android, all." + }, + "json": { + "type": "boolean", + "description": "Enable JSON output, non-JSON messages will be printed to stderr", + "default": false + }, + "profile": { + "type": "string", + "description": "Name of the build profile from eas.json. Defaults to \"production\" if defined in eas.json.", + "examples": ["PROFILE_NAME"] + }, + "nonInteractive": { + "type": "boolean", + "description": "Run command in non-interactive mode", + "default": false + }, + "local": { + "type": "boolean", + "description": "Run build locally [experimental]", + "default": false + }, + "wait": { + "type": "boolean", + "description": "Wait for build(s) to complete", + "default": true + }, + "clearCache": { + "type": "boolean", + "description": "Clear cache before the build", + "default": false + }, + "autoSubmit": { + "type": "boolean", + "description": "Submit on build complete using the submit profile with the same name as the build profile", + "default": false + }, + "autoSubmitWithProfile": { + "type": "string", + "description": "Submit on build complete using the submit profile with provided name", + "examples": ["PROFILE_NAME"] + } + }, + "required": [], + "presets": [] + }, + "description": "Start an EAS build for your expo project", + "aliases": [], + "hidden": false, + "path": "/packages/expo/src/executors/build/schema.json" + }, + { + "name": "build-list", + "implementation": "/packages/expo/src/executors/build-list/build-list.impl.ts", + "schema": { + "$schema": "http://json-schema.org/schema", + "$id": "NxExpoEasBuildList", + "cli": "nx", + "title": "Expo EAS Build List executor", + "description": "List all EAS builds for your Expo project", + "type": "object", + "properties": { + "platform": { + "enum": ["ios", "android", "all"], + "alias": "p", + "description": "The platform to build the app, example values: ios, android, all." + }, + "json": { + "type": "boolean", + "description": "Enable JSON output, non-JSON messages will be printed to stderr" + }, + "status": { + "enum": [ + "new", + "in-queue", + "in-progress", + "errored", + "finished", + "canceled" + ], + "description": "Status of EAS build" + }, + "distribution": { + "enum": ["store", "internal", "simulator"], + "description": "Distribution of EAS build" + }, + "channel": { + "type": "string", + "description": "Channel of EAS build" + }, + "appVersion": { + "type": "string", + "description": "App version of EAS build" + }, + "appBuildVersion": { + "type": "string", + "description": "App build version of EAS build" + }, + "sdkVersion": { + "type": "string", + "description": "SDK version of EAS build" + }, + "runtimeVersion": { + "type": "string", + "description": "Runtime version of EAS build" + }, + "appIdentifier": { + "type": "string", + "description": "App identifier of EAS build" + }, + "buildProfile": { + "type": "string", + "description": "Build profile of EAS build" + }, + "gitCommitHash": { + "type": "string", + "description": "Git commit hash of EAS build" + }, + "limit": { + "type": "number", + "description": "Limit of numbers to list EAS builds" + } + }, + "required": [], + "presets": [] + }, + "description": "List all EAS builds for your Expo project", + "aliases": [], + "hidden": false, + "path": "/packages/expo/src/executors/build-list/schema.json" + }, + { + "name": "download", + "implementation": "/packages/expo/src/executors/download/download.impl.ts", + "schema": { + "$schema": "http://json-schema.org/schema", + "$id": "NxExpoDownloadEasBuild", + "cli": "nx", + "title": "Download EAS Build executor", + "description": "Download an EAS build", + "type": "object", + "properties": { + "platform": { + "enum": ["ios", "android"], + "alias": "p", + "description": "The platform to build the app, example values: ios, android, all." + }, + "distribution": { + "enum": ["store", "internal", "simulator"], + "description": "Distribution of EAS build" + }, + "channel": { + "type": "string", + "description": "Channel of EAS build" + }, + "appVersion": { + "type": "string", + "description": "App version of EAS build" + }, + "appBuildVersion": { + "type": "string", + "description": "App build version of EAS build" + }, + "sdkVersion": { + "type": "string", + "description": "SDK version of EAS build" + }, + "runtimeVersion": { + "type": "string", + "description": "Runtime version of EAS build" + }, + "appIdentifier": { + "type": "string", + "description": "App identifier of EAS build" + }, + "buildProfile": { + "type": "string", + "description": "Build profile of EAS build" + }, + "gitCommitHash": { + "type": "string", + "description": "Git commit hash of EAS build" + }, + "output": { + "type": "string", + "description": "Output directory for the download build" + } + }, + "required": ["output"], + "presets": [] + }, + "description": "Download an EAS build", + "aliases": [], + "hidden": false, + "path": "/packages/expo/src/executors/download/schema.json" + }, + { + "name": "build-ios", + "implementation": "/packages/expo/src/executors/build-ios/build-ios.impl.ts", + "schema": { + "$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": [], + "presets": [] + }, + "description": "Build and sign a standalone IPA for the Apple App Store", + "aliases": [], + "hidden": false, + "path": "/packages/expo/src/executors/build-ios/schema.json" + }, + { + "name": "build-android", + "implementation": "/packages/expo/src/executors/build-android/build-android.impl.ts", + "schema": { + "$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." + }, + "sync": { + "type": "boolean", + "description": "Syncs npm dependencies to package.json (for React Native autolink). Always true when --install is used.", + "default": true + } + }, + "required": [], + "presets": [] + }, + "description": "Build and sign a standalone APK or App Bundle for the Google Play Store", + "aliases": [], + "hidden": false, + "path": "/packages/expo/src/executors/build-android/schema.json" + }, + { + "name": "build-web", + "implementation": "/packages/expo/src/executors/build-web/build-web.impl.ts", + "schema": { + "$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": [], + "presets": [] + }, + "description": "Build the web app for production", + "aliases": [], + "hidden": false, + "path": "/packages/expo/src/executors/build-web/schema.json" + }, + { + "name": "build-status", + "implementation": "/packages/expo/src/executors/build-status/build-status.impl.ts", + "schema": { + "$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": [], + "presets": [] + }, + "description": "Get the status of the latest build for the project", + "aliases": [], + "hidden": false, + "path": "/packages/expo/src/executors/build-status/schema.json" + }, + { + "name": "publish", + "implementation": "/packages/expo/src/executors/publish/publish.impl.ts", + "schema": { + "cli": "nx", + "$id": "NxExpoPublish", + "$schema": "http://json-schema.org/schema", + "title": "Publish for Expo", + "description": "Deploy a project to Expo hosting", + "type": "object", + "properties": { + "quiet": { + "type": "boolean", + "description": "Suppress verbose output from the Metro bundler", + "default": false, + "alias": "q" + }, + "sendTo": { + "type": "string", + "description": "A phone number or email address to send a link to", + "alias": "s" + }, + "clear": { + "type": "boolean", + "description": "Clear the Metro bundler cache", + "default": false, + "alias": "c" + }, + "target": { + "enum": ["managed", "bare"], + "default": "managed", + "description": "Target environment for which this publish is intended. Options are managed or bare.", + "alias": "t" + }, + "maxWorkers": { + "type": "number", + "description": "Maximum number of tasks to allow Metro to spawn" + }, + "releaseChannel": { + "type": "string", + "description": "The release channel to publish to. Default is 'default'.", + "default": "default" + } + }, + "presets": [] + }, + "description": "Deploy a project to Expo hosting", + "aliases": [], + "hidden": false, + "path": "/packages/expo/src/executors/publish/schema.json" + }, + { + "name": "publish-set", + "implementation": "/packages/expo/src/executors/publish-set/publish-set.impl.ts", + "schema": { + "cli": "nx", + "$id": "NxExpoPublishSet", + "$schema": "http://json-schema.org/schema", + "title": "Set Publish Channel for Expo", + "description": "Specify the channel to serve a published release", + "type": "object", + "properties": { + "releaseChannel": { + "type": "string", + "description": "The release channel to publish to." + }, + "publishId": { + "type": "string", + "description": "The id of the published release to serve from the channel." + } + }, + "required": ["releaseChannel", "publishId"], + "presets": [] + }, + "description": "Specify the channel to serve a published release", + "aliases": [], + "hidden": false, + "path": "/packages/expo/src/executors/publish-set/schema.json" + }, + { + "name": "rollback", + "implementation": "/packages/expo/src/executors/rollback/rollback.impl.ts", + "schema": { + "cli": "nx", + "$id": "NxExpoRollback", + "$schema": "http://json-schema.org/schema", + "title": "Rollback Publish Command for Expo", + "description": "Undo an update to a channel", + "type": "object", + "properties": { + "releaseChannel": { + "type": "string", + "description": "The release channel to publish to." + }, + "sdkVersion": { + "type": "string", + "description": "The sdk version to rollback." + }, + "platform": { + "enum": ["ios", "android"], + "description": "The platform to rollback." + } + }, + "required": ["releaseChannel", "sdkVersion"], + "presets": [] + }, + "description": "Undo an update to a channel", + "aliases": [], + "hidden": false, + "path": "/packages/expo/src/executors/rollback/schema.json" + }, + { + "name": "run", + "implementation": "/packages/expo/src/executors/run/run.impl.ts", + "schema": { + "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"], + "presets": [] + }, + "description": "Run the Android app binary locally or run the iOS app binary locally", + "aliases": [], + "hidden": false, + "path": "/packages/expo/src/executors/run/schema.json" + }, + { + "name": "start", + "implementation": "/packages/expo/src/executors/start/start.impl.ts", + "schema": { + "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." + } + }, + "presets": [] + }, + "description": "Start a local dev server for the app or start a Webpack dev server for the web app", + "aliases": [], + "hidden": false, + "path": "/packages/expo/src/executors/start/schema.json" + }, + { + "name": "sync-deps", + "implementation": "/packages/expo/src/executors/sync-deps/sync-deps.impl.ts", + "schema": { + "cli": "nx", + "$id": "NxExpoSyncDeps", + "$schema": "http://json-schema.org/schema", + "title": "Sync Deps for Expo", + "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'" + } + }, + "presets": [] + }, + "description": "Syncs dependencies to package.json (required for autolinking).", + "aliases": [], + "hidden": false, + "path": "/packages/expo/src/executors/sync-deps/schema.json" + }, + { + "name": "ensure-symlink", + "implementation": "/packages/expo/src/executors/ensure-symlink/ensure-symlink.impl.ts", + "schema": { + "cli": "nx", + "$id": "NxExpoEnsureSymlink", + "$schema": "http://json-schema.org/schema", + "title": "Ensure Symlink for Expo", + "description": "Ensure workspace node_modules is symlink under app's node_modules folder.", + "type": "object", + "properties": {}, + "presets": [] + }, + "description": "Ensure workspace node_modules is symlink under app's node_modules folder.", + "aliases": [], + "hidden": false, + "path": "/packages/expo/src/executors/ensure-symlink/schema.json" + }, + { + "name": "eject", + "implementation": "/packages/expo/src/executors/eject/eject.impl.ts", + "schema": { + "cli": "nx", + "$id": "NxExpoEject", + "$schema": "http://json-schema.org/schema", + "title": "Expo Eject", + "description": "Create native iOS and Android project files", + "type": "object", + "properties": { + "install": { + "type": "boolean", + "description": "Install CocoaPods.", + "default": true + }, + "platform": { + "type": "string", + "description": "Platforms to sync", + "default": "all", + "examples": ["ios", "android", "all"] + } + }, + "presets": [] + }, + "description": "Create native iOS and Android project files.", + "aliases": [], + "hidden": false, + "path": "/packages/expo/src/executors/eject/schema.json" + } + ] +} diff --git a/docs/generated/packages/nx.json b/docs/generated/packages/nx.json index e8b9da9888..295dac4320 100644 --- a/docs/generated/packages/nx.json +++ b/docs/generated/packages/nx.json @@ -10,7 +10,7 @@ "name": "create-nx-workspace", "id": "create-nx-workspace", "file": "generated/cli/create-nx-workspace", - "content": "---\ntitle: 'create-nx-workspace - CLI command'\ndescription: 'Create a new Nx workspace'\n---\n\n# create-nx-workspace\n\nCreate a new Nx workspace\n\n## Usage\n\n```bash\ncreate-nx-workspace [name] [options]\n```\n\nInstall `create-nx-workspace` globally to invoke the command directly, or use `npx create-nx-workspace`, `yarn create nx-workspace`, or `pnpx create-nx-workspace`.\n\n## Options\n\n### allPrompts\n\nType: boolean\n\nDefault: false\n\nShow all prompts\n\n### appName\n\nType: string\n\nThe name of the application when a preset with pregenerated app is selected\n\n### ci\n\nType: string\n\nChoices: [github, circleci, azure]\n\nGenerate a CI workflow file\n\n### cli\n\nType: string\n\nChoices: [nx, angular]\n\nCLI to power the Nx workspace\n\n### commit.email\n\nType: string\n\nE-mail of the committer\n\n### commit.message\n\nType: string\n\nDefault: Initial commit\n\nCommit message\n\n### commit.name\n\nType: string\n\nName of the committer\n\n### defaultBase\n\nType: string\n\nDefault: main\n\nDefault base to use for new projects\n\n### help\n\nType: boolean\n\nShow help\n\n### interactive\n\nType: boolean\n\nEnable interactive mode with presets\n\n### name\n\nType: string\n\nWorkspace name (e.g. org name)\n\n### nxCloud\n\nType: boolean\n\nEnable distributed caching to make your CI faster\n\n### packageManager\n\nType: string\n\nChoices: [npm, pnpm, yarn]\n\nDefault: npm\n\nPackage manager to use\n\n### preset\n\nType: string\n\nCustomizes the initial content of your workspace. Default presets include: [\"apps\", \"empty\", \"core\", \"npm\", \"ts\", \"web-components\", \"angular\", \"angular-nest\", \"react\", \"react-express\", \"react-native\", \"next\", \"nest\", \"express\"]. To build your own see https://nx.dev/packages/nx-plugin#preset\n\n### skipGit\n\nType: boolean\n\nDefault: false\n\nSkip initializing a git repository.\n\n### style\n\nType: string\n\nStyle option to be used when a preset with pregenerated app is selected\n\n### version\n\nType: boolean\n\nShow version number\n" + "content": "---\ntitle: 'create-nx-workspace - CLI command'\ndescription: 'Create a new Nx workspace'\n---\n\n# create-nx-workspace\n\nCreate a new Nx workspace\n\n## Usage\n\n```bash\ncreate-nx-workspace [name] [options]\n```\n\nInstall `create-nx-workspace` globally to invoke the command directly, or use `npx create-nx-workspace`, `yarn create nx-workspace`, or `pnpx create-nx-workspace`.\n\n## Options\n\n### allPrompts\n\nType: boolean\n\nDefault: false\n\nShow all prompts\n\n### appName\n\nType: string\n\nThe name of the application when a preset with pregenerated app is selected\n\n### ci\n\nType: string\n\nChoices: [github, circleci, azure]\n\nGenerate a CI workflow file\n\n### cli\n\nType: string\n\nChoices: [nx, angular]\n\nCLI to power the Nx workspace\n\n### commit.email\n\nType: string\n\nE-mail of the committer\n\n### commit.message\n\nType: string\n\nDefault: Initial commit\n\nCommit message\n\n### commit.name\n\nType: string\n\nName of the committer\n\n### defaultBase\n\nType: string\n\nDefault: main\n\nDefault base to use for new projects\n\n### help\n\nType: boolean\n\nShow help\n\n### interactive\n\nType: boolean\n\nEnable interactive mode with presets\n\n### name\n\nType: string\n\nWorkspace name (e.g. org name)\n\n### nxCloud\n\nType: boolean\n\nEnable distributed caching to make your CI faster\n\n### packageManager\n\nType: string\n\nChoices: [npm, pnpm, yarn]\n\nDefault: npm\n\nPackage manager to use\n\n### preset\n\nType: string\n\nCustomizes the initial content of your workspace. Default presets include: [\"apps\", \"empty\", \"core\", \"npm\", \"ts\", \"web-components\", \"angular\", \"angular-nest\", \"react\", \"react-express\", \"react-native\", \"expo\", \"next\", \"nest\", \"express\"]. To build your own see https://nx.dev/packages/nx-plugin#preset\n\n### skipGit\n\nType: boolean\n\nDefault: false\n\nSkip initializing a git repository.\n\n### style\n\nType: string\n\nStyle option to be used when a preset with pregenerated app is selected\n\n### version\n\nType: boolean\n\nShow version number\n" }, { "name": "init", diff --git a/docs/packages.json b/docs/packages.json index 12f9e27e15..ac9c933297 100644 --- a/docs/packages.json +++ b/docs/packages.json @@ -115,6 +115,33 @@ "path": "generated/packages/eslint-plugin-nx.json", "schemas": { "executors": [], "generators": [] } }, + { + "name": "expo", + "packageName": "expo", + "description": "Expo Plugin for Nx", + "path": "generated/packages/expo.json", + "schemas": { + "executors": [ + "update", + "build", + "build-list", + "download", + "build-ios", + "build-android", + "build-web", + "build-status", + "publish", + "publish-set", + "rollback", + "run", + "start", + "sync-deps", + "ensure-symlink", + "eject" + ], + "generators": ["init", "application", "library", "component"] + } + }, { "name": "express", "packageName": "express", diff --git a/e2e/detox/src/detox.test.ts b/e2e/detox/src/detox.test.ts index 56e0cd2055..60fd4c8e1d 100644 --- a/e2e/detox/src/detox.test.ts +++ b/e2e/detox/src/detox.test.ts @@ -13,12 +13,12 @@ describe('Detox', () => { beforeAll(() => { newProject(); - runCLI( - `generate @nrwl/react-native:app ${appName} --e2eTestRunner=detox --linter=eslint` - ); }); - it('should create files and run lint command', async () => { + it('should create files and run lint command for react-native apps', async () => { + runCLI( + `generate @nrwl/react-native:app ${appName} --e2eTestRunner=detox --linter=eslint --install=false` + ); checkFilesExist(`apps/${appName}-e2e/.detoxrc.json`); checkFilesExist(`apps/${appName}-e2e/tsconfig.json`); checkFilesExist(`apps/${appName}-e2e/tsconfig.e2e.json`); @@ -29,6 +29,21 @@ describe('Detox', () => { expect(lintResults.combinedOutput).toContain('All files pass linting'); }); + it('should create files and run lint command for expo apps', async () => { + const expoAppName = uniq('myapp'); + runCLI( + `generate @nrwl/expo:app ${expoAppName} --e2eTestRunner=detox --linter=eslint` + ); + checkFilesExist(`apps/${expoAppName}-e2e/.detoxrc.json`); + checkFilesExist(`apps/${expoAppName}-e2e/tsconfig.json`); + checkFilesExist(`apps/${expoAppName}-e2e/tsconfig.e2e.json`); + checkFilesExist(`apps/${expoAppName}-e2e/test-setup.ts`); + checkFilesExist(`apps/${expoAppName}-e2e/src/app.spec.ts`); + + const lintResults = await runCLIAsync(`lint ${expoAppName}-e2e`); + expect(lintResults.combinedOutput).toContain('All files pass linting'); + }); + describe('React Native Detox MACOS-Tests', () => { if (isOSX()) { it('should test ios MACOS-Tests', async () => { diff --git a/e2e/expo/jest.config.ts b/e2e/expo/jest.config.ts new file mode 100644 index 0000000000..913b9f7c4e --- /dev/null +++ b/e2e/expo/jest.config.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ +export default { + 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, + preset: '../../jest.preset.js', +}; diff --git a/e2e/expo/project.json b/e2e/expo/project.json new file mode 100644 index 0000000000..1fd1590757 --- /dev/null +++ b/e2e/expo/project.json @@ -0,0 +1,34 @@ +{ + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "e2e/expo", + "projectType": "application", + "targets": { + "e2e": { + "executor": "nx: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.ts", + "passWithNoTests": true, + "runInBand": true + }, + "outputs": ["coverage/e2e/expo"] + } + }, + "implicitDependencies": ["expo"] +} diff --git a/e2e/expo/src/expo.test.ts b/e2e/expo/src/expo.test.ts new file mode 100644 index 0000000000..e1cc236b3d --- /dev/null +++ b/e2e/expo/src/expo.test.ts @@ -0,0 +1,45 @@ +import { + cleanupProject, + expectTestsPass, + newProject, + runCLI, + runCLIAsync, + uniq, + updateFile, +} from '@nrwl/e2e/utils'; + +describe('expo', () => { + let proj: string; + + beforeEach( + () => (proj = newProject({ name: uniq('proj'), packageManager: 'npm' })) + ); + afterEach(() => cleanupProject()); + + it('should test, lint', 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` + ); + expectTestsPass(await runCLIAsync(`test ${appName}`)); + expectTestsPass(await runCLIAsync(`test ${libName}`)); + + updateFile(`apps/${appName}/src/app/App.tsx`, (content) => { + let updated = `// eslint-disable-next-line @typescript-eslint/no-unused-vars\nimport {${componentName}} from '${proj}/${libName}';\n${content}`; + return updated; + }); + + expectTestsPass(await runCLIAsync(`test ${appName}`)); + + 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.'); + }, 1000000); +}); diff --git a/e2e/expo/tsconfig.json b/e2e/expo/tsconfig.json new file mode 100644 index 0000000000..6d5abf8483 --- /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 0000000000..1a24bfb0a1 --- /dev/null +++ b/e2e/expo/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "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", + "jest.config.ts" + ] +} diff --git a/e2e/utils/index.ts b/e2e/utils/index.ts index db6861437c..47df7169b7 100644 --- a/e2e/utils/index.ts +++ b/e2e/utils/index.ts @@ -319,6 +319,7 @@ export function newProject({ `@nrwl/web`, `@nrwl/webpack`, `@nrwl/react-native`, + `@nrwl/expo`, ]; packageInstall(packages.join(` `), projScope); diff --git a/e2e/workspace-create/src/create-nx-workspace.test.ts b/e2e/workspace-create/src/create-nx-workspace.test.ts index 56fcc35489..6d1c3b62db 100644 --- a/e2e/workspace-create/src/create-nx-workspace.test.ts +++ b/e2e/workspace-create/src/create-nx-workspace.test.ts @@ -190,6 +190,30 @@ describe('create-nx-workspace', () => { expectNoAngularDevkit(); }); + it('should be able to create react-native workspace', () => { + const wsName = uniq('react-native'); + const appName = uniq('app'); + runCreateWorkspace(wsName, { + preset: 'react-native', + appName, + packageManager: 'npm', + }); + + expectNoAngularDevkit(); + }); + + it('should be able to create an expo workspace', () => { + const wsName = uniq('expo'); + const appName = uniq('app'); + runCreateWorkspace(wsName, { + preset: 'expo', + appName, + packageManager: 'npm', + }); + + expectNoAngularDevkit(); + }); + it('should be able to create a workspace with a custom base branch and HEAD', () => { const wsName = uniq('branch'); runCreateWorkspace(wsName, { diff --git a/package.json b/package.json index cbf9bb720a..f3b5e474be 100644 --- a/package.json +++ b/package.json @@ -241,6 +241,7 @@ "styled-components": "5.0.0", "stylus": "^0.55.0", "stylus-loader": "^6.2.0", + "tar-fs": "^2.1.1", "tar-stream": "~2.2.0", "tcp-port-used": "^1.0.2", "terser-webpack-plugin": "^5.3.3", diff --git a/packages/create-nx-workspace/bin/create-nx-workspace.ts b/packages/create-nx-workspace/bin/create-nx-workspace.ts index 924227a161..4c9cec6587 100644 --- a/packages/create-nx-workspace/bin/create-nx-workspace.ts +++ b/packages/create-nx-workspace/bin/create-nx-workspace.ts @@ -56,6 +56,7 @@ enum Preset { React = 'react', ReactWithExpress = 'react-express', ReactNative = 'react-native', + Expo = 'expo', NextJs = 'next', Nest = 'nest', Express = 'express', @@ -141,6 +142,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: @@ -625,7 +630,8 @@ async function determineStyle( 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/expo/.eslintrc.json b/packages/expo/.eslintrc.json new file mode 100644 index 0000000000..3c1f9d1789 --- /dev/null +++ b/packages/expo/.eslintrc.json @@ -0,0 +1,19 @@ +{ + "extends": "../../.eslintrc", + "rules": {}, + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": [ + "./package.json", + "./generators.json", + "./executors.json", + "./migrations.json" + ], + "parser": "jsonc-eslint-parser", + "rules": { + "@nrwl/nx/nx-plugin-checks": "error" + } + } + ] +} diff --git a/packages/expo/README.md b/packages/expo/README.md new file mode 100644 index 0000000000..ba7257a359 --- /dev/null +++ b/packages/expo/README.md @@ -0,0 +1,13 @@ +

Nx - Smart, Fast and Extensible Build System

+ +{{links}} + +
+ +# Nx: Smart, Fast and Extensible Build System + +Nx is a next generation build system with first class monorepo support and powerful integrations. + +This package is a [Expo plugin for Nx](https://nx.dev/expo/overview). + +{{content}} diff --git a/packages/expo/collection.json b/packages/expo/collection.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/expo/executors.json b/packages/expo/executors.json new file mode 100644 index 0000000000..9c4c9b93c3 --- /dev/null +++ b/packages/expo/executors.json @@ -0,0 +1,166 @@ +{ + "executors": { + "update": { + "implementation": "./src/executors/update/update.impl", + "schema": "./src/executors/update/schema.json", + "description": "Start an EAS update for your expo project" + }, + "build": { + "implementation": "./src/executors/build/build.impl", + "schema": "./src/executors/build/schema.json", + "description": "Start an EAS build for your expo project" + }, + "build-list": { + "implementation": "./src/executors/build-list/build-list.impl", + "schema": "./src/executors/build-list/schema.json", + "description": "List all EAS builds for your Expo project" + }, + "download": { + "implementation": "./src/executors/download/download.impl", + "schema": "./src/executors/download/schema.json", + "description": "Download an EAS build" + }, + "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" + }, + "publish": { + "implementation": "./src/executors/publish/publish.impl", + "schema": "./src/executors/publish/schema.json", + "description": "Deploy a project to Expo hosting" + }, + "publish-set": { + "implementation": "./src/executors/publish-set/publish-set.impl", + "schema": "./src/executors/publish-set/schema.json", + "description": "Specify the channel to serve a published release" + }, + "rollback": { + "implementation": "./src/executors/rollback/rollback.impl", + "schema": "./src/executors/rollback/schema.json", + "description": "Undo an update to a channel" + }, + "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." + }, + "eject": { + "implementation": "./src/executors/eject/eject.impl", + "schema": "./src/executors/eject/schema.json", + "description": "Create native iOS and Android project files." + } + }, + "builders": { + "update": { + "implementation": "./src/executors/update/compat", + "schema": "./src/executors/update/schema.json", + "description": "Start an EAS update for your expo project" + }, + "build": { + "implementation": "./src/executors/build/compat", + "schema": "./src/executors/build/schema.json", + "description": "Start an EAS build for your expo project" + }, + "build-list": { + "implementation": "./src/executors/build-list/compat", + "schema": "./src/executors/build-list/schema.json", + "description": "List all EAS builds for your Expo project" + }, + "download": { + "implementation": "./src/executors/download/compat", + "schema": "./src/executors/download/schema.json", + "description": "Download an EAS build" + }, + "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" + }, + "publish": { + "implementation": "./src/executors/publish/compat", + "schema": "./src/executors/publish/schema.json", + "description": "Deploy a project to Expo hosting" + }, + "publish-set": { + "implementation": "./src/executors/publish-set/compact", + "schema": "./src/executors/publish-set/schema.json", + "description": "Specify the channel to serve a published release" + }, + "rollback": { + "implementation": "./src/executors/rollback/compact", + "schema": "./src/executors/rollback/schema.json", + "description": "Undo an update to a channel" + }, + "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." + }, + "eject": { + "implementation": "./src/executors/eject/compat", + "schema": "./src/executors/eject/schema.json", + "description": "Create native iOS and Android project files." + } + } +} diff --git a/packages/expo/generators.json b/packages/expo/generators.json new file mode 100644 index 0000000000..904f40a533 --- /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 0000000000..f8aa444e59 --- /dev/null +++ b/packages/expo/index.ts @@ -0,0 +1,4 @@ +export { expoInitGenerator } from './src/generators/init/init'; +export { expoApplicationGenerator } from './src/generators/application/application'; +export { withNxMetro } from './plugins/with-nx-metro'; +export { withNxWebpack } from './plugins/with-nx-webpack'; diff --git a/packages/expo/jest.config.ts b/packages/expo/jest.config.ts new file mode 100644 index 0000000000..fdff4f46da --- /dev/null +++ b/packages/expo/jest.config.ts @@ -0,0 +1,13 @@ +/* eslint-disable */ +export default { + transform: { + '^.+\\.[tj]sx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html', 'json'], + globals: { + 'ts-jest': { tsconfig: '/tsconfig.spec.json' }, + }, + displayName: 'expo', + testEnvironment: 'node', + preset: '../../jest.preset.js', +}; diff --git a/packages/expo/migrations.json b/packages/expo/migrations.json new file mode 100644 index 0000000000..9544bc70b5 --- /dev/null +++ b/packages/expo/migrations.json @@ -0,0 +1,415 @@ +{ + "schematics": { + "add-project-root-metro-config-14-0-0": { + "version": "14.0.1-beta.0", + "cli": "nx", + "description": "Add projectRoot option in metro.config.js", + "factory": "./src/migrations/update-14-0-0/add-project-root-metro-config-14-0-0" + }, + "add-eject-target-14-1-2": { + "version": "14.1.2-beta.0", + "cli": "nx", + "description": "Add target eject for expo projects in project.json", + "factory": "./src/migrations/update-14-1-2/add-eject-target-14-1-2" + }, + "add-build-target-14-4-3": { + "version": "14.4.3-beta.0", + "cli": "nx", + "description": "Add target build and build-list for expo projects in project.json", + "factory": "./src/migrations/update-14-4-3/add-eas-build-target" + }, + "add-update-target-14-5-1": { + "version": "14.5.1-beta.0", + "cli": "nx", + "description": "Add target update for expo projects in project.json", + "factory": "./src/migrations/update-14-5-1/add-eas-update-target" + } + }, + "packageJsonUpdates": { + "13.8.6": { + "version": "13.8.6-beta.0", + "packages": { + "expo-cli": { + "version": "5.3.0", + "alwaysAddToPackageJson": false + } + } + }, + "14.0.0": { + "version": "14.0.0-beta.0", + "packages": { + "expo-cli": { + "version": "5.4.0", + "alwaysAddToPackageJson": false + }, + "babel-preset-expo": { + "version": "~9.0.2", + "alwaysAddToPackageJson": false, + "addToPackageJson": "devDependencies" + } + } + }, + "14.0.1": { + "version": "14.0.1-beta.0", + "packages": { + "expo-cli": { + "version": "5.4.3", + "alwaysAddToPackageJson": false + } + } + }, + "14.0.2": { + "version": "14.0.2-beta.0", + "packages": { + "metro-resolver": { + "version": "0.70.2", + "alwaysAddToPackageJson": false + }, + "expo-dev-client": { + "version": "0.8.5", + "alwaysAddToPackageJson": false + }, + "@expo/metro-config": { + "version": "0.3.16", + "alwaysAddToPackageJson": false + }, + "expo-updates": { + "version": "~0.11.7", + "alwaysAddToPackageJson": false + } + } + }, + "14.1.1": { + "version": "14.1.1-beta.0", + "packages": { + "expo": { + "version": "45.0.4", + "alwaysAddToPackageJson": false + }, + "expo-dev-client": { + "version": "~0.9.6", + "alwaysAddToPackageJson": false + }, + "expo-status-bar": { + "version": "~1.3.0", + "alwaysAddToPackageJson": false + }, + "@expo/metro-config": { + "version": "0.3.17", + "alwaysAddToPackageJson": false + }, + "expo-splash-screen": { + "version": "0.15.1", + "alwaysAddToPackageJson": false + }, + "expo-updates": { + "version": "~0.13.1", + "alwaysAddToPackageJson": false + }, + "jest-expo": { + "version": "45.0.1", + "alwaysAddToPackageJson": false + }, + "expo-cli": { + "version": "5.4.6", + "alwaysAddToPackageJson": false + }, + "babel-preset-expo": { + "version": "~9.1.0", + "alwaysAddToPackageJson": false + }, + "react-native": { + "version": "0.68.2", + "alwaysAddToPackageJson": false + }, + "@types/react-native": { + "version": "0.67.7", + "alwaysAddToPackageJson": false + }, + "react-native-web": { + "version": "0.17.7", + "alwaysAddToPackageJson": false + }, + "react-native-gesture-handler": { + "version": "~2.2.1", + "alwaysAddToPackageJson": false + }, + "react-native-reanimated": { + "version": "~2.8.0", + "alwaysAddToPackageJson": false + }, + "react-native-safe-area-context": { + "version": "4.2.4", + "alwaysAddToPackageJson": false + }, + "react-native-screens": { + "version": "~3.11.1", + "alwaysAddToPackageJson": false + }, + "react-native-svg": { + "version": "12.3.0", + "alwaysAddToPackageJson": false + }, + "metro-resolver": { + "version": "0.70.3", + "alwaysAddToPackageJson": false + }, + "@testing-library/react-native": { + "version": "9.1.0", + "alwaysAddToPackageJson": false + }, + "@testing-library/jest-native": { + "version": "4.0.5", + "alwaysAddToPackageJson": false + } + } + }, + "14.1.2": { + "version": "14.1.2-beta.0", + "packages": { + "expo": { + "version": "45.0.5", + "alwaysAddToPackageJson": false + }, + "expo-cli": { + "version": "5.4.9", + "alwaysAddToPackageJson": false + }, + "metro-resolver": { + "version": "0.71.0", + "alwaysAddToPackageJson": false + }, + "metro-babel-register": { + "version": "0.71.0", + "alwaysAddToPackageJson": false, + "addToPackageJson": "devDependencies" + }, + "react-test-renderer": { + "version": "18.1.0", + "alwaysAddToPackageJson": false, + "addToPackageJson": "devDependencies" + }, + "expo-updates": { + "version": "~0.13.2", + "alwaysAddToPackageJson": false + }, + "@types/react-native": { + "version": "0.67.8", + "alwaysAddToPackageJson": false + } + } + }, + "14.2.3": { + "version": "14.2.3-beta.0", + "packages": { + "expo-dev-client": { + "version": "~0.10.0", + "alwaysAddToPackageJson": false + }, + "expo-structured-headers": { + "version": "~2.2.1", + "alwaysAddToPackageJson": false + } + } + }, + "14.2.4": { + "version": "14.2.4-beta.0", + "packages": { + "expo-dev-client": { + "version": "~1.0.0", + "alwaysAddToPackageJson": false + } + } + }, + "14.3.2": { + "version": "14.3.2-beta.0", + "packages": { + "expo": { + "version": "45.0.6", + "alwaysAddToPackageJson": false + }, + "expo-cli": { + "version": "5.4.11", + "alwaysAddToPackageJson": false + }, + "@types/react-native": { + "version": "0.68.0", + "alwaysAddToPackageJson": false + } + } + }, + "14.4.3": { + "version": "14.4.3-beta.0", + "packages": { + "eas-cli": { + "version": "0.55.1", + "alwaysAddToPackageJson": false, + "addToPackageJson": "devDependencies" + }, + "expo-cli": { + "version": "5.5.1", + "alwaysAddToPackageJson": false + } + } + }, + "14.5.1": { + "version": "14.5.1-beta.0", + "packages": { + "expo": { + "version": "46.0.2", + "alwaysAddToPackageJson": false + }, + "expo-dev-client": { + "version": "~1.1.1", + "alwaysAddToPackageJson": false + }, + "expo-status-bar": { + "version": "~1.4.0", + "alwaysAddToPackageJson": false + }, + "@expo/metro-config": { + "version": "0.3.21", + "alwaysAddToPackageJson": false + }, + "expo-splash-screen": { + "version": "~0.16.1", + "alwaysAddToPackageJson": false + }, + "expo-updates": { + "version": "~0.14.3", + "alwaysAddToPackageJson": false + }, + "jest-expo": { + "version": "46.0.1", + "alwaysAddToPackageJson": false + }, + "expo-cli": { + "version": "6.0.1", + "alwaysAddToPackageJson": false + }, + "eas-cli": { + "version": "0.57.0", + "alwaysAddToPackageJson": false + }, + "babel-preset-expo": { + "version": "~9.2.0", + "alwaysAddToPackageJson": false + }, + "react-native": { + "version": "0.69.3", + "alwaysAddToPackageJson": false + }, + "@types/react-native": { + "version": "0.69.5", + "alwaysAddToPackageJson": false + }, + "react-native-web": { + "version": "~0.18.7", + "alwaysAddToPackageJson": false + }, + "react-native-gesture-handler": { + "version": "~2.5.0", + "alwaysAddToPackageJson": false + }, + "react-native-reanimated": { + "version": "~2.9.1", + "alwaysAddToPackageJson": false + }, + "react-native-safe-area-context": { + "version": "4.3.1", + "alwaysAddToPackageJson": false + }, + "react-native-screens": { + "version": "~3.15.0", + "alwaysAddToPackageJson": false + }, + "react-native-svg": { + "version": "12.4.3", + "alwaysAddToPackageJson": false + }, + "@svgr/webpack": { + "version": "^6.3.1", + "alwaysAddToPackageJson": false + }, + "metro-resolver": { + "version": "0.72.0", + "alwaysAddToPackageJson": false + }, + "metro-babel-register": { + "version": "0.72.0", + "alwaysAddToPackageJson": false + }, + "@testing-library/react-native": { + "version": "11.0.0", + "alwaysAddToPackageJson": false + } + } + }, + "14.5.2": { + "version": "14.5.2-beta.0", + "packages": { + "expo": { + "version": "46.0.9", + "alwaysAddToPackageJson": false + }, + "expo-cli": { + "version": "6.0.5", + "alwaysAddToPackageJson": false + }, + "eas-cli": { + "version": "1.1.1", + "alwaysAddToPackageJson": false + }, + "react-native": { + "version": "0.69.4", + "alwaysAddToPackageJson": false + }, + "react-native-svg": { + "version": "13.0.0", + "alwaysAddToPackageJson": false + }, + "metro-resolver": { + "version": "0.72.1", + "alwaysAddToPackageJson": false + }, + "@testing-library/jest-native": { + "version": "4.0.11", + "alwaysAddToPackageJson": false + }, + "@expo/metro-config": { + "version": "0.3.22", + "alwaysAddToPackageJson": false + } + } + }, + "14.5.3": { + "version": "14.5.3-beta.0", + "packages": { + "expo": { + "version": "46.0.10", + "alwaysAddToPackageJson": false + }, + "eas-cli": { + "version": "2.1.0", + "alwaysAddToPackageJson": false + }, + "react-native": { + "version": "0.69.5", + "alwaysAddToPackageJson": false + }, + "@types/react-native": { + "version": "0.69.8", + "alwaysAddToPackageJson": false + }, + "react-native-svg": { + "version": "13.1.0", + "alwaysAddToPackageJson": false + }, + "metro": { + "version": "0.72.2", + "alwaysAddToPackageJson": false + } + } + } + } +} diff --git a/packages/expo/package.json b/packages/expo/package.json new file mode 100644 index 0000000000..ae4ca357b4 --- /dev/null +++ b/packages/expo/package.json @@ -0,0 +1,54 @@ +{ + "name": "@nrwl/expo", + "version": "0.0.1", + "description": "Expo Plugin for Nx", + "keywords": [ + "Monorepo", + "Expo", + "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": "file:../detox", + "@nrwl/devkit": "file:../devkit", + "@nrwl/jest": "file:../jest", + "@nrwl/linter": "file:../linter", + "@nrwl/react": "file:../react", + "@nrwl/web": "file:../web", + "@nrwl/workspace": "file:../workspace", + "@svgr/webpack": "^6.1.2", + "chalk": "^4.1.0", + "enhanced-resolve": "^5.8.3", + "fs-extra": "^10.1.0", + "metro-resolver": "^0.72.2", + "node-fetch": "^2.6.7", + "tar-fs": "^2.1.1", + "tsconfig-paths": "^3.9.0", + "tsconfig-paths-webpack-plugin": "^3.5.2" + }, + "peerDependencies": { + "expo": "^46.0.10" + }, + "builders": "./executors.json", + "ng-update": { + "requirements": {}, + "migrations": "./migrations.json" + }, + "schematics": "./generators.json" +} diff --git a/packages/expo/plugins/jest/svg-mock.ts b/packages/expo/plugins/jest/svg-mock.ts new file mode 100644 index 0000000000..5d45aae397 --- /dev/null +++ b/packages/expo/plugins/jest/svg-mock.ts @@ -0,0 +1,3 @@ +// From https://github.com/kristerkari/react-native-svg-transformer#usage-with-jest +module.exports = 'SvgMock'; +module.exports.ReactComponent = 'SvgMock'; diff --git a/packages/expo/plugins/metro-resolver.ts b/packages/expo/plugins/metro-resolver.ts new file mode 100644 index 0000000000..1bfb6887b5 --- /dev/null +++ b/packages/expo/plugins/metro-resolver.ts @@ -0,0 +1,183 @@ +import * as metroResolver from 'metro-resolver'; +import type { MatchPath } from 'tsconfig-paths'; +import { createMatchPath, loadConfig } from 'tsconfig-paths'; +import * as chalk from 'chalk'; +import { CachedInputFileSystem, ResolverFactory } from 'enhanced-resolve'; +import { dirname, join } from 'path'; +import * as fs from 'fs'; +import { workspaceRoot } from '@nrwl/devkit'; + +/* + * 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 + ) { + const debug = process.env.NX_REACT_NATIVE_DEBUG === 'true'; + + if (debug) console.log(chalk.cyan(`[Nx] Resolving: ${realModuleName}`)); + + const { resolveRequest, ...context } = _context; + + const resolvedPath = + defaultMetroResolver(context, realModuleName, platform, debug) || + tsconfigPathsResolver( + context, + extensions, + realModuleName, + platform, + debug + ) || + pnpmResolver(extensions, context, realModuleName, debug); + if (resolvedPath) { + return resolvedPath; + } + throw new Error(`Cannot resolve ${chalk.bold(realModuleName)}`); + }; +} + +/** + * This function try to resolve path using metro's default resolver + * @returns path if resolved, else undefined + */ +function defaultMetroResolver( + context: any, + realModuleName: string, + platform: string, + debug: boolean +) { + try { + return metroResolver.resolve(context, realModuleName, platform); + } catch { + if (debug) + console.log( + chalk.cyan( + `[Nx] Unable to resolve with default Metro resolver: ${realModuleName}` + ) + ); + } +} + +/** + * 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: string[], + context: any, + realModuleName: string, + debug: boolean +) { + try { + const pnpmResolve = getPnpmResolver(extensions); + const lookupStartPath = dirname(context.originModulePath); + const filePath = pnpmResolve.resolveSync( + {}, + lookupStartPath, + realModuleName + ); + if (filePath) { + return { type: 'sourceFile', filePath }; + } + } catch { + if (debug) + console.log( + chalk.cyan( + `[Nx] Unable to resolve with default PNPM resolver: ${realModuleName}` + ) + ); + } +} + +/** + * This function try to resolve files that are specified in tsconfig's paths + * @returns path if resolved, else undefined + */ +function tsconfigPathsResolver( + context: any, + extensions: string[], + realModuleName: string, + platform: string, + debug: boolean +) { + const tsConfigPathMatcher = getMatcher(debug); + const match = tsConfigPathMatcher( + realModuleName, + undefined, + undefined, + extensions.map((ext) => `.${ext}`) + ); + + if (match) { + return metroResolver.resolve(context, match, platform); + } else { + if (debug) { + console.log( + chalk.red(`[Nx] Failed to resolve ${chalk.bold(realModuleName)}`) + ); + console.log( + chalk.cyan( + `[Nx] The following tsconfig paths was used:\n:${chalk.bold( + JSON.stringify(paths, null, 2) + )}` + ) + ); + } + } +} + +let matcher: MatchPath; +let absoluteBaseUrl: string; +let paths: Record; + +function getMatcher(debug: boolean) { + 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(extensions: string[]) { + if (!resolver) { + const fileSystem = new CachedInputFileSystem(fs, 4000); + resolver = ResolverFactory.createResolver({ + fileSystem, + extensions: extensions.map((extension) => '.' + extension), + useSyncFileSystemCalls: true, + modules: [join(workspaceRoot, '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 0000000000..7fd0b2998b --- /dev/null +++ b/packages/expo/plugins/with-nx-metro.ts @@ -0,0 +1,40 @@ +import { workspaceLayout, workspaceRoot } from '@nrwl/devkit'; +import { join } from 'path'; +import { existsSync } from 'fs-extra'; + +import { getResolveRequest } from './metro-resolver'; + +interface WithNxOptions { + debug?: boolean; + extensions?: string[]; + projectRoot?: string; + watchFolders?: 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); + + config.projectRoot = opts.projectRoot || workspaceRoot; + + // Add support for paths specified by tsconfig + config.resolver = { + ...config.resolver, + resolveRequest: getResolveRequest(extensions), + }; + + let watchFolders = config.watchFolders || []; + watchFolders = watchFolders.concat([ + join(workspaceRoot, 'node_modules'), + join(workspaceRoot, workspaceLayout().libsDir), + ]); + if (opts.watchFolders?.length) { + watchFolders = watchFolders.concat(opts.watchFolders); + } + + watchFolders = watchFolders.filter((folder) => existsSync(folder)); + config.watchFolders = watchFolders; + + return config; +} diff --git a/packages/expo/plugins/with-nx-webpack.ts b/packages/expo/plugins/with-nx-webpack.ts new file mode 100644 index 0000000000..d609fa4c1f --- /dev/null +++ b/packages/expo/plugins/with-nx-webpack.ts @@ -0,0 +1,100 @@ +import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'; +import { resolve } from 'path'; + +/** + * This function add addtional rules to expo's webpack config to make expo web working + */ +export async function withNxWebpack(config) { + // add additional rule to load files under libs + const rules = config.module.rules[1]?.oneOf; + if (rules) { + rules.push({ + test: /\.(mjs|[jt]sx?)$/, + exclude: /node_modules/, + use: { + loader: require.resolve('@nrwl/web/src/utils/web-babel-loader.js'), + options: { + presets: [ + [ + '@nrwl/react/babel', + { + runtime: 'automatic', + }, + ], + ], + }, + }, + }); + // svg rule from https://github.com/kristerkari/react-native-svg-transformer/issues/135#issuecomment-1008310514 + rules.unshift({ + test: /\.svg$/, + exclude: /node_modules/, + use: [ + { + loader: require.resolve('@svgr/webpack'), + options: { + svgoConfig: { + plugins: { + cleanupAttrs: true, + cleanupEnableBackground: true, + cleanupIDs: true, + cleanupListOfValues: true, + cleanupNumericValues: true, + collapseGroups: true, + convertEllipseToCircle: true, + convertPathData: true, + convertShapeToPath: true, + convertStyleToAttrs: true, + convertTransform: true, + inlineStyles: true, + mergePaths: true, + minifyStyles: true, + moveElemsAttrsToGroup: true, + moveGroupAttrsToElems: true, + removeComments: true, + removeDesc: true, + removeDimensions: false, + removeDoctype: true, + removeEditorsNSData: true, + removeEmptyAttrs: true, + removeEmptyContainers: true, + removeEmptyText: true, + removeHiddenElems: true, + removeMetadata: true, + removeNonInheritableGroupAttrs: true, + removeRasterImages: true, + removeScriptElement: false, + removeStyleElement: false, + removeTitle: true, + removeUnknownsAndDefaults: true, + removeUnusedNS: true, + removeUselessDefs: true, + removeUselessStrokeAndFill: true, + removeViewBox: false, + removeXMLNS: true, + removeXMLProcInst: true, + reusePaths: true, + sortAttrs: true, + sortDefsChildren: true, + convertColors: false, + }, + }, + }, + }, + ], + }); + } + + const extensions = ['.ts', '.tsx', '.mjs', '.js', '.jsx']; + const tsConfigPath = resolve('tsconfig.json'); + + config.resolve.plugins.push( + new TsconfigPathsPlugin({ + configFile: tsConfigPath, + extensions, + }) + ); + + config.resolve.symlinks = true; + return config; +} diff --git a/packages/expo/project.json b/packages/expo/project.json new file mode 100644 index 0000000000..86bfc21fc7 --- /dev/null +++ b/packages/expo/project.json @@ -0,0 +1,92 @@ +{ + "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.ts", + "passWithNoTests": true + }, + "outputs": ["coverage/packages/expo"] + }, + "build-base": { + "executor": "@nrwl/js:tsc", + "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.js.template", + "output": "/" + }, + { + "input": "packages/expo", + "glob": "**/*.json", + "ignore": ["**/tsconfig*.json", "**/project.json"], + "output": "/" + }, + "LICENSE" + ] + }, + "outputs": ["{options.outputPath}"] + }, + "build": { + "executor": "nx:run-commands", + "outputs": ["build/packages/expo"], + "options": { + "command": "node ./scripts/copy-readme.js expo" + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "parallel": false, + "commands": [ + "nx build expo", + "node tools/scripts/publish.mjs expo {args.ver} {args.tag}" + ] + } + } + }, + "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 0000000000..c84e66a8e7 --- /dev/null +++ b/packages/expo/src/executors/build-android/build-android.impl.ts @@ -0,0 +1,88 @@ +import { ExecutorContext, names } from '@nrwl/devkit'; +import { join } from 'path'; +import { ChildProcess, fork } from 'child_process'; + +import { ensureNodeModulesSymlink } from '../../utils/ensure-node-modules-symlink'; + +import { ExpoBuildAndroidOptions } from './schema'; +import { + displayNewlyAddedDepsMessage, + syncDeps, +} from '../sync-deps/sync-deps.impl'; + +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); + if (options.sync) { + displayNewlyAddedDepsMessage( + context.projectName, + await syncDeps(context.projectName, 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-cli/bin/expo.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); + } + }); + }); +} + +const nxOptions = ['sync']; + +function createRunOptions(options: ExpoBuildAndroidOptions) { + return Object.keys(options).reduce((acc, k) => { + const v = options[k]; + if (!nxOptions.includes(k)) { + if (typeof v === 'boolean') { + if (v === true) { + // when true, does not need to pass the value true, just need to pass the flag in kebob case + acc.push(`--${names(k).fileName}`); + } + } else { + acc.push(`--${names(k).fileName}`, v); + } + } + 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 0000000000..12465b9fe8 --- /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 0000000000..390666a53c --- /dev/null +++ b/packages/expo/src/executors/build-android/schema.d.ts @@ -0,0 +1,13 @@ +// 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; + sync: boolean; // default is true +} 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 0000000000..da58f8781a --- /dev/null +++ b/packages/expo/src/executors/build-android/schema.json @@ -0,0 +1,54 @@ +{ + "$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." + }, + "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-ios/build-ios.impl.ts b/packages/expo/src/executors/build-ios/build-ios.impl.ts new file mode 100644 index 0000000000..e0aa01f0cb --- /dev/null +++ b/packages/expo/src/executors/build-ios/build-ios.impl.ts @@ -0,0 +1,88 @@ +import { ExecutorContext, names } from '@nrwl/devkit'; +import { join } from 'path'; +import { ChildProcess, fork } from 'child_process'; + +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-cli/bin/expo.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: ExpoBuildIOSOptions) { + return Object.keys(options).reduce((acc, k) => { + const v = options[k]; + if (!nxOptions.includes(k)) { + if (typeof v === 'boolean') { + if (v === true) { + // when true, does not need to pass the value true, just need to pass the flag in kebob case + acc.push(`--${names(k).fileName}`); + } + } else { + acc.push(`--${names(k).fileName}`, v); + } + } + 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 0000000000..3803011250 --- /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 0000000000..301f6a713b --- /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?: boolean; + 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 0000000000..8c945459a7 --- /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-list/build-list.impl.ts b/packages/expo/src/executors/build-list/build-list.impl.ts new file mode 100644 index 0000000000..9bfb4a9532 --- /dev/null +++ b/packages/expo/src/executors/build-list/build-list.impl.ts @@ -0,0 +1,53 @@ +import { ExecutorContext, logger, names } from '@nrwl/devkit'; +import { join } from 'path'; +import { execSync } from 'child_process'; + +import { ensureNodeModulesSymlink } from '../../utils/ensure-node-modules-symlink'; + +import { ExpoEasBuildListOptions } from './schema'; + +export interface ReactNativeBuildListOutput { + success: boolean; +} + +export default async function* buildListExecutor( + options: ExpoEasBuildListOptions, + context: ExecutorContext +): AsyncGenerator { + const projectRoot = context.workspace.projects[context.projectName].root; + ensureNodeModulesSymlink(context.root, projectRoot); + + logger.info(runCliBuildList(context.root, projectRoot, options)); + yield { success: true }; +} + +export function runCliBuildList( + workspaceRoot: string, + projectRoot: string, + options: ExpoEasBuildListOptions +): string { + return execSync( + `./node_modules/eas-cli/bin/run build:list ${createBuildListOptions( + options + ).join(' ')}`, + { cwd: join(workspaceRoot, projectRoot) } + ).toString(); +} + +const nxOptions = ['output']; +function createBuildListOptions(options: ExpoEasBuildListOptions): string[] { + return Object.keys(options).reduce((acc, k) => { + const v = options[k]; + if (!nxOptions.includes(k)) { + if (typeof v === 'boolean') { + if (v === true) { + // when true, does not need to pass the value true, just need to pass the flag in camel case + acc.push(`--${names(k).propertyName}`); + } + } else { + acc.push(`--${names(k).propertyName}`, v); + } + } + return acc; + }, []); +} diff --git a/packages/expo/src/executors/build-list/compat.ts b/packages/expo/src/executors/build-list/compat.ts new file mode 100644 index 0000000000..9031c2275e --- /dev/null +++ b/packages/expo/src/executors/build-list/compat.ts @@ -0,0 +1,5 @@ +import { convertNxExecutor } from '@nrwl/devkit'; + +import buildListExecutor from './build-list.impl'; + +export default convertNxExecutor(buildListExecutor); diff --git a/packages/expo/src/executors/build-list/schema.d.ts b/packages/expo/src/executors/build-list/schema.d.ts new file mode 100644 index 0000000000..1d1d688b7f --- /dev/null +++ b/packages/expo/src/executors/build-list/schema.d.ts @@ -0,0 +1,24 @@ +// command to run https://github.com/expo/eas-cli/tree/main#eas-buildlist +// options from https://github.com/expo/eas-cli/blob/main/packages/eas-cli/src/commands/build/list.ts +export interface ExpoEasBuildListOptions { + platform: 'ios' | 'android' | 'all'; + json?: boolean; + // status and distribution enum from https://github.com/expo/eas-cli/blob/main/packages/eas-cli/src/build/types.ts + status?: + | 'new' + | 'in-queue' + | 'in-progress' + | 'errored' + | 'finished' + | 'canceled'; + distribution?: 'store' | 'internal' | 'simulator'; + channel?: string; + appVersion?: string; + appBuildVersion?: string; + sdkVersion?: string; + runtimeVersion?: string; + appIdentifier?: string; + buildProject?: string; + gitCommitHash?: string; + limit?: number; +} diff --git a/packages/expo/src/executors/build-list/schema.json b/packages/expo/src/executors/build-list/schema.json new file mode 100644 index 0000000000..70c2c2005a --- /dev/null +++ b/packages/expo/src/executors/build-list/schema.json @@ -0,0 +1,71 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "NxExpoEasBuildList", + "cli": "nx", + "title": "Expo EAS Build List executor", + "description": "List all EAS builds for your Expo project", + "type": "object", + "properties": { + "platform": { + "enum": ["ios", "android", "all"], + "alias": "p", + "description": "The platform to build the app, example values: ios, android, all." + }, + "json": { + "type": "boolean", + "description": "Enable JSON output, non-JSON messages will be printed to stderr" + }, + "status": { + "enum": [ + "new", + "in-queue", + "in-progress", + "errored", + "finished", + "canceled" + ], + "description": "Status of EAS build" + }, + "distribution": { + "enum": ["store", "internal", "simulator"], + "description": "Distribution of EAS build" + }, + "channel": { + "type": "string", + "description": "Channel of EAS build" + }, + "appVersion": { + "type": "string", + "description": "App version of EAS build" + }, + "appBuildVersion": { + "type": "string", + "description": "App build version of EAS build" + }, + "sdkVersion": { + "type": "string", + "description": "SDK version of EAS build" + }, + "runtimeVersion": { + "type": "string", + "description": "Runtime version of EAS build" + }, + "appIdentifier": { + "type": "string", + "description": "App identifier of EAS build" + }, + "buildProfile": { + "type": "string", + "description": "Build profile of EAS build" + }, + "gitCommitHash": { + "type": "string", + "description": "Git commit hash of EAS build" + }, + "limit": { + "type": "number", + "description": "Limit of numbers to list EAS builds" + } + }, + "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 0000000000..784d001954 --- /dev/null +++ b/packages/expo/src/executors/build-status/build-status.impl.ts @@ -0,0 +1,74 @@ +import { ExecutorContext, names } from '@nrwl/devkit'; +import { join } from 'path'; +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-cli/bin/expo.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 (typeof v === 'boolean') { + if (v === true) { + // when true, does not need to pass the value true, just need to pass the flag in kebob case + acc.push(`--${names(k).fileName}`); + } + } else { + acc.push(`--${names(k).fileName}`, v); + } + return acc; + }, []); +} 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 0000000000..b2d2934f15 --- /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 0000000000..241aae4116 --- /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 0000000000..d99c1a90f9 --- /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 0000000000..428b77c2c3 --- /dev/null +++ b/packages/expo/src/executors/build-web/build-web.impl.ts @@ -0,0 +1,74 @@ +import { ExecutorContext, names } from '@nrwl/devkit'; +import { join } from 'path'; +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-cli/bin/expo.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 (typeof v === 'boolean') { + if (v === true) { + // when true, does not need to pass the value true, just need to pass the flag in kebob case + acc.push(`--${names(k).fileName}`); + } + } else { + acc.push(`--${names(k).fileName}`, v); + } + return acc; + }, []); +} 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 0000000000..9596d59212 --- /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 0000000000..e7e55d18e4 --- /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 0000000000..062a0a76b1 --- /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/build/build.impl.ts b/packages/expo/src/executors/build/build.impl.ts new file mode 100644 index 0000000000..6e303b3b38 --- /dev/null +++ b/packages/expo/src/executors/build/build.impl.ts @@ -0,0 +1,77 @@ +import { ExecutorContext, names } from '@nrwl/devkit'; +import { join } from 'path'; +import { ChildProcess, fork } from 'child_process'; + +import { ensureNodeModulesSymlink } from '../../utils/ensure-node-modules-symlink'; + +import { ExpoEasBuildOptions } from './schema'; + +export interface ReactNativeBuildOutput { + success: boolean; +} + +let childProcess: ChildProcess; + +export default async function* buildExecutor( + options: ExpoEasBuildOptions, + 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: ExpoEasBuildOptions +) { + return new Promise((resolve, reject) => { + childProcess = fork( + join(workspaceRoot, './node_modules/eas-cli/bin/run'), + ['build', ...createBuildOptions(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 createBuildOptions(options: ExpoEasBuildOptions) { + return Object.keys(options).reduce((acc, k) => { + const v = options[k]; + if (typeof v === 'boolean') { + if (v === true) { + // when true, does not need to pass the value true, just need to pass the flag in kebob case + acc.push(`--${names(k).fileName}`); + } + if (v === false && k === 'wait') { + acc.push('--no-wait'); + } + } else { + acc.push(`--${names(k).fileName}`, v); + } + return acc; + }, []); +} diff --git a/packages/expo/src/executors/build/compat.ts b/packages/expo/src/executors/build/compat.ts new file mode 100644 index 0000000000..477ba0be5f --- /dev/null +++ b/packages/expo/src/executors/build/compat.ts @@ -0,0 +1,5 @@ +import { convertNxExecutor } from '@nrwl/devkit'; + +import buildExecutor from './build.impl'; + +export default convertNxExecutor(buildExecutor); diff --git a/packages/expo/src/executors/build/schema.d.ts b/packages/expo/src/executors/build/schema.d.ts new file mode 100644 index 0000000000..809cdcd707 --- /dev/null +++ b/packages/expo/src/executors/build/schema.d.ts @@ -0,0 +1,14 @@ +// command to run https://github.com/expo/eas-cli/tree/main#eas-build +// options from github.com/expo/eas-cli/blob/main/packages/eas-cli/src/commands/build/index.ts +export interface ExpoEasBuildOptions { + platform: 'ios' | 'android' | 'all'; + profile?: string; + nonInteractive: boolean; // default is false + local: boolean; // default is false + output?: string; + wait: boolean; // default is true + clearCache: boolean; // default is false + json: boolean; // default is false + autoSubmit: boolean; // default is false + autoSubmitWithProfile?: string; +} diff --git a/packages/expo/src/executors/build/schema.json b/packages/expo/src/executors/build/schema.json new file mode 100644 index 0000000000..1aeba3ef40 --- /dev/null +++ b/packages/expo/src/executors/build/schema.json @@ -0,0 +1,56 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "NxExpoEasBuild", + "cli": "nx", + "title": "Expo EAS Build executor", + "description": "Start an EAS build for your expo project", + "type": "object", + "properties": { + "platform": { + "enum": ["ios", "android", "all"], + "alias": "p", + "description": "The platform to build the app, example values: ios, android, all." + }, + "json": { + "type": "boolean", + "description": "Enable JSON output, non-JSON messages will be printed to stderr", + "default": false + }, + "profile": { + "type": "string", + "description": "Name of the build profile from eas.json. Defaults to \"production\" if defined in eas.json.", + "examples": ["PROFILE_NAME"] + }, + "nonInteractive": { + "type": "boolean", + "description": "Run command in non-interactive mode", + "default": false + }, + "local": { + "type": "boolean", + "description": "Run build locally [experimental]", + "default": false + }, + "wait": { + "type": "boolean", + "description": "Wait for build(s) to complete", + "default": true + }, + "clearCache": { + "type": "boolean", + "description": "Clear cache before the build", + "default": false + }, + "autoSubmit": { + "type": "boolean", + "description": "Submit on build complete using the submit profile with the same name as the build profile", + "default": false + }, + "autoSubmitWithProfile": { + "type": "string", + "description": "Submit on build complete using the submit profile with provided name", + "examples": ["PROFILE_NAME"] + } + }, + "required": [] +} diff --git a/packages/expo/src/executors/download/compat.ts b/packages/expo/src/executors/download/compat.ts new file mode 100644 index 0000000000..5b860aa19b --- /dev/null +++ b/packages/expo/src/executors/download/compat.ts @@ -0,0 +1,5 @@ +import { convertNxExecutor } from '@nrwl/devkit'; + +import downloadExecutor from './download.impl'; + +export default convertNxExecutor(downloadExecutor); diff --git a/packages/expo/src/executors/download/download.impl.ts b/packages/expo/src/executors/download/download.impl.ts new file mode 100644 index 0000000000..368705deb3 --- /dev/null +++ b/packages/expo/src/executors/download/download.impl.ts @@ -0,0 +1,137 @@ +import { ExecutorContext, logger, names } from '@nrwl/devkit'; +import { + copyFile, + createReadStream, + createWriteStream, + existsSync, + mkdirSync, +} from 'fs'; +import fetch from 'node-fetch'; +import { promisify } from 'util'; +import { pipeline } from 'stream'; +import * as chalk from 'chalk'; +import { join } from 'path'; +import * as tar from 'tar-fs'; +import { createUnzip } from 'zlib'; + +import { ensureNodeModulesSymlink } from '../../utils/ensure-node-modules-symlink'; + +import { ExpoEasDownloadOptions } from './schema'; +import { runCliBuildList } from '../build-list/build-list.impl'; + +export interface ReactNativeDownloadOutput { + success: boolean; +} + +const streamPipeline = promisify(pipeline); + +/** + * This executor downloads the latest EAS build. + * It calls the build list exectuor to list EAS builds with options passed in. + */ +export default async function* downloadExecutor( + options: ExpoEasDownloadOptions, + context: ExecutorContext +): AsyncGenerator { + const projectRoot = context.workspace.projects[context.projectName].root; + ensureNodeModulesSymlink(context.root, projectRoot); + + const build = getBuild(context.root, projectRoot, options); + const buildUrl = build?.artifacts?.buildUrl; + if (!buildUrl) { + throw new Error(`No build URL found.`); + } + + if (!existsSync(options.output)) { + mkdirSync(options.output, { recursive: true }); + } + + const downloadFileName = buildUrl.split('/').pop(); + const downloadFilePath = join(options.output, downloadFileName); + await downloadBuild(buildUrl, downloadFilePath); + + const appExtension = getAppExtension(build.platform, downloadFileName); + const appName = `${names(build.project?.name).className}${appExtension}`; + const outputFilePath = join(options.output, appName); + + if (downloadFileName.endsWith('.tar.gz')) { + await unzipBuild(downloadFilePath, options.output); + } else { + await copyBuildFile(downloadFilePath, outputFilePath); + } + + logger.info(`Succesfully download the build to ${outputFilePath}`); + + yield { success: true }; +} + +async function downloadBuild(buildUrl: string, output: string) { + const response = await fetch(buildUrl); + + if (!response.ok) + throw new Error( + `Unable to download the build ${buildUrl}. Error: ${response.statusText}` + ); + + return streamPipeline(response.body, createWriteStream(output)); +} + +export function getBuild( + workspaceRoot: string, + projectRoot: string, + options: ExpoEasDownloadOptions +) { + const buildList = runCliBuildList(workspaceRoot, projectRoot, { + ...options, + json: true, + status: 'finished', + limit: 1, + }); + const builds = JSON.parse(buildList); + if (!builds.length) { + throw new Error( + `No EAS build found. Please check expo.dev to make sure your build is finished.` + ); + } + logger.info(`${chalk.bold.cyan('info')} Found build: ${buildList}`); + + return builds[0]; +} + +export function getAppExtension( + platform: string, + downloadFileName: string +): string { + platform = platform.toLowerCase(); + if (platform === 'ios') { + return '.app'; + } + if (downloadFileName.includes('.')) { + return `.${downloadFileName.split('.').pop()}`; + } + throw new Error(`Invalid build name found: ${downloadFileName}`); +} + +export function unzipBuild( + downloadFilePath: string, + outputDirectoryPath: string +) { + const unzip = createUnzip(); + const extract = tar.extract(outputDirectoryPath); + return streamPipeline(createReadStream(downloadFilePath), unzip, extract); +} + +export function copyBuildFile( + downloadFilePath: string, + outputFilePath: string +) { + return new Promise((resolve, reject) => { + copyFile(downloadFilePath, outputFilePath, (error) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); +} diff --git a/packages/expo/src/executors/download/schema.d.ts b/packages/expo/src/executors/download/schema.d.ts new file mode 100644 index 0000000000..70ba277069 --- /dev/null +++ b/packages/expo/src/executors/download/schema.d.ts @@ -0,0 +1,15 @@ +// subset options from https://github.com/expo/eas-cli/blob/main/packages/eas-cli/src/commands/build/list.ts +export interface ExpoEasDownloadOptions { + platform: 'ios' | 'android' | 'all'; + // status and distribution enum from https://github.com/expo/eas-cli/blob/main/packages/eas-cli/src/build/types.ts + distribution?: 'store' | 'internal' | 'simulator'; + channel?: string; + appVersion?: string; + appBuildVersion?: string; + sdkVersion?: string; + runtimeVersion?: string; + appIdentifier?: string; + buildProject?: string; + gitCommitHash?: string; + output: string; +} diff --git a/packages/expo/src/executors/download/schema.json b/packages/expo/src/executors/download/schema.json new file mode 100644 index 0000000000..f7c895857f --- /dev/null +++ b/packages/expo/src/executors/download/schema.json @@ -0,0 +1,56 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "NxExpoDownloadEasBuild", + "cli": "nx", + "title": "Download EAS Build executor", + "description": "Download an EAS build", + "type": "object", + "properties": { + "platform": { + "enum": ["ios", "android"], + "alias": "p", + "description": "The platform to build the app, example values: ios, android, all." + }, + "distribution": { + "enum": ["store", "internal", "simulator"], + "description": "Distribution of EAS build" + }, + "channel": { + "type": "string", + "description": "Channel of EAS build" + }, + "appVersion": { + "type": "string", + "description": "App version of EAS build" + }, + "appBuildVersion": { + "type": "string", + "description": "App build version of EAS build" + }, + "sdkVersion": { + "type": "string", + "description": "SDK version of EAS build" + }, + "runtimeVersion": { + "type": "string", + "description": "Runtime version of EAS build" + }, + "appIdentifier": { + "type": "string", + "description": "App identifier of EAS build" + }, + "buildProfile": { + "type": "string", + "description": "Build profile of EAS build" + }, + "gitCommitHash": { + "type": "string", + "description": "Git commit hash of EAS build" + }, + "output": { + "type": "string", + "description": "Output directory for the download build" + } + }, + "required": ["output"] +} diff --git a/packages/expo/src/executors/eject/compat.ts b/packages/expo/src/executors/eject/compat.ts new file mode 100644 index 0000000000..4f1b8c5432 --- /dev/null +++ b/packages/expo/src/executors/eject/compat.ts @@ -0,0 +1,5 @@ +import { convertNxExecutor } from '@nrwl/devkit'; + +import ejectExecutor from './eject.impl'; + +export default convertNxExecutor(ejectExecutor); diff --git a/packages/expo/src/executors/eject/eject.impl.ts b/packages/expo/src/executors/eject/eject.impl.ts new file mode 100644 index 0000000000..07b3c7f651 --- /dev/null +++ b/packages/expo/src/executors/eject/eject.impl.ts @@ -0,0 +1,77 @@ +import { ExecutorContext } from '@nrwl/devkit'; +import { ChildProcess, fork } from 'child_process'; +import { join } from 'path'; + +import { ensureNodeModulesSymlink } from '../../utils/ensure-node-modules-symlink'; +import { podInstall } from '../../utils/pod-install-task'; +import { ExpoEjectOptions } from './schema'; + +export interface ExpoEjectOutput { + success: boolean; +} + +let childProcess: ChildProcess; + +export default async function* ejectExecutor( + options: ExpoEjectOptions, + context: ExecutorContext +): AsyncGenerator { + const projectRoot = context.workspace.projects[context.projectName].root; + ensureNodeModulesSymlink(context.root, projectRoot); + + try { + await ejectAsync(context.root, projectRoot, options); + + if (options.install) { + await podInstall(join(context.root, projectRoot, 'ios')); + } + + yield { + success: true, + }; + } finally { + if (childProcess) { + childProcess.kill(); + } + } +} + +function ejectAsync( + workspaceRoot: string, + projectRoot: string, + options: ExpoEjectOptions +): Promise { + return new Promise((resolve, reject) => { + childProcess = fork( + join(workspaceRoot, './node_modules/expo-cli/bin/expo.js'), + ['eject', ...createEjectOptions(options), '--no-install'], + { 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 = ['install']; +function createEjectOptions(options: ExpoEjectOptions) { + return Object.keys(options).reduce((acc, k) => { + const v = options[k]; + if (!nxOptions.includes(k)) { + acc.push(`--${k}`, v); + } + return acc; + }, []); +} diff --git a/packages/expo/src/executors/eject/schema.d.ts b/packages/expo/src/executors/eject/schema.d.ts new file mode 100644 index 0000000000..38f363d495 --- /dev/null +++ b/packages/expo/src/executors/eject/schema.d.ts @@ -0,0 +1,6 @@ +// options from https://docs.expo.dev/workflow/expo-cli/#eject + +export interface ExpoEjectOptions { + install: boolean; // default is true + platform: 'all' | 'android' | 'ios'; // default is all +} diff --git a/packages/expo/src/executors/eject/schema.json b/packages/expo/src/executors/eject/schema.json new file mode 100644 index 0000000000..d95ded6bf1 --- /dev/null +++ b/packages/expo/src/executors/eject/schema.json @@ -0,0 +1,21 @@ +{ + "cli": "nx", + "$id": "NxExpoEject", + "$schema": "http://json-schema.org/schema", + "title": "Expo Eject", + "description": "Create native iOS and Android project files", + "type": "object", + "properties": { + "install": { + "type": "boolean", + "description": "Install CocoaPods.", + "default": true + }, + "platform": { + "type": "string", + "description": "Platforms to sync", + "default": "all", + "examples": ["ios", "android", "all"] + } + } +} 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 0000000000..94c2df8239 --- /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 0000000000..440230c33d --- /dev/null +++ b/packages/expo/src/executors/ensure-symlink/ensure-symlink.impl.ts @@ -0,0 +1,18 @@ +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 0000000000..5652db3bdb --- /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 Expo", + "description": "Ensure workspace node_modules is symlink under app's node_modules folder.", + "type": "object", + "properties": {} +} diff --git a/packages/expo/src/executors/publish-set/compat.ts b/packages/expo/src/executors/publish-set/compat.ts new file mode 100644 index 0000000000..4f8b7b0bb7 --- /dev/null +++ b/packages/expo/src/executors/publish-set/compat.ts @@ -0,0 +1,5 @@ +import { convertNxExecutor } from '@nrwl/devkit'; + +import publishSetExecutor from './publish-set.impl'; + +export default convertNxExecutor(publishSetExecutor); diff --git a/packages/expo/src/executors/publish-set/publish-set.impl.ts b/packages/expo/src/executors/publish-set/publish-set.impl.ts new file mode 100644 index 0000000000..90a767094d --- /dev/null +++ b/packages/expo/src/executors/publish-set/publish-set.impl.ts @@ -0,0 +1,76 @@ +import { ExecutorContext, names } from '@nrwl/devkit'; +import { join } from 'path'; +import { ChildProcess, fork } from 'child_process'; + +import { ensureNodeModulesSymlink } from '../../utils/ensure-node-modules-symlink'; +import { ExpoPublishSetOptions } from './schema'; + +export interface ExpoPublishSetOutput { + success: boolean; +} + +let childProcess: ChildProcess; + +export default async function* publishSetExecutor( + options: ExpoPublishSetOptions, + context: ExecutorContext +): AsyncGenerator { + const projectRoot = context.workspace.projects[context.projectName].root; + ensureNodeModulesSymlink(context.root, projectRoot); + + try { + await runCliPublishSet(context.root, projectRoot, options); + + yield { success: true }; + } finally { + if (childProcess) { + childProcess.kill(); + } + } +} + +function runCliPublishSet( + workspaceRoot: string, + projectRoot: string, + options: ExpoPublishSetOptions +) { + return new Promise((resolve, reject) => { + childProcess = fork( + join(workspaceRoot, './node_modules/expo-cli/bin/expo.js'), + ['publish:set', ...createPublishSetOptions(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); + } + }); + }); +} + +function createPublishSetOptions(options: ExpoPublishSetOptions) { + return Object.keys(options).reduce((acc, k) => { + const v = options[k]; + if (typeof v === 'boolean') { + if (v === true) { + // when true, does not need to pass the value true, just need to pass the flag in kebob case + acc.push(`--${names(k).fileName}`); + } + } else { + acc.push(`--${names(k).fileName}`, v); + } + return acc; + }, []); +} diff --git a/packages/expo/src/executors/publish-set/schema.d.ts b/packages/expo/src/executors/publish-set/schema.d.ts new file mode 100644 index 0000000000..24dc6fce8c --- /dev/null +++ b/packages/expo/src/executors/publish-set/schema.d.ts @@ -0,0 +1,6 @@ +// options from https://docs.expo.dev/workflow/expo-cli/#expo-publishrollback +export interface ExpoPublishSetOptions { + releaseChannel: string; + sdkVersion: string; + platform?: 'ios' | 'android'; +} diff --git a/packages/expo/src/executors/publish-set/schema.json b/packages/expo/src/executors/publish-set/schema.json new file mode 100644 index 0000000000..55a9ffbf04 --- /dev/null +++ b/packages/expo/src/executors/publish-set/schema.json @@ -0,0 +1,19 @@ +{ + "cli": "nx", + "$id": "NxExpoPublishSet", + "$schema": "http://json-schema.org/schema", + "title": "Set Publish Channel for Expo", + "description": "Specify the channel to serve a published release", + "type": "object", + "properties": { + "releaseChannel": { + "type": "string", + "description": "The release channel to publish to." + }, + "publishId": { + "type": "string", + "description": "The id of the published release to serve from the channel." + } + }, + "required": ["releaseChannel", "publishId"] +} diff --git a/packages/expo/src/executors/publish/compat.ts b/packages/expo/src/executors/publish/compat.ts new file mode 100644 index 0000000000..facc63b274 --- /dev/null +++ b/packages/expo/src/executors/publish/compat.ts @@ -0,0 +1,5 @@ +import { convertNxExecutor } from '@nrwl/devkit'; + +import publishExecutor from './publish.impl'; + +export default convertNxExecutor(publishExecutor); diff --git a/packages/expo/src/executors/publish/publish.impl.ts b/packages/expo/src/executors/publish/publish.impl.ts new file mode 100644 index 0000000000..1f1d0eec7e --- /dev/null +++ b/packages/expo/src/executors/publish/publish.impl.ts @@ -0,0 +1,94 @@ +import { ExecutorContext, names } from '@nrwl/devkit'; +import { join } from 'path'; +import { ChildProcess, fork } from 'child_process'; + +import { ensureNodeModulesSymlink } from '../../utils/ensure-node-modules-symlink'; +import { + displayNewlyAddedDepsMessage, + syncDeps, +} from '../sync-deps/sync-deps.impl'; +import { ExpoPublishOptions } from './schema'; + +export interface ExpoPublishOutput { + success: boolean; +} + +let childProcess: ChildProcess; + +export default async function* publishExecutor( + options: ExpoPublishOptions, + 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 runCliPublish(context.root, projectRoot, options); + + yield { success: true }; + } finally { + if (childProcess) { + childProcess.kill(); + } + } +} + +function runCliPublish( + workspaceRoot: string, + projectRoot: string, + options: ExpoPublishOptions +) { + return new Promise((resolve, reject) => { + childProcess = fork( + join(workspaceRoot, './node_modules/expo-cli/bin/expo.js'), + [ + 'publish', + join(workspaceRoot, projectRoot), + ...createPublishOptions(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']; + +function createPublishOptions(options: ExpoPublishOptions) { + return Object.keys(options).reduce((acc, k) => { + const v = options[k]; + if (!nxOptions.includes(k)) { + if (typeof v === 'boolean') { + if (v === true) { + // when true, does not need to pass the value true, just need to pass the flag in kebob case + acc.push(`--${names(k).fileName}`); + } + } else { + acc.push(`--${names(k).fileName}`, v); + } + } + return acc; + }, []); +} diff --git a/packages/expo/src/executors/publish/schema.d.ts b/packages/expo/src/executors/publish/schema.d.ts new file mode 100644 index 0000000000..2a9ac7096b --- /dev/null +++ b/packages/expo/src/executors/publish/schema.d.ts @@ -0,0 +1,10 @@ +// options from https://docs.expo.dev/workflow/expo-cli/#expo-publish +export interface ExpoPublishOptions { + quiet: boolean; // default is false + sendTo?: string; + clear: boolean; // default is false + target: 'managed' | 'bare'; + maxWorkers?: number; + releaseChannel: string; // default is 'default' + sync: boolean; // default is true +} diff --git a/packages/expo/src/executors/publish/schema.json b/packages/expo/src/executors/publish/schema.json new file mode 100644 index 0000000000..b5c506e132 --- /dev/null +++ b/packages/expo/src/executors/publish/schema.json @@ -0,0 +1,42 @@ +{ + "cli": "nx", + "$id": "NxExpoPublish", + "$schema": "http://json-schema.org/schema", + "title": "Publish for Expo", + "description": "Deploy a project to Expo hosting", + "type": "object", + "properties": { + "quiet": { + "type": "boolean", + "description": "Suppress verbose output from the Metro bundler", + "default": false, + "alias": "q" + }, + "sendTo": { + "type": "string", + "description": "A phone number or email address to send a link to", + "alias": "s" + }, + "clear": { + "type": "boolean", + "description": "Clear the Metro bundler cache", + "default": false, + "alias": "c" + }, + "target": { + "enum": ["managed", "bare"], + "default": "managed", + "description": "Target environment for which this publish is intended. Options are managed or bare.", + "alias": "t" + }, + "maxWorkers": { + "type": "number", + "description": "Maximum number of tasks to allow Metro to spawn" + }, + "releaseChannel": { + "type": "string", + "description": "The release channel to publish to. Default is 'default'.", + "default": "default" + } + } +} diff --git a/packages/expo/src/executors/rollback/compat.ts b/packages/expo/src/executors/rollback/compat.ts new file mode 100644 index 0000000000..de76d98188 --- /dev/null +++ b/packages/expo/src/executors/rollback/compat.ts @@ -0,0 +1,5 @@ +import { convertNxExecutor } from '@nrwl/devkit'; + +import rollbackExecutor from './rollback.impl'; + +export default convertNxExecutor(rollbackExecutor); diff --git a/packages/expo/src/executors/rollback/rollback.impl.ts b/packages/expo/src/executors/rollback/rollback.impl.ts new file mode 100644 index 0000000000..3c294137dc --- /dev/null +++ b/packages/expo/src/executors/rollback/rollback.impl.ts @@ -0,0 +1,76 @@ +import { ExecutorContext, names } from '@nrwl/devkit'; +import { join } from 'path'; +import { ChildProcess, fork } from 'child_process'; + +import { ensureNodeModulesSymlink } from '../../utils/ensure-node-modules-symlink'; +import { ExpoRollbackOptions } from './schema'; + +export interface ExpoRollbackOutput { + success: boolean; +} + +let childProcess: ChildProcess; + +export default async function* rollbackExecutor( + options: ExpoRollbackOptions, + context: ExecutorContext +): AsyncGenerator { + const projectRoot = context.workspace.projects[context.projectName].root; + ensureNodeModulesSymlink(context.root, projectRoot); + + try { + await runCliRollback(context.root, projectRoot, options); + + yield { success: true }; + } finally { + if (childProcess) { + childProcess.kill(); + } + } +} + +function runCliRollback( + workspaceRoot: string, + projectRoot: string, + options: ExpoRollbackOptions +) { + return new Promise((resolve, reject) => { + childProcess = fork( + join(workspaceRoot, './node_modules/expo-cli/bin/expo.js'), + ['publish:rollback', ...createRollbackOptions(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); + } + }); + }); +} + +function createRollbackOptions(options: ExpoRollbackOptions) { + return Object.keys(options).reduce((acc, k) => { + const v = options[k]; + if (typeof v === 'boolean') { + if (v === true) { + // when true, does not need to pass the value true, just need to pass the flag in kebob case + acc.push(`--${names(k).fileName}`); + } + } else { + acc.push(`--${names(k).fileName}`, v); + } + return acc; + }, []); +} diff --git a/packages/expo/src/executors/rollback/schema.d.ts b/packages/expo/src/executors/rollback/schema.d.ts new file mode 100644 index 0000000000..83c65f2579 --- /dev/null +++ b/packages/expo/src/executors/rollback/schema.d.ts @@ -0,0 +1,6 @@ +// options from https://docs.expo.dev/workflow/expo-cli/#expo-publishrollback +export interface ExpoRollbackOptions { + releaseChannel: string; + sdkVersion: string; + platform?: 'ios' | 'android'; +} diff --git a/packages/expo/src/executors/rollback/schema.json b/packages/expo/src/executors/rollback/schema.json new file mode 100644 index 0000000000..aa2ddb7e16 --- /dev/null +++ b/packages/expo/src/executors/rollback/schema.json @@ -0,0 +1,23 @@ +{ + "cli": "nx", + "$id": "NxExpoRollback", + "$schema": "http://json-schema.org/schema", + "title": "Rollback Publish Command for Expo", + "description": "Undo an update to a channel", + "type": "object", + "properties": { + "releaseChannel": { + "type": "string", + "description": "The release channel to publish to." + }, + "sdkVersion": { + "type": "string", + "description": "The sdk version to rollback." + }, + "platform": { + "enum": ["ios", "android"], + "description": "The platform to rollback." + } + }, + "required": ["releaseChannel", "sdkVersion"] +} diff --git a/packages/expo/src/executors/run/compat.ts b/packages/expo/src/executors/run/compat.ts new file mode 100644 index 0000000000..54c6c1a970 --- /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 0000000000..746d4b381a --- /dev/null +++ b/packages/expo/src/executors/run/run.impl.ts @@ -0,0 +1,109 @@ +import { ExecutorContext, names } from '@nrwl/devkit'; +import { join } from 'path'; +import { ChildProcess, fork } from 'child_process'; +import { platform } from 'os'; + +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-cli/bin/expo.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 (typeof v === 'boolean') { + if (v === true) { + // when true, does not need to pass the value true, just need to pass the flag in kebob case + acc.push(`--${names(k).fileName}`); + } + } else { + acc.push(`--${names(k).fileName}`, v); + } + } + return acc; + }, []); +} 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 0000000000..b67a428ad9 --- /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 0000000000..1e53e7676b --- /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 0000000000..dfc3aa7e71 --- /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 0000000000..7f6940efd6 --- /dev/null +++ b/packages/expo/src/executors/start/schema.d.ts @@ -0,0 +1,22 @@ +// options from https://docs.expo.dev/workflow/expo-cli/#expo-start + +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 0000000000..a013b65bb5 --- /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 0000000000..474daef925 --- /dev/null +++ b/packages/expo/src/executors/start/start.impl.ts @@ -0,0 +1,91 @@ +import * as chalk from 'chalk'; +import { ExecutorContext, logger, names } from '@nrwl/devkit'; +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-cli/bin/expo.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 (typeof v === 'boolean') { + if (v === true) { + // when true, does not need to pass the value true, just need to pass the flag in kebob case + acc.push(`--${names(k).fileName}`); + } + } else { + acc.push(`--${names(k).fileName}`, v); + } + } + return acc; + }, []); +} 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 0000000000..91634e4ee6 --- /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 0000000000..e8043201c4 --- /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 0000000000..b8fe639632 --- /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 Expo", + "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 0000000000..94e2dee228 --- /dev/null +++ b/packages/expo/src/executors/sync-deps/sync-deps.impl.ts @@ -0,0 +1,85 @@ +import { join } from 'path'; +import * as chalk from 'chalk'; +import { + ExecutorContext, + logger, + readJsonFile, + writeJsonFile, + createProjectGraphAsync, +} from '@nrwl/devkit'; + +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/executors/update/compat.ts b/packages/expo/src/executors/update/compat.ts new file mode 100644 index 0000000000..d3517da9a9 --- /dev/null +++ b/packages/expo/src/executors/update/compat.ts @@ -0,0 +1,5 @@ +import { convertNxExecutor } from '@nrwl/devkit'; + +import buildExecutor from './update.impl'; + +export default convertNxExecutor(buildExecutor); diff --git a/packages/expo/src/executors/update/schema.d.ts b/packages/expo/src/executors/update/schema.d.ts new file mode 100644 index 0000000000..d7a74424e6 --- /dev/null +++ b/packages/expo/src/executors/update/schema.d.ts @@ -0,0 +1,15 @@ +// command to run https://github.com/expo/eas-cli/tree/main#eas-update +// options from github.com/expo/eas-cli/blob/main/packages/eas-cli/src/commands/update/index.ts +export interface ExpoEasUpdateOptions { + branch?: string; + message?: string; + republish: boolean; // default is false + group?: string; + inputDir: string; // default is "dist" + skipBundler: boolean; // default is false + platform: 'ios' | 'android' | 'all'; // default is "all" + json: boolean; // default is false + auto: boolean; // default is false + privateKeyPath?: string; + nonInteractive: boolean; // default is false +} diff --git a/packages/expo/src/executors/update/schema.json b/packages/expo/src/executors/update/schema.json new file mode 100644 index 0000000000..9aac8d8cd5 --- /dev/null +++ b/packages/expo/src/executors/update/schema.json @@ -0,0 +1,62 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "NxExpoEasUpdate", + "cli": "nx", + "title": "Expo EAS Update executor", + "description": "Start an EAS update for your expo project", + "type": "object", + "properties": { + "branch": { + "type": "string", + "description": "Branch to publish the update group on" + }, + "message": { + "type": "string", + "description": "A short message describing the update" + }, + "republish": { + "type": "boolean", + "description": "Enable JSON output, non-JSON messages will be printed to stderr", + "default": false + }, + "group": { + "type": "string", + "description": "Update group to republish" + }, + "inputDir": { + "type": "string", + "description": "Location of the bundle" + }, + "skipBundler": { + "type": "boolean", + "description": "Skip running Expo CLI to bundle the app before publishing", + "default": false + }, + "platform": { + "enum": ["ios", "android", "all"], + "alias": "p", + "description": "The platform to build the app, example values: ios, android, all.", + "default": "all" + }, + "json": { + "type": "boolean", + "description": "Enable JSON output, non-JSON messages will be printed to stderr", + "default": false + }, + "auto": { + "type": "boolean", + "description": "Use the current git branch and commit message for the EAS branch and update message", + "default": false + }, + "privateKeyPath": { + "type": "string", + "description": "File containing the PEM-encoded private key corresponding to the certificate in expo-updates' configuration. Defaults to a file named \"private-key.pem\" in the certificate's directory." + }, + "nonInteractive": { + "type": "boolean", + "description": "Run command in non-interactive mode", + "default": false + } + }, + "required": [] +} diff --git a/packages/expo/src/executors/update/update.impl.ts b/packages/expo/src/executors/update/update.impl.ts new file mode 100644 index 0000000000..12be401655 --- /dev/null +++ b/packages/expo/src/executors/update/update.impl.ts @@ -0,0 +1,74 @@ +import { ExecutorContext, names } from '@nrwl/devkit'; +import { join } from 'path'; +import { ChildProcess, fork } from 'child_process'; + +import { ensureNodeModulesSymlink } from '../../utils/ensure-node-modules-symlink'; + +import { ExpoEasUpdateOptions } from './schema'; + +export interface ReactNativeUpdateOutput { + success: boolean; +} + +let childProcess: ChildProcess; + +export default async function* buildExecutor( + options: ExpoEasUpdateOptions, + context: ExecutorContext +): AsyncGenerator { + const projectRoot = context.workspace.projects[context.projectName].root; + ensureNodeModulesSymlink(context.root, projectRoot); + + try { + await runCliUpdate(context.root, projectRoot, options); + yield { success: true }; + } finally { + if (childProcess) { + childProcess.kill(); + } + } +} + +function runCliUpdate( + workspaceRoot: string, + projectRoot: string, + options: ExpoEasUpdateOptions +) { + return new Promise((resolve, reject) => { + childProcess = fork( + join(workspaceRoot, './node_modules/eas-cli/bin/run'), + ['update', ...createUpdateOptions(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 createUpdateOptions(options: ExpoEasUpdateOptions) { + return Object.keys(options).reduce((acc, k) => { + const v = options[k]; + if (typeof v === 'boolean') { + if (v === true) { + // when true, does not need to pass the value true, just need to pass the flag in kebob case + acc.push(`--${names(k).fileName}`); + } + } else { + acc.push(`--${names(k).fileName}`, v); + } + return acc; + }, []); +} 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 0000000000..9b922b7b45 --- /dev/null +++ b/packages/expo/src/generators/application/application.spec.ts @@ -0,0 +1,92 @@ +import { + Tree, + readWorkspaceConfiguration, + getProjects, + readJson, + readProjectConfiguration, +} 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 projectConfiguration = readProjectConfiguration(appTree, 'my-app'); + expect(projectConfiguration).toMatchObject({ + 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 0000000000..046bb45cc4 --- /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/app.json.template b/packages/expo/src/generators/application/files/app.json.template new file mode 100644 index 0000000000..80f4484303 --- /dev/null +++ b/packages/expo/src/generators/application/files/app.json.template @@ -0,0 +1,32 @@ +{ + "expo": { + "name": "<%= displayName %>", + "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 0000000000..03d6f6b6c6 Binary files /dev/null and b/packages/expo/src/generators/application/files/assets/adaptive-icon.png differ diff --git a/packages/expo/src/generators/application/files/assets/favicon.png b/packages/expo/src/generators/application/files/assets/favicon.png new file mode 100644 index 0000000000..e75f697b18 Binary files /dev/null and b/packages/expo/src/generators/application/files/assets/favicon.png differ diff --git a/packages/expo/src/generators/application/files/assets/icon.png b/packages/expo/src/generators/application/files/assets/icon.png new file mode 100644 index 0000000000..a0b1526fc7 Binary files /dev/null and b/packages/expo/src/generators/application/files/assets/icon.png differ diff --git a/packages/expo/src/generators/application/files/assets/logo.png b/packages/expo/src/generators/application/files/assets/logo.png new file mode 100644 index 0000000000..e9b9b6eb62 Binary files /dev/null and b/packages/expo/src/generators/application/files/assets/logo.png differ diff --git a/packages/expo/src/generators/application/files/assets/splash.png b/packages/expo/src/generators/application/files/assets/splash.png new file mode 100644 index 0000000000..0e89705a94 Binary files /dev/null and b/packages/expo/src/generators/application/files/assets/splash.png differ diff --git a/packages/expo/src/generators/application/files/assets/star.svg b/packages/expo/src/generators/application/files/assets/star.svg new file mode 100644 index 0000000000..901053d385 --- /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/babel.config.js.template b/packages/expo/src/generators/application/files/babel.config.js.template new file mode 100644 index 0000000000..33acf98572 --- /dev/null +++ b/packages/expo/src/generators/application/files/babel.config.js.template @@ -0,0 +1,6 @@ +module.exports = function (api) { + api.cache(true); + return { + presets: ['babel-preset-expo'] + }; +}; diff --git a/packages/expo/src/generators/application/files/eas.json.template b/packages/expo/src/generators/application/files/eas.json.template new file mode 100644 index 0000000000..5922677a1d --- /dev/null +++ b/packages/expo/src/generators/application/files/eas.json.template @@ -0,0 +1,28 @@ +{ + "build": { + "production": { + "android": { + "buildType": "app-bundle" + } + }, + "development": { + "developmentClient": true, + "distribution": "internal" + }, + "preview": { + "distribution": "internal", + "ios": { + "simulator": true + }, + "android": { + "buildType": "apk" + } + } + }, + "submit": { + "production": {} + }, + "cli": { + "version": ">= 0.55.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 0000000000..0602236b3c --- /dev/null +++ b/packages/expo/src/generators/application/files/index.js.template @@ -0,0 +1,8 @@ +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 0000000000..003680bfff --- /dev/null +++ b/packages/expo/src/generators/application/files/metro.config.js.template @@ -0,0 +1,25 @@ +const { withNxMetro } = require('@nrwl/expo'); +const { getDefaultConfig } = require('@expo/metro-config'); + +const defaultConfig = getDefaultConfig(__dirname); + +module.exports = (async () => { + defaultConfig.transformer.babelTransformerPath = require.resolve( + 'react-native-svg-transformer' + ); + defaultConfig.resolver.assetExts = defaultConfig.resolver.assetExts.filter( + (ext) => ext !== 'svg' + ); + defaultConfig.resolver.sourceExts.push('svg'); + return 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: [], + // the project root to start the metro server + projectRoot: __dirname, + // Specify any additional (to projectRoot) watch folders, this is used to know which files to watch + watchFolders: [] + }); +})(); 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 0000000000..42a0946bf8 --- /dev/null +++ b/packages/expo/src/generators/application/files/package.json.template @@ -0,0 +1,17 @@ +{ + "name": "<%= projectName %>", + "version": "0.0.1", + "private": true, + "dependencies": { + "@testing-library/jest-native": "*", + "@testing-library/react-native": "*", + "react-native": "*", + "expo": "*", + "react-native-svg": "*", + "react-native-web": "*" + }, + "scripts": { + "eas-build-pre-install": "cd <%= offsetFromRoot %> && cp <%= packageLockFile %> ./<%= appProjectRoot %>/", + "postinstall": "rm -r node_modules && cd <%= offsetFromRoot %> && <%= packageManager %> install && npx nx sync-deps <%= projectName %> && npx nx ensure-symlink <%= projectName %>" + } +} 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 0000000000..485394bdf3 --- /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 0000000000..ccab5feeb6 --- /dev/null +++ b/packages/expo/src/generators/application/files/src/app/App.tsx.template @@ -0,0 +1,484 @@ +/* eslint-disable jsx-a11y/accessible-emoji */ +import React, { useRef, useState } from 'react'; +import { + SafeAreaView, + StyleSheet, + ScrollView, + View, + Text, + StatusBar, + TouchableOpacity, + Linking +} from 'react-native'; + +import Checkmark from './icons/checkmark.svg'; +import Book from './icons/book.svg'; +import ChevronRight from './icons/chevron-right.svg'; +import Blog from './icons/blog.svg'; +import Courses from './icons/courses.svg'; +import YouTube from './icons/youtube.svg'; +import Pointer from './icons/pointer.svg'; +import VSCode from './icons/vscode.svg'; +import NxCloud from './icons/nx-cloud.svg'; +import GitHub from './icons/github.svg'; +import Terminal from './icons/terminal.svg'; +import Heart from './icons/heart.svg'; + +const App = () => { + const [whatsNextYCoord, setWhatsNextYCoord] = useState(0); + const scrollViewRef = useRef(null); + + return ( + <> + + + { + scrollViewRef.current = ref; + }} + contentInsetAdjustmentBehavior="automatic" + style={styles.scrollView} + > + + Hello there, + + Welcome <%= displayName %> 👋 + + + + + + + + You're up and running + + + { + scrollViewRef.current?.scrollTo({ + x: 0, + y: whatsNextYCoord, + }); + }} + > + + What's next? + + + + + + + + Learning materials + + + Linking.openURL( + 'https://nx.dev/getting-started/intro?utm_source=nx-project' + ) + } + > + + + Documentation + + Everything is in there + + + + + + Linking.openURL( + 'https://blog.nrwl.io/?utm_source=nx-project' + ) + } + > + + + Blog + + Changelog, features & events + + + + + + Linking.openURL( + 'https://www.youtube.com/c/Nrwl_io/videos?utm_source=nx-project' + ) + } + > + + + Youtube channel + + Nx Show, talks & tutorials + + + + + + Linking.openURL( + 'https://nx.dev/tutorial/01-create-application?utm_source=nx-project' + ) + } + > + + + Interactive tutorials + + Create an app, step by step + + + + + + Linking.openURL( + 'https://nxplaybook.com/?utm_source=nx-project' + ) + } + > + + + Video courses + + Nx custom courses + + + + + + + + + Linking.openURL( + 'https://marketplace.visualstudio.com/items?itemName=nrwl.angular-console&utm_source=nx-project' + ) + } + > + + + + + Install Nx Console + + + Plugin for VSCode + + + + + + + + Linking.openURL('https://nx.app/?utm_source=nx-project') + } + > + + + + + + Nx Cloud + + + Enable faster CI & better DX + + + + + nx connect-to-nx-cloud + + + + + + + Linking.openURL('https://nx.app/?utm_source=nx-project') + } + > + + + + + Nx is open source + + + Love Nx? Give us a star! + + + + + + { + const layout = event.nativeEvent.layout; + setWhatsNextYCoord(layout.y); + }} + > + + + Next steps + + + Here are some things you can do with Nx: + + + + + Add UI library + + + + + # Generate UI lib + + + nx g @nrwl/expo:lib ui + + + # Add a component + + nx g \ + + @nrwl/expo:component \ + + + button --project ui + + + + + + + View interactive dependency graph + + + + + + nx dep-graph + + + + + + Run affected commands + + + + + # See what's affected by changes + + + nx affected:dep-graph + + + # run tests for current changes + + + nx affected:text + + + # run e2e tests for current + + + # changes + + + nx affected:e2e + + + + + Carefully crafted with + + + + + + + ); +}; +const styles = StyleSheet.create({ + scrollView: { + backgroundColor: '#ffffff', + }, + codeBlock: { + backgroundColor: 'rgba(55, 65, 81, 1)', + marginVertical: 12, + padding: 12, + borderRadius: 4, + }, + monospace: { + color: '#ffffff', + fontFamily: 'Courier New', + marginVertical: 4, + }, + comment: { + color: '#cccccc', + }, + marginBottomSm: { + marginBottom: 6, + }, + marginBottomMd: { + marginBottom: 18, + }, + marginBottomLg: { + marginBottom: 24, + }, + textLight: { + fontWeight: '300', + }, + textBold: { + fontWeight: '500', + }, + textCenter: { + textAlign: 'center', + }, + text2XS: { + fontSize: 12, + }, + textXS: { + fontSize: 14, + }, + textSm: { + fontSize: 16, + }, + textMd: { + fontSize: 18, + }, + textLg: { + fontSize: 24, + }, + textXL: { + fontSize: 48, + }, + textContainer: { + marginVertical: 12, + }, + textSubtle: { + color: '#6b7280', + }, + section: { + marginVertical: 24, + marginHorizontal: 12, + }, + shadowBox: { + backgroundColor: 'white', + borderRadius: 24, + shadowColor: 'black', + shadowOpacity: 0.15, + shadowOffset: { + width: 1, + height: 4, + }, + shadowRadius: 12, + padding: 24, + marginBottom: 24, + }, + listItem: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + }, + listItemTextContainer: { + marginLeft: 12, + flex: 1, + }, + appTitleText: { + paddingTop: 12, + fontWeight: '500', + }, + hero: { + borderRadius: 12, + backgroundColor: '#143055', + padding: 36, + marginBottom: 24, + }, + heroTitle: { + flex: 1, + flexDirection: 'row', + }, + heroTitleText: { + color: '#ffffff', + marginLeft: 12, + }, + heroText: { + color: '#ffffff', + marginVertical: 12, + }, + whatsNextButton: { + backgroundColor: '#ffffff', + paddingVertical: 16, + borderRadius: 8, + width: '50%', + marginTop: 24, + }, + learning: { + marginVertical: 12, + }, + love: { + marginTop: 12, + justifyContent: 'center', + }, +}); + +export default App; diff --git a/packages/expo/src/generators/application/files/src/app/icons/blog.svg b/packages/expo/src/generators/application/files/src/app/icons/blog.svg new file mode 100644 index 0000000000..b7618efb90 --- /dev/null +++ b/packages/expo/src/generators/application/files/src/app/icons/blog.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/expo/src/generators/application/files/src/app/icons/book.svg b/packages/expo/src/generators/application/files/src/app/icons/book.svg new file mode 100644 index 0000000000..af2d6df129 --- /dev/null +++ b/packages/expo/src/generators/application/files/src/app/icons/book.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/expo/src/generators/application/files/src/app/icons/checkmark.svg b/packages/expo/src/generators/application/files/src/app/icons/checkmark.svg new file mode 100644 index 0000000000..fe2608c90b --- /dev/null +++ b/packages/expo/src/generators/application/files/src/app/icons/checkmark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/expo/src/generators/application/files/src/app/icons/chevron-right.svg b/packages/expo/src/generators/application/files/src/app/icons/chevron-right.svg new file mode 100644 index 0000000000..332879444a --- /dev/null +++ b/packages/expo/src/generators/application/files/src/app/icons/chevron-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/expo/src/generators/application/files/src/app/icons/courses.svg b/packages/expo/src/generators/application/files/src/app/icons/courses.svg new file mode 100644 index 0000000000..1a3d480024 --- /dev/null +++ b/packages/expo/src/generators/application/files/src/app/icons/courses.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/expo/src/generators/application/files/src/app/icons/github.svg b/packages/expo/src/generators/application/files/src/app/icons/github.svg new file mode 100644 index 0000000000..266c1f766d --- /dev/null +++ b/packages/expo/src/generators/application/files/src/app/icons/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/expo/src/generators/application/files/src/app/icons/heart.svg b/packages/expo/src/generators/application/files/src/app/icons/heart.svg new file mode 100644 index 0000000000..74c66cb728 --- /dev/null +++ b/packages/expo/src/generators/application/files/src/app/icons/heart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/expo/src/generators/application/files/src/app/icons/logo.png b/packages/expo/src/generators/application/files/src/app/icons/logo.png new file mode 100644 index 0000000000..e9b9b6eb62 Binary files /dev/null and b/packages/expo/src/generators/application/files/src/app/icons/logo.png differ diff --git a/packages/expo/src/generators/application/files/src/app/icons/nx-cloud.svg b/packages/expo/src/generators/application/files/src/app/icons/nx-cloud.svg new file mode 100644 index 0000000000..0637cbd3db --- /dev/null +++ b/packages/expo/src/generators/application/files/src/app/icons/nx-cloud.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/expo/src/generators/application/files/src/app/icons/pointer.svg b/packages/expo/src/generators/application/files/src/app/icons/pointer.svg new file mode 100644 index 0000000000..47cb0edce4 --- /dev/null +++ b/packages/expo/src/generators/application/files/src/app/icons/pointer.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/expo/src/generators/application/files/src/app/icons/terminal.svg b/packages/expo/src/generators/application/files/src/app/icons/terminal.svg new file mode 100644 index 0000000000..2041db0dd3 --- /dev/null +++ b/packages/expo/src/generators/application/files/src/app/icons/terminal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/expo/src/generators/application/files/src/app/icons/vscode.svg b/packages/expo/src/generators/application/files/src/app/icons/vscode.svg new file mode 100644 index 0000000000..70ae6e92c9 --- /dev/null +++ b/packages/expo/src/generators/application/files/src/app/icons/vscode.svg @@ -0,0 +1 @@ +Visual Studio Code \ No newline at end of file diff --git a/packages/expo/src/generators/application/files/src/app/icons/youtube.svg b/packages/expo/src/generators/application/files/src/app/icons/youtube.svg new file mode 100644 index 0000000000..b527eaa1c1 --- /dev/null +++ b/packages/expo/src/generators/application/files/src/app/icons/youtube.svg @@ -0,0 +1 @@ +YouTube \ No newline at end of file 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 0000000000..9f28ad211b --- /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 0000000000..7799be4ddb --- /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": ["jest.config.ts", "**/*.spec.ts", "**/*.spec.tsx", "test-setup.ts"], + "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 0000000000..819d3b3033 --- /dev/null +++ b/packages/expo/src/generators/application/files/tsconfig.json.template @@ -0,0 +1,25 @@ +{ + "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": [ + "../../node_modules/@nrwl/expo/typings/svg.d.ts" + ], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + } + ], + "exclude": [ + "node_modules" + ] +} diff --git a/packages/expo/src/generators/application/files/webpack.config.js.template b/packages/expo/src/generators/application/files/webpack.config.js.template new file mode 100644 index 0000000000..5c284d2b41 --- /dev/null +++ b/packages/expo/src/generators/application/files/webpack.config.js.template @@ -0,0 +1,15 @@ +const createExpoWebpackConfigAsync = require('@expo/webpack-config'); +const { withNxWebpack } = require('@nrwl/expo'); + +module.exports = async function (env, argv) { + let config = await createExpoWebpackConfigAsync(env, argv); + config = await withNxWebpack(config); + + // You can override the config here, for example: + // config.resolve.alias = { + // ...config.resolve.alias, + // react: path.resolve('../../node_modules/react'), + // }; + + return config; +}; 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 0000000000..cc82e9ae02 --- /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, + framework: '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 0000000000..1785b7c092 --- /dev/null +++ b/packages/expo/src/generators/application/lib/add-project.ts @@ -0,0 +1,143 @@ +import { + addProjectConfiguration, + ProjectConfiguration, + readWorkspaceConfiguration, + TargetConfiguration, + Tree, + updateWorkspaceConfiguration, +} from '@nrwl/devkit'; +import { NormalizedSchema } from './normalize-options'; + +export function addProject(host: Tree, options: NormalizedSchema) { + const projectConfiguration: ProjectConfiguration = { + root: options.appProjectRoot, + sourceRoot: `${options.appProjectRoot}/src`, + projectType: 'application', + targets: { ...getTargets(options) }, + tags: options.parsedTags, + }; + + addProjectConfiguration( + host, + options.projectName, + projectConfiguration, + options.standaloneConfig + ); + + 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'] = { + executor: '@nrwl/expo:build', + options: {}, + }; + + architect['build-list'] = { + executor: '@nrwl/expo:build-list', + options: {}, + }; + + architect['download'] = { + executor: '@nrwl/expo:download', + options: { + output: `${options.appProjectRoot}/dist`, + }, + }; + + 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: {}, + }; + + architect['publish'] = { + executor: '@nrwl/expo:publish', + options: {}, + }; + + architect['publish-set'] = { + executor: '@nrwl/expo:publish-set', + options: {}, + }; + + architect['rollback'] = { + executor: '@nrwl/expo:rollback', + options: {}, + }; + + architect['eject'] = { + executor: '@nrwl/expo:eject', + 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 0000000000..1e219c08ed --- /dev/null +++ b/packages/expo/src/generators/application/lib/create-application-files.ts @@ -0,0 +1,32 @@ +import { + detectPackageManager, + generateFiles, + offsetFromRoot, + PackageManager, + toJS, + Tree, +} from '@nrwl/devkit'; +import { join } from 'path'; +import { NormalizedSchema } from './normalize-options'; + +export function createApplicationFiles(host: Tree, options: NormalizedSchema) { + const packageManagerLockFile: Record = { + npm: 'package-lock.json', + yarn: 'yarn.lock', + pnpm: 'pnpm-lock.yaml', + }; + const packageManager = detectPackageManager(host.root); + const packageLockFile = packageManagerLockFile[packageManager]; + generateFiles(host, join(__dirname, '../files'), options.appProjectRoot, { + ...options, + offsetFromRoot: offsetFromRoot(options.appProjectRoot), + packageManager, + packageLockFile, + }); + 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 0000000000..994b0fbb40 --- /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 0000000000..deacb9f41e --- /dev/null +++ b/packages/expo/src/generators/application/lib/normalize-options.ts @@ -0,0 +1,48 @@ +import { names } 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 0000000000..ece0671bfc --- /dev/null +++ b/packages/expo/src/generators/application/schema.d.ts @@ -0,0 +1,18 @@ +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 + standaloneConfig?: boolean; +} diff --git a/packages/expo/src/generators/application/schema.json b/packages/expo/src/generators/application/schema.json new file mode 100644 index 0000000000..90b56af00b --- /dev/null +++ b/packages/expo/src/generators/application/schema.json @@ -0,0 +1,80 @@ +{ + "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" + }, + "standaloneConfig": { + "description": "Split the project configuration into `/project.json` rather than including it inside `workspace.json`.", + "type": "boolean" + } + }, + "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 0000000000..0341f659a4 --- /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 0000000000..f1ccb40f7a --- /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 0000000000..d42366c2ff --- /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 0000000000..130b2fe926 --- /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 0000000000..b151ee2226 --- /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 0000000000..23bc23c724 --- /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 0000000000..05440ac661 --- /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 0000000000..54944090f9 --- /dev/null +++ b/packages/expo/src/generators/component/schema.json @@ -0,0 +1,82 @@ +{ + "cli": "nx", + "$id": "NxExpoComponent", + "$schema": "http://json-schema.org/schema", + "title": "Create a Expo 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 0000000000..f23f8f617f --- /dev/null +++ b/packages/expo/src/generators/init/init.spec.ts @@ -0,0 +1,36 @@ +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/); + }); +}); diff --git a/packages/expo/src/generators/init/init.ts b/packages/expo/src/generators/init/init.ts new file mode 100644 index 0000000000..aa8c979fbb --- /dev/null +++ b/packages/expo/src/generators/init/init.ts @@ -0,0 +1,99 @@ +import { + addDependenciesToPackageJson, + convertNxGenerator, + formatFiles, + removeDependenciesFromPackageJson, + Tree, +} from '@nrwl/devkit'; +import { Schema } from './schema'; +import { + expoVersion, + nxVersion, + reactNativeVersion, + reactNativeWebVersion, + typesReactNativeVersion, + expoMetroConfigVersion, + metroVersion, + testingLibraryReactNativeVersion, + testingLibraryJestNativeVersion, + jestExpoVersion, + reactNativeSvgTransformerVersion, + reactNativeSvgVersion, + expoCliVersion, + svgrWebpackVersion, + babelPresetExpoVersion, + easCliVersion, +} from '../../utils/versions'; + +import { + reactDomVersion, + reactVersion, + reactTestRendererVersion, + typesReactVersion, +} from '@nrwl/react/src/utils/versions'; +import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial'; +import { jestInitGenerator } from '@nrwl/jest'; +import { detoxInitGenerator } from '@nrwl/detox'; + +import { addGitIgnoreEntry } from './lib/add-git-ignore-entry'; +import { initRootBabelConfig } from './lib/init-root-babel-config'; + +export async function expoInitGenerator(host: Tree, schema: Schema) { + addGitIgnoreEntry(host); + initRootBabelConfig(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': reactDomVersion, + 'react-native': reactNativeVersion, + expo: expoVersion, + 'react-native-web': reactNativeWebVersion, + '@expo/metro-config': expoMetroConfigVersion, + 'react-native-svg-transformer': reactNativeSvgTransformerVersion, + 'react-native-svg': reactNativeSvgVersion, + }, + { + '@nrwl/expo': nxVersion, + '@types/react': typesReactVersion, + '@types/react-native': typesReactNativeVersion, + 'metro-resolver': metroVersion, + 'react-test-renderer': reactTestRendererVersion, + '@testing-library/react-native': testingLibraryReactNativeVersion, + '@testing-library/jest-native': testingLibraryJestNativeVersion, + 'jest-expo': jestExpoVersion, + 'expo-cli': expoCliVersion, + 'eas-cli': easCliVersion, + '@svgr/webpack': svgrWebpackVersion, + 'babel-preset-expo': babelPresetExpoVersion, + } + ); +} + +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 0000000000..d886962064 --- /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 0000000000..a0dec25ed8 --- /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/lib/init-root-babel-config.ts b/packages/expo/src/generators/init/lib/init-root-babel-config.ts new file mode 100644 index 0000000000..a3f079a2ce --- /dev/null +++ b/packages/expo/src/generators/init/lib/init-root-babel-config.ts @@ -0,0 +1,11 @@ +import { Tree, writeJson } from '@nrwl/devkit'; + +export function initRootBabelConfig(tree: Tree) { + if (tree.exists('/babel.config.json') || tree.exists('/babel.config.js')) { + return; + } + + writeJson(tree, '/babel.config.json', { + babelrcRoots: ['*'], // Make sure .babelrc files other than root can be loaded in a monorepo + }); +} 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 0000000000..11ea540ee8 --- /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 0000000000..42a4cd481e --- /dev/null +++ b/packages/expo/src/generators/init/schema.json @@ -0,0 +1,27 @@ +{ + "cli": "nx", + "$id": "NxExpoInit", + "$schema": "http://json-schema.org/schema", + "title": "Add Nx Expo 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/README.md b/packages/expo/src/generators/library/files/lib/README.md new file mode 100644 index 0000000000..b74453ce2e --- /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/babel.config.json b/packages/expo/src/generators/library/files/lib/babel.config.json new file mode 100644 index 0000000000..7d30f8bf06 --- /dev/null +++ b/packages/expo/src/generators/library/files/lib/babel.config.json @@ -0,0 +1,3 @@ +{ + "presets": ["babel-preset-expo"] +} 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 0000000000..b2da0955b9 --- /dev/null +++ b/packages/expo/src/generators/library/files/lib/package.json.template @@ -0,0 +1,5 @@ +{ + "name": "<%= name %>", + "version": "0.0.1", + "main": "<%= appMain %>" +} 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 0000000000..e69de29bb2 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 0000000000..9f28ad211b --- /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 0000000000..441ee25621 --- /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 0000000000..800eaa9333 --- /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", "test-setup.ts"], + "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 0000000000..60f6bee1ad --- /dev/null +++ b/packages/expo/src/generators/library/lib/normalize-options.ts @@ -0,0 +1,54 @@ +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; +} + +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 appMain = options.js ? 'src/index.js' : 'src/index.ts'; + + const normalized: NormalizedSchema = { + ...options, + fileName, + routePath: `/${name}`, + name: projectName, + projectRoot, + projectDirectory, + parsedTags, + importPath, + appMain, + }; + + 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 0000000000..d6270e167f --- /dev/null +++ b/packages/expo/src/generators/library/library.spec.ts @@ -0,0 +1,348 @@ +import { + getProjects, + readJson, + readProjectConfiguration, + 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 project.json', async () => { + await expoLibraryGenerator(appTree, { + ...defaultSchema, + tags: 'one,two', + }); + const projectConfiguration = readProjectConfiguration(appTree, 'my-lib'); + expect(projectConfiguration.root).toEqual('libs/my-lib'); + expect(projectConfiguration.targets.build).toBeUndefined(); + expect(projectConfiguration.targets.lint).toEqual({ + executor: '@nrwl/linter:eslint', + outputs: ['{options.outputFile}'], + options: { + lintFilePatterns: ['libs/my-lib/**/*.{ts,tsx,js,jsx}'], + }, + }); + expect(projectConfiguration.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 project.json', async () => { + await expoLibraryGenerator(appTree, { + ...defaultSchema, + directory: 'myDir', + tags: 'one', + }); + const projectConfiguration = readProjectConfiguration( + appTree, + 'my-dir-my-lib' + ); + expect(projectConfiguration).toMatchObject({ + tags: ['one'], + }); + + await expoLibraryGenerator(appTree, { + ...defaultSchema, + name: 'myLib2', + directory: 'myDir', + tags: 'one,two', + }); + + const lib2ProjectConfiguration = readProjectConfiguration( + appTree, + 'my-dir-my-lib2' + ); + expect(lib2ProjectConfiguration).toMatchObject({ + tags: ['one', 'two'], + }); + }); + + it('should update project.json', async () => { + await expoLibraryGenerator(appTree, { + ...defaultSchema, + directory: 'myDir', + }); + const projectConfiguration = readProjectConfiguration( + appTree, + 'my-dir-my-lib' + ); + expect(projectConfiguration.targets.lint).toEqual({ + executor: '@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.ts')).toBeFalsy(); + const projectConfiguration = readProjectConfiguration(appTree, 'my-lib'); + expect(projectConfiguration.targets.test).toBeUndefined(); + expect(projectConfiguration.targets.lint).toMatchObject({ + executor: '@nrwl/linter:eslint', + options: { + lintFilePatterns: ['libs/my-lib/**/*.{ts,tsx,js,jsx}'], + }, + outputs: ['{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 0000000000..bdd3ea5b4d --- /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 0000000000..11cb3ecd70 --- /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 0000000000..a0a4002403 --- /dev/null +++ b/packages/expo/src/generators/library/schema.json @@ -0,0 +1,97 @@ +{ + "cli": "nx", + "$id": "NxExpoLibrary", + "$schema": "http://json-schema.org/schema", + "title": "Create a Expo 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/migrations/update-14-0-0/add-project-root-metro-config-14-0-0.spec.ts b/packages/expo/src/migrations/update-14-0-0/add-project-root-metro-config-14-0-0.spec.ts new file mode 100644 index 0000000000..48f49074c0 --- /dev/null +++ b/packages/expo/src/migrations/update-14-0-0/add-project-root-metro-config-14-0-0.spec.ts @@ -0,0 +1,188 @@ +import { addProjectConfiguration, readJson, Tree } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; + +import update from './add-project-root-metro-config-14-0-0'; + +describe('Add projectRoot option in metro.config.js', () => { + let tree: Tree; + + beforeEach(async () => { + tree = createTreeWithEmptyWorkspace(); + addProjectConfiguration(tree, 'products', { + root: 'apps/products', + sourceRoot: 'apps/products/src', + targets: { + start: { + executor: '@nrwl/react-native:start', + options: { + port: 8081, + }, + }, + }, + }); + }); + + it(`should udpate metro.config.js and add key projectRoot`, async () => { + tree.write( + 'apps/products/metro.config.js', + ` +const { withNxMetro } = require('@nrwl/react-native'); +const { getDefaultConfig } = require('metro-config'); + +module.exports = (async () => { + const { + resolver: { sourceExts, assetExts }, + } = await getDefaultConfig(); + // console.log(getModulesRunBeforeMainModule); + return withNxMetro( + { + transformer: { + getTransformOptions: async () => ({ + transform: { + experimentalImportSupport: false, + inlineRequires: true, + }, + }), + babelTransformerPath: require.resolve('react-native-svg-transformer'), + }, + resolver: { + assetExts: assetExts.filter((ext) => ext !== 'svg'), + sourceExts: [...sourceExts, 'svg'], + resolverMainFields: ['sbmodern', 'browser', 'main'], + }, + }, + { + // 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: [], + } + ); +})(); +` + ); + await update(tree); + + expect(tree.read('apps/products/metro.config.js', 'utf-8')).toEqual(` +const { withNxMetro } = require('@nrwl/react-native'); +const { getDefaultConfig } = require('metro-config'); + +module.exports = (async () => { + const { + resolver: { sourceExts, assetExts }, + } = await getDefaultConfig(); + // console.log(getModulesRunBeforeMainModule); + return withNxMetro( + { + transformer: { + getTransformOptions: async () => ({ + transform: { + experimentalImportSupport: false, + inlineRequires: true, + }, + }), + babelTransformerPath: require.resolve('react-native-svg-transformer'), + }, + resolver: { + assetExts: assetExts.filter((ext) => ext !== 'svg'), + sourceExts: [...sourceExts, 'svg'], + resolverMainFields: ['sbmodern', 'browser', 'main'], + }, + }, + { + // Change this to true to see debugging info. + // Useful if you have issues resolving modules + projectRoot: __dirname, watchFolders: [], debug: false, + // all the file extensions used for imports other than 'ts', 'tsx', 'js', 'jsx' + extensions: [], + } + ); +})(); +`); + }); + + it(`should not udpate metro.config.js if projectRoot already exists`, async () => { + tree.write( + 'apps/products/metro.config.js', + ` +const { withNxMetro } = require('@nrwl/react-native'); +const { getDefaultConfig } = require('metro-config'); + +module.exports = (async () => { + const { + resolver: { sourceExts, assetExts }, + } = await getDefaultConfig(); + // console.log(getModulesRunBeforeMainModule); + return withNxMetro( + { + transformer: { + getTransformOptions: async () => ({ + transform: { + experimentalImportSupport: false, + inlineRequires: true, + }, + }), + babelTransformerPath: require.resolve('react-native-svg-transformer'), + }, + resolver: { + assetExts: assetExts.filter((ext) => ext !== 'svg'), + sourceExts: [...sourceExts, 'svg'], + resolverMainFields: ['sbmodern', 'browser', 'main'], + }, + }, + { + projectRoot: __dirname, + // 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: [], + } + ); +})(); +` + ); + await update(tree); + + expect(tree.read('apps/products/metro.config.js', 'utf-8')).toEqual( + ` +const { withNxMetro } = require('@nrwl/react-native'); +const { getDefaultConfig } = require('metro-config'); + +module.exports = (async () => { + const { + resolver: { sourceExts, assetExts }, + } = await getDefaultConfig(); + // console.log(getModulesRunBeforeMainModule); + return withNxMetro( + { + transformer: { + getTransformOptions: async () => ({ + transform: { + experimentalImportSupport: false, + inlineRequires: true, + }, + }), + babelTransformerPath: require.resolve('react-native-svg-transformer'), + }, + resolver: { + assetExts: assetExts.filter((ext) => ext !== 'svg'), + sourceExts: [...sourceExts, 'svg'], + resolverMainFields: ['sbmodern', 'browser', 'main'], + }, + }, + { + projectRoot: __dirname, + // 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/migrations/update-14-0-0/add-project-root-metro-config-14-0-0.ts b/packages/expo/src/migrations/update-14-0-0/add-project-root-metro-config-14-0-0.ts new file mode 100644 index 0000000000..8c0782ac49 --- /dev/null +++ b/packages/expo/src/migrations/update-14-0-0/add-project-root-metro-config-14-0-0.ts @@ -0,0 +1,43 @@ +import { + formatFiles, + getProjects, + logger, + stripIndents, + Tree, +} from '@nrwl/devkit'; + +/** + * Add projectRoot and watchFolders options in metro.config.js + * @param tree + */ +export default async function update(tree: Tree) { + const projects = getProjects(tree); + + projects.forEach((project) => { + const metroConfigPath = `${project.root}/metro.config.js`; + if ( + project.targets?.start?.executor !== '@nrwl/react-native:start' || + !tree.exists(metroConfigPath) + ) + return; + try { + const metroConfigContent = tree.read(metroConfigPath, 'utf-8'); + if (metroConfigContent.includes('projectRoot: __dirname')) { + return; + } + if (metroConfigContent.includes('projectRoot')) return; + tree.write( + metroConfigPath, + metroConfigContent.replace( + 'debug: ', + 'projectRoot: __dirname, watchFolders: [], debug: ' + ) + ); + } catch { + logger.error( + stripIndents`Unable to update ${metroConfigPath} for project ${project.root}.` + ); + } + }); + await formatFiles(tree); +} diff --git a/packages/expo/src/migrations/update-14-1-2/add-eject-target-14-1-2.spec.ts b/packages/expo/src/migrations/update-14-1-2/add-eject-target-14-1-2.spec.ts new file mode 100644 index 0000000000..801d12f33f --- /dev/null +++ b/packages/expo/src/migrations/update-14-1-2/add-eject-target-14-1-2.spec.ts @@ -0,0 +1,31 @@ +import { addProjectConfiguration, getProjects, Tree } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import update from './add-eject-target-14-1-2'; + +describe('add-eject-target-14-1-2', () => { + let tree: Tree; + + beforeEach(async () => { + tree = createTreeWithEmptyWorkspace(); + addProjectConfiguration(tree, 'product', { + root: 'apps/product', + sourceRoot: 'apps/product/src', + targets: { + start: { + executor: '@nrwl/expo:start', + }, + }, + }); + }); + + it(`should update project.json with target eject`, async () => { + await update(tree); + + getProjects(tree).forEach((project) => { + expect(project.targets['eject']).toEqual({ + executor: '@nrwl/expo:eject', + options: {}, + }); + }); + }); +}); diff --git a/packages/expo/src/migrations/update-14-1-2/add-eject-target-14-1-2.ts b/packages/expo/src/migrations/update-14-1-2/add-eject-target-14-1-2.ts new file mode 100644 index 0000000000..ed8146a22a --- /dev/null +++ b/packages/expo/src/migrations/update-14-1-2/add-eject-target-14-1-2.ts @@ -0,0 +1,26 @@ +import { + Tree, + formatFiles, + getProjects, + updateProjectConfiguration, +} from '@nrwl/devkit'; + +/** + * Add eject to targets for expo app + */ +export default async function update(tree: Tree) { + const projects = getProjects(tree); + + for (const [name, config] of projects.entries()) { + if (config.targets?.['start']?.executor === '@nrwl/expo:start') { + config.targets['eject'] = { + executor: '@nrwl/expo:eject', + options: {}, + }; + } + + updateProjectConfiguration(tree, name, config); + } + + await formatFiles(tree); +} diff --git a/packages/expo/src/migrations/update-14-4-3/add-eas-build-target.spec.ts b/packages/expo/src/migrations/update-14-4-3/add-eas-build-target.spec.ts new file mode 100644 index 0000000000..f8685e80d6 --- /dev/null +++ b/packages/expo/src/migrations/update-14-4-3/add-eas-build-target.spec.ts @@ -0,0 +1,41 @@ +import { addProjectConfiguration, getProjects, Tree } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import update from './add-eas-build-target'; + +describe('add-eas-build-target', () => { + let tree: Tree; + + beforeEach(async () => { + tree = createTreeWithEmptyWorkspace(); + addProjectConfiguration(tree, 'product', { + root: 'apps/product', + sourceRoot: 'apps/product/src', + targets: { + start: { + executor: '@nrwl/expo:start', + }, + }, + }); + }); + + it(`should update project.json with target build and build-list`, async () => { + await update(tree); + + getProjects(tree).forEach((project) => { + expect(project.targets['build']).toEqual({ + executor: '@nrwl/expo:build', + options: {}, + }); + expect(project.targets['build-list']).toEqual({ + executor: '@nrwl/expo:build-list', + options: {}, + }); + expect(project.targets['download']).toEqual({ + executor: '@nrwl/expo:download', + options: { + output: 'apps/product/dist', + }, + }); + }); + }); +}); diff --git a/packages/expo/src/migrations/update-14-4-3/add-eas-build-target.ts b/packages/expo/src/migrations/update-14-4-3/add-eas-build-target.ts new file mode 100644 index 0000000000..55a4b36bb3 --- /dev/null +++ b/packages/expo/src/migrations/update-14-4-3/add-eas-build-target.ts @@ -0,0 +1,43 @@ +import { + Tree, + formatFiles, + getProjects, + updateProjectConfiguration, +} from '@nrwl/devkit'; +import { join } from 'path'; + +/** + * Add eas build and build-list target for expo + */ +export default async function update(tree: Tree) { + const projects = getProjects(tree); + + for (const [name, config] of projects.entries()) { + if (config.targets?.['start']?.executor === '@nrwl/expo:start') { + if (!config.targets['build']) { + config.targets['build'] = { + executor: '@nrwl/expo:build', + options: {}, + }; + } + if (!config.targets['build-list']) { + config.targets['build-list'] = { + executor: '@nrwl/expo:build-list', + options: {}, + }; + } + if (!config.targets['download']) { + config.targets['download'] = { + executor: '@nrwl/expo:download', + options: { + output: join(config.root, 'dist'), + }, + }; + } + } + + updateProjectConfiguration(tree, name, config); + } + + await formatFiles(tree); +} diff --git a/packages/expo/src/migrations/update-14-5-1/add-eas-update-target.spec.ts b/packages/expo/src/migrations/update-14-5-1/add-eas-update-target.spec.ts new file mode 100644 index 0000000000..f0d2e76b62 --- /dev/null +++ b/packages/expo/src/migrations/update-14-5-1/add-eas-update-target.spec.ts @@ -0,0 +1,31 @@ +import { addProjectConfiguration, getProjects, Tree } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import update from './add-eas-update-target'; + +describe('add-eas-update-target', () => { + let tree: Tree; + + beforeEach(async () => { + tree = createTreeWithEmptyWorkspace(); + addProjectConfiguration(tree, 'product', { + root: 'apps/product', + sourceRoot: 'apps/product/src', + targets: { + start: { + executor: '@nrwl/expo:start', + }, + }, + }); + }); + + it(`should update project.json with target build and build-list`, async () => { + await update(tree); + + getProjects(tree).forEach((project) => { + expect(project.targets['update']).toEqual({ + executor: '@nrwl/expo:update', + options: {}, + }); + }); + }); +}); diff --git a/packages/expo/src/migrations/update-14-5-1/add-eas-update-target.ts b/packages/expo/src/migrations/update-14-5-1/add-eas-update-target.ts new file mode 100644 index 0000000000..d1b1aa7eeb --- /dev/null +++ b/packages/expo/src/migrations/update-14-5-1/add-eas-update-target.ts @@ -0,0 +1,28 @@ +import { + Tree, + formatFiles, + getProjects, + updateProjectConfiguration, +} from '@nrwl/devkit'; + +/** + * Add eas update target for expo + */ +export default async function update(tree: Tree) { + const projects = getProjects(tree); + + for (const [name, config] of projects.entries()) { + if (config.targets?.['start']?.executor === '@nrwl/expo:start') { + if (!config.targets['update']) { + config.targets['update'] = { + executor: '@nrwl/expo:update', + options: {}, + }; + } + } + + updateProjectConfiguration(tree, name, config); + } + + await formatFiles(tree); +} diff --git a/packages/expo/src/utils/add-jest.ts b/packages/expo/src/utils/add-jest.ts new file mode 100644 index 0000000000..11e6f99cb9 --- /dev/null +++ b/packages/expo/src/utils/add-jest.ts @@ -0,0 +1,41 @@ +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.ts file because react native needs to have special transform property + const configPath = `${appProjectRoot}/jest.config.${js ? 'js' : 'ts'}`; + 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'}'], + moduleNameMapper: { + '\\.svg': '@nrwl/expo/plugins/jest/svg-mock' + } +};`; + 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 0000000000..c06e679b32 --- /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 0000000000..a2cdb65654 --- /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', 'web-build']; + + // 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 0000000000..e1f3ad39e4 --- /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 0000000000..1ef5d84d33 --- /dev/null +++ b/packages/expo/src/utils/ensure-node-modules-symlink.ts @@ -0,0 +1,30 @@ +import { join } from 'path'; +import { platform } from 'os'; +import * as fs from 'fs'; + +/** + * 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 0000000000..d2e565713e --- /dev/null +++ b/packages/expo/src/utils/find-all-npm-dependencies.spec.ts @@ -0,0 +1,103 @@ +import { findAllNpmDependencies } from './find-all-npm-dependencies'; +import { DependencyType, ProjectGraph } from '@nrwl/devkit'; + +test('findAllNpmDependencies', () => { + const graph: ProjectGraph = { + nodes: { + myapp: { + type: 'app', + name: 'myapp', + data: { files: [] }, + }, + lib1: { + type: 'lib', + name: 'lib1', + data: { files: [] }, + }, + lib2: { + type: 'lib', + name: 'lib2', + data: { files: [] }, + }, + lib3: { + type: 'lib', + name: 'lib3', + data: { files: [] }, + }, + }, + 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: DependencyType.static, source: 'myapp', target: 'lib1' }, + { type: DependencyType.static, source: 'myapp', target: 'lib2' }, + { + type: DependencyType.static, + source: 'myapp', + target: 'npm:react-native-image-picker', + }, + { + type: DependencyType.static, + source: 'myapp', + target: 'npm:@nrwl/react-native', + }, + ], + lib1: [ + { type: DependencyType.static, source: 'lib1', target: 'lib2' }, + { + type: DependencyType.static, + source: 'lib3', + target: 'npm:react-native-snackbar', + }, + ], + lib2: [{ type: DependencyType.static, source: 'lib2', target: 'lib3' }], + lib3: [ + { + type: DependencyType.static, + source: 'lib3', + target: 'npm:react-native-dialog', + }, + ], + }, + }; + + const result = findAllNpmDependencies(graph, 'myapp'); + + expect(result).toEqual([ + 'react-native-dialog', + 'react-native-snackbar', + 'react-native-image-picker', + ]); +}); 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 0000000000..30b0551794 --- /dev/null +++ b/packages/expo/src/utils/find-all-npm-dependencies.ts @@ -0,0 +1,30 @@ +import { ProjectGraph } from '@nrwl/devkit'; + +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/pod-install-task.ts b/packages/expo/src/utils/pod-install-task.ts new file mode 100644 index 0000000000..31fb65fb77 --- /dev/null +++ b/packages/expo/src/utils/pod-install-task.ts @@ -0,0 +1,54 @@ +import { spawn } from 'child_process'; +import { platform } from 'os'; +import * as chalk from 'chalk'; +import { GeneratorCallback, logger } from '@nrwl/devkit'; + +const podInstallErrorMessage = ` +Running ${chalk.bold('pod install')} failed, see above. +Do you have CocoaPods (https://cocoapods.org/) installed? + +Check that your XCode path is correct: +${chalk.bold('sudo xcode-select --print-path')} + +If the path is wrong, switch the path: (your path may be different) +${chalk.bold('sudo xcode-select --switch /Applications/Xcode.app')} +`; + +/** + * Run pod install on ios directory + * @param iosDirectory ios directory that contains Podfile + * @returns resolve with 0 if not error, reject with error otherwise + */ +export function runPodInstall(iosDirectory: string): GeneratorCallback { + return () => { + if (platform() !== 'darwin') { + logger.info('Skipping `pod install` on non-darwin platform'); + return; + } + + logger.info(`Running \`pod install\` from "${iosDirectory}"`); + + return podInstall(iosDirectory); + }; +} + +export function podInstall(iosDirectory: string): Promise { + return new Promise((resolve, reject) => { + const process = spawn('pod', ['install'], { + cwd: iosDirectory, + stdio: [0, 1, 2], + }); + + process.on('close', (code: number) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(podInstallErrorMessage)); + } + }); + + process.on('error', () => { + reject(new Error(podInstallErrorMessage)); + }); + }); +} diff --git a/packages/expo/src/utils/symlink-task.ts b/packages/expo/src/utils/symlink-task.ts new file mode 100644 index 0000000000..47662c59e5 --- /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 0000000000..204eaa467c --- /dev/null +++ b/packages/expo/src/utils/versions.ts @@ -0,0 +1,21 @@ +export const nxVersion = '*'; + +export const expoVersion = '46.0.10'; +export const expoMetroConfigVersion = '0.3.22'; +export const jestExpoVersion = '46.0.1'; +export const expoCliVersion = '6.0.5'; +export const easCliVersion = '2.1.0'; +export const babelPresetExpoVersion = '~9.2.0'; + +export const reactNativeVersion = '0.69.5'; +export const typesReactNativeVersion = '0.69.8'; +export const reactNativeWebVersion = '~0.18.7'; + +export const reactNativeSvgTransformerVersion = '1.0.0'; +export const reactNativeSvgVersion = '13.1.0'; +export const svgrWebpackVersion = '^6.3.1'; + +export const metroVersion = '0.72.2'; + +export const testingLibraryReactNativeVersion = '11.0.0'; +export const testingLibraryJestNativeVersion = '4.0.11'; diff --git a/packages/expo/tsconfig.json b/packages/expo/tsconfig.json new file mode 100644 index 0000000000..62ebbd9464 --- /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 0000000000..5c589aef9e --- /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", "jest.config.ts"], + "include": ["**/*.ts"] +} diff --git a/packages/expo/tsconfig.spec.json b/packages/expo/tsconfig.spec.json new file mode 100644 index 0000000000..46f9467f3b --- /dev/null +++ b/packages/expo/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx", + "**/*.d.ts", + "jest.config.ts" + ] +} diff --git a/packages/expo/typings/svg.d.ts b/packages/expo/typings/svg.d.ts new file mode 100644 index 0000000000..fed8d16634 --- /dev/null +++ b/packages/expo/typings/svg.d.ts @@ -0,0 +1,6 @@ +declare module '*.svg' { + import React from 'react'; + import { SvgProps } from 'react-native-svg'; + const content: React.FC; + export default content; +} diff --git a/packages/nx/package.json b/packages/nx/package.json index a47c2b99ff..ff0f539246 100644 --- a/packages/nx/package.json +++ b/packages/nx/package.json @@ -98,7 +98,8 @@ "version": "latest" }, "@nrwl/react-native", - "@nrwl/detox" + "@nrwl/detox", + "@nrwl/expo" ] }, "executors": "./executors.json" diff --git a/packages/nx/src/command-line/migrate.ts b/packages/nx/src/command-line/migrate.ts index ac9b9a5852..7894a9a7ba 100644 --- a/packages/nx/src/command-line/migrate.ts +++ b/packages/nx/src/command-line/migrate.ts @@ -255,6 +255,7 @@ export class Migrator { '@nrwl/nx-cloud': 'latest', '@nrwl/react-native': targetVersion, '@nrwl/detox': targetVersion, + '@nrwl/expo': targetVersion, }; } diff --git a/packages/nx/src/command-line/report.ts b/packages/nx/src/command-line/report.ts index bf4345eb35..504d95b2e0 100644 --- a/packages/nx/src/command-line/report.ts +++ b/packages/nx/src/command-line/report.ts @@ -21,6 +21,7 @@ export const packagesWeCareAbout = [ '@nrwl/detox', '@nrwl/devkit', '@nrwl/eslint-plugin-nx', + '@nrwl/expo', '@nrwl/express', '@nrwl/jest', '@nrwl/js', diff --git a/packages/react-native/src/generators/application/application.spec.ts b/packages/react-native/src/generators/application/application.spec.ts index 979a4b3304..bfcc9f4bde 100644 --- a/packages/react-native/src/generators/application/application.spec.ts +++ b/packages/react-native/src/generators/application/application.spec.ts @@ -3,8 +3,9 @@ import { readWorkspaceConfiguration, getProjects, readJson, + readProjectConfiguration, } from '@nrwl/devkit'; -import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import { Linter } from '@nrwl/linter'; import { reactNativeApplicationGenerator } from './application'; @@ -12,7 +13,7 @@ describe('app', () => { let appTree: Tree; beforeEach(() => { - appTree = createTreeWithEmptyV1Workspace(); + appTree = createTreeWithEmptyWorkspace(); appTree.write('.gitignore', ''); }); @@ -41,11 +42,9 @@ describe('app', () => { install: false, }); - const { projects } = readJson(appTree, '/workspace.json'); - expect(projects).toMatchObject({ - 'my-app': { - tags: ['one', 'two'], - }, + const projectConfiguration = readProjectConfiguration(appTree, 'my-app'); + expect(projectConfiguration).toMatchObject({ + tags: ['one', 'two'], }); }); diff --git a/packages/react-native/src/generators/application/lib/nomalize-options.spec.ts b/packages/react-native/src/generators/application/lib/nomalize-options.spec.ts index beea0d7666..b322807712 100644 --- a/packages/react-native/src/generators/application/lib/nomalize-options.spec.ts +++ b/packages/react-native/src/generators/application/lib/nomalize-options.spec.ts @@ -1,5 +1,5 @@ import { Tree } from '@nrwl/devkit'; -import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import { Linter } from '@nrwl/linter'; import { Schema } from '../schema'; import { normalizeOptions } from './normalize-options'; @@ -8,7 +8,7 @@ describe('Normalize Options', () => { let appTree: Tree; beforeEach(() => { - appTree = createTreeWithEmptyV1Workspace(); + appTree = createTreeWithEmptyWorkspace(); }); it('should normalize options with name in kebab case', () => { diff --git a/packages/react-native/src/generators/component-story/component-story.spec.ts b/packages/react-native/src/generators/component-story/component-story.spec.ts index 566e10b01b..c5116819ce 100644 --- a/packages/react-native/src/generators/component-story/component-story.spec.ts +++ b/packages/react-native/src/generators/component-story/component-story.spec.ts @@ -1,5 +1,5 @@ import { getProjects, Tree, updateProjectConfiguration } from '@nrwl/devkit'; -import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import componentStoryGenerator from './component-story'; import { Linter } from '@nrwl/linter'; @@ -408,7 +408,7 @@ export async function createTestUILib( libName: string, useEsLint = false ): Promise { - let appTree = createTreeWithEmptyV1Workspace(); + let appTree = createTreeWithEmptyWorkspace(); appTree.write('.gitignore', ''); await libraryGenerator(appTree, { diff --git a/packages/react-native/src/generators/component/component.spec.ts b/packages/react-native/src/generators/component/component.spec.ts index ae4d2e5663..63060ad914 100644 --- a/packages/react-native/src/generators/component/component.spec.ts +++ b/packages/react-native/src/generators/component/component.spec.ts @@ -1,5 +1,5 @@ import { logger, Tree } from '@nrwl/devkit'; -import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import { createApp, createLib } from '../../utils/testing-generators'; import { reactNativeComponentGenerator } from './component'; @@ -9,7 +9,7 @@ describe('component', () => { beforeEach(async () => { projectName = 'my-lib'; - appTree = createTreeWithEmptyV1Workspace(); + appTree = createTreeWithEmptyWorkspace(); appTree.write('.gitignore', ''); await createApp(appTree, 'my-app'); await createLib(appTree, projectName); diff --git a/packages/react-native/src/generators/init/init.spec.ts b/packages/react-native/src/generators/init/init.spec.ts index 1af24307a8..1fc9d26b7e 100644 --- a/packages/react-native/src/generators/init/init.spec.ts +++ b/packages/react-native/src/generators/init/init.spec.ts @@ -1,13 +1,13 @@ import { NxJsonConfiguration } from '@nrwl/devkit'; import { Tree, readJson, updateJson } from '@nrwl/devkit'; -import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import { reactNativeInitGenerator } from './init'; describe('init', () => { let tree: Tree; beforeEach(() => { - tree = createTreeWithEmptyV1Workspace(); + tree = createTreeWithEmptyWorkspace(); tree.write('.gitignore', ''); }); diff --git a/packages/react-native/src/generators/library/library.spec.ts b/packages/react-native/src/generators/library/library.spec.ts index 10d8c7429e..846c7736ac 100644 --- a/packages/react-native/src/generators/library/library.spec.ts +++ b/packages/react-native/src/generators/library/library.spec.ts @@ -1,5 +1,10 @@ -import { getProjects, readJson, Tree, updateJson } from '@nrwl/devkit'; -import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; +import { + readJson, + readProjectConfiguration, + Tree, + updateJson, +} from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import libraryGenerator from './library'; import { Linter } from '@nrwl/linter'; import { Schema } from './schema'; @@ -17,24 +22,24 @@ describe('lib', () => { }; beforeEach(() => { - appTree = createTreeWithEmptyV1Workspace(); + appTree = createTreeWithEmptyWorkspace(); appTree.write('.gitignore', ''); }); describe('not nested', () => { - it('should update workspace.json', async () => { + it('should update project.json', async () => { await libraryGenerator(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', + const projectConfiguration = readProjectConfiguration(appTree, 'my-lib'); + expect(projectConfiguration.root).toEqual('libs/my-lib'); + expect(projectConfiguration.targets.build).toBeUndefined(); + expect(projectConfiguration.targets.lint).toEqual({ + executor: '@nrwl/linter:eslint', outputs: ['{options.outputFile}'], options: { lintFilePatterns: ['libs/my-lib/**/*.{ts,tsx,js,jsx}'], }, }); - expect(workspaceJson.projects['my-lib'].tags).toEqual(['one', 'two']); + expect(projectConfiguration.tags).toEqual(['one', 'two']); }); it('should update root tsconfig.base.json', async () => { @@ -121,11 +126,12 @@ describe('lib', () => { directory: 'myDir', tags: 'one', }); - const workspaceJson = readJson(appTree, '/workspace.json'); - expect(workspaceJson.projects).toMatchObject({ - 'my-dir-my-lib': { - tags: ['one'], - }, + const projectConfiguration = readProjectConfiguration( + appTree, + 'my-dir-my-lib' + ); + expect(projectConfiguration).toMatchObject({ + tags: ['one'], }); await libraryGenerator(appTree, { @@ -135,26 +141,25 @@ describe('lib', () => { 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'], - }, + const lib2ProjectConfiguration = readProjectConfiguration( + appTree, + 'my-dir-my-lib2' + ); + expect(lib2ProjectConfiguration).toMatchObject({ + tags: ['one', 'two'], }); }); - it('should update workspace.json', async () => { + it('should update project.json', async () => { await libraryGenerator(appTree, { ...defaultSchema, directory: 'myDir' }); - const workspaceJson = readJson(appTree, '/workspace.json'); - - expect(workspaceJson.projects['my-dir-my-lib'].root).toEqual( - 'libs/my-dir/my-lib' + const projectConfiguration = readProjectConfiguration( + appTree, + 'my-dir-my-lib' ); - expect(workspaceJson.projects['my-dir-my-lib'].architect.lint).toEqual({ - builder: '@nrwl/linter:eslint', + + expect(projectConfiguration.root).toEqual('libs/my-dir/my-lib'); + expect(projectConfiguration.targets.lint).toEqual({ + executor: '@nrwl/linter:eslint', outputs: ['{options.outputFile}'], options: { lintFilePatterns: ['libs/my-dir/my-lib/**/*.{ts,tsx,js,jsx}'], @@ -227,22 +232,15 @@ describe('lib', () => { expect(appTree.exists('libs/my-lib/tsconfig.spec.json')).toBeFalsy(); expect(appTree.exists('libs/my-lib/jest.config.ts')).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}", - ], - } - `); + const projectConfiguration = readProjectConfiguration(appTree, 'my-lib'); + expect(projectConfiguration.targets.test).toBeUndefined(); + expect(projectConfiguration.targets.lint).toMatchObject({ + executor: '@nrwl/linter:eslint', + options: { + lintFilePatterns: ['libs/my-lib/**/*.{ts,tsx,js,jsx}'], + }, + outputs: ['{options.outputFile}'], + }); }); }); @@ -253,9 +251,9 @@ describe('lib', () => { buildable: true, }); - const workspaceJson = getProjects(appTree); + const projectConfiguration = readProjectConfiguration(appTree, 'my-lib'); - expect(workspaceJson.get('my-lib').targets.build).toBeDefined(); + expect(projectConfiguration.targets.build).toBeDefined(); }); }); @@ -267,9 +265,9 @@ describe('lib', () => { importPath: '@proj/my-lib', }); - const workspaceJson = getProjects(appTree); + const projectConfiguration = readProjectConfiguration(appTree, 'my-lib'); - expect(workspaceJson.get('my-lib').targets.build).toMatchObject({ + expect(projectConfiguration.targets.build).toMatchObject({ executor: '@nrwl/web:rollup', outputs: ['{options.outputPath}'], options: { diff --git a/packages/react-native/src/generators/stories/stories-app.spec.ts b/packages/react-native/src/generators/stories/stories-app.spec.ts index 1746e5a9f2..c3227fa505 100644 --- a/packages/react-native/src/generators/stories/stories-app.spec.ts +++ b/packages/react-native/src/generators/stories/stories-app.spec.ts @@ -1,6 +1,6 @@ import { Tree } from '@nrwl/devkit'; import storiesGenerator from './stories'; -import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import applicationGenerator from '../application/application'; import { Linter } from '@nrwl/linter'; import { reactNativeComponentGenerator } from '../component/component'; @@ -130,7 +130,7 @@ describe('react:stories for applications', () => { }); export async function createTestUIApp(libName: string): Promise { - let appTree = createTreeWithEmptyV1Workspace(); + let appTree = createTreeWithEmptyWorkspace(); appTree.write('.gitignore', ''); await applicationGenerator(appTree, { diff --git a/packages/react-native/src/generators/stories/stories-lib.spec.ts b/packages/react-native/src/generators/stories/stories-lib.spec.ts index 534f566867..079380d9f0 100644 --- a/packages/react-native/src/generators/stories/stories-lib.spec.ts +++ b/packages/react-native/src/generators/stories/stories-lib.spec.ts @@ -1,6 +1,6 @@ import { Tree } from '@nrwl/devkit'; import storiesGenerator from './stories'; -import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import applicationGenerator from '../application/application'; import { Linter } from '@nrwl/linter'; import libraryGenerator from '../library/library'; @@ -90,7 +90,7 @@ describe('react-native:stories for libraries', () => { }); export async function createTestUILib(libName: string): Promise { - let appTree = createTreeWithEmptyV1Workspace(); + let appTree = createTreeWithEmptyWorkspace(); appTree.write('.gitignore', ''); await libraryGenerator(appTree, { diff --git a/packages/react-native/src/generators/storybook-configuration/configuration.spec.ts b/packages/react-native/src/generators/storybook-configuration/configuration.spec.ts index f4a8bef243..de1789dfe0 100644 --- a/packages/react-native/src/generators/storybook-configuration/configuration.spec.ts +++ b/packages/react-native/src/generators/storybook-configuration/configuration.spec.ts @@ -1,5 +1,5 @@ import { Tree } from '@nrwl/devkit'; -import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import { Linter } from '@nrwl/linter'; import { logger } from '@nrwl/devkit'; @@ -103,7 +103,7 @@ describe('react-native:storybook-configuration', () => { }); export async function createTestUILib(libName: string): Promise { - let appTree = createTreeWithEmptyV1Workspace(); + let appTree = createTreeWithEmptyWorkspace(); await libraryGenerator(appTree, { linter: Linter.EsLint, @@ -119,7 +119,7 @@ export async function createTestAppLib( libName: string, plainJS = false ): Promise { - let appTree = createTreeWithEmptyV1Workspace(); + let appTree = createTreeWithEmptyWorkspace(); await applicationGenerator(appTree, { e2eTestRunner: 'none', diff --git a/packages/react-native/src/generators/storybook-configuration/lib/add-resolver-main-fields-to-metro-config.spec.ts b/packages/react-native/src/generators/storybook-configuration/lib/add-resolver-main-fields-to-metro-config.spec.ts index 2b62c40a35..a40ebe24e9 100644 --- a/packages/react-native/src/generators/storybook-configuration/lib/add-resolver-main-fields-to-metro-config.spec.ts +++ b/packages/react-native/src/generators/storybook-configuration/lib/add-resolver-main-fields-to-metro-config.spec.ts @@ -1,5 +1,5 @@ import { addProjectConfiguration, Tree } from '@nrwl/devkit'; -import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import { formatFile } from '../../../utils/format-file'; @@ -9,7 +9,7 @@ describe('addResolverMainFieldsToMetroConfig', () => { let tree: Tree; beforeEach(() => { - tree = createTreeWithEmptyV1Workspace(); + tree = createTreeWithEmptyWorkspace(); addProjectConfiguration(tree, 'products', { root: 'apps/products', diff --git a/packages/react-native/src/generators/storybook-configuration/lib/replace-app-import-with-storybook-toggle.spec.ts b/packages/react-native/src/generators/storybook-configuration/lib/replace-app-import-with-storybook-toggle.spec.ts index eb5afdaa38..d53386e673 100644 --- a/packages/react-native/src/generators/storybook-configuration/lib/replace-app-import-with-storybook-toggle.spec.ts +++ b/packages/react-native/src/generators/storybook-configuration/lib/replace-app-import-with-storybook-toggle.spec.ts @@ -1,5 +1,5 @@ import { addProjectConfiguration, Tree } from '@nrwl/devkit'; -import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import { formatFile } from '../../../utils/format-file'; @@ -9,7 +9,7 @@ describe('replaceAppImportWithStorybookToggle', () => { let tree: Tree; beforeEach(() => { - tree = createTreeWithEmptyV1Workspace(); + tree = createTreeWithEmptyWorkspace(); addProjectConfiguration(tree, 'products', { root: 'apps/products', diff --git a/packages/react-native/src/migrations/update-12-10-0/add-react-native-svg-12-10-0.spec.ts b/packages/react-native/src/migrations/update-12-10-0/add-react-native-svg-12-10-0.spec.ts index 8c675a9f30..7b5d7b35f2 100644 --- a/packages/react-native/src/migrations/update-12-10-0/add-react-native-svg-12-10-0.spec.ts +++ b/packages/react-native/src/migrations/update-12-10-0/add-react-native-svg-12-10-0.spec.ts @@ -1,5 +1,5 @@ import { addProjectConfiguration, readJson, Tree } from '@nrwl/devkit'; -import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import { reactNativeSvgTransformerVersion, reactNativeSvgVersion, @@ -10,7 +10,7 @@ describe('Add react-native-svg to dev dependencies', () => { let tree: Tree; beforeEach(async () => { - tree = createTreeWithEmptyV1Workspace(); + tree = createTreeWithEmptyWorkspace(); addProjectConfiguration(tree, 'products', { root: 'apps/products', sourceRoot: 'apps/products/src', diff --git a/packages/react-native/src/migrations/update-13-5-0/add-babel-config-root-13-5-0.spec.ts b/packages/react-native/src/migrations/update-13-5-0/add-babel-config-root-13-5-0.spec.ts index 39c4d362c5..8e88190a97 100644 --- a/packages/react-native/src/migrations/update-13-5-0/add-babel-config-root-13-5-0.spec.ts +++ b/packages/react-native/src/migrations/update-13-5-0/add-babel-config-root-13-5-0.spec.ts @@ -1,12 +1,12 @@ import { addProjectConfiguration, readJson, Tree } from '@nrwl/devkit'; -import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import update from './add-babel-config-root-13-5-0'; describe('Add react-native-svg to dev dependencies', () => { let tree: Tree; beforeEach(async () => { - tree = createTreeWithEmptyV1Workspace(); + tree = createTreeWithEmptyWorkspace(); addProjectConfiguration(tree, 'products', { root: 'apps/products', sourceRoot: 'apps/products/src', diff --git a/packages/react-native/src/migrations/update-13-5-0/update-react-native-typing-svg-13-5-0.spec.ts b/packages/react-native/src/migrations/update-13-5-0/update-react-native-typing-svg-13-5-0.spec.ts index bae0a6a6b9..615c6c6b26 100644 --- a/packages/react-native/src/migrations/update-13-5-0/update-react-native-typing-svg-13-5-0.spec.ts +++ b/packages/react-native/src/migrations/update-13-5-0/update-react-native-typing-svg-13-5-0.spec.ts @@ -1,12 +1,12 @@ import { addProjectConfiguration, readJson, Tree } from '@nrwl/devkit'; -import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import update from './update-react-native-typing-svg-13-5-0'; describe('Update svg typings in tsconfig for react native app', () => { let tree: Tree; beforeEach(async () => { - tree = createTreeWithEmptyV1Workspace(); + tree = createTreeWithEmptyWorkspace(); addProjectConfiguration(tree, 'products', { root: 'apps/products', sourceRoot: 'apps/products/src', diff --git a/packages/react-native/src/migrations/update-14-0-0/add-project-root-metro-config-14-0-0.spec.ts b/packages/react-native/src/migrations/update-14-0-0/add-project-root-metro-config-14-0-0.spec.ts index 2dff4dd8ba..ab67d16dc1 100644 --- a/packages/react-native/src/migrations/update-14-0-0/add-project-root-metro-config-14-0-0.spec.ts +++ b/packages/react-native/src/migrations/update-14-0-0/add-project-root-metro-config-14-0-0.spec.ts @@ -1,5 +1,5 @@ import { addProjectConfiguration, Tree } from '@nrwl/devkit'; -import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import update from './add-project-root-metro-config-14-0-0'; @@ -7,7 +7,7 @@ describe('Add projectRoot option in metro.config.js', () => { let tree: Tree; beforeEach(async () => { - tree = createTreeWithEmptyV1Workspace(); + tree = createTreeWithEmptyWorkspace(); addProjectConfiguration(tree, 'products', { root: 'apps/products', sourceRoot: 'apps/products/src', diff --git a/packages/react-native/src/migrations/update-14-0-0/update-entry-file-bundle-14-0-0.spec.ts b/packages/react-native/src/migrations/update-14-0-0/update-entry-file-bundle-14-0-0.spec.ts index 52d8883a48..a90373c940 100644 --- a/packages/react-native/src/migrations/update-14-0-0/update-entry-file-bundle-14-0-0.spec.ts +++ b/packages/react-native/src/migrations/update-14-0-0/update-entry-file-bundle-14-0-0.spec.ts @@ -1,5 +1,5 @@ import { addProjectConfiguration, Tree, getProjects } from '@nrwl/devkit'; -import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import update from './update-entry-file-bundle-14-0-0'; @@ -7,7 +7,7 @@ describe('Update entryFile for bundle target for react native apps', () => { let tree: Tree; beforeEach(async () => { - tree = createTreeWithEmptyV1Workspace(); + tree = createTreeWithEmptyWorkspace(); addProjectConfiguration(tree, 'products', { root: 'apps/products', sourceRoot: 'apps/products/src', diff --git a/packages/react-native/src/migrations/update-14-0-2/change-main-to-class-name-14-0-2.spec.ts b/packages/react-native/src/migrations/update-14-0-2/change-main-to-class-name-14-0-2.spec.ts index 03cdb15090..8b40d3c706 100644 --- a/packages/react-native/src/migrations/update-14-0-2/change-main-to-class-name-14-0-2.spec.ts +++ b/packages/react-native/src/migrations/update-14-0-2/change-main-to-class-name-14-0-2.spec.ts @@ -1,5 +1,5 @@ import { addProjectConfiguration, Tree } from '@nrwl/devkit'; -import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import update from './change-main-to-class-name-14-0-2'; @@ -7,7 +7,7 @@ describe('Change from main tag to className tag', () => { let tree: Tree; beforeEach(async () => { - tree = createTreeWithEmptyV1Workspace(); + tree = createTreeWithEmptyWorkspace(); addProjectConfiguration(tree, 'products', { root: 'apps/products', sourceRoot: 'apps/products/src', diff --git a/packages/react-native/src/migrations/update-14-2-1/rename-blockList-metro-config.spec.ts b/packages/react-native/src/migrations/update-14-2-1/rename-blockList-metro-config.spec.ts index 246b426f55..5043451f79 100644 --- a/packages/react-native/src/migrations/update-14-2-1/rename-blockList-metro-config.spec.ts +++ b/packages/react-native/src/migrations/update-14-2-1/rename-blockList-metro-config.spec.ts @@ -1,5 +1,5 @@ import { addProjectConfiguration, Tree, getProjects } from '@nrwl/devkit'; -import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import update from './rename-blockList-metro-config'; @@ -7,7 +7,7 @@ describe('Rename blacklistRE to blockList in metro.config.js for react native ap let tree: Tree; beforeEach(async () => { - tree = createTreeWithEmptyV1Workspace(); + tree = createTreeWithEmptyWorkspace(); addProjectConfiguration(tree, 'products', { root: 'apps/products', sourceRoot: 'apps/products/src', diff --git a/packages/react-native/src/migrations/update-14-5-5/add-exclude-sync-deps.spec.ts b/packages/react-native/src/migrations/update-14-5-5/add-exclude-sync-deps.spec.ts index bee8e5bf81..ddd39605eb 100644 --- a/packages/react-native/src/migrations/update-14-5-5/add-exclude-sync-deps.spec.ts +++ b/packages/react-native/src/migrations/update-14-5-5/add-exclude-sync-deps.spec.ts @@ -3,14 +3,14 @@ import { readProjectConfiguration, Tree, } from '@nrwl/devkit'; -import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import update from './add-exclude-sync-deps'; describe('add-exclude-sync-deps', () => { let tree: Tree; beforeEach(async () => { - tree = createTreeWithEmptyV1Workspace(); + tree = createTreeWithEmptyWorkspace(); addProjectConfiguration(tree, 'products', { root: 'apps/products', sourceRoot: 'apps/products/src', diff --git a/packages/react-native/src/migrations/update-14-5-8/change-searchDir-storybook.spec.ts b/packages/react-native/src/migrations/update-14-5-8/change-searchDir-storybook.spec.ts index 0b75f28627..9c2f6d7556 100644 --- a/packages/react-native/src/migrations/update-14-5-8/change-searchDir-storybook.spec.ts +++ b/packages/react-native/src/migrations/update-14-5-8/change-searchDir-storybook.spec.ts @@ -3,14 +3,14 @@ import { readProjectConfiguration, Tree, } from '@nrwl/devkit'; -import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import update from './change-searchDir-storybook'; describe('change-searchDir-storybook', () => { let tree: Tree; beforeEach(async () => { - tree = createTreeWithEmptyV1Workspace(); + tree = createTreeWithEmptyWorkspace(); addProjectConfiguration(tree, 'products', { root: 'apps/products', sourceRoot: 'apps/products/src', diff --git a/packages/react-native/src/utils/add-linting.spec.ts b/packages/react-native/src/utils/add-linting.spec.ts index f9ba126f4c..bf56bc0341 100644 --- a/packages/react-native/src/utils/add-linting.spec.ts +++ b/packages/react-native/src/utils/add-linting.spec.ts @@ -1,5 +1,5 @@ import { readProjectConfiguration, Tree } from '@nrwl/devkit'; -import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; +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'; @@ -8,7 +8,7 @@ describe('Add Linting', () => { let tree: Tree; beforeEach(async () => { - tree = createTreeWithEmptyV1Workspace(); + tree = createTreeWithEmptyWorkspace(); await libraryGenerator(tree, { name: 'my-lib', linter: Linter.None, diff --git a/packages/workspace/package.json b/packages/workspace/package.json index ef48748011..cf60cd6412 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -51,6 +51,7 @@ "@nrwl/tao": "*", "@nrwl/nx-cloud": "latest", "@nrwl/react-native": "*", + "@nrwl/expo": "*", "@nrwl/detox": "*", "nx": "*" } diff --git a/packages/workspace/src/generators/new/new.ts b/packages/workspace/src/generators/new/new.ts index 5296742e32..0f8cc10829 100644 --- a/packages/workspace/src/generators/new/new.ts +++ b/packages/workspace/src/generators/new/new.ts @@ -205,6 +205,9 @@ function getPresetDependencies(preset: string, version?: string) { case Preset.ReactNative: return { dependencies: {}, dev: { '@nrwl/react-native': nxVersion } }; + case Preset.Expo: + return { dependencies: {}, dev: { '@nrwl/expo': nxVersion } }; + case Preset.WebComponents: return { dependencies: {}, dev: { '@nrwl/web': nxVersion } }; diff --git a/packages/workspace/src/generators/preset/preset.spec.ts b/packages/workspace/src/generators/preset/preset.spec.ts index 7032413224..020ce2e2cd 100644 --- a/packages/workspace/src/generators/preset/preset.spec.ts +++ b/packages/workspace/src/generators/preset/preset.spec.ts @@ -1,4 +1,4 @@ -import { Tree, readJson, NxJsonConfiguration } from '@nrwl/devkit'; +import { Tree, readJson } from '@nrwl/devkit'; import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; import { overrideCollectionResolutionForTesting } from '@nrwl/devkit/ngcli-adapter'; import { presetGenerator } from './preset'; diff --git a/packages/workspace/src/generators/preset/preset.ts b/packages/workspace/src/generators/preset/preset.ts index 85ba5496ab..8544083cf1 100644 --- a/packages/workspace/src/generators/preset/preset.ts +++ b/packages/workspace/src/generators/preset/preset.ts @@ -3,7 +3,6 @@ import { convertNxGenerator, formatFiles, generateFiles, - getWorkspaceLayout, installPackagesTask, names, readWorkspaceConfiguration, @@ -161,7 +160,7 @@ async function createPreset(tree: Tree, options: Schema) { linter: options.linter, standaloneConfig: options.standaloneConfig, }); - } else if (options.preset === 'react-native') { + } else if (options.preset === Preset.ReactNative) { const { reactNativeApplicationGenerator } = require('@nrwl' + '/react-native'); await reactNativeApplicationGenerator(tree, { @@ -170,6 +169,14 @@ async function createPreset(tree: Tree, options: Schema) { standaloneConfig: options.standaloneConfig, e2eTestRunner: 'detox', }); + } 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', + }); } else if (options.preset === Preset.Core || options.preset === Preset.NPM) { setupPackageManagerWorkspaces(tree, options); if (options.preset === Preset.Core) { diff --git a/packages/workspace/src/generators/utils/presets.ts b/packages/workspace/src/generators/utils/presets.ts index e4a91772c2..fef48bdb43 100644 --- a/packages/workspace/src/generators/utils/presets.ts +++ b/packages/workspace/src/generators/utils/presets.ts @@ -10,6 +10,7 @@ export enum Preset { React = 'react', ReactWithExpress = 'react-express', ReactNative = 'react-native', + Expo = 'expo', NextJs = 'next', Nest = 'nest', Express = 'express', diff --git a/tsconfig.base.json b/tsconfig.base.json index 1709a34a56..9e38d55e1c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -27,6 +27,7 @@ "@nrwl/devkit/*": ["packages/devkit/*"], "@nrwl/e2e/utils": ["e2e/utils"], "@nrwl/eslint-plugin-nx": ["packages/eslint-plugin-nx/src"], + "@nrwl/expo": ["packages/expo"], "@nrwl/express": ["packages/express"], "@nrwl/jest": ["packages/jest"], "@nrwl/jest/*": ["packages/jest/*"], diff --git a/workspace.json b/workspace.json index e927ee5570..cee7552213 100644 --- a/workspace.json +++ b/workspace.json @@ -16,6 +16,7 @@ "e2e-angular-extensions": "e2e/angular-extensions", "e2e-cypress": "e2e/cypress", "e2e-detox": "e2e/detox", + "e2e-expo": "e2e/expo", "e2e-graph-client": "graph/client-e2e", "e2e-jest": "e2e/jest", "e2e-js": "e2e/js", @@ -38,6 +39,7 @@ "e2e-workspace-create": "e2e/workspace-create", "eslint-plugin-nx": "packages/eslint-plugin-nx", "eslint-rules": "tools/eslint-rules", + "expo": "packages/expo", "express": "packages/express", "graph-client": "graph/client", "jest": "packages/jest", diff --git a/yarn.lock b/yarn.lock index 1d1bb3189c..1bda2d0829 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17282,6 +17282,11 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" +mkdirp-classic@^0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + mkdirp-infer-owner@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/mkdirp-infer-owner/-/mkdirp-infer-owner-2.0.0.tgz#55d3b368e7d89065c38f32fd38e638f0ab61d316" @@ -22482,7 +22487,17 @@ tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== -tar-stream@~2.2.0: +tar-fs@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" + integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.1.4" + +tar-stream@^2.1.4, tar-stream@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==