mirror of
https://github.com/status-im/react-native.git
synced 2025-01-12 02:24:23 +00:00
2015-02-05 updates
- [ReactServer] Fix newly introduced bug | Amjad Masad - [ReactServer] Last sync from github | Amjad Masad - [RFC-ReactNative] Subscribable overhaul, clean up AppState/Reachability | Eric Vicenti - [ReactKit] Enable tappable <Text /> subnodes | Alex Akers
This commit is contained in:
parent
fd8b7dee77
commit
4f613e20d2
@ -34,22 +34,20 @@ var AttributeToggler = React.createClass({
|
||||
render: function() {
|
||||
var curStyle = {fontSize: this.state.fontSize};
|
||||
return (
|
||||
<View>
|
||||
<Text>
|
||||
<Text style={curStyle}>
|
||||
Tap the controls below to change attributes.
|
||||
</Text>
|
||||
<Text>
|
||||
<Text>
|
||||
See how it will even work on{' '}
|
||||
<Text style={curStyle}>
|
||||
this nested text
|
||||
</Text>
|
||||
See how it will even work on{' '}
|
||||
<Text style={curStyle}>
|
||||
this nested text
|
||||
</Text>
|
||||
<Text onPress={this.increaseSize}>
|
||||
{'>> Increase Size <<'}
|
||||
</Text>
|
||||
</Text>
|
||||
<Text onPress={this.increaseSize}>
|
||||
{'>> Increase Size <<'}
|
||||
</Text>
|
||||
</View>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -5,34 +5,270 @@
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var Subscribable = {
|
||||
Mixin: {
|
||||
componentWillMount: function() {
|
||||
this._subscriptions = [];
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
this._subscriptions.forEach((subscription) => subscription.remove());
|
||||
this._subscriptions = null;
|
||||
},
|
||||
/**
|
||||
* Subscribable wraps EventEmitter in a clean interface, and provides a mixin
|
||||
* so components can easily subscribe to events and not worry about cleanup on
|
||||
* unmount.
|
||||
*
|
||||
* Also acts as a basic store because it records the last data that it emitted,
|
||||
* and provides a way to populate the initial data. The most recent data can be
|
||||
* fetched from the Subscribable by calling `get()`
|
||||
*
|
||||
* Advantages over EventEmitter + Subscibable.Mixin.addListenerOn:
|
||||
* - Cleaner usage: no strings to identify the event
|
||||
* - Lifespan pattern enforces cleanup
|
||||
* - More logical: Subscribable.Mixin now uses a Subscribable class
|
||||
* - Subscribable saves the last data and makes it available with `.get()`
|
||||
*
|
||||
* Legacy Subscribable.Mixin.addListenerOn allowed automatic subscription to
|
||||
* EventEmitters. Now we should avoid EventEmitters and wrap with Subscribable
|
||||
* instead:
|
||||
*
|
||||
* ```
|
||||
* AppState.networkReachability = new Subscribable(
|
||||
* RCTDeviceEventEmitter,
|
||||
* 'reachabilityDidChange',
|
||||
* (resp) => resp.network_reachability,
|
||||
* RKReachability.getCurrentReachability
|
||||
* );
|
||||
*
|
||||
* var myComponent = React.createClass({
|
||||
* mixins: [Subscribable.Mixin],
|
||||
* getInitialState: function() {
|
||||
* return {
|
||||
* isConnected: AppState.networkReachability.get() !== 'none'
|
||||
* };
|
||||
* },
|
||||
* componentDidMount: function() {
|
||||
* this._reachSubscription = this.subscribeTo(
|
||||
* AppState.networkReachability,
|
||||
* (reachability) => {
|
||||
* this.setState({ isConnected: reachability !== 'none' })
|
||||
* }
|
||||
* );
|
||||
* },
|
||||
* render: function() {
|
||||
* return (
|
||||
* <Text>
|
||||
* {this.state.isConnected ? 'Network Connected' : 'No network'}
|
||||
* </Text>
|
||||
* <Text onPress={() => this._reachSubscription.remove()}>
|
||||
* End reachability subscription
|
||||
* </Text>
|
||||
* );
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
|
||||
/**
|
||||
* Special form of calling `addListener` that *guarantees* that a
|
||||
* subscription *must* be tied to a component instance, and therefore will
|
||||
* be cleaned up when the component is unmounted. It is impossible to create
|
||||
* the subscription and pass it in - this method must be the one to create
|
||||
* the subscription and therefore can guarantee it is retained in a way that
|
||||
* will be cleaned up.
|
||||
*
|
||||
* @param {EventEmitter} eventEmitter emitter to subscribe to.
|
||||
* @param {string} eventType Type of event to listen to.
|
||||
* @param {function} listener Function to invoke when event occurs.
|
||||
* @param {object} context Object to use as listener context.
|
||||
*/
|
||||
addListenerOn: function(eventEmitter, eventType, listener, context) {
|
||||
this._subscriptions.push(
|
||||
eventEmitter.addListener(eventType, listener, context)
|
||||
);
|
||||
var EventEmitter = require('EventEmitter');
|
||||
|
||||
var invariant = require('invariant');
|
||||
var logError = require('logError');
|
||||
|
||||
var SUBSCRIBABLE_INTERNAL_EVENT = 'subscriptionEvent';
|
||||
|
||||
|
||||
class Subscribable {
|
||||
/**
|
||||
* Creates a new Subscribable object
|
||||
*
|
||||
* @param {EventEmitter} eventEmitter Emitter to trigger subscription events.
|
||||
* @param {string} eventName Name of emitted event that triggers subscription
|
||||
* events.
|
||||
* @param {function} eventMapping (optional) Function to convert the output
|
||||
* of the eventEmitter to the subscription output.
|
||||
* @param {function} getInitData (optional) Async function to grab the initial
|
||||
* data to publish. Signature `function(successCallback, errorCallback)`.
|
||||
* The resolved data will be transformed with the eventMapping before it
|
||||
* gets emitted.
|
||||
*/
|
||||
constructor(eventEmitter, eventName, eventMapping, getInitData) {
|
||||
|
||||
this._internalEmitter = new EventEmitter();
|
||||
this._eventMapping = eventMapping || (data => data);
|
||||
|
||||
eventEmitter.addListener(
|
||||
eventName,
|
||||
this._handleEmit,
|
||||
this
|
||||
);
|
||||
|
||||
// Asyncronously get the initial data, if provided
|
||||
getInitData && getInitData(this._handleInitData.bind(this), logError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last data emitted from the Subscribable, or undefined
|
||||
*/
|
||||
get() {
|
||||
return this._lastData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new listener to the subscribable. This should almost never be used
|
||||
* directly, and instead through Subscribable.Mixin.subscribeTo
|
||||
*
|
||||
* @param {object} lifespan Object with `addUnmountCallback` that accepts
|
||||
* a handler to be called when the component unmounts. This is required and
|
||||
* desirable because it enforces cleanup. There is no easy way to leave the
|
||||
* subsciption hanging
|
||||
* {
|
||||
* addUnmountCallback: function(newUnmountHanlder) {...},
|
||||
* }
|
||||
* @param {function} callback Handler to call when Subscribable has data
|
||||
* updates
|
||||
* @param {object} context Object to bind the handler on, as "this"
|
||||
*
|
||||
* @return {object} the subscription object:
|
||||
* {
|
||||
* remove: function() {...},
|
||||
* }
|
||||
* Call `remove` to terminate the subscription before unmounting
|
||||
*/
|
||||
subscribe(lifespan, callback, context) {
|
||||
invariant(
|
||||
typeof lifespan.addUnmountCallback === 'function',
|
||||
'Must provide a valid lifespan, which provides a way to add a ' +
|
||||
'callback for when subscription can be cleaned up. This is used ' +
|
||||
'automatically by Subscribable.Mixin'
|
||||
);
|
||||
invariant(
|
||||
typeof callback === 'function',
|
||||
'Must provide a valid subscription handler.'
|
||||
);
|
||||
|
||||
// Add a listener to the internal EventEmitter
|
||||
var subscription = this._internalEmitter.addListener(
|
||||
SUBSCRIBABLE_INTERNAL_EVENT,
|
||||
callback,
|
||||
context
|
||||
);
|
||||
|
||||
// Clean up subscription upon the lifespan unmount callback
|
||||
lifespan.addUnmountCallback(() => {
|
||||
subscription.remove();
|
||||
});
|
||||
|
||||
return subscription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for the initial data resolution. Currently behaves the same as
|
||||
* `_handleEmit`, but we may eventually want to keep track of the difference
|
||||
*/
|
||||
_handleInitData(dataInput) {
|
||||
var emitData = this._eventMapping(dataInput);
|
||||
this._lastData = emitData;
|
||||
this._internalEmitter.emit(SUBSCRIBABLE_INTERNAL_EVENT, emitData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle new data emissions. Pass the data through our eventMapping
|
||||
* transformation, store it for later `get()`ing, and emit it for subscribers
|
||||
*/
|
||||
_handleEmit(dataInput) {
|
||||
var emitData = this._eventMapping(dataInput);
|
||||
this._lastData = emitData;
|
||||
this._internalEmitter.emit(SUBSCRIBABLE_INTERNAL_EVENT, emitData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Subscribable.Mixin = {
|
||||
|
||||
/**
|
||||
* @return {object} lifespan Object with `addUnmountCallback` that accepts
|
||||
* a handler to be called when the component unmounts
|
||||
* {
|
||||
* addUnmountCallback: function(newUnmountHanlder) {...},
|
||||
* }
|
||||
*/
|
||||
_getSubscribableLifespan: function() {
|
||||
if (!this._subscribableLifespan) {
|
||||
this._subscribableLifespan = {
|
||||
addUnmountCallback: (cb) => {
|
||||
this._endSubscribableLifespanCallbacks.push(cb);
|
||||
},
|
||||
};
|
||||
}
|
||||
return this._subscribableLifespan;
|
||||
},
|
||||
|
||||
_endSubscribableLifespan: function() {
|
||||
this._endSubscribableLifespanCallbacks.forEach(cb => cb());
|
||||
},
|
||||
|
||||
/**
|
||||
* Components use `subscribeTo` for listening to Subscribable stores. Cleanup
|
||||
* is automatic on component unmount.
|
||||
*
|
||||
* To stop listening to the subscribable and end the subscription early,
|
||||
* components should store the returned subscription object and invoke the
|
||||
* `remove()` function on it
|
||||
*
|
||||
* @param {Subscribable} subscription to subscribe to.
|
||||
* @param {function} listener Function to invoke when event occurs.
|
||||
* @param {object} context Object to bind the handler on, as "this"
|
||||
*
|
||||
* @return {object} the subscription object:
|
||||
* {
|
||||
* remove: function() {...},
|
||||
* }
|
||||
* Call `remove` to terminate the subscription before unmounting
|
||||
*/
|
||||
subscribeTo: function(subscribable, handler, context) {
|
||||
invariant(
|
||||
subscribable instanceof Subscribable,
|
||||
'Must provide a Subscribable'
|
||||
);
|
||||
return subscribable.subscribe(
|
||||
this._getSubscribableLifespan(),
|
||||
handler,
|
||||
context
|
||||
);
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._endSubscribableLifespanCallbacks = [];
|
||||
|
||||
// DEPRECATED addListenerOn* usage:
|
||||
this._subscribableSubscriptions = [];
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
// Resolve the lifespan, which will cause Subscribable to clean any
|
||||
// remaining subscriptions
|
||||
this._endSubscribableLifespan && this._endSubscribableLifespan();
|
||||
|
||||
// DEPRECATED addListenerOn* usage uses _subscribableSubscriptions array
|
||||
// instead of lifespan
|
||||
this._subscribableSubscriptions.forEach(
|
||||
(subscription) => subscription.remove()
|
||||
);
|
||||
this._subscribableSubscriptions = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* DEPRECATED - Use `Subscribable` and `Mixin.subscribeTo` instead.
|
||||
* `addListenerOn` subscribes the component to an `EventEmitter`.
|
||||
*
|
||||
* Special form of calling `addListener` that *guarantees* that a
|
||||
* subscription *must* be tied to a component instance, and therefore will
|
||||
* be cleaned up when the component is unmounted. It is impossible to create
|
||||
* the subscription and pass it in - this method must be the one to create
|
||||
* the subscription and therefore can guarantee it is retained in a way that
|
||||
* will be cleaned up.
|
||||
*
|
||||
* @param {EventEmitter} eventEmitter emitter to subscribe to.
|
||||
* @param {string} eventType Type of event to listen to.
|
||||
* @param {function} listener Function to invoke when event occurs.
|
||||
* @param {object} context Object to use as listener context.
|
||||
*/
|
||||
addListenerOn: function(eventEmitter, eventType, listener, context) {
|
||||
this._subscribableSubscriptions.push(
|
||||
eventEmitter.addListener(eventType, listener, context)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -80,7 +80,7 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) {
|
||||
|
||||
RCTAssert(targetView.reactTag && targetView.userInteractionEnabled,
|
||||
@"No react view found for touch - something went wrong.");
|
||||
|
||||
|
||||
// Get new, unique touch id
|
||||
const NSUInteger RCTMaxTouches = 11; // This is the maximum supported by iDevices
|
||||
NSInteger touchID = ([_reactTouches.lastObject[@"target"] integerValue] + 1) % RCTMaxTouches;
|
||||
@ -97,7 +97,7 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) {
|
||||
|
||||
// Create touch
|
||||
NSMutableDictionary *reactTouch = [[NSMutableDictionary alloc] initWithCapacity:9];
|
||||
reactTouch[@"target"] = targetView.reactTag;
|
||||
reactTouch[@"target"] = [targetView reactTagAtPoint:[touch locationInView:targetView]];
|
||||
reactTouch[@"identifier"] = @(touchID);
|
||||
reactTouch[@"touches"] = [NSNull null]; // We hijack this touchObj to serve both as an event
|
||||
reactTouch[@"changedTouches"] = [NSNull null]; // and as a Touch object, so making this JIT friendly.
|
||||
|
@ -13,6 +13,7 @@
|
||||
- (void)insertReactSubview:(id<RCTViewNodeProtocol>)subview atIndex:(NSInteger)atIndex;
|
||||
- (void)removeReactSubview:(id<RCTViewNodeProtocol>)subview;
|
||||
- (NSMutableArray *)reactSubviews;
|
||||
- (NSNumber *)reactTagAtPoint:(CGPoint)point;
|
||||
|
||||
// View is an RCTRootView
|
||||
- (BOOL)isReactRootView;
|
||||
|
@ -41,6 +41,7 @@
|
||||
13E067591A70F44B002CDEE1 /* UIView+ReactKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067541A70F44B002CDEE1 /* UIView+ReactKit.m */; };
|
||||
830A229E1A66C68A008503DA /* RCTRootView.m in Sources */ = {isa = PBXBuildFile; fileRef = 830A229D1A66C68A008503DA /* RCTRootView.m */; };
|
||||
832348161A77A5AA00B55238 /* Layout.c in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FC71A68125100A75B9A /* Layout.c */; };
|
||||
835DD1321A7FDFB600D561F7 /* RCTText.m in Sources */ = {isa = PBXBuildFile; fileRef = 835DD1311A7FDFB600D561F7 /* RCTText.m */; };
|
||||
83CBBA511A601E3B00E9B192 /* RCTAssert.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA4B1A601E3B00E9B192 /* RCTAssert.m */; };
|
||||
83CBBA521A601E3B00E9B192 /* RCTLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA4E1A601E3B00E9B192 /* RCTLog.m */; };
|
||||
83CBBA531A601E3B00E9B192 /* RCTUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA501A601E3B00E9B192 /* RCTUtils.m */; };
|
||||
@ -134,6 +135,8 @@
|
||||
830213F31A654E0800B993E6 /* RCTBridgeModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTBridgeModule.h; sourceTree = "<group>"; };
|
||||
830A229C1A66C68A008503DA /* RCTRootView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRootView.h; sourceTree = "<group>"; };
|
||||
830A229D1A66C68A008503DA /* RCTRootView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRootView.m; sourceTree = "<group>"; };
|
||||
835DD1301A7FDFB600D561F7 /* RCTText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTText.h; sourceTree = "<group>"; };
|
||||
835DD1311A7FDFB600D561F7 /* RCTText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTText.m; sourceTree = "<group>"; };
|
||||
83BEE46C1A6D19BC00B5863B /* RCTSparseArray.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSparseArray.h; sourceTree = "<group>"; };
|
||||
83BEE46D1A6D19BC00B5863B /* RCTSparseArray.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSparseArray.m; sourceTree = "<group>"; };
|
||||
83CBBA2E1A601D0E00E9B192 /* libReactKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libReactKit.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@ -228,6 +231,8 @@
|
||||
137029581A6C197000575408 /* RCTRawTextManager.m */,
|
||||
13B07FFC1A6947C200A75B9A /* RCTShadowText.h */,
|
||||
13B07FFD1A6947C200A75B9A /* RCTShadowText.m */,
|
||||
835DD1301A7FDFB600D561F7 /* RCTText.h */,
|
||||
835DD1311A7FDFB600D561F7 /* RCTText.m */,
|
||||
13B080021A6947C200A75B9A /* RCTTextManager.h */,
|
||||
13B080031A6947C200A75B9A /* RCTTextManager.m */,
|
||||
13B0800C1A69489C00A75B9A /* RCTNavigator.h */,
|
||||
@ -416,6 +421,7 @@
|
||||
13B0801F1A69489C00A75B9A /* RCTTextFieldManager.m in Sources */,
|
||||
134FCB3D1A6E7F0800051CC8 /* RCTContextExecutor.m in Sources */,
|
||||
13E067591A70F44B002CDEE1 /* UIView+ReactKit.m in Sources */,
|
||||
835DD1321A7FDFB600D561F7 /* RCTText.m in Sources */,
|
||||
137029531A69923600575408 /* RCTImageDownloader.m in Sources */,
|
||||
83CBBA981A6020BB00E9B192 /* RCTTouchHandler.m in Sources */,
|
||||
83CBBA521A601E3B00E9B192 /* RCTLog.m in Sources */,
|
||||
|
@ -22,6 +22,5 @@ extern NSString *const RCTReactTagAttributeName;
|
||||
@property (nonatomic, assign) NSLineBreakMode truncationMode;
|
||||
|
||||
- (NSAttributedString *)attributedString;
|
||||
- (NSAttributedString *)reactTagAttributedString;
|
||||
|
||||
@end
|
||||
|
@ -14,8 +14,7 @@ NSString *const RCTReactTagAttributeName = @"ReactTagAttributeName";
|
||||
static css_dim_t RCTMeasure(void *context, float width)
|
||||
{
|
||||
RCTShadowText *shadowText = (__bridge RCTShadowText *)context;
|
||||
if (isnan(width)) width = MAXFLOAT;
|
||||
CGSize computedSize = [[shadowText attributedString] boundingRectWithSize:(CGSize){width, CGFLOAT_MAX} options:NSStringDrawingUsesLineFragmentOrigin context:nil].size;
|
||||
CGSize computedSize = [[shadowText attributedString] boundingRectWithSize:(CGSize){isnan(width) ? CGFLOAT_MAX : width, CGFLOAT_MAX} options:NSStringDrawingUsesLineFragmentOrigin context:nil].size;
|
||||
|
||||
css_dim_t result;
|
||||
result.dimensions[CSS_WIDTH] = RCTCeilPixelValue(computedSize.width);
|
||||
@ -26,7 +25,6 @@ static css_dim_t RCTMeasure(void *context, float width)
|
||||
@implementation RCTShadowText
|
||||
{
|
||||
NSAttributedString *_cachedAttributedString;
|
||||
NSAttributedString *_cachedReactTagAttributedString;
|
||||
UIFont *_font;
|
||||
}
|
||||
|
||||
@ -39,33 +37,6 @@ static css_dim_t RCTMeasure(void *context, float width)
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSAttributedString *)reactTagAttributedString
|
||||
{
|
||||
if (![self isTextDirty] && _cachedReactTagAttributedString) {
|
||||
return _cachedReactTagAttributedString;
|
||||
}
|
||||
|
||||
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] init];
|
||||
for (RCTShadowView *child in [self reactSubviews]) {
|
||||
if ([child isKindOfClass:[RCTShadowText class]]) {
|
||||
RCTShadowText *shadowText = (RCTShadowText *)child;
|
||||
[attributedString appendAttributedString:[shadowText reactTagAttributedString]];
|
||||
} else if ([child isKindOfClass:[RCTShadowRawText class]]) {
|
||||
RCTShadowRawText *shadowRawText = (RCTShadowRawText *)child;
|
||||
[attributedString appendAttributedString:[[NSAttributedString alloc] initWithString:[shadowRawText text] ?: @""]];
|
||||
} else {
|
||||
RCTLogError(@"<Text> can't have any children except <Text> or raw strings");
|
||||
}
|
||||
}
|
||||
|
||||
[self _addAttribute:RCTReactTagAttributeName
|
||||
withValue:self.reactTag
|
||||
toAttributedString:attributedString];
|
||||
|
||||
_cachedReactTagAttributedString = attributedString;
|
||||
return _cachedReactTagAttributedString;
|
||||
}
|
||||
|
||||
- (NSAttributedString *)attributedString
|
||||
{
|
||||
return [self _attributedStringWithFontFamily:nil
|
||||
@ -81,9 +52,6 @@ static css_dim_t RCTMeasure(void *context, float width)
|
||||
return _cachedAttributedString;
|
||||
}
|
||||
|
||||
// while we're updating the attributed string, also update the react tag attributed string
|
||||
[self reactTagAttributedString];
|
||||
|
||||
if (_fontSize && !isnan(_fontSize)) {
|
||||
fontSize = _fontSize;
|
||||
}
|
||||
@ -121,7 +89,7 @@ static css_dim_t RCTMeasure(void *context, float width)
|
||||
|
||||
_font = [RCTConvert UIFont:nil withFamily:fontFamily size:@(fontSize) weight:fontWeight];
|
||||
[self _addAttribute:NSFontAttributeName withValue:_font toAttributedString:attributedString];
|
||||
|
||||
[self _addAttribute:RCTReactTagAttributeName withValue:self.reactTag toAttributedString:attributedString];
|
||||
[self _setParagraphStyleOnAttributedString:attributedString];
|
||||
|
||||
// create a non-mutable attributedString for use by the Text system which avoids copies down the line
|
||||
@ -179,10 +147,10 @@ static css_dim_t RCTMeasure(void *context, float width)
|
||||
// if we found anything, set it :D
|
||||
if (hasParagraphStyle) {
|
||||
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
|
||||
paragraphStyle.alignment = _textAlign;
|
||||
paragraphStyle.baseWritingDirection = _writingDirection;
|
||||
paragraphStyle.minimumLineHeight = _lineHeight;
|
||||
paragraphStyle.maximumLineHeight = _lineHeight;
|
||||
[paragraphStyle setAlignment:_textAlign];
|
||||
[attributedString addAttribute:NSParagraphStyleAttributeName
|
||||
value:paragraphStyle
|
||||
range:(NSRange){0, attributedString.length}];
|
||||
|
@ -376,6 +376,21 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st
|
||||
return _reactSubviews;
|
||||
}
|
||||
|
||||
- (NSNumber *)reactTagAtPoint:(CGPoint)point
|
||||
{
|
||||
for (RCTShadowView *shadowView in _reactSubviews) {
|
||||
if (CGRectContainsPoint(shadowView.frame, point)) {
|
||||
CGPoint relativePoint = point;
|
||||
CGPoint origin = shadowView.frame.origin;
|
||||
relativePoint.x -= origin.x;
|
||||
relativePoint.y -= origin.y;
|
||||
return [shadowView reactTagAtPoint:relativePoint];
|
||||
}
|
||||
}
|
||||
|
||||
return self.reactTag;
|
||||
}
|
||||
|
||||
- (void)updateShadowViewLayout
|
||||
{
|
||||
if (_recomputePadding) {
|
||||
|
13
ReactKit/Views/RCTText.h
Normal file
13
ReactKit/Views/RCTText.h
Normal file
@ -0,0 +1,13 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface RCTText : UIView
|
||||
|
||||
@property (nonatomic, copy) NSAttributedString *attributedText;
|
||||
@property (nonatomic, assign) NSLineBreakMode lineBreakMode;
|
||||
@property (nonatomic, assign) NSInteger numberOfLines;
|
||||
|
||||
- (NSNumber *)reactTagAtPoint:(CGPoint)point;
|
||||
|
||||
@end
|
97
ReactKit/Views/RCTText.m
Normal file
97
ReactKit/Views/RCTText.m
Normal file
@ -0,0 +1,97 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
#import "RCTText.h"
|
||||
|
||||
#import "RCTShadowText.h"
|
||||
#import "RCTUtils.h"
|
||||
#import "UIView+ReactKit.h"
|
||||
|
||||
@implementation RCTText
|
||||
{
|
||||
NSLayoutManager *_layoutManager;
|
||||
NSTextStorage *_textStorage;
|
||||
NSTextContainer *_textContainer;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if ((self = [super initWithFrame:frame])) {
|
||||
_textContainer = [[NSTextContainer alloc] init];
|
||||
_textContainer.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
_textContainer.lineFragmentPadding = 0.0;
|
||||
|
||||
_layoutManager = [[NSLayoutManager alloc] init];
|
||||
[_layoutManager addTextContainer:_textContainer];
|
||||
|
||||
_textStorage = [[NSTextStorage alloc] init];
|
||||
[_textStorage addLayoutManager:_layoutManager];
|
||||
|
||||
self.contentMode = UIViewContentModeRedraw;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSAttributedString *)attributedText
|
||||
{
|
||||
return [_textStorage copy];
|
||||
}
|
||||
|
||||
- (void)setAttributedText:(NSAttributedString *)attributedText
|
||||
{
|
||||
[_textStorage setAttributedString:attributedText];
|
||||
}
|
||||
|
||||
- (NSInteger)numberOfLines
|
||||
{
|
||||
return _textContainer.maximumNumberOfLines;
|
||||
}
|
||||
|
||||
- (void)setNumberOfLines:(NSInteger)numberOfLines
|
||||
{
|
||||
_textContainer.maximumNumberOfLines = MAX(0, numberOfLines);
|
||||
}
|
||||
|
||||
- (NSLineBreakMode)lineBreakMode
|
||||
{
|
||||
return _textContainer.lineBreakMode;
|
||||
}
|
||||
|
||||
- (void)setLineBreakMode:(NSLineBreakMode)lineBreakMode
|
||||
{
|
||||
_textContainer.lineBreakMode = lineBreakMode;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
|
||||
// The header comment for `size` says that a height of 0.0 should be enough,
|
||||
// but it isn't.
|
||||
_textContainer.size = CGSizeMake(self.bounds.size.width, CGFLOAT_MAX);
|
||||
}
|
||||
|
||||
- (void)drawRect:(CGRect)rect
|
||||
{
|
||||
NSRange glyphRange = [_layoutManager glyphRangeForTextContainer:_textContainer];
|
||||
[_layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:CGPointZero];
|
||||
[_layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:CGPointZero];
|
||||
}
|
||||
|
||||
- (NSNumber *)reactTagAtPoint:(CGPoint)point
|
||||
{
|
||||
CGFloat fraction;
|
||||
NSUInteger characterIndex = [_layoutManager characterIndexForPoint:point inTextContainer:_textContainer fractionOfDistanceBetweenInsertionPoints:&fraction];
|
||||
|
||||
NSNumber *reactTag = nil;
|
||||
|
||||
// If the point is not before (fraction == 0.0) the first character and not
|
||||
// after (fraction == 1.0) the last character, then the attribute is valid.
|
||||
if (_textStorage.length > 0 && (fraction > 0 || characterIndex > 0) && (fraction < 1 || characterIndex < _textStorage.length - 1)) {
|
||||
reactTag = [_textStorage attribute:RCTReactTagAttributeName atIndex:characterIndex effectiveRange:NULL];
|
||||
}
|
||||
|
||||
return reactTag ?: self.reactTag;
|
||||
}
|
||||
|
||||
@end
|
@ -5,19 +5,17 @@
|
||||
#import "RCTAssert.h"
|
||||
#import "RCTConvert.h"
|
||||
#import "RCTLog.h"
|
||||
#import "RCTSparseArray.h"
|
||||
#import "UIView+ReactKit.h"
|
||||
|
||||
#import "RCTShadowRawText.h"
|
||||
#import "RCTShadowText.h"
|
||||
#import "RCTSparseArray.h"
|
||||
#import "RCTText.h"
|
||||
#import "UIView+ReactKit.h"
|
||||
|
||||
@implementation RCTTextManager
|
||||
|
||||
- (UIView *)view
|
||||
{
|
||||
UILabel *label = [[UILabel alloc] init];
|
||||
label.numberOfLines = 0;
|
||||
return label;
|
||||
return [[RCTText alloc] init];
|
||||
}
|
||||
|
||||
- (RCTShadowView *)shadowView
|
||||
@ -26,11 +24,17 @@
|
||||
}
|
||||
|
||||
RCT_REMAP_VIEW_PROPERTY(containerBackgroundColor, backgroundColor)
|
||||
RCT_REMAP_VIEW_PROPERTY(textAlign, textAlignment);
|
||||
|
||||
- (void)set_textAlign:(id)json
|
||||
forShadowView:(RCTShadowText *)shadowView
|
||||
withDefaultView:(RCTShadowText *)defaultView
|
||||
{
|
||||
shadowView.textAlign = json ? [RCTConvert NSTextAlignment:json] : defaultView.textAlign;
|
||||
}
|
||||
|
||||
- (void)set_numberOfLines:(id)json
|
||||
forView:(UILabel *)view
|
||||
withDefaultView:(UILabel *)defaultView
|
||||
forView:(RCTText *)view
|
||||
withDefaultView:(RCTText *)defaultView
|
||||
{
|
||||
NSLineBreakMode truncationMode = NSLineBreakByClipping;
|
||||
view.numberOfLines = json ? [RCTConvert NSInteger:json] : defaultView.numberOfLines;
|
||||
@ -80,7 +84,6 @@ RCT_REMAP_VIEW_PROPERTY(textAlign, textAlignment);
|
||||
|
||||
// TODO: are modules global, or specific to a given rootView?
|
||||
for (RCTShadowView *rootView in shadowViewRegistry.allObjects) {
|
||||
|
||||
if (![rootView isReactRootView]) {
|
||||
// This isn't a host view
|
||||
continue;
|
||||
@ -92,7 +95,7 @@ RCT_REMAP_VIEW_PROPERTY(textAlign, textAlignment);
|
||||
}
|
||||
|
||||
// TODO: this is a slightly weird way to do this - a recursive approach would be cleaner
|
||||
RCTSparseArray *attributedStringForTag = [[RCTSparseArray alloc] init];
|
||||
RCTSparseArray *reactTaggedAttributedStrings = [[RCTSparseArray alloc] init];
|
||||
NSMutableArray *queue = [NSMutableArray arrayWithObject:rootView];
|
||||
for (NSInteger i = 0; i < [queue count]; i++) {
|
||||
RCTShadowView *shadowView = queue[i];
|
||||
@ -100,11 +103,9 @@ RCT_REMAP_VIEW_PROPERTY(textAlign, textAlignment);
|
||||
|
||||
if ([shadowView isKindOfClass:[RCTShadowText class]]) {
|
||||
RCTShadowText *shadowText = (RCTShadowText *)shadowView;
|
||||
NSNumber *reactTag = shadowText.reactTag;
|
||||
attributedStringForTag[reactTag] = [shadowText attributedString];
|
||||
reactTaggedAttributedStrings[shadowText.reactTag] = [shadowText attributedString];
|
||||
} else if ([shadowView isKindOfClass:[RCTShadowRawText class]]) {
|
||||
RCTShadowRawText *shadowRawText = (RCTShadowRawText *)shadowView;
|
||||
RCTLogError(@"Raw text cannot be used outside of a <Text> tag. Not rendering string: '%@'", [shadowRawText text]);
|
||||
RCTLogError(@"Raw text cannot be used outside of a <Text> tag. Not rendering string: '%@'", [(RCTShadowRawText *)shadowView text]);
|
||||
} else {
|
||||
for (RCTShadowView *child in [shadowView reactSubviews]) {
|
||||
if ([child isTextDirty]) {
|
||||
@ -117,9 +118,9 @@ RCT_REMAP_VIEW_PROPERTY(textAlign, textAlignment);
|
||||
}
|
||||
|
||||
[shadowBlocks addObject:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) {
|
||||
[attributedStringForTag enumerateObjectsUsingBlock:^(NSAttributedString *attributedString, NSNumber *reactTag, BOOL *stop) {
|
||||
UILabel *textView = viewRegistry[reactTag];
|
||||
[textView setAttributedText:attributedString];
|
||||
[reactTaggedAttributedStrings enumerateObjectsUsingBlock:^(NSAttributedString *attributedString, NSNumber *reactTag, BOOL *stop) {
|
||||
RCTText *text = viewRegistry[reactTag];
|
||||
text.attributedText = attributedString;
|
||||
}];
|
||||
}];
|
||||
}
|
||||
|
@ -23,6 +23,15 @@
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSNumber *)reactTagAtPoint:(CGPoint)point
|
||||
{
|
||||
UIView *view = [self hitTest:point withEvent:nil];
|
||||
while (view && !view.reactTag) {
|
||||
view = view.superview;
|
||||
}
|
||||
return view.reactTag;
|
||||
}
|
||||
|
||||
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex
|
||||
{
|
||||
[self insertSubview:subview atIndex:atIndex];
|
||||
|
109
packager/react-packager/index.js
vendored
109
packager/react-packager/index.js
vendored
@ -1,100 +1,19 @@
|
||||
var Packager = require('./src/Packager');
|
||||
'use strict';
|
||||
|
||||
var Activity = require('./src/Activity');
|
||||
var url = require('url');
|
||||
var Server = require('./src/Server');
|
||||
|
||||
exports.middleware = function(options) {
|
||||
var server = new Server(options);
|
||||
return server.processRequest.bind(server);
|
||||
};
|
||||
|
||||
exports.buildPackageFromUrl = function(options, reqUrl) {
|
||||
Activity.disable();
|
||||
var packager = createPackager(options);
|
||||
var params = getOptionsFromPath(url.parse(reqUrl).pathname);
|
||||
return packager.package(
|
||||
params.main,
|
||||
params.runModule,
|
||||
params.sourceMapUrl
|
||||
).then(function(p) {
|
||||
packager.kill();
|
||||
return p;
|
||||
});
|
||||
var server = new Server(options);
|
||||
return server.buildPackageFromUrl(reqUrl)
|
||||
.then(function(p) {
|
||||
server.kill();
|
||||
return p;
|
||||
});
|
||||
};
|
||||
|
||||
exports.catalystMiddleware = function(options) {
|
||||
var packager = createPackager(options);
|
||||
|
||||
return function(req, res, next) {
|
||||
var options;
|
||||
if (req.url.match(/\.bundle$/)) {
|
||||
options = getOptionsFromPath(url.parse(req.url).pathname);
|
||||
packager.package(
|
||||
options.main,
|
||||
options.runModule,
|
||||
options.sourceMapUrl
|
||||
).then(
|
||||
function(package) {
|
||||
res.end(package.getSource());
|
||||
},
|
||||
function(error) {
|
||||
handleError(res, error);
|
||||
}
|
||||
).done();
|
||||
} else if (req.url.match(/\.map$/)) {
|
||||
options = getOptionsFromPath(url.parse(req.url).pathname);
|
||||
packager.package(
|
||||
options.main,
|
||||
options.runModule,
|
||||
options.sourceMapUrl
|
||||
).then(
|
||||
function(package) {
|
||||
res.end(JSON.stringify(package.getSourceMap()));
|
||||
},
|
||||
function(error) {
|
||||
handleError(res, error);
|
||||
}
|
||||
).done();
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function getOptionsFromPath(pathname) {
|
||||
var parts = pathname.split('.');
|
||||
// Remove the leading slash.
|
||||
var main = parts[0].slice(1) + '.js';
|
||||
return {
|
||||
runModule: parts.slice(1).some(function(part) {
|
||||
return part === 'runModule';
|
||||
}),
|
||||
main: main,
|
||||
sourceMapUrl: parts.slice(0, -1).join('.') + '.map'
|
||||
};
|
||||
}
|
||||
|
||||
function handleError(res, error) {
|
||||
res.writeHead(500, {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
});
|
||||
|
||||
if (error.type === 'TransformError') {
|
||||
res.end(JSON.stringify(error));
|
||||
} else {
|
||||
console.error(error.stack || error);
|
||||
res.end(JSON.stringify({
|
||||
type: 'InternalError',
|
||||
message: 'React packager has encountered an internal error, ' +
|
||||
'please check your terminal error output for more details',
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function createPackager(options) {
|
||||
return new Packager({
|
||||
projectRoot: options.projectRoot,
|
||||
blacklistRE: options.blacklistRE,
|
||||
polyfillModuleNames: options.polyfillModuleNames || [],
|
||||
runtimeCode: options.runtimeCode,
|
||||
cacheVersion: options.cacheVersion,
|
||||
resetCache: options.resetCache,
|
||||
dev: options.dev,
|
||||
});
|
||||
}
|
||||
|
||||
exports.kill = Packager.kill;
|
||||
|
@ -1,6 +1,10 @@
|
||||
{
|
||||
"name": "react-packager",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"description": "",
|
||||
"main": "index.js"
|
||||
"main": "index.js",
|
||||
"jest": {
|
||||
"unmockedModulePathPatterns": ["source-map"],
|
||||
"testPathIgnorePatterns": ["JSAppServer/node_modules"]
|
||||
}
|
||||
}
|
||||
|
@ -131,11 +131,12 @@ function _writeAction(action) {
|
||||
|
||||
case 'endEvent':
|
||||
var startAction = _eventStarts[action.eventId];
|
||||
var startData = startAction.data ? ': ' + JSON.stringify(startAction.data) : '';
|
||||
console.log(
|
||||
'[' + fmtTime + '] ' +
|
||||
'<END> ' + startAction.eventName +
|
||||
'(' + (action.tstamp - startAction.tstamp) + 'ms)' +
|
||||
data
|
||||
startData
|
||||
);
|
||||
delete _eventStarts[action.eventId];
|
||||
break;
|
||||
|
@ -29,7 +29,7 @@ function HasteDependencyResolver(config) {
|
||||
return filepath.indexOf('__tests__') !== -1 ||
|
||||
(config.blacklistRE && config.blacklistRE.test(filepath));
|
||||
},
|
||||
fileWatcher: new FileWatcher(config)
|
||||
fileWatcher: new FileWatcher(config.projectRoot)
|
||||
});
|
||||
|
||||
this._polyfillModuleNames = [
|
||||
|
@ -17,14 +17,14 @@ describe('FileWatcher', function() {
|
||||
});
|
||||
|
||||
pit('it should get the watcher instance when ready', function() {
|
||||
var fileWatcher = new FileWatcher({projectRoot: 'rootDir'});
|
||||
var fileWatcher = new FileWatcher('rootDir');
|
||||
return fileWatcher.getWatcher().then(function(watcher) {
|
||||
expect(watcher instanceof Watcher).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
pit('it should end the watcher', function() {
|
||||
var fileWatcher = new FileWatcher({projectRoot: 'rootDir'});
|
||||
var fileWatcher = new FileWatcher('rootDir');
|
||||
Watcher.prototype.close.mockImplementation(function(callback) {
|
||||
callback();
|
||||
});
|
||||
|
10
packager/react-packager/src/FileWatcher/index.js
vendored
10
packager/react-packager/src/FileWatcher/index.js
vendored
@ -22,15 +22,15 @@ var MAX_WAIT_TIME = 3000;
|
||||
|
||||
var memoizedInstances = Object.create(null);
|
||||
|
||||
function FileWatcher(projectConfig) {
|
||||
if (memoizedInstances[projectConfig.projectRoot]) {
|
||||
return memoizedInstances[projectConfig.projectRoot];
|
||||
function FileWatcher(projectRoot) {
|
||||
if (memoizedInstances[projectRoot]) {
|
||||
return memoizedInstances[projectRoot];
|
||||
} else {
|
||||
memoizedInstances[projectConfig.projectRoot] = this;
|
||||
memoizedInstances[projectRoot] = this;
|
||||
}
|
||||
|
||||
this._loadingWatcher = detectingWatcherClass.then(function(Watcher) {
|
||||
var watcher = new Watcher(projectConfig.projectRoot, {glob: '**/*.js'});
|
||||
var watcher = new Watcher(projectRoot, {glob: '**/*.js'});
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
var rejectTimeout = setTimeout(function() {
|
||||
|
@ -4,7 +4,6 @@ var path = require('path');
|
||||
var version = require('../../package.json').version;
|
||||
var tmpdir = require('os').tmpDir();
|
||||
var pathUtils = require('../fb-path-utils');
|
||||
var FileWatcher = require('../FileWatcher');
|
||||
var fs = require('fs');
|
||||
var _ = require('underscore');
|
||||
var q = require('q');
|
||||
@ -34,16 +33,6 @@ function Cache(projectConfig) {
|
||||
this._persistCache.bind(this),
|
||||
2000
|
||||
);
|
||||
|
||||
this._fileWatcher = new FileWatcher(projectConfig);
|
||||
this._fileWatcher
|
||||
.getWatcher()
|
||||
.done(function(watcher) {
|
||||
watcher.on('all', function(type, filepath) {
|
||||
var absPath = path.join(projectConfig.projectRoot, filepath);
|
||||
delete data[absPath];
|
||||
});
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
Cache.prototype.get = function(filepath, loaderCb) {
|
||||
@ -75,11 +64,14 @@ Cache.prototype._set = function(filepath, loaderPromise) {
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
Cache.prototype.invalidate = function(filepath){
|
||||
if(this._has(filepath)) {
|
||||
delete this._data[filepath];
|
||||
}
|
||||
}
|
||||
|
||||
Cache.prototype.end = function() {
|
||||
return q.all([
|
||||
this._persistCache(),
|
||||
this._fileWatcher.end()
|
||||
]);
|
||||
return this._persistCache();
|
||||
};
|
||||
|
||||
Cache.prototype._persistCache = function() {
|
||||
|
5
packager/react-packager/src/JSTransformer/__mocks__/worker.js
vendored
Normal file
5
packager/react-packager/src/JSTransformer/__mocks__/worker.js
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function (data, callback) {
|
||||
callback(null, {});
|
||||
};
|
@ -19,13 +19,6 @@ describe('JSTransformer Cache', function() {
|
||||
});
|
||||
|
||||
Cache = require('../Cache');
|
||||
|
||||
var FileWatcher = require('../../FileWatcher');
|
||||
FileWatcher.prototype.getWatcher = function() {
|
||||
return q({
|
||||
on: function() {}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
describe('getting/settig', function() {
|
||||
@ -76,41 +69,6 @@ describe('JSTransformer Cache', function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
pit('it invalidates cache after a file has changed', function() {
|
||||
require('fs').stat.mockImpl(function(file, callback) {
|
||||
callback(null, {
|
||||
mtime: {
|
||||
getTime: function() {}
|
||||
}
|
||||
});
|
||||
});
|
||||
var FileWatcher = require('../../FileWatcher');
|
||||
var triggerChangeFile;
|
||||
FileWatcher.prototype.getWatcher = function() {
|
||||
return q({
|
||||
on: function(type, callback) {
|
||||
triggerChangeFile = callback;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var cache = new Cache({projectRoot: '/rootDir'});
|
||||
var loaderCb = jest.genMockFn().mockImpl(function() {
|
||||
return q('lol');
|
||||
});
|
||||
|
||||
return cache.get('/rootDir/someFile', loaderCb).then(function(value) {
|
||||
expect(value).toBe('lol');
|
||||
triggerChangeFile('change', 'someFile');
|
||||
var loaderCb2 = jest.genMockFn().mockImpl(function() {
|
||||
return q('lol2');
|
||||
});
|
||||
return cache.get('/rootDir/someFile', loaderCb2).then(function(value2) {
|
||||
expect(value2).toBe('lol2');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('loading cache from disk', function() {
|
||||
|
@ -25,6 +25,12 @@ Transformer.prototype.kill = function() {
|
||||
return this._cache.end();
|
||||
};
|
||||
|
||||
Transformer.prototype.invalidateFile = function(filePath) {
|
||||
this._cache.invalidate(filePath);
|
||||
//TODO: We can read the file and put it into the cache right here
|
||||
// This would simplify some caching logic as we can be sure that the cache is up to date
|
||||
}
|
||||
|
||||
Transformer.prototype.loadFileAndTransform = function(
|
||||
transformSets,
|
||||
filePath,
|
||||
|
@ -42,9 +42,11 @@ Package.prototype.finalize = function(options) {
|
||||
};
|
||||
|
||||
Package.prototype.getSource = function() {
|
||||
return _.pluck(this._modules, 'transformedCode').join('\n') + '\n' +
|
||||
'RAW_SOURCE_MAP = ' + JSON.stringify(this.getSourceMap({excludeSource: true})) + ';\n' +
|
||||
'\/\/@ sourceMappingURL=' + this._sourceMapUrl;
|
||||
return this._source || (
|
||||
this._source = _.pluck(this._modules, 'transformedCode').join('\n') + '\n' +
|
||||
'RAW_SOURCE_MAP = ' + JSON.stringify(this.getSourceMap({excludeSource: true})) +
|
||||
';\n' + '\/\/@ sourceMappingURL=' + this._sourceMapUrl
|
||||
);
|
||||
};
|
||||
|
||||
Package.prototype.getSourceMap = function(options) {
|
||||
|
@ -97,6 +97,10 @@ Packager.prototype.package = function(main, runModule, sourceMapUrl) {
|
||||
});
|
||||
};
|
||||
|
||||
Packager.prototype.invalidateFile = function(filePath){
|
||||
this._transformer.invalidateFile(filePath);
|
||||
}
|
||||
|
||||
Packager.prototype._transformModule = function(module) {
|
||||
var resolver = this._resolver;
|
||||
return this._transformer.loadFileAndTransform(
|
||||
|
166
packager/react-packager/src/Server/__tests__/Server-test.js
vendored
Normal file
166
packager/react-packager/src/Server/__tests__/Server-test.js
vendored
Normal file
@ -0,0 +1,166 @@
|
||||
'use strict';
|
||||
|
||||
jest.dontMock('worker-farm')
|
||||
.dontMock('q')
|
||||
.dontMock('os')
|
||||
.dontMock('errno/custom')
|
||||
.dontMock('path')
|
||||
.dontMock('url')
|
||||
.dontMock('../');
|
||||
|
||||
|
||||
var server = require('../');
|
||||
var q = require('q');
|
||||
|
||||
describe('processRequest', function(){
|
||||
var server;
|
||||
var Activity;
|
||||
var Packager;
|
||||
var FileWatcher;
|
||||
|
||||
var options = {
|
||||
projectRoot: 'root',
|
||||
blacklistRE: null,
|
||||
cacheVersion: null,
|
||||
polyfillModuleNames: null
|
||||
};
|
||||
|
||||
var makeRequest = function(requestHandler, requrl){
|
||||
var deferred = q.defer();
|
||||
requestHandler({
|
||||
url: requrl
|
||||
},{
|
||||
end: function(res){
|
||||
deferred.resolve(res);
|
||||
}
|
||||
},{
|
||||
next: function(){}
|
||||
}
|
||||
);
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
var invalidatorFunc = jest.genMockFunction();
|
||||
var watcherFunc = jest.genMockFunction();
|
||||
var requestHandler;
|
||||
|
||||
beforeEach(function(){
|
||||
Activity = require('../../Activity');
|
||||
Packager = require('../../Packager');
|
||||
FileWatcher = require('../../FileWatcher')
|
||||
|
||||
Packager.prototype.package = function(main, runModule, sourceMapUrl) {
|
||||
return q({
|
||||
getSource: function(){
|
||||
return "this is the source"
|
||||
},
|
||||
getSourceMap: function(){
|
||||
return "this is the source map"
|
||||
}
|
||||
})
|
||||
};
|
||||
FileWatcher.prototype.getWatcher = function() {
|
||||
return q({
|
||||
on: watcherFunc
|
||||
});
|
||||
};
|
||||
|
||||
Packager.prototype.invalidateFile = invalidatorFunc;
|
||||
|
||||
var Server = require('../');
|
||||
server = new Server(options);
|
||||
requestHandler = server.processRequest.bind(server);
|
||||
});
|
||||
|
||||
pit('returns JS bundle source on request of *.bundle',function(){
|
||||
result = makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle');
|
||||
return result.then(function(response){
|
||||
expect(response).toEqual("this is the source");
|
||||
});
|
||||
});
|
||||
|
||||
pit('returns sourcemap on request of *.map', function(){
|
||||
result = makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle.map');
|
||||
return result.then(function(response){
|
||||
expect(response).toEqual('"this is the source map"');
|
||||
});
|
||||
});
|
||||
|
||||
pit('watches all files in projectRoot', function(){
|
||||
result = makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle');
|
||||
return result.then(function(response){
|
||||
expect(watcherFunc.mock.calls[0][0]).toEqual('all');
|
||||
expect(watcherFunc.mock.calls[0][1]).not.toBe(null);
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
describe('file changes', function() {
|
||||
var triggerFileChange;
|
||||
beforeEach(function() {
|
||||
FileWatcher.prototype.getWatcher = function() {
|
||||
return q({
|
||||
on: function(eventType, callback) {
|
||||
if (eventType !== 'all') {
|
||||
throw new Error('Can only handle "all" event in watcher.');
|
||||
}
|
||||
triggerFileChange = callback;
|
||||
return this;
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
pit('invalides files in package when file is updated', function() {
|
||||
result = makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle');
|
||||
return result.then(function(response){
|
||||
var onFileChange = watcherFunc.mock.calls[0][1];
|
||||
onFileChange('all','path/file.js');
|
||||
expect(invalidatorFunc.mock.calls[0][0]).toEqual('root/path/file.js');
|
||||
});
|
||||
});
|
||||
|
||||
pit('rebuilds the packages that contain a file when that file is changed', function() {
|
||||
var packageFunc = jest.genMockFunction();
|
||||
packageFunc
|
||||
.mockReturnValueOnce(
|
||||
q({
|
||||
getSource: function(){
|
||||
return "this is the first source"
|
||||
},
|
||||
getSourceMap: function(){},
|
||||
})
|
||||
)
|
||||
.mockReturnValue(
|
||||
q({
|
||||
getSource: function(){
|
||||
return "this is the rebuilt source"
|
||||
},
|
||||
getSourceMap: function(){},
|
||||
})
|
||||
);
|
||||
|
||||
Packager.prototype.package = packageFunc;
|
||||
|
||||
var Server = require('../../Server');
|
||||
var server = new Server(options);
|
||||
|
||||
requestHandler = server.processRequest.bind(server);
|
||||
|
||||
|
||||
return makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle')
|
||||
.then(function(response){
|
||||
expect(response).toEqual("this is the first source");
|
||||
expect(packageFunc.mock.calls.length).toBe(1);
|
||||
triggerFileChange('all','path/file.js');
|
||||
})
|
||||
.then(function(){
|
||||
expect(packageFunc.mock.calls.length).toBe(2);
|
||||
return makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle')
|
||||
.then(function(response){
|
||||
expect(response).toEqual("this is the rebuilt source");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
128
packager/react-packager/src/Server/index.js
vendored
Normal file
128
packager/react-packager/src/Server/index.js
vendored
Normal file
@ -0,0 +1,128 @@
|
||||
var url = require('url');
|
||||
var path = require('path');
|
||||
var FileWatcher = require('../FileWatcher')
|
||||
var Packager = require('../Packager');
|
||||
var Activity = require('../Activity');
|
||||
var q = require('q');
|
||||
|
||||
module.exports = Server;
|
||||
|
||||
function Server(options) {
|
||||
this._projectRoot = options.projectRoot;
|
||||
this._packages = Object.create(null);
|
||||
this._packager = new Packager({
|
||||
projectRoot: options.projectRoot,
|
||||
blacklistRE: options.blacklistRE,
|
||||
polyfillModuleNames: options.polyfillModuleNames || [],
|
||||
runtimeCode: options.runtimeCode,
|
||||
cacheVersion: options.cacheVersion,
|
||||
resetCache: options.resetCache,
|
||||
dev: options.dev,
|
||||
});
|
||||
|
||||
this._fileWatcher = new FileWatcher(options.projectRoot);
|
||||
|
||||
var onFileChange = this._onFileChange.bind(this);
|
||||
this._fileWatcher.getWatcher().done(function(watcher) {
|
||||
watcher.on('all', onFileChange);
|
||||
});
|
||||
}
|
||||
|
||||
Server.prototype._onFileChange = function(type, filepath) {
|
||||
var absPath = path.join(this._projectRoot, filepath);
|
||||
this._packager.invalidateFile(absPath);
|
||||
this._rebuildPackages(absPath);
|
||||
};
|
||||
|
||||
Server.prototype._rebuildPackages = function(filepath) {
|
||||
var buildPackage = this._buildPackage.bind(this);
|
||||
var packages = this._packages;
|
||||
Object.keys(packages).forEach(function(key) {
|
||||
var options = getOptionsFromPath(url.parse(key).pathname);
|
||||
packages[key] = buildPackage(options).then(function(p) {
|
||||
// Make a throwaway call to getSource to cache the source string.
|
||||
p.getSource();
|
||||
return p;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Server.prototype.kill = function() {
|
||||
q.all([
|
||||
this._fileWatcher.end(),
|
||||
this._packager.kill(),
|
||||
]);
|
||||
};
|
||||
|
||||
Server.prototype._buildPackage = function(options) {
|
||||
return this._packager.package(
|
||||
options.main,
|
||||
options.runModule,
|
||||
options.sourceMapUrl
|
||||
);
|
||||
};
|
||||
|
||||
Server.prototype.buildPackageFromUrl = function(reqUrl) {
|
||||
var options = getOptionsFromPath(url.parse(reqUrl).pathname);
|
||||
return this._buildPackage(options);
|
||||
};
|
||||
|
||||
Server.prototype.processRequest = function(req, res, next) {
|
||||
var requestType;
|
||||
if (req.url.match(/\.bundle$/)) {
|
||||
requestType = 'bundle';
|
||||
} else if (req.url.match(/\.map$/)) {
|
||||
requestType = 'map';
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
|
||||
var startReqEventId = Activity.startEvent('request:' + req.url);
|
||||
var options = getOptionsFromPath(url.parse(req.url).pathname);
|
||||
var building = this._packages[req.url] || this._buildPackage(options)
|
||||
this._packages[req.url] = building;
|
||||
building.then(
|
||||
function(p) {
|
||||
if (requestType === 'bundle') {
|
||||
res.end(p.getSource());
|
||||
Activity.endEvent(startReqEventId);
|
||||
} else if (requestType === 'map') {
|
||||
res.end(JSON.stringify(p.getSourceMap()));
|
||||
Activity.endEvent(startReqEventId);
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
handleError(res, error);
|
||||
}
|
||||
).done();
|
||||
};
|
||||
|
||||
function getOptionsFromPath(pathname) {
|
||||
var parts = pathname.split('.');
|
||||
// Remove the leading slash.
|
||||
var main = parts[0].slice(1) + '.js';
|
||||
return {
|
||||
runModule: parts.slice(1).some(function(part) {
|
||||
return part === 'runModule';
|
||||
}),
|
||||
main: main,
|
||||
sourceMapUrl: parts.slice(0, -1).join('.') + '.map'
|
||||
};
|
||||
}
|
||||
|
||||
function handleError(res, error) {
|
||||
res.writeHead(500, {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
});
|
||||
|
||||
if (error.type === 'TransformError') {
|
||||
res.end(JSON.stringify(error));
|
||||
} else {
|
||||
console.error(error.stack || error);
|
||||
res.end(JSON.stringify({
|
||||
type: 'InternalError',
|
||||
message: 'react-packager has encountered an internal error, ' +
|
||||
'please check your terminal error output for more details',
|
||||
}));
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user