Feature/move expo (#11712)

* feat(expo): move expo to main repo

* feat(expo): add e2e expo tests and fix expo unit tests

* feat(expo): add generated docs

* feat(expo): update cz-config to add expo to commit message

* feat(expo): add @nrwl/expo to e2e test setup

* feat(expo): add expo to preset

* feat(expo): add detox tests

* feat(expo): update docs

* feat(expo): fix e2e tests

* feat(expo): upgrade expo to 46.0.10

* fix(expo): correct eas-cli build:info parameters names

* fix(expo): add cleanupProject to e2e test
This commit is contained in:
Emily Xiong 2022-09-16 11:56:28 -04:00 committed by GitHub
parent d90bdaec5f
commit a411e85e42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
213 changed files with 8697 additions and 108 deletions

View File

@ -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',

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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",

View File

@ -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 () => {

12
e2e/expo/jest.config.ts Normal file
View File

@ -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: '<rootDir>/tsconfig.spec.json' } },
displayName: 'e2e-expo',
testTimeout: 600000,
preset: '../../jest.preset.js',
};

34
e2e/expo/project.json Normal file
View File

@ -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"]
}

45
e2e/expo/src/expo.test.ts Normal file
View File

@ -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);
});

13
e2e/expo/tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"types": ["node", "jest"]
},
"include": [],
"files": [],
"references": [
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@ -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"
]
}

View File

@ -319,6 +319,7 @@ export function newProject({
`@nrwl/web`,
`@nrwl/webpack`,
`@nrwl/react-native`,
`@nrwl/expo`,
];
packageInstall(packages.join(` `), projScope);

View File

@ -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, {

View File

@ -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",

View File

@ -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);
}

View File

@ -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"
}
}
]
}

13
packages/expo/README.md Normal file
View File

@ -0,0 +1,13 @@
<p style="text-align: center;"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx.png" width="600" alt="Nx - Smart, Fast and Extensible Build System"></p>
{{links}}
<hr>
# 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}}

View File

View File

@ -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."
}
}
}

View File

@ -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"]
}
}
}

4
packages/expo/index.ts Normal file
View File

@ -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';

View File

@ -0,0 +1,13 @@
/* eslint-disable */
export default {
transform: {
'^.+\\.[tj]sx?$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html', 'json'],
globals: {
'ts-jest': { tsconfig: '<rootDir>/tsconfig.spec.json' },
},
displayName: 'expo',
testEnvironment: 'node',
preset: '../../jest.preset.js',
};

View File

@ -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
}
}
}
}
}

View File

@ -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"
}

View File

@ -0,0 +1,3 @@
// From https://github.com/kristerkari/react-native-svg-transformer#usage-with-jest
module.exports = 'SvgMock';
module.exports.ReactComponent = 'SvgMock';

View File

@ -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<string, string[]>;
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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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": []
}

View File

@ -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<ReactNativeBuildOutput> {
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;
}, []);
}

View File

@ -0,0 +1,5 @@
import { convertNxExecutor } from '@nrwl/devkit';
import buildAndroidExecutor from './build-android.impl';
export default convertNxExecutor(buildAndroidExecutor);

View File

@ -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
}

View File

@ -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": []
}

View File

@ -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<ExpoRunOutput> {
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;
}, []);
}

View File

@ -0,0 +1,5 @@
import { convertNxExecutor } from '@nrwl/devkit';
import buildIosExecutor from './build-ios.impl';
export default convertNxExecutor(buildIosExecutor);

View File

@ -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
}

View File

@ -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": []
}

View File

@ -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<ReactNativeBuildListOutput> {
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;
}, []);
}

View File

@ -0,0 +1,5 @@
import { convertNxExecutor } from '@nrwl/devkit';
import buildListExecutor from './build-list.impl';
export default convertNxExecutor(buildListExecutor);

View File

@ -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;
}

View File

@ -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": []
}

View File

@ -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<ReactNativeBuildOutput> {
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;
}, []);
}

View File

@ -0,0 +1,5 @@
import { convertNxExecutor } from '@nrwl/devkit';
import buildStatusExecutor from './build-status.impl';
export default convertNxExecutor(buildStatusExecutor);

View File

@ -0,0 +1,4 @@
// options from https://docs.expo.dev/workflow/expo-cli/#expo-buildweb
export interface ExpoBuildStatusOptions {
publicUrl: string;
}

View File

@ -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": []
}

View File

@ -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<ReactNativeBuildOutput> {
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;
}, []);
}

View File

