feat(react-native): enable pnpm support for react-native (#7781)
This commit is contained in:
parent
445d72c38a
commit
624f3f944e
@ -5,7 +5,6 @@ import {
|
||||
runCLI,
|
||||
runCLIAsync,
|
||||
uniq,
|
||||
getSelectedPackageManager,
|
||||
killPorts,
|
||||
} from '@nrwl/e2e/utils';
|
||||
|
||||
@ -13,8 +12,6 @@ describe('Detox', () => {
|
||||
beforeEach(() => newProject());
|
||||
|
||||
it('should create files and run lint command', async () => {
|
||||
// currently react native does not support pnpm: https://github.com/pnpm/pnpm/issues/3321
|
||||
if (getSelectedPackageManager() === 'pnpm') return;
|
||||
const appName = uniq('myapp');
|
||||
runCLI(
|
||||
`generate @nrwl/react-native:app ${appName} --e2eTestRunner=detox --linter=eslint`
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
import {
|
||||
checkFilesExist,
|
||||
expectTestsPass,
|
||||
getSelectedPackageManager,
|
||||
isOSX,
|
||||
killPorts,
|
||||
newProject,
|
||||
readJson,
|
||||
runCLI,
|
||||
@ -19,9 +16,6 @@ describe('react native', () => {
|
||||
beforeEach(() => (proj = newProject()));
|
||||
|
||||
it('should test, create ios and android JS bundles', async () => {
|
||||
// currently react native does not support pnpm: https://github.com/pnpm/pnpm/issues/3321
|
||||
if (getSelectedPackageManager() === 'pnpm') return;
|
||||
|
||||
const appName = uniq('my-app');
|
||||
const libName = uniq('lib');
|
||||
const componentName = uniq('component');
|
||||
@ -64,9 +58,6 @@ describe('react native', () => {
|
||||
});
|
||||
|
||||
it('sync npm dependencies for autolink', async () => {
|
||||
// currently react native does not support pnpm: https://github.com/pnpm/pnpm/issues/3321
|
||||
if (getSelectedPackageManager() === 'pnpm') return;
|
||||
|
||||
const appName = uniq('my-app');
|
||||
runCLI(`generate @nrwl/react-native:application ${appName}`);
|
||||
// Add npm package with native modules
|
||||
|
||||
@ -31,6 +31,7 @@
|
||||
"@nrwl/react": "*",
|
||||
"@nrwl/workspace": "*",
|
||||
"chalk": "^4.1.0",
|
||||
"enhanced-resolve": "^5.8.3",
|
||||
"ignore": "^5.0.4",
|
||||
"metro-resolver": "^0.66.2",
|
||||
"node-fetch": "^2.6.1",
|
||||
|
||||
@ -2,6 +2,11 @@ import * as metroResolver from 'metro-resolver';
|
||||
import type { MatchPath } from 'tsconfig-paths';
|
||||
import { createMatchPath, loadConfig } from 'tsconfig-paths';
|
||||
import * as chalk from 'chalk';
|
||||
import { detectPackageManager } from '@nrwl/devkit';
|
||||
import { CachedInputFileSystem, ResolverFactory } from 'enhanced-resolve';
|
||||
import { dirname, join } from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { appRootPath } from '@nrwl/tao/src/utils/app-root';
|
||||
|
||||
/*
|
||||
* Use tsconfig to resolve additional workspace libs.
|
||||
@ -21,49 +26,122 @@ export function getResolveRequest(extensions: string[]) {
|
||||
if (DEBUG) console.log(chalk.cyan(`[Nx] Resolving: ${moduleName}`));
|
||||
|
||||
const { resolveRequest, ...context } = _context;
|
||||
try {
|
||||
return metroResolver.resolve(context, moduleName, platform);
|
||||
} catch {
|
||||
if (DEBUG)
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`[Nx] Unable to resolve with default Metro resolver: ${moduleName}`
|
||||
)
|
||||
);
|
||||
}
|
||||
const matcher = getMatcher();
|
||||
let match;
|
||||
const matchExtension = extensions.find((extension) => {
|
||||
match = matcher(realModuleName, undefined, undefined, ['.' + extension]);
|
||||
return !!match;
|
||||
});
|
||||
|
||||
if (match) {
|
||||
return {
|
||||
type: 'sourceFile',
|
||||
filePath:
|
||||
!matchExtension || match.endsWith(`.${matchExtension}`)
|
||||
? match
|
||||
: `${match}.${matchExtension}`,
|
||||
};
|
||||
} else {
|
||||
if (DEBUG) {
|
||||
console.log(
|
||||
chalk.red(`[Nx] Failed to resolve ${chalk.bold(moduleName)}`)
|
||||
);
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`[Nx] The following tsconfig paths was used:\n:${chalk.bold(
|
||||
JSON.stringify(paths, null, 2)
|
||||
)}`
|
||||
)
|
||||
);
|
||||
}
|
||||
throw new Error(`Cannot resolve ${chalk.bold(moduleName)}`);
|
||||
let resolvedPath = defaultMetroResolver(context, moduleName, platform);
|
||||
if (resolvedPath) {
|
||||
return resolvedPath;
|
||||
}
|
||||
|
||||
if (detectPackageManager(appRootPath) === 'pnpm') {
|
||||
resolvedPath = pnpmResolver(
|
||||
extensions,
|
||||
context,
|
||||
realModuleName,
|
||||
moduleName
|
||||
);
|
||||
if (resolvedPath) {
|
||||
return resolvedPath;
|
||||
}
|
||||
}
|
||||
|
||||
return tsconfigPathsResolver(extensions, realModuleName, moduleName);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This function try to resolve path using metro's default resolver
|
||||
* @returns path if resolved, else undefined
|
||||
*/
|
||||
function defaultMetroResolver(
|
||||
context: string,
|
||||
moduleName: string,
|
||||
platform: string
|
||||
) {
|
||||
const DEBUG = process.env.NX_REACT_NATIVE_DEBUG === 'true';
|
||||
try {
|
||||
return metroResolver.resolve(context, moduleName, platform);
|
||||
} catch {
|
||||
if (DEBUG)
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`[Nx] Unable to resolve with default Metro resolver: ${moduleName}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This resolver try to resolve module for pnpm.
|
||||
* @returns path if resolved, else undefined
|
||||
* This pnpm resolver is inspired from https://github.com/vjpr/pnpm-react-native-example/blob/main/packages/pnpm-expo-helper/util/make-resolver.js
|
||||
*/
|
||||
function pnpmResolver(extensions, context, realModuleName, moduleName) {
|
||||
const DEBUG = process.env.NX_REACT_NATIVE_DEBUG === 'true';
|
||||
try {
|
||||
const pnpmResolver = getPnpmResolver(appRootPath, extensions);
|
||||
const lookupStartPath = dirname(context.originModulePath);
|
||||
const filePath = pnpmResolver.resolveSync(
|
||||
{},
|
||||
lookupStartPath,
|
||||
realModuleName
|
||||
);
|
||||
if (filePath) {
|
||||
return { type: 'sourceFile', filePath };
|
||||
}
|
||||
} catch {
|
||||
if (DEBUG)
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`[Nx] Unable to resolve with default PNPM resolver: ${moduleName}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function try to resolve files that are specified in tsconfig's paths
|
||||
* @returns path if resolved, else undefined
|
||||
*/
|
||||
function tsconfigPathsResolver(
|
||||
extensions: string[],
|
||||
realModuleName: string,
|
||||
moduleName: string
|
||||
) {
|
||||
const DEBUG = process.env.NX_REACT_NATIVE_DEBUG === 'true';
|
||||
const matcher = getMatcher();
|
||||
let match;
|
||||
|
||||
// find out the file extension
|
||||
const matchExtension = extensions.find((extension) => {
|
||||
match = matcher(realModuleName, undefined, undefined, ['.' + extension]);
|
||||
return !!match;
|
||||
});
|
||||
|
||||
if (match) {
|
||||
return {
|
||||
type: 'sourceFile',
|
||||
filePath:
|
||||
!matchExtension || match.endsWith(`.${matchExtension}`)
|
||||
? match
|
||||
: `${match}.${matchExtension}`,
|
||||
};
|
||||
} else {
|
||||
if (DEBUG) {
|
||||
console.log(
|
||||
chalk.red(`[Nx] Failed to resolve ${chalk.bold(moduleName)}`)
|
||||
);
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`[Nx] The following tsconfig paths was used:\n:${chalk.bold(
|
||||
JSON.stringify(paths, null, 2)
|
||||
)}`
|
||||
)
|
||||
);
|
||||
}
|
||||
throw new Error(`Cannot resolve ${chalk.bold(moduleName)}`);
|
||||
}
|
||||
}
|
||||
|
||||
let matcher: MatchPath;
|
||||
let absoluteBaseUrl: string;
|
||||
let paths: Record<string, string[]>;
|
||||
@ -96,3 +174,21 @@ function getMatcher() {
|
||||
}
|
||||
return matcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function returns resolver for pnpm.
|
||||
* It is inspired form https://github.com/vjpr/pnpm-expo-example/blob/main/packages/pnpm-expo-helper/util/make-resolver.js.
|
||||
*/
|
||||
let resolver;
|
||||
function getPnpmResolver(appRootPath: string, extensions: string[]) {
|
||||
if (!resolver) {
|
||||
const fileSystem = new CachedInputFileSystem(fs, 4000);
|
||||
resolver = ResolverFactory.createResolver({
|
||||
fileSystem,
|
||||
extensions: extensions.map((extension) => '.' + extension),
|
||||
useSyncFileSystemCalls: true,
|
||||
modules: [join(appRootPath, 'node_modules'), 'node_modules'],
|
||||
});
|
||||
}
|
||||
return resolver;
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ interface WithNxOptions {
|
||||
}
|
||||
|
||||
export function withNxMetro(config: any, opts: WithNxOptions = {}) {
|
||||
const extensions = ['', 'ts', 'tsx', 'js', 'jsx'];
|
||||
const extensions = ['', 'ts', 'tsx', 'js', 'jsx', 'json'];
|
||||
if (opts.debug) process.env.NX_REACT_NATIVE_DEBUG = 'true';
|
||||
if (opts.extensions) extensions.push(...opts.extensions);
|
||||
|
||||
|
||||
@ -2,12 +2,14 @@ import { setDefaultCollection } from '@nrwl/workspace/src/utilities/set-default-
|
||||
import {
|
||||
addDependenciesToPackageJson,
|
||||
convertNxGenerator,
|
||||
detectPackageManager,
|
||||
formatFiles,
|
||||
removeDependenciesFromPackageJson,
|
||||
Tree,
|
||||
} from '@nrwl/devkit';
|
||||
import { Schema } from './schema';
|
||||
import {
|
||||
babelRuntimeVersion,
|
||||
jestReactNativeVersion,
|
||||
metroReactNativeBabelPresetVersion,
|
||||
metroVersion,
|
||||
@ -56,6 +58,7 @@ export async function reactNativeInitGenerator(host: Tree, schema: Schema) {
|
||||
}
|
||||
|
||||
export function updateDependencies(host: Tree) {
|
||||
const isPnpm = detectPackageManager(host.root) === 'pnpm';
|
||||
return addDependenciesToPackageJson(
|
||||
host,
|
||||
{
|
||||
@ -79,6 +82,12 @@ export function updateDependencies(host: Tree) {
|
||||
'react-test-renderer': reactTestRendererVersion,
|
||||
'react-native-svg-transformer': reactNativeSvgTransformerVersion,
|
||||
'react-native-svg': reactNativeSvgVersion,
|
||||
...(isPnpm
|
||||
? {
|
||||
'metro-config': metroVersion, // metro-config is used by metro.config.js
|
||||
'@babel/runtime': babelRuntimeVersion, // @babel/runtime is used by react-native-svg
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -90,85 +90,6 @@ describe('ensureNodeModulesSymlink', () => {
|
||||
expectSymlinkToExist('random');
|
||||
});
|
||||
|
||||
it('should support pnpm', () => {
|
||||
createPnpmDirectory('@nrwl/react-native', '9999.9.9');
|
||||
createPnpmDirectory(
|
||||
'@react-native-community/cli-platform-android',
|
||||
'7777.7.7'
|
||||
);
|
||||
createPnpmDirectory('@react-native-community/cli-platform-ios', '7777.7.7');
|
||||
createPnpmDirectory('hermes-engine', '3333.3.3');
|
||||
createPnpmDirectory('react-native', '0.9999.0');
|
||||
createPnpmDirectory('jsc-android', '888888.0.0');
|
||||
createPnpmDirectory('@babel/runtime', '5555.0.0');
|
||||
|
||||
ensureNodeModulesSymlink(workspaceDir, appDir);
|
||||
|
||||
expectSymlinkToExist('react-native');
|
||||
expectSymlinkToExist('jsc-android');
|
||||
expectSymlinkToExist('hermes-engine');
|
||||
expectSymlinkToExist('@react-native-community/cli-platform-ios');
|
||||
expectSymlinkToExist('@react-native-community/cli-platform-android');
|
||||
expectSymlinkToExist('@babel/runtime');
|
||||
});
|
||||
|
||||
it('should support pnpm with multiple react-native versions', () => {
|
||||
createPnpmDirectory('@nrwl/react-native', '9999.9.9');
|
||||
createPnpmDirectory(
|
||||
'@react-native-community/cli-platform-android',
|
||||
'7777.7.7'
|
||||
);
|
||||
createPnpmDirectory('@react-native-community/cli-platform-ios', '7777.7.7');
|
||||
createPnpmDirectory('hermes-engine', '3333.3.3');
|
||||
createPnpmDirectory('react-native', '0.9999.0');
|
||||
createPnpmDirectory('react-native', '0.59.1');
|
||||
createPnpmDirectory('jsc-android', '888888.0.0');
|
||||
createPnpmDirectory('@babel/runtime', '5555.0.0');
|
||||
|
||||
ensureNodeModulesSymlink(workspaceDir, appDir);
|
||||
|
||||
expectSymlinkToExist('react-native');
|
||||
expectSymlinkToExist('jsc-android');
|
||||
expectSymlinkToExist('hermes-engine');
|
||||
expectSymlinkToExist('@react-native-community/cli-platform-ios');
|
||||
expectSymlinkToExist('@react-native-community/cli-platform-android');
|
||||
expectSymlinkToExist('@babel/runtime');
|
||||
});
|
||||
|
||||
it('should throw error if pnpm package cannot be matched', () => {
|
||||
createPnpmDirectory('@nrwl/react-native', '9999.9.9');
|
||||
createPnpmDirectory(
|
||||
'@react-native-community/cli-platform-android',
|
||||
'7777.7.7'
|
||||
);
|
||||
createPnpmDirectory('@react-native-community/cli-platform-ios', '7777.7.7');
|
||||
createPnpmDirectory('hermes-engine', '3333.3.3');
|
||||
createPnpmDirectory('jsc-android', '888888.0.0');
|
||||
createPnpmDirectory('react-native', '0.60.1');
|
||||
createPnpmDirectory('react-native', '0.59.1');
|
||||
createPnpmDirectory('@babel/runtime', '5555.0.0');
|
||||
|
||||
expect(() => {
|
||||
ensureNodeModulesSymlink(workspaceDir, appDir);
|
||||
}).toThrow(/Cannot find/);
|
||||
});
|
||||
|
||||
function createPnpmDirectory(packageName, version) {
|
||||
const dir = join(
|
||||
workspaceDir,
|
||||
`node_modules/.pnpm/${packageName.replace(
|
||||
'/',
|
||||
'+'
|
||||
)}@${version}/node_modules/${packageName}`
|
||||
);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
join(dir, 'package.json'),
|
||||
JSON.stringify({ name: packageName, version: version })
|
||||
);
|
||||
return dir;
|
||||
}
|
||||
|
||||
function createNpmDirectory(packageName, version) {
|
||||
const dir = join(workspaceDir, `node_modules/${packageName}`);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
@ -1,22 +1,8 @@
|
||||
import { join } from 'path';
|
||||
import { platform } from 'os';
|
||||
import * as fs from 'fs';
|
||||
import {
|
||||
createDirectory,
|
||||
readJsonFile,
|
||||
} from '@nrwl/workspace/src/utilities/fileutils';
|
||||
import chalk = require('chalk');
|
||||
|
||||
const requiredPackages = [
|
||||
'react-native',
|
||||
'jsc-android',
|
||||
'@react-native-community/cli-platform-ios',
|
||||
'@react-native-community/cli-platform-android',
|
||||
'hermes-engine',
|
||||
'@nrwl/react-native',
|
||||
'@babel/runtime',
|
||||
];
|
||||
|
||||
/**
|
||||
* This function symlink workspace node_modules folder with app project's node_mdules folder.
|
||||
* For yarn and npm, it will symlink the entire node_modules folder.
|
||||
@ -42,103 +28,4 @@ export function ensureNodeModulesSymlink(
|
||||
fs.rmdirSync(appNodeModulesPath, { recursive: true });
|
||||
}
|
||||
fs.symlinkSync(worksapceNodeModulesPath, appNodeModulesPath, symlinkType);
|
||||
|
||||
if (isPnpm(workspaceRoot)) {
|
||||
symlinkPnpm(workspaceRoot, appNodeModulesPath, symlinkType);
|
||||
}
|
||||
}
|
||||
|
||||
function isPnpm(workspaceRoot: string): boolean {
|
||||
const pnpmDir = join(workspaceRoot, 'node_modules/.pnpm');
|
||||
return fs.existsSync(pnpmDir);
|
||||
}
|
||||
|
||||
function symlinkPnpm(
|
||||
workspaceRoot: string,
|
||||
appNodeModulesPath: string,
|
||||
symlinkType: 'junction' | 'dir'
|
||||
) {
|
||||
const worksapcePackageJsonPath = join(workspaceRoot, 'package.json');
|
||||
const workspacePackageJson = readJsonFile(worksapcePackageJsonPath);
|
||||
const workspacePackages = Object.keys({
|
||||
...workspacePackageJson.dependencies,
|
||||
...workspacePackageJson.devDependencies,
|
||||
});
|
||||
|
||||
const packagesToSymlink = new Set([
|
||||
...workspacePackages,
|
||||
...requiredPackages,
|
||||
]);
|
||||
|
||||
createDirectory(appNodeModulesPath);
|
||||
|
||||
packagesToSymlink.forEach((p) => {
|
||||
const dir = join(appNodeModulesPath, p);
|
||||
if (!fs.existsSync(dir)) {
|
||||
if (isScopedPackage(p))
|
||||
createDirectory(join(appNodeModulesPath, getScopedData(p).scope));
|
||||
fs.symlinkSync(locateNpmPackage(workspaceRoot, p), dir, symlinkType);
|
||||
}
|
||||
if (!fs.existsSync(join(dir, 'package.json'))) {
|
||||
throw new Error(
|
||||
`Invalid symlink ${chalk.bold(dir)}. Remove ${chalk.bold(
|
||||
appNodeModulesPath
|
||||
)} and try again.`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function locateNpmPackage(workspaceRoot: string, packageName: string): string {
|
||||
const pnpmDir = join(workspaceRoot, 'node_modules/.pnpm');
|
||||
|
||||
let candidates: string[];
|
||||
if (isScopedPackage(packageName)) {
|
||||
const { scope, name } = getScopedData(packageName);
|
||||
candidates = fs
|
||||
.readdirSync(pnpmDir)
|
||||
.filter(
|
||||
(f) =>
|
||||
f.startsWith(`${scope}+${name}`) &&
|
||||
fs.lstatSync(join(pnpmDir, f)).isDirectory()
|
||||
);
|
||||
} else {
|
||||
candidates = fs
|
||||
.readdirSync(pnpmDir)
|
||||
.filter(
|
||||
(f) =>
|
||||
f.startsWith(packageName) &&
|
||||
fs.lstatSync(join(pnpmDir, f)).isDirectory()
|
||||
);
|
||||
}
|
||||
|
||||
if (candidates.length === 0) {
|
||||
throw new Error(`Could not locate pnpm directory for ${packageName}`);
|
||||
} else if (candidates.length === 1) {
|
||||
return join(pnpmDir, candidates[0], 'node_modules', packageName);
|
||||
} else {
|
||||
const packageJson = readJsonFile(join(workspaceRoot, 'package.json'));
|
||||
const deps = {
|
||||
...packageJson.dependencies,
|
||||
...packageJson.devDependencies,
|
||||
};
|
||||
const version = deps[packageName];
|
||||
const found = candidates.find((c) => c.includes(version));
|
||||
if (found) {
|
||||
return join(pnpmDir, found, 'node_modules', packageName);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Cannot find ${packageName}@${version}. Install it with 'pnpm install --save ${packageName}@${version}'.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isScopedPackage(p) {
|
||||
return p.startsWith('@');
|
||||
}
|
||||
|
||||
function getScopedData(p) {
|
||||
const [scope, name] = p.split('/');
|
||||
return { scope, name };
|
||||
}
|
||||
|
||||
@ -20,3 +20,5 @@ export const reactTestRendererVersion = '17.0.2';
|
||||
|
||||
export const reactNativeSvgTransformerVersion = '0.14.3';
|
||||
export const reactNativeSvgVersion = '12.1.1';
|
||||
|
||||
export const babelRuntimeVersion = '7.16.3';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user