mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a41995ff5c | ||
|
|
6d64892c8a | ||
|
|
5d95e9950c | ||
|
|
6b39d644ea | ||
|
|
dff572c174 | ||
|
|
af3e1d4a86 | ||
|
|
897a3a5d5e | ||
|
|
0fea3515a1 | ||
|
|
80e8efe8e7 | ||
|
|
d1888650c5 | ||
|
|
f65b9e883a | ||
|
|
aa0b2ad1cd | ||
|
|
0a319978ff | ||
|
|
29e5deb85f | ||
|
|
031feb6942 | ||
|
|
d82138ff91 |
@@ -44,7 +44,10 @@ RCT_EXPORT_MODULE()
|
||||
// form of an, NSURL which is what assets-library uses.
|
||||
NSString *assetID = @"";
|
||||
PHFetchResult *results;
|
||||
if ([imageURL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame) {
|
||||
if (!imageURL) {
|
||||
completionHandler(RCTErrorWithMessage(@"Cannot load a photo library asset with no URL"), nil);
|
||||
return ^{};
|
||||
} else if ([imageURL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame) {
|
||||
assetID = [imageURL absoluteString];
|
||||
results = [PHAsset fetchAssetsWithALAssetURLs:@[imageURL] options:nil];
|
||||
} else {
|
||||
|
||||
@@ -927,6 +927,10 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR
|
||||
{
|
||||
RCTAssertJSThread();
|
||||
|
||||
if (!self.valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (buffer != nil && buffer != (id)kCFNull) {
|
||||
_wasBatchActive = YES;
|
||||
[self handleBuffer:buffer];
|
||||
|
||||
@@ -373,6 +373,8 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init);
|
||||
- (dispatch_queue_t)methodQueue
|
||||
{
|
||||
(void)[self instance];
|
||||
RCTAssert(_methodQueue != nullptr, @"Module %@ has no methodQueue (instance: %@, bridge.valid: %d)",
|
||||
self, _instance, _bridge.valid);
|
||||
return _methodQueue;
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
// Throttle progress events so we don't send more that around 60 per second.
|
||||
CFTimeInterval currentTime = CACurrentMediaTime();
|
||||
|
||||
NSUInteger headersContentLength = headers[@"Content-Length"] != nil ? [headers[@"Content-Length"] unsignedIntValue] : 0;
|
||||
NSInteger headersContentLength = headers[@"Content-Length"] != nil ? [headers[@"Content-Length"] integerValue] : 0;
|
||||
if (callback && (currentTime - _lastDownloadProgress > 0.016 || final)) {
|
||||
_lastDownloadProgress = currentTime;
|
||||
callback(headers, @(headersContentLength), @(contentLength));
|
||||
|
||||
@@ -125,13 +125,12 @@ struct RCTInstanceCallback : public InstanceCallback {
|
||||
[bridge_ partialBatchDidFlush];
|
||||
[bridge_ batchDidComplete];
|
||||
}
|
||||
void incrementPendingJSCalls() override {}
|
||||
void decrementPendingJSCalls() override {}
|
||||
};
|
||||
|
||||
@implementation RCTCxxBridge
|
||||
{
|
||||
BOOL _wasBatchActive;
|
||||
BOOL _didInvalidate;
|
||||
|
||||
NSMutableArray<dispatch_block_t> *_pendingCalls;
|
||||
std::atomic<NSInteger> _pendingCount;
|
||||
@@ -169,7 +168,7 @@ struct RCTInstanceCallback : public InstanceCallback {
|
||||
|
||||
- (JSGlobalContextRef)jsContextRef
|
||||
{
|
||||
return (JSGlobalContextRef)self->_reactInstance->getJavaScriptContext();
|
||||
return (JSGlobalContextRef)(self->_reactInstance ? self->_reactInstance->getJavaScriptContext() : nullptr);
|
||||
}
|
||||
|
||||
- (instancetype)initWithParentBridge:(RCTBridge *)bridge
|
||||
@@ -204,7 +203,7 @@ struct RCTInstanceCallback : public InstanceCallback {
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)runJSRunLoop
|
||||
+ (void)runRunLoop
|
||||
{
|
||||
@autoreleasepool {
|
||||
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTCxxBridge runJSRunLoop] setup", nil);
|
||||
@@ -267,8 +266,8 @@ struct RCTInstanceCallback : public InstanceCallback {
|
||||
object:_parentBridge userInfo:@{@"bridge": self}];
|
||||
|
||||
// Set up the JS thread early
|
||||
_jsThread = [[NSThread alloc] initWithTarget:self
|
||||
selector:@selector(runJSRunLoop)
|
||||
_jsThread = [[NSThread alloc] initWithTarget:[self class]
|
||||
selector:@selector(runRunLoop)
|
||||
object:nil];
|
||||
_jsThread.name = RCTJSThreadName;
|
||||
_jsThread.qualityOfService = NSOperationQualityOfServiceUserInteractive;
|
||||
@@ -493,7 +492,7 @@ struct RCTInstanceCallback : public InstanceCallback {
|
||||
if (_reactInstance) {
|
||||
// This is async, but any calls into JS are blocked by the m_syncReady CV in Instance
|
||||
_reactInstance->initializeBridge(
|
||||
std::unique_ptr<RCTInstanceCallback>(new RCTInstanceCallback(self)),
|
||||
std::make_unique<RCTInstanceCallback>(self),
|
||||
executorFactory,
|
||||
_jsMessageThread,
|
||||
[self _buildModuleRegistry]);
|
||||
@@ -816,6 +815,7 @@ struct RCTInstanceCallback : public InstanceCallback {
|
||||
}
|
||||
|
||||
RCTFatal(error);
|
||||
|
||||
// RN will stop, but let the rest of the app keep going.
|
||||
return;
|
||||
}
|
||||
@@ -826,27 +826,27 @@ struct RCTInstanceCallback : public InstanceCallback {
|
||||
|
||||
// Hack: once the bridge is invalidated below, it won't initialize any new native
|
||||
// modules. Initialize the redbox module now so we can still report this error.
|
||||
[self redBox];
|
||||
RCTRedBox *redBox = [self redBox];
|
||||
|
||||
_loading = NO;
|
||||
_valid = NO;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self->_jsMessageThread) {
|
||||
auto thread = self->_jsMessageThread;
|
||||
self->_jsMessageThread->runOnQueue([thread] {
|
||||
thread->quitSynchronous();
|
||||
});
|
||||
self->_jsMessageThread.reset();
|
||||
// Make sure initializeBridge completed
|
||||
self->_jsMessageThread->runOnQueueSync([] {});
|
||||
}
|
||||
|
||||
self->_reactInstance.reset();
|
||||
self->_jsMessageThread.reset();
|
||||
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
postNotificationName:RCTJavaScriptDidFailToLoadNotification
|
||||
object:self->_parentBridge userInfo:@{@"bridge": self, @"error": error}];
|
||||
|
||||
if ([error userInfo][RCTJSRawStackTraceKey]) {
|
||||
[self.redBox showErrorMessage:[error localizedDescription]
|
||||
withRawStack:[error userInfo][RCTJSRawStackTraceKey]];
|
||||
[redBox showErrorMessage:[error localizedDescription]
|
||||
withRawStack:[error userInfo][RCTJSRawStackTraceKey]];
|
||||
}
|
||||
|
||||
RCTFatal(error);
|
||||
@@ -913,63 +913,68 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR
|
||||
|
||||
- (void)invalidate
|
||||
{
|
||||
if (!_valid) {
|
||||
if (_didInvalidate) {
|
||||
return;
|
||||
}
|
||||
|
||||
RCTAssertMainQueue();
|
||||
RCTAssert(_reactInstance != nil, @"Can't complete invalidation without a react instance");
|
||||
RCTLogInfo(@"Invalidating %@ (parent: %@, executor: %@)", self, _parentBridge, [self executorClass]);
|
||||
|
||||
_loading = NO;
|
||||
_valid = NO;
|
||||
_didInvalidate = YES;
|
||||
|
||||
if ([RCTBridge currentBridge] == self) {
|
||||
[RCTBridge setCurrentBridge:nil];
|
||||
}
|
||||
|
||||
// Invalidate modules
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
for (RCTModuleData *moduleData in _moduleDataByID) {
|
||||
// Be careful when grabbing an instance here, we don't want to instantiate
|
||||
// any modules just to invalidate them.
|
||||
if (![moduleData hasInstance]) {
|
||||
continue;
|
||||
// Stop JS instance and message thread
|
||||
[self ensureOnJavaScriptThread:^{
|
||||
[self->_displayLink invalidate];
|
||||
self->_displayLink = nil;
|
||||
|
||||
if (RCTProfileIsProfiling()) {
|
||||
RCTProfileUnhookModules(self);
|
||||
}
|
||||
|
||||
if ([moduleData.instance respondsToSelector:@selector(invalidate)]) {
|
||||
dispatch_group_enter(group);
|
||||
[self dispatchBlock:^{
|
||||
[(id<RCTInvalidating>)moduleData.instance invalidate];
|
||||
dispatch_group_leave(group);
|
||||
} queue:moduleData.methodQueue];
|
||||
// Invalidate modules
|
||||
// We're on the JS thread (which we'll be suspending soon), so no new calls will be made to native modules after
|
||||
// this completes. We must ensure all previous calls were dispatched before deallocating the instance (and module
|
||||
// wrappers) or we may have invalid pointers still in flight.
|
||||
dispatch_group_t moduleInvalidation = dispatch_group_create();
|
||||
for (RCTModuleData *moduleData in self->_moduleDataByID) {
|
||||
// Be careful when grabbing an instance here, we don't want to instantiate
|
||||
// any modules just to invalidate them.
|
||||
if (![moduleData hasInstance]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ([moduleData.instance respondsToSelector:@selector(invalidate)]) {
|
||||
dispatch_group_enter(moduleInvalidation);
|
||||
[self dispatchBlock:^{
|
||||
[(id<RCTInvalidating>)moduleData.instance invalidate];
|
||||
dispatch_group_leave(moduleInvalidation);
|
||||
} queue:moduleData.methodQueue];
|
||||
}
|
||||
[moduleData invalidate];
|
||||
}
|
||||
[moduleData invalidate];
|
||||
}
|
||||
|
||||
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
|
||||
[self ensureOnJavaScriptThread:^{
|
||||
[self->_displayLink invalidate];
|
||||
self->_displayLink = nil;
|
||||
if (dispatch_group_wait(moduleInvalidation, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC))) {
|
||||
RCTLogError(@"Timed out waiting for modules to be invalidated");
|
||||
}
|
||||
|
||||
self->_reactInstance.reset();
|
||||
if (self->_jsMessageThread) {
|
||||
self->_jsMessageThread->quitSynchronous();
|
||||
self->_jsMessageThread.reset();
|
||||
}
|
||||
self->_reactInstance.reset();
|
||||
self->_jsMessageThread.reset();
|
||||
|
||||
if (RCTProfileIsProfiling()) {
|
||||
RCTProfileUnhookModules(self);
|
||||
}
|
||||
self->_moduleDataByName = nil;
|
||||
self->_moduleDataByID = nil;
|
||||
self->_moduleClassesByID = nil;
|
||||
self->_pendingCalls = nil;
|
||||
|
||||
self->_moduleDataByName = nil;
|
||||
self->_moduleDataByID = nil;
|
||||
self->_moduleClassesByID = nil;
|
||||
self->_pendingCalls = nil;
|
||||
|
||||
[self->_jsThread cancel];
|
||||
self->_jsThread = nil;
|
||||
CFRunLoopStop(CFRunLoopGetCurrent());
|
||||
}];
|
||||
});
|
||||
[self->_jsThread cancel];
|
||||
self->_jsThread = nil;
|
||||
CFRunLoopStop(CFRunLoopGetCurrent());
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)logMessage:(NSString *)message level:(NSString *)level
|
||||
@@ -1098,7 +1103,6 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR
|
||||
*/
|
||||
|
||||
RCTProfileBeginFlowEvent();
|
||||
|
||||
[self _runAfterLoad:^{
|
||||
RCTProfileEndFlowEvent();
|
||||
|
||||
@@ -1189,25 +1193,25 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR
|
||||
if (!_reactInstance) {
|
||||
if (error) {
|
||||
*error = RCTErrorWithMessage(
|
||||
@"Attempt to call sync callFunctionOnModule: on uninitialized bridge");
|
||||
@"callFunctionOnModule was called on uninitialized bridge");
|
||||
}
|
||||
return nil;
|
||||
} else if (self.executorClass) {
|
||||
if (error) {
|
||||
*error = RCTErrorWithMessage(
|
||||
@"sync callFunctionOnModule: can only be used with JSC executor");
|
||||
@"callFunctionOnModule can only be used with JSC executor");
|
||||
}
|
||||
return nil;
|
||||
} else if (!self.valid) {
|
||||
if (error) {
|
||||
*error = RCTErrorWithMessage(
|
||||
@"sync callFunctionOnModule: bridge is no longer valid");
|
||||
@"Bridge is no longer valid");
|
||||
}
|
||||
return nil;
|
||||
} else if (self.loading) {
|
||||
if (error) {
|
||||
*error = RCTErrorWithMessage(
|
||||
@"sync callFunctionOnModule: bridge is still loading");
|
||||
@"Bridge is still loading");
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ public:
|
||||
|
||||
void runOnQueue(std::function<void()>&& func) override {
|
||||
dispatch_queue_t queue = moduleData_.methodQueue;
|
||||
RCTAssert(queue != nullptr, @"Module %@ provided invalid queue", moduleData_);
|
||||
dispatch_block_t block = [func=std::move(func)] { func(); };
|
||||
RCTAssert(block != nullptr, @"Invalid block generated in call to %@", moduleData_);
|
||||
if (queue && block) {
|
||||
|
||||
+32
-19
@@ -36,33 +36,46 @@
|
||||
typedef CGFloat RCTFontWeight;
|
||||
static RCTFontWeight weightOfFont(UIFont *font)
|
||||
{
|
||||
static NSDictionary *nameToWeight;
|
||||
static NSArray *fontNames;
|
||||
static NSArray *fontWeights;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
nameToWeight = @{
|
||||
@"normal": @(UIFontWeightRegular),
|
||||
@"bold": @(UIFontWeightBold),
|
||||
@"ultralight": @(UIFontWeightUltraLight),
|
||||
@"thin": @(UIFontWeightThin),
|
||||
@"light": @(UIFontWeightLight),
|
||||
@"regular": @(UIFontWeightRegular),
|
||||
@"medium": @(UIFontWeightMedium),
|
||||
@"semibold": @(UIFontWeightSemibold),
|
||||
@"bold": @(UIFontWeightBold),
|
||||
@"heavy": @(UIFontWeightHeavy),
|
||||
@"black": @(UIFontWeightBlack),
|
||||
};
|
||||
// We use two arrays instead of one map because
|
||||
// the order is important for suffix matching.
|
||||
fontNames = @[
|
||||
@"normal",
|
||||
@"ultralight",
|
||||
@"thin",
|
||||
@"light",
|
||||
@"regular",
|
||||
@"medium",
|
||||
@"semibold",
|
||||
@"bold",
|
||||
@"heavy",
|
||||
@"black"
|
||||
];
|
||||
fontWeights = @[
|
||||
@(UIFontWeightRegular),
|
||||
@(UIFontWeightUltraLight),
|
||||
@(UIFontWeightThin),
|
||||
@(UIFontWeightLight),
|
||||
@(UIFontWeightRegular),
|
||||
@(UIFontWeightMedium),
|
||||
@(UIFontWeightSemibold),
|
||||
@(UIFontWeightBold),
|
||||
@(UIFontWeightHeavy),
|
||||
@(UIFontWeightBlack)
|
||||
];
|
||||
});
|
||||
|
||||
for (NSString *name in nameToWeight) {
|
||||
if ([font.fontName.lowercaseString hasSuffix:name]) {
|
||||
return [nameToWeight[name] doubleValue];
|
||||
for (NSInteger i = 0; i < fontNames.count; i++) {
|
||||
if ([font.fontName.lowercaseString hasSuffix:fontNames[i]]) {
|
||||
return (RCTFontWeight)[fontWeights[i] doubleValue];
|
||||
}
|
||||
}
|
||||
|
||||
NSDictionary *traits = [font.fontDescriptor objectForKey:UIFontDescriptorTraitsAttribute];
|
||||
RCTFontWeight weight = [traits[UIFontWeightTrait] doubleValue];
|
||||
return weight;
|
||||
return (RCTFontWeight)[traits[UIFontWeightTrait] doubleValue];
|
||||
}
|
||||
|
||||
static BOOL isItalicFont(UIFont *font)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
VERSION_NAME=1000.0.0-master
|
||||
VERSION_NAME=0.49.0-rc.4
|
||||
GROUP=com.facebook.react
|
||||
|
||||
POM_NAME=ReactNative
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ import com.facebook.react.bridge.ReactContext;
|
||||
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
|
||||
WindowManager.LayoutParams.MATCH_PARENT,
|
||||
WindowManager.LayoutParams.MATCH_PARENT,
|
||||
WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
|
||||
WindowOverlayCompat.TYPE_SYSTEM_OVERLAY,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
|
||||
PixelFormat.TRANSLUCENT);
|
||||
|
||||
+1
-1
@@ -143,7 +143,7 @@ public class DevLoadingViewController {
|
||||
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
|
||||
WindowManager.LayoutParams.MATCH_PARENT,
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
|
||||
WindowOverlayCompat.TYPE_SYSTEM_OVERLAY,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
PixelFormat.TRANSLUCENT);
|
||||
params.gravity = Gravity.TOP;
|
||||
|
||||
@@ -36,7 +36,6 @@ import android.content.pm.PackageManager;
|
||||
import android.hardware.SensorManager;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.facebook.common.logging.FLog;
|
||||
@@ -337,7 +336,7 @@ public class DevSupportManagerImpl implements
|
||||
public void run() {
|
||||
if (mRedBoxDialog == null) {
|
||||
mRedBoxDialog = new RedBoxDialog(mApplicationContext, DevSupportManagerImpl.this, mRedBoxHandler);
|
||||
mRedBoxDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
|
||||
mRedBoxDialog.getWindow().setType(WindowOverlayCompat.TYPE_SYSTEM_ALERT);
|
||||
}
|
||||
if (mRedBoxDialog.isShowing()) {
|
||||
// Sometimes errors cause multiple errors to be thrown in JS in quick succession. Only
|
||||
@@ -466,7 +465,7 @@ public class DevSupportManagerImpl implements
|
||||
}
|
||||
})
|
||||
.create();
|
||||
mDevOptionsDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
|
||||
mDevOptionsDialog.getWindow().setType(WindowOverlayCompat.TYPE_SYSTEM_ALERT);
|
||||
mDevOptionsDialog.show();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.facebook.react.devsupport;
|
||||
|
||||
import android.os.Build;
|
||||
import android.view.WindowManager;
|
||||
|
||||
/**
|
||||
* Compatibility wrapper for apps targeting API level 26 or later.
|
||||
* See https://developer.android.com/about/versions/oreo/android-8.0-changes.html#cwt
|
||||
*/
|
||||
/* package */ class WindowOverlayCompat {
|
||||
|
||||
private static final int ANDROID_OREO = 26;
|
||||
private static final int TYPE_APPLICATION_OVERLAY = 2038;
|
||||
|
||||
static final int TYPE_SYSTEM_ALERT = Build.VERSION.SDK_INT < ANDROID_OREO ? WindowManager.LayoutParams.TYPE_SYSTEM_ALERT : TYPE_APPLICATION_OVERLAY;
|
||||
static final int TYPE_SYSTEM_OVERLAY = Build.VERSION.SDK_INT < ANDROID_OREO ? WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY : TYPE_APPLICATION_OVERLAY;
|
||||
|
||||
}
|
||||
@@ -27,8 +27,6 @@ public class OnScrollDispatchHelper {
|
||||
|
||||
private long mLastScrollEventTimeMs = -(MIN_EVENT_SEPARATION_MS + 1);
|
||||
|
||||
private static final float THRESHOLD = 0.1f; // Threshold for end fling
|
||||
|
||||
/**
|
||||
* Call from a ScrollView in onScrollChanged, returns true if this onScrollChanged is legit (not a
|
||||
* duplicate) and should be dispatched.
|
||||
@@ -40,11 +38,6 @@ public class OnScrollDispatchHelper {
|
||||
mPrevX != x ||
|
||||
mPrevY != y;
|
||||
|
||||
// Skip the first calculation in each scroll
|
||||
if (Math.abs(mXFlingVelocity) < THRESHOLD && Math.abs(mYFlingVelocity) < THRESHOLD) {
|
||||
shouldDispatch = false;
|
||||
}
|
||||
|
||||
if (eventTime - mLastScrollEventTimeMs != 0) {
|
||||
mXFlingVelocity = (float) (x - mPrevX) / (eventTime - mLastScrollEventTimeMs);
|
||||
mYFlingVelocity = (float) (y - mPrevY) / (eventTime - mLastScrollEventTimeMs);
|
||||
|
||||
@@ -27,9 +27,9 @@ class ModuleRegistry;
|
||||
|
||||
struct InstanceCallback {
|
||||
virtual ~InstanceCallback() {}
|
||||
virtual void onBatchComplete() = 0;
|
||||
virtual void incrementPendingJSCalls() = 0;
|
||||
virtual void decrementPendingJSCalls() = 0;
|
||||
virtual void onBatchComplete() {}
|
||||
virtual void incrementPendingJSCalls() {}
|
||||
virtual void decrementPendingJSCalls() {}
|
||||
};
|
||||
|
||||
class RN_EXPORT Instance {
|
||||
|
||||
+6
-2
@@ -58,7 +58,8 @@ test:
|
||||
- cat <(echo eslint; npm run lint --silent -- --format=json; echo flow; npm run flow --silent -- check --json) | GITHUB_TOKEN="af6ef0d15709bc91d""06a6217a5a826a226fb57b7" CI_USER=$CIRCLE_PROJECT_USERNAME CI_REPO=$CIRCLE_PROJECT_REPONAME PULL_REQUEST_NUMBER=$CIRCLE_PR_NUMBER node bots/code-analysis-bot.js
|
||||
- npm run lint
|
||||
# JS tests for dependencies installed with npm3
|
||||
- npm run flow -- check
|
||||
# Commenting out Flow tests
|
||||
# - npm run flow -- check
|
||||
- npm test -- --maxWorkers=1
|
||||
|
||||
# build app
|
||||
@@ -76,8 +77,11 @@ test:
|
||||
# integration tests
|
||||
# build JS bundle for instrumentation tests
|
||||
- node local-cli/cli.js bundle --max-workers 1 --platform android --dev true --entry-file ReactAndroid/src/androidTest/js/TestBundle.js --bundle-output ReactAndroid/src/androidTest/assets/AndroidTestBundle.js
|
||||
|
||||
# build test APK
|
||||
- buck install ReactAndroid/src/androidTest/buck-runner:instrumentation-tests --config build.threads=1
|
||||
# Commented out due to test failures. Please uncomment the next line once these have been fixed. See Issue #15726.
|
||||
# - buck install ReactAndroid/src/androidTest/buck-runner:instrumentation-tests --config build.threads=1
|
||||
|
||||
# run installed apk with tests
|
||||
# - node ./scripts/run-android-ci-instrumentation-tests.js --retries 3 --path ./ReactAndroid/src/androidTest/java/com/facebook/react/tests --package com.facebook.react.tests
|
||||
|
||||
|
||||
@@ -157,9 +157,11 @@ Go to the root directory for your project and create a new `package.json` file w
|
||||
Next, you will install the `react` and `react-native` packages. Open a terminal or command prompt, then navigate to the root directory for your project and type the following commands:
|
||||
|
||||
```
|
||||
$ npm install --save react react-native
|
||||
$ npm install --save react@16.0.0-beta.5 react-native
|
||||
```
|
||||
|
||||
> Make sure you use the same React version as specified in the [React Native `package.json` file](https://github.com/facebook/react-native/blob/0.49-stable/package.json). This will only be necessary as long as React Native depends on a pre-release version of React.
|
||||
|
||||
This will create a new `/node_modules` folder in your project's root directory. This folder stores all the JavaScript dependencies required to build your project.
|
||||
|
||||
<block class="objc swift" />
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
'use strict';
|
||||
|
||||
const blacklist = require('metro-bundler/src/blacklist');
|
||||
const findSymlinksPaths = require('./findSymlinksPaths');
|
||||
const findSymlinkedModules = require('./findSymlinkedModules');
|
||||
const fs = require('fs');
|
||||
const getPolyfills = require('../../rn-get-polyfills');
|
||||
const invariant = require('fbjs/lib/invariant');
|
||||
@@ -150,14 +150,15 @@ function getProjectPath() {
|
||||
return path.resolve(__dirname, '../..');
|
||||
}
|
||||
|
||||
const resolveSymlink = (roots) =>
|
||||
roots.concat(
|
||||
findSymlinksPaths(
|
||||
path.join(getProjectPath(), 'node_modules'),
|
||||
roots
|
||||
)
|
||||
const resolveSymlinksForRoots = roots =>
|
||||
roots.reduce(
|
||||
(arr, rootPath) => arr.concat(
|
||||
findSymlinkedModules(rootPath, roots)
|
||||
),
|
||||
[...roots]
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* Module capable of getting the configuration out of a given file.
|
||||
*
|
||||
@@ -177,9 +178,9 @@ const Config = {
|
||||
getProjectRoots: () => {
|
||||
const root = process.env.REACT_NATIVE_APP_ROOT;
|
||||
if (root) {
|
||||
return resolveSymlink([path.resolve(root)]);
|
||||
return resolveSymlinksForRoots([path.resolve(root)]);
|
||||
}
|
||||
return resolveSymlink([getProjectPath()]);
|
||||
return resolveSymlinksForRoots([getProjectPath()]);
|
||||
},
|
||||
getProvidesModuleNodeModules: () => providesModuleNodeModules.slice(),
|
||||
getSourceExts: () => [],
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const {EventEmitter} = require('events');
|
||||
const {dirname} = require.requireActual('path');
|
||||
const fs = jest.genMockFromModule('fs');
|
||||
const path = require('path');
|
||||
const stream = require.requireActual('stream');
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
function asyncCallback(cb) {
|
||||
return function() {
|
||||
setImmediate(() => cb.apply(this, arguments));
|
||||
};
|
||||
}
|
||||
|
||||
const mtime = {
|
||||
getTime: () => Math.ceil(Math.random() * 10000000),
|
||||
};
|
||||
|
||||
fs.realpath.mockImplementation((filepath, callback) => {
|
||||
callback = asyncCallback(callback);
|
||||
let node;
|
||||
try {
|
||||
node = getToNode(filepath);
|
||||
} catch (e) {
|
||||
return callback(e);
|
||||
}
|
||||
if (node && typeof node === 'object' && node.SYMLINK != null) {
|
||||
return callback(null, node.SYMLINK);
|
||||
}
|
||||
return callback(null, filepath);
|
||||
});
|
||||
|
||||
fs.readdirSync.mockImplementation(filepath => Object.keys(getToNode(filepath)));
|
||||
|
||||
fs.readdir.mockImplementation((filepath, callback) => {
|
||||
callback = asyncCallback(callback);
|
||||
let node;
|
||||
try {
|
||||
node = getToNode(filepath);
|
||||
if (node && typeof node === 'object' && node.SYMLINK != null) {
|
||||
node = getToNode(node.SYMLINK);
|
||||
}
|
||||
} catch (e) {
|
||||
return callback(e);
|
||||
}
|
||||
|
||||
if (!(node && typeof node === 'object' && node.SYMLINK == null)) {
|
||||
return callback(new Error(filepath + ' is not a directory.'));
|
||||
}
|
||||
|
||||
return callback(null, Object.keys(node));
|
||||
});
|
||||
|
||||
fs.readFile.mockImplementation(function(filepath, encoding, callback) {
|
||||
callback = asyncCallback(callback);
|
||||
if (arguments.length === 2) {
|
||||
callback = encoding;
|
||||
encoding = null;
|
||||
}
|
||||
|
||||
let node;
|
||||
try {
|
||||
node = getToNode(filepath);
|
||||
// dir check
|
||||
if (node && typeof node === 'object' && node.SYMLINK == null) {
|
||||
callback(new Error('Error readFile a dir: ' + filepath));
|
||||
}
|
||||
if (node == null) {
|
||||
return callback(Error('No such file: ' + filepath));
|
||||
} else {
|
||||
return callback(null, node);
|
||||
}
|
||||
} catch (e) {
|
||||
return callback(e);
|
||||
}
|
||||
});
|
||||
|
||||
fs.readFileSync.mockImplementation(function(filepath, encoding) {
|
||||
const node = getToNode(filepath);
|
||||
// dir check
|
||||
if (node && typeof node === 'object' && node.SYMLINK == null) {
|
||||
throw new Error('Error readFileSync a dir: ' + filepath);
|
||||
}
|
||||
return node;
|
||||
});
|
||||
|
||||
function readlinkSync(filepath) {
|
||||
const node = getToNode(filepath);
|
||||
if (node !== null && typeof node === 'object' && !!node.SYMLINK) {
|
||||
return node.SYMLINK;
|
||||
} else {
|
||||
throw new Error(`EINVAL: invalid argument, readlink '${filepath}'`);
|
||||
}
|
||||
}
|
||||
|
||||
fs.readlink.mockImplementation((filepath, callback) => {
|
||||
callback = asyncCallback(callback);
|
||||
let result;
|
||||
try {
|
||||
result = readlinkSync(filepath);
|
||||
} catch (e) {
|
||||
callback(e);
|
||||
return;
|
||||
}
|
||||
callback(null, result);
|
||||
});
|
||||
|
||||
fs.readlinkSync.mockImplementation(readlinkSync);
|
||||
|
||||
function existsSync(filepath) {
|
||||
try {
|
||||
const node = getToNode(filepath);
|
||||
return node !== null;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
fs.exists.mockImplementation((filepath, callback) => {
|
||||
callback = asyncCallback(callback);
|
||||
let result;
|
||||
try {
|
||||
result = existsSync(filepath);
|
||||
} catch (e) {
|
||||
callback(e);
|
||||
return;
|
||||
}
|
||||
callback(null, result);
|
||||
});
|
||||
|
||||
fs.existsSync.mockImplementation(existsSync);
|
||||
|
||||
function makeStatResult(node) {
|
||||
const isSymlink = node != null && node.SYMLINK != null;
|
||||
return {
|
||||
isBlockDevice: () => false,
|
||||
isCharacterDevice: () => false,
|
||||
isDirectory: () => node != null && typeof node === 'object' && !isSymlink,
|
||||
isFIFO: () => false,
|
||||
isFile: () => node != null && typeof node === 'string',
|
||||
isSocket: () => false,
|
||||
isSymbolicLink: () => isSymlink,
|
||||
mtime,
|
||||
};
|
||||
}
|
||||
|
||||
function statSync(filepath) {
|
||||
const node = getToNode(filepath);
|
||||
if (node != null && node.SYMLINK) {
|
||||
return statSync(node.SYMLINK);
|
||||
}
|
||||
return makeStatResult(node);
|
||||
}
|
||||
|
||||
fs.stat.mockImplementation((filepath, callback) => {
|
||||
callback = asyncCallback(callback);
|
||||
let result;
|
||||
try {
|
||||
result = statSync(filepath);
|
||||
} catch (e) {
|
||||
callback(e);
|
||||
return;
|
||||
}
|
||||
callback(null, result);
|
||||
});
|
||||
|
||||
fs.statSync.mockImplementation(statSync);
|
||||
|
||||
function lstatSync(filepath) {
|
||||
const node = getToNode(filepath);
|
||||
return makeStatResult(node);
|
||||
}
|
||||
|
||||
fs.lstat.mockImplementation((filepath, callback) => {
|
||||
callback = asyncCallback(callback);
|
||||
let result;
|
||||
try {
|
||||
result = lstatSync(filepath);
|
||||
} catch (e) {
|
||||
callback(e);
|
||||
return;
|
||||
}
|
||||
callback(null, result);
|
||||
});
|
||||
|
||||
fs.lstatSync.mockImplementation(lstatSync);
|
||||
|
||||
fs.open.mockImplementation(function(filepath) {
|
||||
const callback = arguments[arguments.length - 1] || noop;
|
||||
let data, error, fd;
|
||||
try {
|
||||
data = getToNode(filepath);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error || data == null) {
|
||||
error = Error(`ENOENT: no such file or directory, open ${filepath}`);
|
||||
}
|
||||
if (data != null) {
|
||||
/* global Buffer: true */
|
||||
fd = {buffer: new Buffer(data, 'utf8'), position: 0};
|
||||
}
|
||||
|
||||
callback(error, fd);
|
||||
});
|
||||
|
||||
fs.read.mockImplementation(
|
||||
(fd, buffer, writeOffset, length, position, callback = noop) => {
|
||||
let bytesWritten;
|
||||
try {
|
||||
if (position == null || position < 0) {
|
||||
({position} = fd);
|
||||
}
|
||||
bytesWritten = fd.buffer.copy(
|
||||
buffer,
|
||||
writeOffset,
|
||||
position,
|
||||
position + length,
|
||||
);
|
||||
fd.position = position + bytesWritten;
|
||||
} catch (e) {
|
||||
callback(Error('invalid argument'));
|
||||
return;
|
||||
}
|
||||
callback(null, bytesWritten, buffer);
|
||||
},
|
||||
);
|
||||
|
||||
fs.close.mockImplementation((fd, callback = noop) => {
|
||||
try {
|
||||
fd.buffer = fs.position = undefined;
|
||||
} catch (e) {
|
||||
callback(Error('invalid argument'));
|
||||
return;
|
||||
}
|
||||
callback(null);
|
||||
});
|
||||
|
||||
let filesystem;
|
||||
|
||||
fs.createReadStream.mockImplementation(filepath => {
|
||||
if (!filepath.startsWith('/')) {
|
||||
throw Error('Cannot open file ' + filepath);
|
||||
}
|
||||
|
||||
const parts = filepath.split('/').slice(1);
|
||||
let file = filesystem;
|
||||
|
||||
for (const part of parts) {
|
||||
file = file[part];
|
||||
if (!file) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof file !== 'string') {
|
||||
throw Error('Cannot open file ' + filepath);
|
||||
}
|
||||
|
||||
return new stream.Readable({
|
||||
read() {
|
||||
this.push(file, 'utf8');
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
fs.createWriteStream.mockImplementation(file => {
|
||||
let node;
|
||||
try {
|
||||
node = getToNode(dirname(file));
|
||||
} finally {
|
||||
if (typeof node === 'object') {
|
||||
const writeStream = new stream.Writable({
|
||||
write(chunk) {
|
||||
this.__chunks.push(chunk);
|
||||
},
|
||||
});
|
||||
writeStream.__file = file;
|
||||
writeStream.__chunks = [];
|
||||
writeStream.end = jest.fn(writeStream.end);
|
||||
fs.createWriteStream.mock.returned.push(writeStream);
|
||||
return writeStream;
|
||||
} else {
|
||||
throw new Error('Cannot open file ' + file);
|
||||
}
|
||||
}
|
||||
});
|
||||
fs.createWriteStream.mock.returned = [];
|
||||
|
||||
fs.__setMockFilesystem = object => (filesystem = object);
|
||||
|
||||
const watcherListByPath = new Map();
|
||||
|
||||
fs.watch.mockImplementation((filename, options, listener) => {
|
||||
if (options.recursive) {
|
||||
throw new Error('recursive watch not implemented');
|
||||
}
|
||||
let watcherList = watcherListByPath.get(filename);
|
||||
if (watcherList == null) {
|
||||
watcherList = [];
|
||||
watcherListByPath.set(filename, watcherList);
|
||||
}
|
||||
const fsWatcher = new EventEmitter();
|
||||
fsWatcher.on('change', listener);
|
||||
fsWatcher.close = () => {
|
||||
watcherList.splice(watcherList.indexOf(fsWatcher), 1);
|
||||
fsWatcher.close = () => {
|
||||
throw new Error('FSWatcher is already closed');
|
||||
};
|
||||
};
|
||||
watcherList.push(fsWatcher);
|
||||
});
|
||||
|
||||
fs.__triggerWatchEvent = (eventType, filename) => {
|
||||
const directWatchers = watcherListByPath.get(filename) || [];
|
||||
directWatchers.forEach(wtc => wtc.emit('change', eventType));
|
||||
const dirPath = path.dirname(filename);
|
||||
const dirWatchers = watcherListByPath.get(dirPath) || [];
|
||||
dirWatchers.forEach(wtc =>
|
||||
wtc.emit('change', eventType, path.relative(dirPath, filename)),
|
||||
);
|
||||
};
|
||||
|
||||
function getToNode(filepath) {
|
||||
// Ignore the drive for Windows paths.
|
||||
if (filepath.match(/^[a-zA-Z]:\\/)) {
|
||||
filepath = filepath.substring(2);
|
||||
}
|
||||
|
||||
if (filepath.endsWith(path.sep)) {
|
||||
filepath = filepath.slice(0, -1);
|
||||
}
|
||||
const parts = filepath.split(/[\/\\]/);
|
||||
if (parts[0] !== '') {
|
||||
throw new Error('Make sure all paths are absolute.');
|
||||
}
|
||||
let node = filesystem;
|
||||
parts.slice(1).forEach(part => {
|
||||
if (node && node.SYMLINK) {
|
||||
node = getToNode(node.SYMLINK);
|
||||
}
|
||||
node = node[part];
|
||||
if (node == null) {
|
||||
const err = new Error('ENOENT: no such file or directory');
|
||||
err.code = 'ENOENT';
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
module.exports = fs;
|
||||
@@ -0,0 +1,354 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @format
|
||||
* @emails oncall+javascript_foundation
|
||||
*/
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
const fs = require('fs');
|
||||
const findSymlinkedModules = require('../findSymlinkedModules');
|
||||
|
||||
describe('findSymlinksForProjectRoot', () => {
|
||||
it('correctly finds normal module symlinks', () => {
|
||||
fs.__setMockFilesystem({
|
||||
root: {
|
||||
projectA: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'projectA',
|
||||
main: 'main.js',
|
||||
}),
|
||||
node_modules: {
|
||||
depFoo: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'depFoo',
|
||||
main: 'main.js',
|
||||
}),
|
||||
},
|
||||
projectB: {
|
||||
SYMLINK: '/root/projectB',
|
||||
},
|
||||
},
|
||||
},
|
||||
projectB: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'projectB',
|
||||
main: 'main.js',
|
||||
}),
|
||||
node_modules: {
|
||||
depBar: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'depBar',
|
||||
main: 'main.js',
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const symlinkedModules = findSymlinkedModules('/root/projectA', []);
|
||||
expect(symlinkedModules).toEqual(['/root/projectB']);
|
||||
});
|
||||
|
||||
it('correctly finds scoped module symlinks', () => {
|
||||
fs.__setMockFilesystem({
|
||||
root: {
|
||||
projectA: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'projectA',
|
||||
main: 'main.js',
|
||||
}),
|
||||
node_modules: {
|
||||
depFoo: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'depFoo',
|
||||
main: 'main.js',
|
||||
}),
|
||||
},
|
||||
'@scoped': {
|
||||
projectC: {
|
||||
SYMLINK: '/root/@scoped/projectC',
|
||||
},
|
||||
},
|
||||
projectB: {
|
||||
SYMLINK: '/root/projectB',
|
||||
},
|
||||
},
|
||||
},
|
||||
projectB: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'projectB',
|
||||
main: 'main.js',
|
||||
}),
|
||||
node_modules: {
|
||||
depBar: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'depBar',
|
||||
main: 'main.js',
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
'@scoped': {
|
||||
projectC: {
|
||||
'package.json': JSON.stringify({
|
||||
name: '@scoped/projectC',
|
||||
main: 'main.js',
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const symlinkedModules = findSymlinkedModules('/root/projectA', []);
|
||||
expect(symlinkedModules).toEqual([
|
||||
'/root/@scoped/projectC',
|
||||
'/root/projectB',
|
||||
]);
|
||||
});
|
||||
|
||||
it('correctly finds module symlinks within other module symlinks', () => {
|
||||
fs.__setMockFilesystem({
|
||||
root: {
|
||||
projectA: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'projectA',
|
||||
main: 'main.js',
|
||||
}),
|
||||
node_modules: {
|
||||
depFoo: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'depFoo',
|
||||
main: 'main.js',
|
||||
}),
|
||||
},
|
||||
'@scoped': {
|
||||
projectC: {
|
||||
SYMLINK: '/root/@scoped/projectC',
|
||||
},
|
||||
},
|
||||
projectB: {
|
||||
SYMLINK: '/root/projectB',
|
||||
},
|
||||
},
|
||||
},
|
||||
projectB: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'projectB',
|
||||
main: 'main.js',
|
||||
}),
|
||||
node_modules: {
|
||||
depBar: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'depBar',
|
||||
main: 'main.js',
|
||||
}),
|
||||
},
|
||||
projectD: {
|
||||
SYMLINK: '/root/projectD',
|
||||
},
|
||||
},
|
||||
},
|
||||
'@scoped': {
|
||||
projectC: {
|
||||
'package.json': JSON.stringify({
|
||||
name: '@scoped/projectC',
|
||||
main: 'main.js',
|
||||
}),
|
||||
},
|
||||
},
|
||||
projectD: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'projectD',
|
||||
main: 'main.js',
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const symlinkedModules = findSymlinkedModules('/root/projectA', []);
|
||||
expect(symlinkedModules).toEqual([
|
||||
'/root/@scoped/projectC',
|
||||
'/root/projectB',
|
||||
'/root/projectD',
|
||||
]);
|
||||
});
|
||||
|
||||
it('correctly handles duplicate symlink paths', () => {
|
||||
// projectA ->
|
||||
// -> projectC
|
||||
// -> projectB -> projectC
|
||||
// Final list should only contain projectC once
|
||||
fs.__setMockFilesystem({
|
||||
root: {
|
||||
projectA: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'projectA',
|
||||
main: 'main.js',
|
||||
}),
|
||||
node_modules: {
|
||||
depFoo: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'depFoo',
|
||||
main: 'main.js',
|
||||
}),
|
||||
},
|
||||
'@scoped': {
|
||||
projectC: {
|
||||
SYMLINK: '/root/@scoped/projectC',
|
||||
},
|
||||
},
|
||||
projectB: {
|
||||
SYMLINK: '/root/projectB',
|
||||
},
|
||||
},
|
||||
},
|
||||
projectB: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'projectB',
|
||||
main: 'main.js',
|
||||
}),
|
||||
node_modules: {
|
||||
depBar: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'depBar',
|
||||
main: 'main.js',
|
||||
}),
|
||||
},
|
||||
'@scoped': {
|
||||
projectC: {
|
||||
SYMLINK: '/root/@scoped/projectC',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'@scoped': {
|
||||
projectC: {
|
||||
'package.json': JSON.stringify({
|
||||
name: '@scoped/projectC',
|
||||
main: 'main.js',
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const symlinkedModules = findSymlinkedModules('/root/projectA', []);
|
||||
expect(symlinkedModules).toEqual([
|
||||
'/root/@scoped/projectC',
|
||||
'/root/projectB',
|
||||
]);
|
||||
});
|
||||
|
||||
it('correctly handles symlink recursion', () => {
|
||||
// projectA ->
|
||||
// -> projectC -> projectD -> projectA
|
||||
// -> projectB -> projectC -> projectA
|
||||
// -> projectD -> projectC -> projectA
|
||||
// Should not infinite loop, should not contain projectA
|
||||
fs.__setMockFilesystem({
|
||||
root: {
|
||||
projectA: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'projectA',
|
||||
main: 'main.js',
|
||||
}),
|
||||
node_modules: {
|
||||
depFoo: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'depFoo',
|
||||
main: 'main.js',
|
||||
}),
|
||||
},
|
||||
'@scoped': {
|
||||
projectC: {
|
||||
SYMLINK: '/root/@scoped/projectC',
|
||||
},
|
||||
},
|
||||
projectB: {
|
||||
SYMLINK: '/root/projectB',
|
||||
},
|
||||
},
|
||||
},
|
||||
projectB: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'projectB',
|
||||
main: 'main.js',
|
||||
}),
|
||||
node_modules: {
|
||||
depBar: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'depBar',
|
||||
main: 'main.js',
|
||||
}),
|
||||
},
|
||||
projectD: {
|
||||
SYMLINK: '/root/projectD',
|
||||
},
|
||||
'@scoped': {
|
||||
projectC: {
|
||||
SYMLINK: '/root/@scoped/projectC',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'@scoped': {
|
||||
projectC: {
|
||||
'package.json': JSON.stringify({
|
||||
name: '@scoped/projectC',
|
||||
main: 'main.js',
|
||||
}),
|
||||
node_modules: {
|
||||
projectA: {
|
||||
SYMLINK: '/root/projectA',
|
||||
},
|
||||
projectD: {
|
||||
SYMLINK: '/root/projectD',
|
||||
},
|
||||
projectE: {
|
||||
SYMLINK: '/root/projectE',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
projectD: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'projectD',
|
||||
main: 'main.js',
|
||||
}),
|
||||
node_modules: {
|
||||
'@scoped': {
|
||||
projectC: {
|
||||
SYMLINK: '/root/@scoped/projectC',
|
||||
},
|
||||
},
|
||||
projectE: {
|
||||
SYMLINK: '/root/projectE',
|
||||
},
|
||||
},
|
||||
},
|
||||
projectE: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'projectD',
|
||||
main: 'main.js',
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const symlinkedModules = findSymlinkedModules('/root/projectA');
|
||||
expect(symlinkedModules).toEqual([
|
||||
'/root/@scoped/projectC',
|
||||
'/root/projectB',
|
||||
'/root/projectD',
|
||||
'/root/projectE',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @format
|
||||
* @flow
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
* Find symlinked modules inside "node_modules."
|
||||
*
|
||||
* Naively, we could just perform a depth-first search of all folders in
|
||||
* node_modules, recursing when we find a symlink.
|
||||
*
|
||||
* We can be smarter than this due to our knowledge of how npm/Yarn lays out
|
||||
* "node_modules" / how tools that build on top of npm/Yarn (such as Lerna)
|
||||
* install dependencies.
|
||||
*
|
||||
* Starting from a given root node_modules folder, this algorithm will look at
|
||||
* both the top level descendants of the node_modules folder or second level
|
||||
* descendants of folders that start with "@" (which indicates a scoped
|
||||
* package). If any of those folders is a symlink, it will recurse into the
|
||||
* link, and perform the same search in the linked folder.
|
||||
*
|
||||
* The end result should be a list of all resolved module symlinks for a given
|
||||
* root.
|
||||
*/
|
||||
module.exports = function findSymlinkedModules(
|
||||
projectRoot: string,
|
||||
ignoredRoots?: Array<string> = [],
|
||||
) {
|
||||
const timeStart = Date.now();
|
||||
const nodeModuleRoot = path.join(projectRoot, 'node_modules');
|
||||
const resolvedSymlinks = findModuleSymlinks(nodeModuleRoot, [
|
||||
...ignoredRoots,
|
||||
projectRoot,
|
||||
]);
|
||||
const timeEnd = Date.now();
|
||||
|
||||
console.log(
|
||||
`Scanning folders for symlinks in ${nodeModuleRoot} (${timeEnd -
|
||||
timeStart}ms)`,
|
||||
);
|
||||
|
||||
return resolvedSymlinks;
|
||||
};
|
||||
|
||||
function findModuleSymlinks(
|
||||
modulesPath: string,
|
||||
ignoredPaths: Array<string> = [],
|
||||
): Array<string> {
|
||||
if (!fs.existsSync(modulesPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Find module symlinks
|
||||
const moduleFolders = fs.readdirSync(modulesPath);
|
||||
const symlinks = moduleFolders.reduce((links, folderName) => {
|
||||
const folderPath = path.join(modulesPath, folderName);
|
||||
const maybeSymlinkPaths = [];
|
||||
if (folderName.startsWith('@')) {
|
||||
const scopedModuleFolders = fs.readdirSync(folderPath);
|
||||
maybeSymlinkPaths.push(
|
||||
...scopedModuleFolders.map(name => path.join(folderPath, name)),
|
||||
);
|
||||
} else {
|
||||
maybeSymlinkPaths.push(folderPath);
|
||||
}
|
||||
return links.concat(resolveSymlinkPaths(maybeSymlinkPaths, ignoredPaths));
|
||||
}, []);
|
||||
|
||||
// For any symlinks found, look in _that_ modules node_modules directory
|
||||
// and find any symlinked modules
|
||||
const nestedSymlinks = symlinks.reduce(
|
||||
(links, symlinkPath) =>
|
||||
links.concat(
|
||||
// We ignore any found symlinks or anything from the ignored list,
|
||||
// to prevent infinite recursion
|
||||
findModuleSymlinks(path.join(symlinkPath, 'node_modules'), [
|
||||
...ignoredPaths,
|
||||
...symlinks,
|
||||
]),
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
return [...new Set([...symlinks, ...nestedSymlinks])];
|
||||
}
|
||||
|
||||
function resolveSymlinkPaths(maybeSymlinkPaths, ignoredPaths) {
|
||||
return maybeSymlinkPaths.reduce((links, maybeSymlinkPath) => {
|
||||
if (fs.lstatSync(maybeSymlinkPath).isSymbolicLink()) {
|
||||
const resolved = path.resolve(
|
||||
path.dirname(maybeSymlinkPath),
|
||||
fs.readlinkSync(maybeSymlinkPath),
|
||||
);
|
||||
if (ignoredPaths.indexOf(resolved) === -1 && fs.existsSync(resolved)) {
|
||||
links.push(resolved);
|
||||
}
|
||||
}
|
||||
return links;
|
||||
}, []);
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-native",
|
||||
"version": "1000.0.0",
|
||||
"version": "0.49.0-rc.4",
|
||||
"description": "A framework for building native apps using React",
|
||||
"license": "BSD-3-Clause",
|
||||
"repository": {
|
||||
@@ -205,4 +205,4 @@
|
||||
"shelljs": "0.6.0",
|
||||
"sinon": "^2.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user