Skip to content

Commit

Permalink
feat(expo): support createNodes for expo (nrwl#21014)
Browse files Browse the repository at this point in the history
  • Loading branch information
xiongemi authored Jan 8, 2024
1 parent 2b652a4 commit 2360918
Show file tree
Hide file tree
Showing 7 changed files with 340 additions and 5 deletions.
69 changes: 69 additions & 0 deletions e2e/expo/src/expo-pcv3.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { ChildProcess } from 'child_process';
import {
runCLI,
cleanupProject,
newProject,
uniq,
readJson,
runCommandUntil,
killProcessAndPorts,
checkFilesExist,
} from 'e2e/utils';

describe('@nx/expo/plugin', () => {
let project: string;
let appName: string;

beforeAll(() => {
project = newProject();
appName = uniq('app');
runCLI(
`generate @nx/expo:app ${appName} --project-name-and-root-format=as-provided --no-interactive`,
{ env: { NX_PCV3: 'true' } }
);
});

afterAll(() => cleanupProject());

it('nx.json should contain plugin configuration', () => {
const nxJson = readJson('nx.json');
const expoPlugin = nxJson.plugins.find(
(plugin) => plugin.plugin === '@nx/expo/plugin'
);
expect(expoPlugin).toBeDefined();
expect(expoPlugin.options).toBeDefined();
expect(expoPlugin.options.exportTargetName).toEqual('export');
expect(expoPlugin.options.startTargetName).toEqual('start');
});

it('should export the app', async () => {
const result = runCLI(`export ${appName}`);
checkFilesExist(
`${appName}/dist/index.html`,
`${appName}/dist/metadata.json`
);

expect(result).toContain(
`Successfully ran target export for project ${appName}`
);
}, 200_000);

it('should start the app', async () => {
let process: ChildProcess;
const port = 8081;

try {
process = await runCommandUntil(
`start ${appName} --port=${port}`,
(output) => output.includes(`http://localhost:8081`)
);
} catch (err) {
console.error(err);
}

// port and process cleanup
if (process && process.pid) {
await killProcessAndPorts(process.pid, port);
}
});
});
4 changes: 1 addition & 3 deletions e2e/expo/src/expo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,7 @@ describe('expo', () => {
// run start command
const startProcess = await runCommandUntil(
`start ${appName} -- --port=8081`,
(output) =>
output.includes(`Packager is ready at http://localhost:8081`) ||
output.includes(`Web is waiting on http://localhost:8081`)
(output) => output.includes(`http://localhost:8081`)
);

// port and process cleanup
Expand Down
1 change: 1 addition & 0 deletions packages/expo/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { createNodes, ExpoPluginOptions } from './plugins/plugin';
220 changes: 220 additions & 0 deletions packages/expo/plugins/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import {
CreateDependencies,
CreateNodes,
CreateNodesContext,
detectPackageManager,
NxJsonConfiguration,
readJsonFile,
TargetConfiguration,
workspaceRoot,
writeJsonFile,
} from '@nx/devkit';
import { dirname, join } from 'path';
import { getLockFileName } from '@nx/js';
import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs';
import { existsSync, readdirSync } from 'fs';
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory';

export interface ExpoPluginOptions {
startTargetName?: string;
runIosTargetName?: string;
runAndroidTargetName?: string;
exportTargetName?: string;
exportWebTargetName?: string;
prebuildTargetName?: string;
installTargetName?: string;
buildTargetName?: string;
submitTargetName?: string;
}

const cachePath = join(projectGraphCacheDirectory, 'expo.hash');
const targetsCache = existsSync(cachePath) ? readTargetsCache() : {};

const calculatedTargets: Record<
string,
Record<string, TargetConfiguration>
> = {};

function readTargetsCache(): Record<
string,
Record<string, TargetConfiguration<ExpoPluginOptions>>
> {
return readJsonFile(cachePath);
}

function writeTargetsToCache(
targets: Record<
string,
Record<string, TargetConfiguration<ExpoPluginOptions>>
>
) {
writeJsonFile(cachePath, targets);
}

export const createDependencies: CreateDependencies = () => {
writeTargetsToCache(calculatedTargets);
return [];
};

export const createNodes: CreateNodes<ExpoPluginOptions> = [
'**/app.{json,config.js}',
(configFilePath, options, context) => {
options = normalizeOptions(options);
const projectRoot = dirname(configFilePath);

// Do not create a project if package.json or project.json or metro.config.js isn't there.
const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot));
if (
!siblingFiles.includes('package.json') ||
!siblingFiles.includes('project.json') ||
!siblingFiles.includes('metro.config.js')
) {
return {};
}
const appConfig = getAppConfig(configFilePath, context);
// if appConfig.expo is not defined
if (!appConfig.expo) {
return {};
}

const hash = calculateHashForCreateNodes(projectRoot, options, context, [
getLockFileName(detectPackageManager(context.workspaceRoot)),
]);

const targets = targetsCache[hash]
? targetsCache[hash]
: buildExpoTargets(projectRoot, options, context);

calculatedTargets[hash] = targets;

return {
projects: {
[projectRoot]: {
targets,
},
},
};
},
];

function buildExpoTargets(
projectRoot: string,
options: ExpoPluginOptions,
context: CreateNodesContext
) {
const namedInputs = getNamedInputs(projectRoot, context);

const targets: Record<string, TargetConfiguration> = {
[options.startTargetName]: {
command: `expo start`,
options: { cwd: projectRoot },
},
[options.runIosTargetName]: {
command: `expo run:ios`,
options: { cwd: projectRoot },
},
[options.runAndroidTargetName]: {
command: `expo run:android`,
options: { cwd: projectRoot },
},
[options.exportTargetName]: {
command: `expo export`,
options: { cwd: projectRoot },
cache: true,
dependsOn: [`^${options.exportTargetName}`],
inputs: getInputs(namedInputs),
outputs: [getOutputs(projectRoot, 'dist')],
},
[options.exportWebTargetName]: {
command: `expo export:web`,
options: { cwd: projectRoot },
cache: true,
dependsOn: [`^${options.exportWebTargetName}`],
inputs: getInputs(namedInputs),
outputs: [getOutputs(projectRoot, 'web-build')],
},
[options.installTargetName]: {
command: `expo install`,
options: { cwd: workspaceRoot }, // install at workspace root
},
[options.prebuildTargetName]: {
command: `expo prebuild`,
options: { cwd: projectRoot },
},
[options.buildTargetName]: {
command: `eas build`,
options: { cwd: projectRoot },
dependsOn: [`^${options.buildTargetName}`],
inputs: getInputs(namedInputs),
},
[options.submitTargetName]: {
command: `eas submit`,
options: { cwd: projectRoot },
dependsOn: [`^${options.submitTargetName}`],
inputs: getInputs(namedInputs),
},
};

return targets;
}

function getAppConfig(
configFilePath: string,
context: CreateNodesContext
): any {
const resolvedPath = join(context.workspaceRoot, configFilePath);

let module = load(resolvedPath);
return module.default ?? module;
}

function getInputs(
namedInputs: NxJsonConfiguration['namedInputs']
): TargetConfiguration['inputs'] {
return [
...('production' in namedInputs
? ['default', '^production']
: ['default', '^default']),
{
externalDependencies: ['react-native'],
},
];
}

function getOutputs(projectRoot: string, dir: string) {
if (projectRoot === '.') {
return `{projectRoot}/${dir}`;
} else {
return `{workspaceRoot}/${projectRoot}/${dir}`;
}
}

/**
* Load the module after ensuring that the require cache is cleared.
*/
function load(path: string): any {
// Clear cache if the path is in the cache
if (require.cache[path]) {
for (const k of Object.keys(require.cache)) {
delete require.cache[k];
}
}

// Then require
return require(path);
}

function normalizeOptions(options: ExpoPluginOptions): ExpoPluginOptions {
options ??= {};
options.startTargetName ??= 'start';
options.runIosTargetName ??= 'run-ios';
options.runAndroidTargetName ??= 'run-android';
options.exportTargetName ??= 'export';
options.exportWebTargetName ??= 'export-web';
options.prebuildTargetName ??= 'prebuild';
options.installTargetName ??= 'install';
options.buildTargetName ??= 'build';
options.submitTargetName ??= 'submit';
return options;
}
3 changes: 2 additions & 1 deletion packages/expo/src/generators/application/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ export async function expoApplicationGeneratorInternal(
): Promise<GeneratorCallback> {
const options = await normalizeOptions(host, schema);

const initTask = await initGenerator(host, { ...options, skipFormat: true });

createApplicationFiles(host, options);
addProject(host, options);

const initTask = await initGenerator(host, { ...options, skipFormat: true });
const lintTask = await addLinting(host, {
...options,
projectRoot: options.appProjectRoot,
Expand Down
10 changes: 9 additions & 1 deletion packages/expo/src/generators/application/lib/add-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,25 @@ import {
addProjectConfiguration,
offsetFromRoot,
ProjectConfiguration,
readNxJson,
TargetConfiguration,
Tree,
} from '@nx/devkit';
import { NormalizedSchema } from './normalize-options';

export function addProject(host: Tree, options: NormalizedSchema) {
const nxJson = readNxJson(host);
const hasPlugin = nxJson.plugins?.some((p) =>
typeof p === 'string'
? p === '@nx/expo/plugin'
: p.plugin === '@nx/expo/plugin'
);

const projectConfiguration: ProjectConfiguration = {
root: options.appProjectRoot,
sourceRoot: `${options.appProjectRoot}/src`,
projectType: 'application',
targets: { ...getTargets(options) },
targets: hasPlugin ? {} : getTargets(options),
tags: options.parsedTags,
};

Expand Down
Loading

0 comments on commit 2360918

Please sign in to comment.