@ -0,0 +1,5 @@
import { convertNxExecutor } from '@nrwl/devkit';
import buildWebExecutor from './build-web.impl';
export default convertNxExecutor(buildWebExecutor);

View File

@ -0,0 +1,6 @@
// options from https://docs.expo.dev/workflow/expo-cli/#expo-buildweb
export interface ExpoBuildWebOptions {
clear?: boolean;
noPwa?: boolean;
dev?: boolean;
}

View File

@ -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": []
}

View File

@ -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<ReactNativeBuildOutput> {
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;
}, []);
}

View File

@ -0,0 +1,5 @@
import { convertNxExecutor } from '@nrwl/devkit';
import buildExecutor from './build.impl';
export default convertNxExecutor(buildExecutor);

View File

@ -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;
}

View File

@ -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": []
}

View File

@ -0,0 +1,5 @@
import { convertNxExecutor } from '@nrwl/devkit';
import downloadExecutor from './download.impl';
export default convertNxExecutor(downloadExecutor);

View File

@ -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<ReactNativeDownloadOutput> {
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<void>((resolve, reject) => {
copyFile(downloadFilePath, outputFilePath, (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}

View File

@ -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;
}

View File

@ -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"]
}

View File

@ -0,0 +1,5 @@
import { convertNxExecutor } from '@nrwl/devkit';
import ejectExecutor from './eject.impl';
export default convertNxExecutor(ejectExecutor);

View File

@ -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<ExpoEjectOutput> {
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<number> {
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;
}, []);
}

View File

@ -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
}

View File

@ -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"]
}
}
}

View File

@ -0,0 +1,5 @@
import { convertNxExecutor } from '@nrwl/devkit';
import ensureSymlinkExecutor from './ensure-symlink.impl';
export default convertNxExecutor(ensureSymlinkExecutor);

View File

@ -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<ExpoEnsureSymlinkOutput> {
const projectRoot = context.workspace.projects[context.projectName].root;
ensureNodeModulesSymlink(context.root, projectRoot);
yield { success: true };
}

View File

@ -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": {}
}

View File

@ -0,0 +1,5 @@
import { convertNxExecutor } from '@nrwl/devkit';
import publishSetExecutor from './publish-set.impl';
export default convertNxExecutor(publishSetExecutor);

View File

@ -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<ExpoPublishSetOutput> {
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;
}, []);
}

View File

@ -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';
}

View File

@ -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"]
}

View File

@ -0,0 +1,5 @@
import { convertNxExecutor } from '@nrwl/devkit';
import publishExecutor from './publish.impl';
export default convertNxExecutor(publishExecutor);

View File

@ -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<ExpoPublishOutput> {
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;
}, []);
}

View File

@ -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
}

View File

@ -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"
}
}
}

View File

@ -0,0 +1,5 @@
import { convertNxExecutor } from '@nrwl/devkit';
import rollbackExecutor from './rollback.impl';
export default convertNxExecutor(rollbackExecutor);

View File

@ -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<ExpoRollbackOutput> {
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;
}, []);
}

View File

@ -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';
}

View File

@ -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"]
}

View File

@ -0,0 +1,5 @@
import { convertNxExecutor } from '@nrwl/devkit';
import runExecutor from './run.impl';
export default convertNxExecutor(runExecutor);

View File

@ -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<ExpoRunOutput> {
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;
}, []);
}

View File

@ -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;
}

View File

@ -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"]
}

View File

@ -0,0 +1,5 @@
import { convertNxExecutor } from '@nrwl/devkit';
import startExecutor from './start.impl';
export default convertNxExecutor(startExecutor);

View File

@ -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;
}

View File

@ -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."
}
}
}

View File

@ -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<ExpoStartOutput> {
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<number> {
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;
}, []);
}

View File

@ -0,0 +1,5 @@
import { convertNxExecutor } from '@nrwl/devkit';
import syncDepsExecutor from './sync-deps.impl';
export default convertNxExecutor(syncDepsExecutor);

View File

@ -0,0 +1,3 @@
export interface ExpoSyncDepsOptions {
include: string;
}

View File

@ -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'"
}
}
}

View File

@ -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<ExpoSyncDepsOutput> {
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<string[]> {
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.`
);
}
}

View File

@ -0,0 +1,5 @@
import { convertNxExecutor } from '@nrwl/devkit';
import buildExecutor from './update.impl';
export default convertNxExecutor(buildExecutor);

View File

@ -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
}

View File

@ -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": []
}

View File

@ -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<ReactNativeUpdateOutput> {
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;
}, []);
}

View File

@ -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);
});
});

View File

@ -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<GeneratorCallback> {
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
);

View File

@ -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"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

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