mirror of
https://github.com/vale981/Vulcan
synced 2025-03-06 01:51:40 -05:00
update meteor, add patched react-router-ssr (npm check)
This commit is contained in:
parent
464e20a96c
commit
084188de3c
71 changed files with 856 additions and 1658 deletions
|
@ -30,7 +30,7 @@ nova:base-routes
|
||||||
nova:email-templates
|
nova:email-templates
|
||||||
nova:i18n-en-us
|
nova:i18n-en-us
|
||||||
|
|
||||||
accounts-password@1.3.2
|
accounts-password@1.3.3
|
||||||
# accounts-twitter
|
# accounts-twitter
|
||||||
# accounts-facebook
|
# accounts-facebook
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
METEOR@1.4.2.1
|
METEOR@1.4.2.3
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
accounts-base@1.2.14
|
accounts-base@1.2.14
|
||||||
accounts-password@1.3.2
|
accounts-password@1.3.3
|
||||||
aldeed:collection2@2.10.0
|
aldeed:collection2@2.10.0
|
||||||
aldeed:collection2-core@1.2.0
|
aldeed:collection2-core@1.2.0
|
||||||
aldeed:schema-deny@1.1.0
|
aldeed:schema-deny@1.1.0
|
||||||
|
@ -8,7 +8,7 @@ aldeed:simple-schema@1.5.3
|
||||||
allow-deny@1.0.5
|
allow-deny@1.0.5
|
||||||
autoupdate@1.3.12
|
autoupdate@1.3.12
|
||||||
babel-compiler@6.13.0
|
babel-compiler@6.13.0
|
||||||
babel-runtime@1.0.0
|
babel-runtime@1.0.1
|
||||||
base64@1.0.10
|
base64@1.0.10
|
||||||
binary-heap@1.0.10
|
binary-heap@1.0.10
|
||||||
blaze@2.2.0
|
blaze@2.2.0
|
||||||
|
@ -18,7 +18,7 @@ caching-compiler@1.1.9
|
||||||
callback-hook@1.0.10
|
callback-hook@1.0.10
|
||||||
check@1.2.4
|
check@1.2.4
|
||||||
chuangbo:cookie@1.1.0
|
chuangbo:cookie@1.1.0
|
||||||
coffeescript@1.11.1_3
|
coffeescript@1.11.1_4
|
||||||
dburles:collection-helpers@1.1.0
|
dburles:collection-helpers@1.1.0
|
||||||
ddp@1.2.5
|
ddp@1.2.5
|
||||||
ddp-client@1.3.2
|
ddp-client@1.3.2
|
||||||
|
@ -27,7 +27,7 @@ ddp-rate-limiter@1.0.6
|
||||||
ddp-server@1.3.12
|
ddp-server@1.3.12
|
||||||
deps@1.0.12
|
deps@1.0.12
|
||||||
diff-sequence@1.0.7
|
diff-sequence@1.0.7
|
||||||
ecmascript@0.6.0
|
ecmascript@0.6.1
|
||||||
ecmascript-runtime@0.3.15
|
ecmascript-runtime@0.3.15
|
||||||
ejson@1.0.13
|
ejson@1.0.13
|
||||||
email@1.1.18
|
email@1.1.18
|
||||||
|
@ -59,7 +59,7 @@ meteorhacks:subs-manager@1.6.4
|
||||||
meteorhacks:unblock@1.1.0
|
meteorhacks:unblock@1.1.0
|
||||||
minifier-css@1.2.15
|
minifier-css@1.2.15
|
||||||
minifier-js@1.2.15
|
minifier-js@1.2.15
|
||||||
minimongo@1.0.18
|
minimongo@1.0.19
|
||||||
modules@0.7.7
|
modules@0.7.7
|
||||||
modules-runtime@0.7.7
|
modules-runtime@0.7.7
|
||||||
mongo@1.1.14
|
mongo@1.1.14
|
||||||
|
@ -105,7 +105,7 @@ rate-limit@1.0.6
|
||||||
react-meteor-data@0.2.9
|
react-meteor-data@0.2.9
|
||||||
reactive-dict@1.1.8
|
reactive-dict@1.1.8
|
||||||
reactive-var@1.0.11
|
reactive-var@1.0.11
|
||||||
reactrouter:react-router-ssr@3.1.5
|
reactrouter:react-router-ssr@3.1.6-nova-patch
|
||||||
reload@1.1.11
|
reload@1.1.11
|
||||||
retry@1.0.9
|
retry@1.0.9
|
||||||
routepolicy@1.0.12
|
routepolicy@1.0.12
|
||||||
|
@ -113,7 +113,7 @@ service-configuration@1.0.11
|
||||||
session@1.1.7
|
session@1.1.7
|
||||||
sha@1.0.9
|
sha@1.0.9
|
||||||
shell-server@0.2.1
|
shell-server@0.2.1
|
||||||
softwarerero:accounts-t9n@1.3.5
|
softwarerero:accounts-t9n@1.3.6
|
||||||
spacebars@1.0.13
|
spacebars@1.0.13
|
||||||
spacebars-compiler@1.0.13
|
spacebars-compiler@1.0.13
|
||||||
srp@1.0.10
|
srp@1.0.10
|
||||||
|
|
1
packages/_nova-invites/.gitignore
vendored
1
packages/_nova-invites/.gitignore
vendored
|
@ -1 +0,0 @@
|
||||||
.build*
|
|
|
@ -1,58 +0,0 @@
|
||||||
aldeed:autoform@5.1.2
|
|
||||||
aldeed:simple-schema@1.3.2
|
|
||||||
base64@1.0.3
|
|
||||||
binary-heap@1.0.3
|
|
||||||
blaze@2.1.2
|
|
||||||
blaze-tools@1.0.3
|
|
||||||
boilerplate-generator@1.0.3
|
|
||||||
callback-hook@1.0.3
|
|
||||||
cfs:http-methods@0.0.28
|
|
||||||
check@1.0.5
|
|
||||||
coffeescript@1.0.6
|
|
||||||
ddp@1.1.0
|
|
||||||
deps@1.0.7
|
|
||||||
ejson@1.0.6
|
|
||||||
geojson-utils@1.0.3
|
|
||||||
html-tools@1.0.4
|
|
||||||
htmljs@1.0.4
|
|
||||||
id-map@1.0.3
|
|
||||||
iron:controller@1.0.7
|
|
||||||
iron:core@1.0.7
|
|
||||||
iron:dynamic-template@1.0.7
|
|
||||||
iron:layout@1.0.7
|
|
||||||
iron:location@1.0.7
|
|
||||||
iron:middleware-stack@1.0.7
|
|
||||||
iron:router@1.0.7
|
|
||||||
iron:url@1.0.7
|
|
||||||
jquery@1.11.3_2
|
|
||||||
json@1.0.3
|
|
||||||
livedata@1.0.13
|
|
||||||
logging@1.0.7
|
|
||||||
meteor@1.1.6
|
|
||||||
minifiers@1.1.5
|
|
||||||
minimongo@1.0.8
|
|
||||||
momentjs:moment@2.8.4
|
|
||||||
mongo@1.1.0
|
|
||||||
observe-sequence@1.0.6
|
|
||||||
ordered-dict@1.0.3
|
|
||||||
random@1.0.3
|
|
||||||
reactive-dict@1.1.0
|
|
||||||
reactive-var@1.0.5
|
|
||||||
retry@1.0.3
|
|
||||||
routepolicy@1.0.5
|
|
||||||
session@1.1.0
|
|
||||||
spacebars@1.0.6
|
|
||||||
spacebars-compiler@1.0.6
|
|
||||||
tap:i18n@1.4.1
|
|
||||||
nova:i18n@0.1.0
|
|
||||||
nova:invites@0.1.0
|
|
||||||
nova:lib@0.3.1
|
|
||||||
nova:messages@0.1.0
|
|
||||||
nova:settings@0.1.0
|
|
||||||
nova:users@0.1.0
|
|
||||||
templating@1.1.1
|
|
||||||
tracker@1.0.7
|
|
||||||
ui@1.0.6
|
|
||||||
underscore@1.0.3
|
|
||||||
webapp@1.2.0
|
|
||||||
webapp-hashing@1.0.3
|
|
|
@ -1 +0,0 @@
|
||||||
Telescope invites package, used internally.
|
|
|
@ -1,2 +0,0 @@
|
||||||
{
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
{
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"translation_key" : "Proměnná překladu"
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
{
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"translation_key" : "translation string"
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
{
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"translation_key" : "translation string"
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
{
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"translation_key" : "tõlkimise string"
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"translation_key" : "رشته ترجمه"
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"translation_key" : "traduire une chaîne de caractères"
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
{
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"translation_key" : "translation string"
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"translation_key" : "translation string"
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
{
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"translation_key" : "аударма текст"
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"translation_key" : "번역 문자열"
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
{
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
{
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
{
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"translation_key" : "translation string"
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"translation_key" : "chave de tradução"
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
{
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
{
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
{
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"translation_key" : "preveden niz"
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"translation_key" : "översättningstext"
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
{
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
{
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"translation_key" : "çeviri dizgisi"
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
{
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"translation_key" : "translation string"
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
<template name="user_invites">
|
|
||||||
<div class="grid-small grid-block dialog admin">
|
|
||||||
<h3>Invites</h3>
|
|
||||||
|
|
||||||
{{#if canCurrentUserInvite}}
|
|
||||||
{{#autoForm schema=invitesSchema id="inviteForm" class="form-block" type="method" meteormethod="inviteUser"}}
|
|
||||||
<h3>Invite someone</h3>
|
|
||||||
<div class="control-group">
|
|
||||||
<label>Email</label>
|
|
||||||
<div class="controls">
|
|
||||||
{{> afFieldInput name="invitedUserEmail"}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-actions">
|
|
||||||
{{#if afFieldIsInvalid name="invitedUserEmail"}}
|
|
||||||
This is not a valid email
|
|
||||||
{{/if}}
|
|
||||||
<input type="submit" class="button btn btn-primary" value="Invite" />
|
|
||||||
</div>
|
|
||||||
{{/autoForm}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<td>Email</td>
|
|
||||||
<td>Accepted</td>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{#each invites}}
|
|
||||||
<tr>
|
|
||||||
<td>{{invitedUserEmail}}</td>
|
|
||||||
<td>{{#if accepted}}{{{icon "voted"}}}{{/if}}</td>
|
|
||||||
</tr>
|
|
||||||
{{/each}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
|
@ -1,62 +0,0 @@
|
||||||
Template.user_invites.created = function () {
|
|
||||||
|
|
||||||
var user = this.data;
|
|
||||||
var instance = this;
|
|
||||||
|
|
||||||
instance.invites = new ReactiveVar({});
|
|
||||||
|
|
||||||
Meteor.autorun(function () {
|
|
||||||
Telescope.subsManager.subscribe('invites', user._id);
|
|
||||||
var invites = Invites.find({invitingUserId: user._id});
|
|
||||||
instance.invites.set(invites);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Template.user_invites.helpers({
|
|
||||||
canCurrentUserInvite: function () {
|
|
||||||
var currentUser = Meteor.user();
|
|
||||||
return currentUser && (Users.isAdmin(currentUser) || currentUser.telescope.inviteCount > 0 && Users.can.invite(currentUser));
|
|
||||||
},
|
|
||||||
invitesLeft: function () {
|
|
||||||
var currentUser = Meteor.user();
|
|
||||||
return (currentUser && !(Users.isAdmin(currentUser))) ? (currentUser.telescope.inviteCount - currentUser.telescope.invitedCount) : 0
|
|
||||||
},
|
|
||||||
invitesSchema: function () {
|
|
||||||
// expose schema for Invites (used by AutoForm)
|
|
||||||
return Invites.simpleSchema();
|
|
||||||
},
|
|
||||||
invites: function () {
|
|
||||||
return Template.instance().invites.get();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var scrollUp = function(){
|
|
||||||
Deps.afterFlush(function() {
|
|
||||||
var element = $('.grid > .error');
|
|
||||||
$('html, body').animate({scrollTop: element.offset().top});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
AutoForm.hooks({
|
|
||||||
inviteForm: {
|
|
||||||
onSuccess: function(operation, result) {
|
|
||||||
Messages.clearSeen();
|
|
||||||
|
|
||||||
if(result && result.newUser){
|
|
||||||
Messages.flash('An invite has been sent out. Thank you!', "success");
|
|
||||||
} else {
|
|
||||||
Messages.flash('Thank you!', "info");
|
|
||||||
}
|
|
||||||
scrollUp();
|
|
||||||
},
|
|
||||||
|
|
||||||
onError: function(operation, error) {
|
|
||||||
Messages.clearSeen();
|
|
||||||
|
|
||||||
if(error && error.reason){
|
|
||||||
Messages.flash(error.reason, "error");
|
|
||||||
scrollUp();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,130 +0,0 @@
|
||||||
var InviteSchema = new SimpleSchema({
|
|
||||||
_id: {
|
|
||||||
type: String,
|
|
||||||
optional: true
|
|
||||||
},
|
|
||||||
invitingUserId: {
|
|
||||||
type: String,
|
|
||||||
optional: true
|
|
||||||
},
|
|
||||||
invitedUserEmail: {
|
|
||||||
type: String,
|
|
||||||
regEx: SimpleSchema.RegEx.Email
|
|
||||||
},
|
|
||||||
accepted: {
|
|
||||||
type: Boolean,
|
|
||||||
optional: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Invites = new Meteor.Collection("invites");
|
|
||||||
Invites.attachSchema(InviteSchema);
|
|
||||||
|
|
||||||
Users.addField([
|
|
||||||
/**
|
|
||||||
A count of the user's remaining invites
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
fieldName: "telescope.inviteCount",
|
|
||||||
fieldSchema: {
|
|
||||||
type: Number,
|
|
||||||
optional: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
A count of how many users have been invited by the user
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
fieldName: "telescope.invitedCount",
|
|
||||||
fieldSchema: {
|
|
||||||
type: Number,
|
|
||||||
optional: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
Whether the user is invited or not
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
fieldName: "telescope.isInvited",
|
|
||||||
fieldSchema: {
|
|
||||||
type: Boolean,
|
|
||||||
publish: true,
|
|
||||||
optional: true,
|
|
||||||
insertableIf: Users.isAdmin,
|
|
||||||
editableIf: Users.isAdmin,
|
|
||||||
autoform: {
|
|
||||||
omit: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
The _id of the user who invited the current user
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
fieldName: "telescope.invitedBy",
|
|
||||||
fieldSchema: {
|
|
||||||
type: String,
|
|
||||||
optional: true,
|
|
||||||
autoform: {
|
|
||||||
omit: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
The name of the user who invited the current user
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
fieldName: "telescope.invitedByName",
|
|
||||||
fieldSchema: {
|
|
||||||
type: String,
|
|
||||||
optional: true,
|
|
||||||
autoform: {
|
|
||||||
omit: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
// invites are managed through Meteor method
|
|
||||||
|
|
||||||
Invites.deny({
|
|
||||||
insert: function(){ return true; },
|
|
||||||
update: function(){ return true; },
|
|
||||||
remove: function(){ return true; }
|
|
||||||
});
|
|
||||||
|
|
||||||
Telescope.modules.add("profileEdit", {
|
|
||||||
template: 'user_invites',
|
|
||||||
order: 2
|
|
||||||
});
|
|
||||||
|
|
||||||
function setStartingInvites (user) {
|
|
||||||
// give new users a few invites (default to 3)
|
|
||||||
user.telescope.inviteCount = Settings.get('startInvitesCount', 3);
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
Telescope.callbacks.add("onCreateUser", setStartingInvites);
|
|
||||||
|
|
||||||
// on profile completion, check if the new user has been invited
|
|
||||||
// if so set her status accordingly and update invitation info
|
|
||||||
function checkIfInvited (user) {
|
|
||||||
|
|
||||||
var invite = Invites.findOne({ invitedUserEmail : Users.getEmail(user) });
|
|
||||||
|
|
||||||
if(invite){
|
|
||||||
|
|
||||||
var invitedBy = Meteor.users.findOne({ _id : invite.invitingUserId });
|
|
||||||
|
|
||||||
Users.update(user._id, { $set: {
|
|
||||||
"telescope.isInvited": true,
|
|
||||||
"telescope.invitedBy": invitedBy._id,
|
|
||||||
"telescope.invitedByName": Users.getDisplayName(invitedBy)
|
|
||||||
}});
|
|
||||||
|
|
||||||
Invites.update(invite._id, {$set : {
|
|
||||||
accepted : true
|
|
||||||
}});
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Telescope.callbacks.add("profileCompletedAsync", checkIfInvited);
|
|
|
@ -1,89 +0,0 @@
|
||||||
Meteor.methods({
|
|
||||||
|
|
||||||
inviteUser: function(invitation){
|
|
||||||
|
|
||||||
check(invitation, Match.Any);
|
|
||||||
|
|
||||||
// invite user returns the following hash
|
|
||||||
// { newUser : true|false }
|
|
||||||
// newUser is true if the person being invited is not on the site yet
|
|
||||||
|
|
||||||
// invitation can either contain userId or an email address :
|
|
||||||
// { invitedUserEmail : 'bob@gmail.com' } or { userId : 'user-id' }
|
|
||||||
|
|
||||||
check(invitation, Match.OneOf(
|
|
||||||
{ invitedUserEmail : String },
|
|
||||||
{ userId : String }
|
|
||||||
));
|
|
||||||
|
|
||||||
var user = invitation.invitedUserEmail ?
|
|
||||||
Meteor.users.findOne({ emails : { $elemMatch: { address: invitation.invitedUserEmail } } }) :
|
|
||||||
Meteor.users.findOne({ _id : invitation.userId });
|
|
||||||
|
|
||||||
var userEmail = invitation.invitedUserEmail ? invitation.invitedUserEmail :Users.getEmail(user);
|
|
||||||
var currentUser = Meteor.user();
|
|
||||||
var currentUserIsAdmin = Users.isAdmin(currentUser);
|
|
||||||
var currentUserCanInvite = currentUserIsAdmin || (currentUser.telescope.inviteCount > 0 && Users.can.invite(currentUser));
|
|
||||||
|
|
||||||
// check if the person is already invited
|
|
||||||
if(user && Users.is.invited(user)){
|
|
||||||
throw new Meteor.Error(403, "This person is already invited.");
|
|
||||||
} else {
|
|
||||||
if (!currentUserCanInvite){
|
|
||||||
throw new Meteor.Error(701, "You can't invite this user, sorry.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't allow duplicate multiple invite for the same person
|
|
||||||
var existingInvite = Invites.findOne({ invitedUserEmail : userEmail });
|
|
||||||
|
|
||||||
if (existingInvite) {
|
|
||||||
throw new Meteor.Error(403, "Somebody has already invited this person.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// create an invite
|
|
||||||
// consider invite accepted if the invited person has an account already
|
|
||||||
Invites.insert({
|
|
||||||
invitingUserId: Meteor.userId(),
|
|
||||||
invitedUserEmail: userEmail,
|
|
||||||
accepted: typeof user !== "undefined"
|
|
||||||
});
|
|
||||||
|
|
||||||
// update invinting user
|
|
||||||
Meteor.users.update(Meteor.userId(), {$inc:{"telescope.inviteCount": -1, "telescope.invitedCount": 1}});
|
|
||||||
|
|
||||||
if(user){
|
|
||||||
// update invited user
|
|
||||||
Meteor.users.update(user._id, {
|
|
||||||
$set: {
|
|
||||||
"telescope.isInvited": true,
|
|
||||||
"telescope.invitedBy": Meteor.userId(),
|
|
||||||
"telescope.invitedByName": Users.getDisplayName(currentUser)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var communityName = Settings.get('title','Telescope'),
|
|
||||||
emailSubject = 'You are invited to try '+communityName,
|
|
||||||
emailProperties = {
|
|
||||||
newUser : typeof user === 'undefined',
|
|
||||||
communityName : communityName,
|
|
||||||
actionLink : user ? Telescope.utils.getSigninUrl() : Telescope.utils.getSignupUrl(),
|
|
||||||
invitedBy : Users.getDisplayName(currentUser),
|
|
||||||
profileUrl : Users.getProfileUrl(currentUser),
|
|
||||||
siteUrl: Settings.get("siteUrl")
|
|
||||||
};
|
|
||||||
|
|
||||||
Meteor.setTimeout(function () {
|
|
||||||
NovaEmail.buildAndSend(userEmail, emailSubject, 'emailInvite', emailProperties);
|
|
||||||
}, 1);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
newUser : typeof user === 'undefined'
|
|
||||||
};
|
|
||||||
},
|
|
||||||
unInviteUser: function (userId) {
|
|
||||||
Meteor.users.update(userId, {$set: {"telescope.isInvited": false}});
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,4 +0,0 @@
|
||||||
Meteor.publish('invites', function (userId) {
|
|
||||||
var invites = Invites.find({invitingUserId: userId});
|
|
||||||
return (this.userId === userId || Users.isAdminById(this.userId)) ? invites : [];
|
|
||||||
});
|
|
|
@ -1,39 +0,0 @@
|
||||||
import NovaEmail from 'meteor/nova:email';
|
|
||||||
|
|
||||||
// Invitation email
|
|
||||||
Picker.route('/email/invite-existing-user/:id?', function(params, req, res, next) {
|
|
||||||
|
|
||||||
var html;
|
|
||||||
var invitingUser = typeof params.id === "undefined" ? Meteor.users.findOne() : Meteor.users.findOne(params.id);
|
|
||||||
|
|
||||||
var communityName = Settings.get('title','Telescope'),
|
|
||||||
emailProperties = {
|
|
||||||
newUser : false,
|
|
||||||
communityName : communityName,
|
|
||||||
actionLink : Telescope.utils.getSigninUrl(),
|
|
||||||
invitedBy : Users.getDisplayName(invitingUser),
|
|
||||||
profileUrl : Users.getProfileUrl(invitingUser)
|
|
||||||
};
|
|
||||||
|
|
||||||
html = NovaEmail.getTemplate('emailInvite')(emailProperties);
|
|
||||||
res.end(NovaEmail.buildTemplate(html));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Invitation email
|
|
||||||
Picker.route('/email/invite-new-user/:id?', function(params, req, res, next) {
|
|
||||||
|
|
||||||
var html;
|
|
||||||
var invitingUser = typeof params.id === "undefined" ? Meteor.users.findOne() : Meteor.users.findOne(params.id);
|
|
||||||
|
|
||||||
var communityName = Settings.get('title','Telescope'),
|
|
||||||
emailProperties = {
|
|
||||||
newUser : true,
|
|
||||||
communityName : communityName,
|
|
||||||
actionLink : Telescope.utils.getSignupUrl(),
|
|
||||||
invitedBy : Users.getDisplayName(invitingUser),
|
|
||||||
profileUrl : Users.getProfileUrl(invitingUser)
|
|
||||||
};
|
|
||||||
|
|
||||||
html = NovaEmail.getTemplate('emailInvite')(emailProperties);
|
|
||||||
res.end(NovaEmail.buildTemplate(html));
|
|
||||||
});
|
|
|
@ -1,5 +0,0 @@
|
||||||
import NovaEmail from 'meteor/nova:email';
|
|
||||||
|
|
||||||
NovaEmail.addTemplates({
|
|
||||||
emailInvite: Assets.getText("lib/server/templates/emailInvite.handlebars")
|
|
||||||
});
|
|
|
@ -1,11 +0,0 @@
|
||||||
<span class="heading">
|
|
||||||
<a href="{{profileUrl}}">{{invitedBy}}</a> invited you to join {{communityName}}
|
|
||||||
</span><br><br>
|
|
||||||
|
|
||||||
{{#if newUser}}
|
|
||||||
<a href="{{siteUrl}}{{actionLink}}">Join {{communityName}}</a>
|
|
||||||
{{else}}
|
|
||||||
<a href="{{siteUrl}}{{actionLink}}">Sign in to {{communityName}}</a>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<br><br>
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"translation_function_name": "__",
|
|
||||||
"helper_name": "_",
|
|
||||||
"namespace": "project"
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
Package.describe({
|
|
||||||
name: "invites",
|
|
||||||
summary: "Telescope invites package",
|
|
||||||
version: "0.27.2-nova",
|
|
||||||
git: "https://github.com/TelescopeJS/telescope-invites.git"
|
|
||||||
});
|
|
||||||
|
|
||||||
Npm.depends({
|
|
||||||
// NPM package dependencies
|
|
||||||
});
|
|
||||||
|
|
||||||
Package.onUse(function (api) {
|
|
||||||
|
|
||||||
api.versionsFrom("METEOR@1.0");
|
|
||||||
|
|
||||||
// --------------------------- 1. Meteor packages dependencies ---------------------------
|
|
||||||
|
|
||||||
// automatic (let the package specify where it's needed)
|
|
||||||
|
|
||||||
api.use(['nova:core@0.27.2-nova']);
|
|
||||||
|
|
||||||
// client
|
|
||||||
|
|
||||||
api.use([
|
|
||||||
'jquery',
|
|
||||||
'underscore',
|
|
||||||
'templating'
|
|
||||||
], ['client']);
|
|
||||||
|
|
||||||
// server
|
|
||||||
|
|
||||||
api.use([
|
|
||||||
//
|
|
||||||
], ['server']);
|
|
||||||
|
|
||||||
// ---------------------------------- 2. Files to include ----------------------------------
|
|
||||||
|
|
||||||
// i18n config (must come first)
|
|
||||||
|
|
||||||
api.addFiles([
|
|
||||||
'package-tap.i18n'
|
|
||||||
], ['client', 'server']);
|
|
||||||
|
|
||||||
// both
|
|
||||||
|
|
||||||
api.addFiles([
|
|
||||||
'lib/invites.js'
|
|
||||||
], ['client', 'server']);
|
|
||||||
|
|
||||||
// client
|
|
||||||
|
|
||||||
api.addFiles([
|
|
||||||
'lib/client/templates/user_invites.html',
|
|
||||||
'lib/client/templates/user_invites.js'
|
|
||||||
], ['client']);
|
|
||||||
|
|
||||||
// server
|
|
||||||
|
|
||||||
api.addAssets([
|
|
||||||
'lib/server/templates/emailInvite.handlebars'
|
|
||||||
], ['server']);
|
|
||||||
|
|
||||||
api.addFiles([
|
|
||||||
'lib/server/invites.js',
|
|
||||||
'lib/server/publications.js',
|
|
||||||
'lib/server/routes.js',
|
|
||||||
'lib/server/templates.js'
|
|
||||||
], ['server']);
|
|
||||||
|
|
||||||
// i18n languages (must come last)
|
|
||||||
|
|
||||||
var languages = ["ar", "bg", "cs", "da", "de", "el", "en", "es", "et", "fr", "hu", "id", "it", "ja", "kk", "ko", "nl", "pl", "pt-BR", "ro", "ru", "sl", "sv", "th", "tr", "vi", "zh-CN"];
|
|
||||||
var languagesPaths = languages.map(function (language) {
|
|
||||||
return "i18n/"+language+".i18n.json";
|
|
||||||
});
|
|
||||||
api.addFiles(languagesPaths, ["client", "server"]);
|
|
||||||
|
|
||||||
// -------------------------------- 3. Variables to export --------------------------------
|
|
||||||
|
|
||||||
api.export("Invites");
|
|
||||||
|
|
||||||
});
|
|
|
@ -1,235 +0,0 @@
|
||||||
{
|
|
||||||
"dependencies": [
|
|
||||||
[
|
|
||||||
"aldeed:autoform",
|
|
||||||
"4.2.0"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"aldeed:simple-schema",
|
|
||||||
"1.2.0"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"application-configuration",
|
|
||||||
"1.0.3"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"base64",
|
|
||||||
"1.0.1"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"binary-heap",
|
|
||||||
"1.0.1"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"blaze",
|
|
||||||
"2.0.3"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"blaze-tools",
|
|
||||||
"1.0.1"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"boilerplate-generator",
|
|
||||||
"1.0.1"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"callback-hook",
|
|
||||||
"1.0.1"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"check",
|
|
||||||
"1.0.2"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"coffeescript",
|
|
||||||
"1.0.4"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"ddp",
|
|
||||||
"1.0.12"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"deps",
|
|
||||||
"1.0.5"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"ejson",
|
|
||||||
"1.0.4"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"follower-livedata",
|
|
||||||
"1.0.2"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"geojson-utils",
|
|
||||||
"1.0.1"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"html-tools",
|
|
||||||
"1.0.2"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"htmljs",
|
|
||||||
"1.0.2"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"id-map",
|
|
||||||
"1.0.1"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"iron:controller",
|
|
||||||
"1.0.3"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"iron:core",
|
|
||||||
"1.0.4"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"iron:dynamic-template",
|
|
||||||
"1.0.5"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"iron:layout",
|
|
||||||
"1.0.5"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"iron:location",
|
|
||||||
"1.0.4"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"iron:middleware-stack",
|
|
||||||
"1.0.3"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"iron:router",
|
|
||||||
"1.0.3"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"iron:url",
|
|
||||||
"1.0.4"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"jquery",
|
|
||||||
"1.0.1"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"json",
|
|
||||||
"1.0.1"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"livedata",
|
|
||||||
"1.0.11"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"logging",
|
|
||||||
"1.0.5"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"meteor",
|
|
||||||
"1.1.3"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"minifiers",
|
|
||||||
"1.1.2"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"minimongo",
|
|
||||||
"1.0.5"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"mongo",
|
|
||||||
"1.0.9"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"mrt:moment",
|
|
||||||
"2.8.1"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"observe-sequence",
|
|
||||||
"1.0.3"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"ordered-dict",
|
|
||||||
"1.0.1"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"random",
|
|
||||||
"1.0.1"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"reactive-dict",
|
|
||||||
"1.0.4"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"reactive-var",
|
|
||||||
"1.0.3"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"retry",
|
|
||||||
"1.0.1"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"routepolicy",
|
|
||||||
"1.0.2"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"session",
|
|
||||||
"1.0.4"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"spacebars",
|
|
||||||
"1.0.3"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"spacebars-compiler",
|
|
||||||
"1.0.3"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"tap:http-methods",
|
|
||||||
"0.0.23"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"tap:i18n",
|
|
||||||
"1.2.1"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"telescope-base",
|
|
||||||
"0.0.0"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"nova:i18n",
|
|
||||||
"0.0.0"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"telescope-lib",
|
|
||||||
"0.2.9"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"templating",
|
|
||||||
"1.0.9"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"tracker",
|
|
||||||
"1.0.3"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"ui",
|
|
||||||
"1.0.4"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"underscore",
|
|
||||||
"1.0.1"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"webapp",
|
|
||||||
"1.1.4"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"webapp-hashing",
|
|
||||||
"1.0.1"
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"pluginDependencies": [],
|
|
||||||
"toolVersion": "meteor-tool@1.0.36",
|
|
||||||
"format": "1.0"
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
Telescope migrations package, used internally.
|
|
|
@ -1,784 +0,0 @@
|
||||||
import marked from 'marked';
|
|
||||||
import Posts from "meteor/nova:posts";
|
|
||||||
import Comments from "meteor/nova:comments";
|
|
||||||
import Categories from "meteor/nova:categories";
|
|
||||||
import Events from "meteor/nova:events";
|
|
||||||
import Users from "meteor/nova:users";
|
|
||||||
|
|
||||||
// TODO: switch over to Tom's migration package.
|
|
||||||
|
|
||||||
// database migrations
|
|
||||||
// http://stackoverflow.com/questions/10365496/meteor-how-to-perform-database-migrations
|
|
||||||
Migrations = new Meteor.Collection('migrations');
|
|
||||||
|
|
||||||
Meteor.startup(function () {
|
|
||||||
allMigrations = Object.keys(migrationsList);
|
|
||||||
_.each(allMigrations, function(migrationName){
|
|
||||||
runMigration(migrationName);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
Meteor.methods({
|
|
||||||
removeMigration: function (name) {
|
|
||||||
check(name, String);
|
|
||||||
if (Users.isAdmin(Meteor.user())) {
|
|
||||||
console.log('// removing migration: ' + name);
|
|
||||||
Migrations.remove({name: name});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// wrapper function for all migrations
|
|
||||||
var runMigration = function (migrationName) {
|
|
||||||
var migration = Migrations.findOne({name: migrationName});
|
|
||||||
|
|
||||||
if (migration){
|
|
||||||
if(typeof migration.finishedAt === 'undefined'){
|
|
||||||
// if migration exists but hasn't finished, remove it and start fresh
|
|
||||||
console.log('!!! Found incomplete migration "'+migrationName+'", removing and running again.');
|
|
||||||
Migrations.remove({name: migrationName});
|
|
||||||
}else{
|
|
||||||
// do nothing
|
|
||||||
// console.log('Migration "'+migrationName+'" already exists, doing nothing.')
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("//----------------------------------------------------------------------//");
|
|
||||||
console.log("//------------// Starting "+migrationName+" Migration //-----------//");
|
|
||||||
console.log("//----------------------------------------------------------------------//");
|
|
||||||
Migrations.insert({name: migrationName, startedAt: new Date(), completed: false});
|
|
||||||
|
|
||||||
// execute migration function
|
|
||||||
var itemsAffected = migrationsList[migrationName]() || 0;
|
|
||||||
|
|
||||||
Migrations.update({name: migrationName}, {$set: {finishedAt: new Date(), completed: true, itemsAffected: itemsAffected}});
|
|
||||||
console.log("//----------------------------------------------------------------------//");
|
|
||||||
console.log("//------------// Ending "+migrationName+" Migration //-----------//");
|
|
||||||
console.log("//----------------------------------------------------------------------//");
|
|
||||||
};
|
|
||||||
|
|
||||||
var migrationsList = {
|
|
||||||
updatePostStatus: function () {
|
|
||||||
var i = 0;
|
|
||||||
Posts.find({status: {$exists : false}}).forEach(function (post) {
|
|
||||||
i++;
|
|
||||||
Posts.update(post._id, {$set: {status: 2}});
|
|
||||||
console.log("---------------------");
|
|
||||||
console.log("Post: "+post.title);
|
|
||||||
console.log("Updating status to approved");
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
updateCategories: function () {
|
|
||||||
if (typeof Categories === "undefined" || Categories === null) return;
|
|
||||||
var i = 0;
|
|
||||||
Categories.find({slug: {$exists : false}}).forEach(function (category) {
|
|
||||||
i++;
|
|
||||||
var slug = Telescope.utils.slugify(category.name);
|
|
||||||
Categories.update(category._id, {$set: {slug: slug}});
|
|
||||||
console.log("---------------------");
|
|
||||||
console.log("Category: "+category.name);
|
|
||||||
console.log("Updating category with new slug: "+slug);
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
updatePostCategories: function () {
|
|
||||||
if (typeof Categories === "undefined" || Categories === null) return;
|
|
||||||
var i = 0;
|
|
||||||
Posts.find().forEach(function (post) {
|
|
||||||
i++;
|
|
||||||
var oldCategories = post.categories;
|
|
||||||
var newCategories = [];
|
|
||||||
var category = {};
|
|
||||||
var updating = false; // by default, assume we're not going to do anything
|
|
||||||
|
|
||||||
// iterate over the post.categories array
|
|
||||||
// if the post has no categories then nothing will happen
|
|
||||||
_.each(oldCategories, function(value){
|
|
||||||
// make sure the categories are strings
|
|
||||||
if((typeof value === "string") && (category = Categories.findOne({name: value}))){
|
|
||||||
// if value is a string, then look for the matching category object
|
|
||||||
// and if it exists push it to the newCategories array
|
|
||||||
updating = true; // we're updating at least one category for this post
|
|
||||||
newCategories.push(category);
|
|
||||||
}else{
|
|
||||||
// if category A) is already an object, or B) it's a string but a matching category object doesn't exist
|
|
||||||
// just keep the current value
|
|
||||||
newCategories.push(value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if(updating){
|
|
||||||
// update categories property on post
|
|
||||||
Posts.update(post._id, {$set: {categories: newCategories}});
|
|
||||||
}
|
|
||||||
|
|
||||||
// START CONSOLE LOGS
|
|
||||||
console.log("---------------------");
|
|
||||||
console.log("Post: "+post.title);
|
|
||||||
if(updating){
|
|
||||||
console.log(oldCategories.length+" categories: "+oldCategories);
|
|
||||||
console.log("Updating categories array to: ");
|
|
||||||
console.log(newCategories);
|
|
||||||
}else{
|
|
||||||
console.log("No updates");
|
|
||||||
}
|
|
||||||
// END CONSOLE LOGS
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
updateUserProfiles: function () {
|
|
||||||
var i = 0;
|
|
||||||
var allUsers = Meteor.users.find();
|
|
||||||
console.log('> Found '+allUsers.count()+' users.\n');
|
|
||||||
|
|
||||||
allUsers.forEach(function(user){
|
|
||||||
i++;
|
|
||||||
console.log('> Updating user '+user._id+' ('+user.username+')');
|
|
||||||
|
|
||||||
var properties = {};
|
|
||||||
properties.telescope = {};
|
|
||||||
// update user slug
|
|
||||||
if(Users.getUserName(user))
|
|
||||||
properties.slug = Telescope.utils.slugify(Users.getUserName(user));
|
|
||||||
|
|
||||||
// update user isAdmin flag
|
|
||||||
if(typeof user.isAdmin === 'undefined')
|
|
||||||
properties.isAdmin = false;
|
|
||||||
|
|
||||||
// update postCount
|
|
||||||
var postsByUser = Posts.find({userId: user._id});
|
|
||||||
properties.telescope.postCount = postsByUser.count();
|
|
||||||
|
|
||||||
// update commentCount
|
|
||||||
var commentsByUser = Comments.find({userId: user._id});
|
|
||||||
properties.telescope.commentCount = commentsByUser.count();
|
|
||||||
|
|
||||||
Meteor.users.update(user._id, {$set:properties});
|
|
||||||
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
resetUpvotesDownvotes: function () {
|
|
||||||
var i = 0;
|
|
||||||
Posts.find().forEach(function (post) {
|
|
||||||
i++;
|
|
||||||
var upvotes = 0,
|
|
||||||
downvotes = 0;
|
|
||||||
console.log("Post: "+post.title);
|
|
||||||
if(post.upvoters){
|
|
||||||
upvotes = post.upvoters.length;
|
|
||||||
console.log("Found "+upvotes+" upvotes.");
|
|
||||||
}
|
|
||||||
if(post.downvoters){
|
|
||||||
downvotes = post.downvoters.length;
|
|
||||||
console.log("Found "+downvotes+" downvotes.");
|
|
||||||
}
|
|
||||||
Posts.update(post._id, {$set: {upvotes: upvotes, downvotes: downvotes}});
|
|
||||||
console.log("---------------------");
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
resetCommentsUpvotesDownvotes: function () {
|
|
||||||
var i = 0;
|
|
||||||
Comments.find().forEach(function (comment) {
|
|
||||||
i++;
|
|
||||||
var upvotes = 0,
|
|
||||||
downvotes = 0;
|
|
||||||
console.log("Comment: "+comment._id);
|
|
||||||
if(comment.upvoters){
|
|
||||||
upvotes = comment.upvoters.length;
|
|
||||||
console.log("Found "+upvotes+" upvotes.");
|
|
||||||
}
|
|
||||||
if(comment.downvoters){
|
|
||||||
downvotes = comment.downvoters.length;
|
|
||||||
console.log("Found "+downvotes+" downvotes.");
|
|
||||||
}
|
|
||||||
Comments.update(comment._id, {$set: {upvotes: upvotes, downvotes: downvotes}});
|
|
||||||
console.log("---------------------");
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
headlineToTitle: function () {
|
|
||||||
var i = 0;
|
|
||||||
Posts.find({title: {$exists : false}}).forEach(function (post) {
|
|
||||||
i++;
|
|
||||||
console.log("Post: "+post.headline+" "+post.title);
|
|
||||||
Posts.update(post._id, { $rename: { 'headline': 'title'}}, {multi: true, validate: false});
|
|
||||||
console.log("---------------------");
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
commentsSubmittedToCreatedAt: function () {
|
|
||||||
var i = 0;
|
|
||||||
Comments.find({createdAt: {$exists: false}}).forEach(function (comment) {
|
|
||||||
i++;
|
|
||||||
console.log("Comment: "+comment._id);
|
|
||||||
Comments.update(comment._id, { $rename: { 'submitted': 'createdAt'}}, {multi: true, validate: false});
|
|
||||||
console.log("---------------------");
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
commentsPostToPostId: function () {
|
|
||||||
var i = 0;
|
|
||||||
Comments.find({postId: {$exists : false}}).forEach(function (comment) {
|
|
||||||
i++;
|
|
||||||
console.log("Comment: "+comment._id);
|
|
||||||
Comments.update(comment._id, { $rename: { 'post': 'postId'}}, {multi: true, validate: false});
|
|
||||||
console.log("---------------------");
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
createdAtSubmittedToDate: function () {
|
|
||||||
var i = 0;
|
|
||||||
Posts.find().forEach(function (post) {
|
|
||||||
if(typeof post.submitted === "number" || typeof post.createdAt === "number"){
|
|
||||||
i++;
|
|
||||||
console.log("Posts: "+post.title);
|
|
||||||
var createdAt = new Date(post.createdAt);
|
|
||||||
var submitted = new Date(post.submitted);
|
|
||||||
console.log(createdAt);
|
|
||||||
Posts.update(post._id, { $set: { 'createdAt': createdAt, submitted: submitted}}, {multi: true, validate: false});
|
|
||||||
console.log("---------------------");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
commentsCreatedAtToDate: function () {
|
|
||||||
var i = 0;
|
|
||||||
Comments.find().forEach(function (comment) {
|
|
||||||
if(typeof comment.createdAt === "number"){
|
|
||||||
i++;
|
|
||||||
console.log("Comment: "+comment._id);
|
|
||||||
var createdAt = new Date(comment.createdAt);
|
|
||||||
console.log(createdAt);
|
|
||||||
Comments.update(comment._id, { $set: { 'createdAt': createdAt}}, {multi: true, validate: false});
|
|
||||||
console.log("---------------------");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
submittedToPostedAt: function () {
|
|
||||||
var i = 0;
|
|
||||||
Posts.find({submitted: {$exists : true}, postedAt: {$exists : false}}).forEach(function (post) {
|
|
||||||
i++;
|
|
||||||
console.log("Post: "+post._id);
|
|
||||||
Posts.update(post._id, { $rename: { 'submitted': 'postedAt'}}, {multi: true, validate: false});
|
|
||||||
console.log("---------------------");
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
addPostedAtToComments: function () {
|
|
||||||
var i = 0;
|
|
||||||
Comments.find({postedAt: {$exists : false}}).forEach(function (comment) {
|
|
||||||
i++;
|
|
||||||
console.log("Comment: "+comment._id);
|
|
||||||
Comments.update(comment._id, { $set: { 'postedAt': comment.createdAt}}, {multi: true, validate: false});
|
|
||||||
console.log("---------------------");
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
parentToParentCommentId: function () {
|
|
||||||
var i = 0;
|
|
||||||
Comments.find({parent: {$exists: true}, parentCommentId: {$exists : false}}).forEach(function (comment) {
|
|
||||||
i++;
|
|
||||||
console.log("Comment: "+comment._id);
|
|
||||||
Comments.update(comment._id, { $set: { 'parentCommentId': comment.parent}}, {multi: true, validate: false});
|
|
||||||
console.log("---------------------");
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
addLastCommentedAt: function () {
|
|
||||||
var i = 0;
|
|
||||||
Posts.find({$and: [
|
|
||||||
{$or: [{comments: {$gt: 0}}, {commentCount: {$gt: 0}}]},
|
|
||||||
{lastCommentedAt: {$exists : false}}
|
|
||||||
]}).forEach(function (post) {
|
|
||||||
i++;
|
|
||||||
console.log("Post: "+post._id);
|
|
||||||
var postComments = Comments.find({$or: [{postId: post._id}, {post: post._id}]}, {sort: {postedAt: -1}}).fetch();
|
|
||||||
var lastComment;
|
|
||||||
if (_.isEmpty(postComments)) {
|
|
||||||
console.log('postComments from post '+post._id+' is empty. Skipping.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lastComment = postComments[0];
|
|
||||||
Posts.update(post._id, { $set: { lastCommentedAt: lastComment.postedAt}}, {multi: false, validate: false});
|
|
||||||
console.log("---------------------");
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
commentsToCommentCount: function () {
|
|
||||||
var i = 0;
|
|
||||||
Posts.find({comments: {$exists : true}, commentCount: {$exists : false}}).forEach(function (post) {
|
|
||||||
i++;
|
|
||||||
console.log("Post: "+post._id);
|
|
||||||
Posts.update(post._id, { $set: { 'commentCount': post.comments}, $unset: { 'comments': ''}}, {multi: true, validate: false});
|
|
||||||
console.log("---------------------");
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
addCommentersToPosts: function () {
|
|
||||||
var i = 0;
|
|
||||||
Comments.find().forEach(function (comment) {
|
|
||||||
i++;
|
|
||||||
console.log("Comment: "+comment._id);
|
|
||||||
console.log("Post: "+comment.postId);
|
|
||||||
Posts.update(comment.postId, { $addToSet: { 'commenters': comment.userId}}, {multi: true, validate: false});
|
|
||||||
console.log("---------------------");
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
moveVotesFromProfile: function () {
|
|
||||||
var i = 0;
|
|
||||||
Meteor.users.find().forEach(function (user) {
|
|
||||||
i++;
|
|
||||||
console.log("User: "+user._id);
|
|
||||||
Meteor.users.update(user._id, {
|
|
||||||
$rename: {
|
|
||||||
'profile.upvotedPosts': 'telescope.upvotedPosts',
|
|
||||||
'profile.downvotedPosts': 'telescope.downvotedPosts',
|
|
||||||
'profile.upvotedComments': 'telescope.upvotedComments',
|
|
||||||
'profile.downvotedComments': 'telescope.downvotedComments'
|
|
||||||
}
|
|
||||||
}, {multi: true, validate: false});
|
|
||||||
console.log("---------------------");
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
addHTMLBody: function () {
|
|
||||||
var i = 0;
|
|
||||||
Posts.find({body: {$exists : true}}).forEach(function (post) {
|
|
||||||
i++;
|
|
||||||
var htmlBody = Telescope.utils.sanitize(marked(post.body));
|
|
||||||
console.log("Post: "+post._id);
|
|
||||||
Posts.update(post._id, { $set: { 'htmlBody': htmlBody}}, {multi: true, validate: false});
|
|
||||||
console.log("---------------------");
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
addHTMLComment: function () {
|
|
||||||
var i = 0;
|
|
||||||
Comments.find({body: {$exists : true}}).forEach(function (comment) {
|
|
||||||
i++;
|
|
||||||
var htmlBody = Telescope.utils.sanitize(marked(comment.body));
|
|
||||||
console.log("Comment: "+comment._id);
|
|
||||||
Comments.update(comment._id, { $set: { 'htmlBody': htmlBody}}, {multi: true, validate: false});
|
|
||||||
console.log("---------------------");
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
clicksToClickCount: function () {
|
|
||||||
var i = 0;
|
|
||||||
Posts.find({"clicks": {$exists: true}, "clickCount": {$exists : false}}).forEach(function (post) {
|
|
||||||
i++;
|
|
||||||
console.log("Post: " + post._id);
|
|
||||||
Posts.update(post._id, { $set: { 'clickCount': post.clicks}, $unset: { 'clicks': ''}}, {multi: true, validate: false});
|
|
||||||
console.log("---------------------");
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
commentsCountToCommentCount: function () {
|
|
||||||
var i = 0;
|
|
||||||
Posts.find({"commentCount": {$exists : false}}).forEach(function (post) {
|
|
||||||
i++;
|
|
||||||
console.log("Post: " + post._id);
|
|
||||||
Posts.update({_id: post._id}, { $set: { 'commentCount': post.commentsCount}, $unset: {'commentsCount': ""}}, {multi: true, validate: false});
|
|
||||||
console.log("---------------------");
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
userDataCommentsCountToCommentCount: function(){
|
|
||||||
var i = 0;
|
|
||||||
Meteor.users.find({'commentCount': {$exists: false}}).forEach(function(user){
|
|
||||||
i++;
|
|
||||||
var commentCount = Comments.find({userId: user._id}).count();
|
|
||||||
console.log("User: " + user._id);
|
|
||||||
Meteor.users.update(user._id, {$set: { telescope : {'commentCount': commentCount}}});
|
|
||||||
console.log("---------------------");
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
clicksToClickCountForRealThisTime: function () { // since both fields might be co-existing, add to clickCount instead of overwriting it
|
|
||||||
var i = 0;
|
|
||||||
Posts.find({'clicks': {$exists: true}}).forEach(function (post) {
|
|
||||||
i++;
|
|
||||||
console.log("Post: " + post._id);
|
|
||||||
Posts.update(post._id, { $inc: { 'clickCount': post.clicks}, $unset: {'clicks': ""}}, {multi: true, validate: false});
|
|
||||||
console.log("---------------------");
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
normalizeCategories: function () {
|
|
||||||
var i = 0;
|
|
||||||
Posts.find({'categories': {$exists: true}}).forEach(function (post) {
|
|
||||||
i++;
|
|
||||||
console.log("Post: " + post._id);
|
|
||||||
var justCategoryIds = post.categories.map(function (category){
|
|
||||||
return category._id;
|
|
||||||
});
|
|
||||||
Posts.update(post._id, {$set: {categories: justCategoryIds, oldCategories: post.categories}}, {multi: true, validate: false});
|
|
||||||
console.log("---------------------");
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
cleanUpStickyProperty: function () {
|
|
||||||
var i = 0;
|
|
||||||
Posts.find({'sticky': {$exists: false}}).forEach(function (post) {
|
|
||||||
i++;
|
|
||||||
console.log("Post: " + post._id);
|
|
||||||
Posts.update(post._id, {$set: {sticky: false}}, {multi: true, validate: false});
|
|
||||||
console.log("---------------------");
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
show0112ReleaseNotes: function () {
|
|
||||||
var i = 0;
|
|
||||||
// if this is the 0.11.2 update, the first run event will not exist yet.
|
|
||||||
// if that's the case, make sure to still show release notes
|
|
||||||
if (!Events.findOne({name: 'firstRun'})) {
|
|
||||||
Releases.update({number:'0.11.2'}, {$set: {read:false}});
|
|
||||||
}
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
removeThumbnailHTTP: function () {
|
|
||||||
var i = 0;
|
|
||||||
Posts.find({thumbnailUrl: {$exists : true}}).forEach(function (post) {
|
|
||||||
i++;
|
|
||||||
var newThumbnailUrl = post.thumbnailUrl.replace("http:", "");
|
|
||||||
console.log("Post: "+post._id);
|
|
||||||
Posts.update(post._id, { $set: { 'thumbnailUrl': newThumbnailUrl}}, {multi: true, validate: false});
|
|
||||||
console.log("---------------------");
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
updateUserNames: function () {
|
|
||||||
var i = 0;
|
|
||||||
var allUsers = Meteor.users.find({username: {$exists: true}, profile: {$exists: true}, 'profile.isDummy': {$ne: true}});
|
|
||||||
|
|
||||||
console.log('> Found '+allUsers.count()+' users.\n');
|
|
||||||
|
|
||||||
allUsers.forEach(function(user){
|
|
||||||
i++;
|
|
||||||
|
|
||||||
// Perform the same transforms done by useraccounts with `lowercaseUsernames` set to `true`
|
|
||||||
var oldUsername = user.username;
|
|
||||||
var username = user.username;
|
|
||||||
username = username.trim().replace(/\s+/gm, ' ');
|
|
||||||
user.profile.username = user.profile.name || username;
|
|
||||||
delete user.profile.name;
|
|
||||||
username = username.toLowerCase().replace(/\s+/gm, '');
|
|
||||||
user.username = username;
|
|
||||||
|
|
||||||
if (user.emails && user.emails.length > 0) {
|
|
||||||
_.each(user.emails, function(email){
|
|
||||||
email.address = email.address.toLowerCase().replace(/\s+/gm, '');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('> Updating user '+user._id+' ('+oldUsername+' -> ' + user.username + ')');
|
|
||||||
|
|
||||||
try {
|
|
||||||
Meteor.users.update(user._id, {
|
|
||||||
$set: {
|
|
||||||
emails: user.emails,
|
|
||||||
profile: user.profile,
|
|
||||||
username: user.username,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
console.warn('> Unable to convert username ' + user.username + ' to lowercase!');
|
|
||||||
console.warn('> Please try to fix it by hand!! :(');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
changeColorNames: function () {
|
|
||||||
var i = 0;
|
|
||||||
var settings = Telescope.settings.collection.findOne();
|
|
||||||
var set = {};
|
|
||||||
|
|
||||||
if (!!settings) {
|
|
||||||
|
|
||||||
if (!!settings.buttonColor)
|
|
||||||
set.accentColor = settings.buttonColor;
|
|
||||||
|
|
||||||
if (!!settings.buttonTextColor)
|
|
||||||
set.accentContrastColor = settings.buttonTextColor;
|
|
||||||
|
|
||||||
if (!!settings.buttonColor)
|
|
||||||
set.secondaryColor = settings.headerColor;
|
|
||||||
|
|
||||||
if (!!settings.buttonColor)
|
|
||||||
set.secondaryContrastColor = settings.headerTextColor;
|
|
||||||
|
|
||||||
if (!_.isEmpty(set)) {
|
|
||||||
Telescope.settings.collection.update(settings._id, {$set: set}, {validate: false});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
migrateUserProfiles: function () {
|
|
||||||
var i = 0;
|
|
||||||
var allUsers = Meteor.users.find({telescope: {$exists: false}});
|
|
||||||
console.log('> Found '+allUsers.count()+' users.\n');
|
|
||||||
|
|
||||||
allUsers.forEach(function(user){
|
|
||||||
i++;
|
|
||||||
|
|
||||||
console.log('> Updating user '+user._id+' (' + user.username + ')');
|
|
||||||
|
|
||||||
var telescopeUserData = {};
|
|
||||||
|
|
||||||
// loop over user data schema
|
|
||||||
_.each(Telescope.schemas.userData._schema, function (property, key) {
|
|
||||||
|
|
||||||
if (!!user[key]) { // look for property on root of user object
|
|
||||||
telescopeUserData[key] = user[key];
|
|
||||||
} else if (user.votes && !!user.votes[key]) { // look for it in user.votes object
|
|
||||||
telescopeUserData[key] = user.votes[key];
|
|
||||||
} else if (user.profile && user.profile[key]) { // look for it in user.profile object
|
|
||||||
telescopeUserData[key] = user.profile[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// console.log(telescopeUserData);
|
|
||||||
|
|
||||||
try {
|
|
||||||
Meteor.users.update(user._id, {
|
|
||||||
$set: {
|
|
||||||
telescope: telescopeUserData
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
console.warn('> Unable to migrate profile for user ' + user.username);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
migrateEmailHash: function () {
|
|
||||||
var i = 0;
|
|
||||||
var allUsers = Meteor.users.find({$and: [{"email_hash": {$exists: true}}, {"telescope.emailHash": {$exists: false}}]});
|
|
||||||
console.log('> Found '+allUsers.count()+' users.\n');
|
|
||||||
|
|
||||||
allUsers.forEach(function(user){
|
|
||||||
i++;
|
|
||||||
|
|
||||||
console.log('> Updating user '+user._id+' (' + user.username + ')');
|
|
||||||
|
|
||||||
var emailHash = user.email_hash;
|
|
||||||
if (!!emailHash) {
|
|
||||||
Meteor.users.update(user._id, {$set: {"telescope.emailHash": emailHash}});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
// addTopLevelCommentIdToComments: function() {
|
|
||||||
// var i = 0;
|
|
||||||
|
|
||||||
// // find all root comments and set topLevelCommentId on their root children
|
|
||||||
// Comments.find({parentCommentId: {$exists : false}}).forEach(function (comment) {
|
|
||||||
|
|
||||||
// // topLevelCommentId is the root comment._id
|
|
||||||
// var topLevelCommentId = comment._id;
|
|
||||||
// console.log("Root Comment found: " + topLevelCommentId);
|
|
||||||
|
|
||||||
// // find childComments that have this root comment as parentCommentId
|
|
||||||
// Comments.find({parentCommentId: comment._id}).forEach(function (childComment) {
|
|
||||||
// i++;
|
|
||||||
// updateParentAndChild(topLevelCommentId, childComment._id);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// });
|
|
||||||
|
|
||||||
// function updateParentAndChild(topLevelCommentId, parentId) {
|
|
||||||
|
|
||||||
// i++;
|
|
||||||
// console.log("Parent Comment: " + parentId, " top level comment " + topLevelCommentId);
|
|
||||||
|
|
||||||
// Comments.update(parentId, {$set: {'topLevelCommentId': topLevelCommentId}}, {multi: false, validate: false});
|
|
||||||
|
|
||||||
// var childComments = Comments.find({topLevelCommentId: {$exists : false}, parentCommentId: parentId});
|
|
||||||
|
|
||||||
// console.log('> Found '+childComments.count()+' child comments.\n');
|
|
||||||
|
|
||||||
// childComments.forEach(function(childComment){
|
|
||||||
// i++;
|
|
||||||
|
|
||||||
// // find all nested childComments and set topLevelCommentId
|
|
||||||
// console.log("Child Comment: " + childComment._id, " top level comment " + topLevelCommentId);
|
|
||||||
|
|
||||||
// // set nested childComment to use parent's topLevelCommentId
|
|
||||||
// Comments.update(childComment._id, {$set: {'topLevelCommentId': topLevelCommentId}}, {multi: false, validate: false});
|
|
||||||
// updateParentAndChild(topLevelCommentId, childComment._id, true);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// }
|
|
||||||
// console.log("---------------------");
|
|
||||||
// return i;
|
|
||||||
// },
|
|
||||||
migrateDisplayName: function () {
|
|
||||||
var i = 0;
|
|
||||||
var displayName;
|
|
||||||
var allUsers = Meteor.users.find({"telescope.displayName": {$exists: false}});
|
|
||||||
console.log('> Found '+allUsers.count()+' users.\n');
|
|
||||||
|
|
||||||
allUsers.forEach(function(user){
|
|
||||||
i++;
|
|
||||||
|
|
||||||
console.log('> Updating user '+user._id+' (' + user.username + ')');
|
|
||||||
if (!!user.profile) {
|
|
||||||
displayName = user.profile.name || user.profile.username;
|
|
||||||
} else {
|
|
||||||
displayName = user.username;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('name: ', displayName);
|
|
||||||
if (!!displayName) {
|
|
||||||
Meteor.users.update(user._id, {$set: {"telescope.displayName": displayName}});
|
|
||||||
} else {
|
|
||||||
console.log("displayName not found :(");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
migrateNewsletterSettings: function () {
|
|
||||||
var i = 0;
|
|
||||||
var allUsers = Meteor.users.find({
|
|
||||||
$or: [
|
|
||||||
{"profile.showBanner": {$exists: true}},
|
|
||||||
{"profile.subscribedToNewsletter": {$exists: true}}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
console.log('> Found '+allUsers.count()+' users.\n');
|
|
||||||
|
|
||||||
allUsers.forEach(function(user){
|
|
||||||
i++;
|
|
||||||
var displayName;
|
|
||||||
|
|
||||||
if (!!user.profile) {
|
|
||||||
displayName = user.profile.name || user.profile.username;
|
|
||||||
} else {
|
|
||||||
displayName = user.username;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('> Updating user '+user._id+' (' + displayName + ')');
|
|
||||||
|
|
||||||
if (user.profile) {
|
|
||||||
|
|
||||||
var set = {};
|
|
||||||
|
|
||||||
var showBanner = user.profile.showBanner;
|
|
||||||
if (typeof showBanner !== "undefined") {
|
|
||||||
// BAD! See below...
|
|
||||||
set["telescope.newsletter.showBanner"] = showBanner;
|
|
||||||
}
|
|
||||||
|
|
||||||
var subscribeToNewsletter = user.profile.subscribedToNewsletter;
|
|
||||||
if (typeof subscribeToNewsletter !== "undefined") {
|
|
||||||
// BAD! `object["x.y.z"] = value` assigns value to a property
|
|
||||||
// called "x.y.z" instead of actually referencing object.x.y.z as intended.
|
|
||||||
// Perhaps this is how newsletter_subscribeToNewsletter happened?
|
|
||||||
// Maybe the dot got turned into an underscore somehow?
|
|
||||||
set["telescope.newsletter.subscribeToNewsletter"] = subscribeToNewsletter;
|
|
||||||
}
|
|
||||||
console.log(set)
|
|
||||||
if (!_.isEmpty(set)) {
|
|
||||||
Meteor.users.update(user._id, {$set: set});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
addSlugsToPosts: function () {
|
|
||||||
var i = 0;
|
|
||||||
Posts.find({slug: {$exists : false}}).forEach(function (post) {
|
|
||||||
i++;
|
|
||||||
var slug = Telescope.utils.slugify(post.title);
|
|
||||||
console.log("Post: "+post._id + " | "+slug);
|
|
||||||
Posts.update(post._id, { $set: { 'slug': slug}});
|
|
||||||
console.log("---------------------");
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
updateNewsletterFrequency: function () {
|
|
||||||
var i = 0;
|
|
||||||
Telescope.settings.collection.find().forEach(function (setting) {
|
|
||||||
if (!!setting.newsletterFrequency) {
|
|
||||||
console.log("Migrating newsletter frequency… ("+setting.newsletterFrequency+")");
|
|
||||||
i++;
|
|
||||||
var days;
|
|
||||||
switch (setting.newsletterFrequency) {
|
|
||||||
case 1:
|
|
||||||
days = [1,2,3,4,5,6,7];
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
days = [2,4,6];
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
days = [2,5];
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
days = [2];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Telescope.settings.collection.update(setting._id, { $set: { newsletterFrequency: days } });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
changeOutsideLinksPointTo: function () {
|
|
||||||
var i = 0;
|
|
||||||
Telescope.settings.collection.find({outsideLinksPointTo: {$exists : true}}).forEach(function (setting) {
|
|
||||||
i++;
|
|
||||||
Telescope.settings.collection.update(setting._id, {$set: {RSSLinksPointTo: setting.outsideLinksPointTo}});
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
renameNewsletterSettings: function () {
|
|
||||||
var i = 0;
|
|
||||||
var affectedUsers = Meteor.users.find({
|
|
||||||
$or: [
|
|
||||||
{"profile.showBanner": {$exists: true}},
|
|
||||||
{"profile.subscribedToNewsletter": {$exists: true}},
|
|
||||||
{'telescope.newsletter_subscribeToNewsletter': {$exists: true}}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('> Renaming newsletter settings: ' + affectedUsers.count() + ' users affected by this migration.\n');
|
|
||||||
|
|
||||||
affectedUsers.forEach(function (user) {
|
|
||||||
i++;
|
|
||||||
var displayName;
|
|
||||||
|
|
||||||
if (user.profile) {
|
|
||||||
displayName = user.profile.name || user.profile.username;
|
|
||||||
} else {
|
|
||||||
displayName = user.username;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('> Updating user ' + user._id + '(' + displayName + ')');
|
|
||||||
|
|
||||||
var showBannerSetting = user.profile && user.profile.showBanner;
|
|
||||||
if (showBannerSetting !== undefined) {
|
|
||||||
Meteor.users.update(user._id, {$set: {'telescope.newsletter.showBanner': showBannerSetting}});
|
|
||||||
}
|
|
||||||
|
|
||||||
var subscribedSetting =
|
|
||||||
(user.telescope && user.telescope.newsletter_subscribeToNewsletter)
|
|
||||||
||
|
|
||||||
(user.profile && user.profile.subscribedToNewsletter);
|
|
||||||
if (subscribedSetting !== undefined) {
|
|
||||||
Meteor.users.update(user._id, {$set: {'telescope.newsletter.subscribed': subscribedSetting}});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,21 +0,0 @@
|
||||||
Package.describe({
|
|
||||||
name: "migrations",
|
|
||||||
summary: "Telescope migrations package",
|
|
||||||
version: "0.27.2-nova",
|
|
||||||
git: "https://github.com/TelescopeJS/Telescope.git"
|
|
||||||
});
|
|
||||||
|
|
||||||
Package.onUse(function(api) {
|
|
||||||
|
|
||||||
api.versionsFrom("METEOR@1.0");
|
|
||||||
|
|
||||||
api.use(['nova:core@0.27.2-nova']);
|
|
||||||
|
|
||||||
api.addFiles([
|
|
||||||
'lib/server/migrations.js'
|
|
||||||
], ['server']);
|
|
||||||
|
|
||||||
api.export([
|
|
||||||
'Migrations'
|
|
||||||
]);
|
|
||||||
});
|
|
7
packages/_react-router-ssr/.npm/package/README
Normal file
7
packages/_react-router-ssr/.npm/package/README
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
This directory and the files immediately inside it are automatically generated
|
||||||
|
when you change this package's NPM dependencies. Commit the files in this
|
||||||
|
directory (npm-shrinkwrap.json, .gitignore, and this README) to source control
|
||||||
|
so that others run the same versions of sub-dependencies.
|
||||||
|
|
||||||
|
You should NOT check in the node_modules directory that Meteor automatically
|
||||||
|
creates; if you are using git, the .gitignore file tells git to ignore it.
|
60
packages/_react-router-ssr/.versions
Normal file
60
packages/_react-router-ssr/.versions
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
accounts-base@1.2.7
|
||||||
|
allow-deny@1.0.4
|
||||||
|
babel-compiler@6.6.4
|
||||||
|
babel-runtime@0.1.8
|
||||||
|
base64@1.0.8
|
||||||
|
binary-heap@1.0.8
|
||||||
|
blaze@2.1.7
|
||||||
|
blaze-tools@1.0.8
|
||||||
|
boilerplate-generator@1.0.8
|
||||||
|
callback-hook@1.0.8
|
||||||
|
check@1.2.1
|
||||||
|
chuangbo:cookie@1.1.0
|
||||||
|
ddp@1.2.5
|
||||||
|
ddp-client@1.2.7
|
||||||
|
ddp-common@1.2.5
|
||||||
|
ddp-rate-limiter@1.0.4
|
||||||
|
ddp-server@1.2.6
|
||||||
|
deps@1.0.12
|
||||||
|
diff-sequence@1.0.5
|
||||||
|
ecmascript@0.4.3
|
||||||
|
ecmascript-runtime@0.2.10
|
||||||
|
ejson@1.0.11
|
||||||
|
geojson-utils@1.0.8
|
||||||
|
html-tools@1.0.9
|
||||||
|
htmljs@1.0.9
|
||||||
|
id-map@1.0.7
|
||||||
|
jquery@1.11.8
|
||||||
|
livedata@1.0.18
|
||||||
|
localstorage@1.0.9
|
||||||
|
logging@1.0.12
|
||||||
|
meteor@1.1.14
|
||||||
|
meteorhacks:fast-render@2.16.0
|
||||||
|
meteorhacks:inject-data@2.0.0
|
||||||
|
meteorhacks:meteorx@1.4.1
|
||||||
|
meteorhacks:picker@1.0.3
|
||||||
|
minimongo@1.0.16
|
||||||
|
modules@0.6.1
|
||||||
|
modules-runtime@0.6.3
|
||||||
|
mongo@1.1.7
|
||||||
|
mongo-id@1.0.4
|
||||||
|
npm-mongo@1.4.43
|
||||||
|
observe-sequence@1.0.11
|
||||||
|
ordered-dict@1.0.7
|
||||||
|
promise@0.6.7
|
||||||
|
random@1.0.9
|
||||||
|
rate-limit@1.0.4
|
||||||
|
reactive-var@1.0.9
|
||||||
|
reactrouter:react-router-ssr@3.1.5
|
||||||
|
retry@1.0.7
|
||||||
|
routepolicy@1.0.10
|
||||||
|
service-configuration@1.0.9
|
||||||
|
spacebars@1.0.11
|
||||||
|
spacebars-compiler@1.0.11
|
||||||
|
tmeasday:check-npm-versions@0.2.0
|
||||||
|
tracker@1.0.13
|
||||||
|
ui@1.0.11
|
||||||
|
underscore@1.0.8
|
||||||
|
url@1.0.9
|
||||||
|
webapp@1.2.8
|
||||||
|
webapp-hashing@1.0.9
|
206
packages/_react-router-ssr/README.md
Normal file
206
packages/_react-router-ssr/README.md
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
# WARNING: This project is no longer in active maintenance. If you would like to maintain it, contact me at benoit@thereactivestack.com
|
||||||
|
|
||||||
|
Server-side rendering for react-router and react-meteor-data rehydratating Meteor subscriptions
|
||||||
|
|
||||||
|
It has a protection against leaking your data. Only subscribed data will be available just the way it would be on the client.
|
||||||
|
|
||||||
|
What about your SEO? Just `npm install react-helmet` and hook it with `htmlHook(html): string` (see the example below).
|
||||||
|
|
||||||
|
## Install
|
||||||
|
`meteor add reactrouter:react-router-ssr`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
### `ReactRouterSSR.Run(routes, [clientOptions], [serverOptions])`
|
||||||
|
The `routes` argument takes the routes you want react-router to use (you don't have to call `ReactDOM.render()` yourself)<br />
|
||||||
|
Read the [react-router documentation](https://github.com/rackt/react-router/tree/master/docs) for more informations.
|
||||||
|
|
||||||
|
#### routes
|
||||||
|
Your main `<Route />` node of your application.<br />
|
||||||
|
**Notice that there is no `<Router />` element, ReactRouterSSR takes care of creating it on the client and server with the correct parameters**
|
||||||
|
|
||||||
|
#### clientOptions (optional)
|
||||||
|
- `historyHook`: [function(history) : newHistory] - Hook something into history client side.
|
||||||
|
- `props` [object]: The additional arguments you would like to give to the `<Router />` component on the client.
|
||||||
|
- `wrapperHook` [function(App) : Component]: You can wrap the react-router element with your own providers.
|
||||||
|
- `rehydrateHook` [function(data)]: Receive the rehydrated object that was dehydrated during server side rendering.
|
||||||
|
- `rootElement` [string]: The root element ID your React application is mounted with (defaults to `react-app`)
|
||||||
|
- `rootElementType` [string]: Set the root element type (defaults to `div`)
|
||||||
|
- `rootElementAttributes`[array]: Set the root element attributes as an array of tag-value pairs. I.e. `[['class', sidebar main], ['style', 'background-color: white']]`
|
||||||
|
|
||||||
|
#### serverOptions (optional)
|
||||||
|
- `props` [object]: The additional arguments you would like to give to the `<Router />` component on the server.
|
||||||
|
- `htmlHook` [function(html) : newHtml]: Prepare the HTML before sending it to the client
|
||||||
|
- `historyHook` [function(history): newHistory]: Hook something on the history server side.
|
||||||
|
- `dehydrateHook` [function() : data]: Supply data that should be dehydrated and sent to client.
|
||||||
|
- `fetchDataHook` [function(components) : Array<Promise>]: Trigger the fetchData on your components that have it
|
||||||
|
- `preRender` [function(req, res)]: Executed just before the renderToString
|
||||||
|
- `postRender` [function(req, res)]: Executed just after the renderToString
|
||||||
|
- `dontMoveScripts` [bool]: Keep the script inside the head tag instead of moving it at the end of the body
|
||||||
|
- `disableSSR` [bool]: Disable server-side rendering, in case the application depends on code which doesn't work on the server.
|
||||||
|
- `loadingScreen` [string]: An HTML string to display while the page renders, in case the `disableSSR` option is set to true.
|
||||||
|
|
||||||
|
### Scripts
|
||||||
|
Unless you disabled it, the scripts yo have in the header will be moved down at the end of the body tag.
|
||||||
|
|
||||||
|
To keep a particuliar code in the head, you can add the `data-dont-move` attribute like this:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script data-dont-move>/* I'll stay in the head tag! */</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Simple Example
|
||||||
|
```javascript
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import ReactMixin from 'react-mixin';
|
||||||
|
import { IndexRoute, Route } from 'react-router';
|
||||||
|
import { ReactRouterSSR } from 'meteor/reactrouter:react-router-ssr';
|
||||||
|
|
||||||
|
AppRoutes = (
|
||||||
|
<Route path="/" component={App}>
|
||||||
|
<IndexRoute component={HomePage} />
|
||||||
|
<Route path="login" component={LoginPage} />
|
||||||
|
<Route path="*" component={NotFoundPage} />
|
||||||
|
{/* ... */}
|
||||||
|
</Route>
|
||||||
|
);
|
||||||
|
|
||||||
|
@ReactMixin(ReactMeteorData)
|
||||||
|
export default class HomePage extends Component
|
||||||
|
getMeteorData() {
|
||||||
|
Meteor.subscribe('profile');
|
||||||
|
|
||||||
|
return {
|
||||||
|
profile: Profile.findOne({ user: Meteor.userId() })
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <div>Hi {profile.name}</div>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ReactRouterSSR.Run(AppRoutes);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complex Example
|
||||||
|
```javascript
|
||||||
|
import { IndexRoute, Route } from 'react-router';
|
||||||
|
import ReactHelmet from 'react-helmet';
|
||||||
|
import ReactCookie from 'react-cookie';
|
||||||
|
|
||||||
|
AppRoutes = (
|
||||||
|
<Route path="/" component={App}>
|
||||||
|
<IndexRoute component={HomePage} />
|
||||||
|
<Route path="login" component={LoginPage} />
|
||||||
|
<Route path="*" component={NotFoundPage} />
|
||||||
|
{/* ... */}
|
||||||
|
</Route>
|
||||||
|
);
|
||||||
|
|
||||||
|
ReactRouterSSR.Run(AppRoutes, {
|
||||||
|
props: {
|
||||||
|
onUpdate() {
|
||||||
|
// Notify the page has been changed to Google Analytics
|
||||||
|
ga('send', 'pageview');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
htmlHook(html) {
|
||||||
|
const head = ReactHelmet.rewind();
|
||||||
|
return html.replace('<head>', '<head>' + head.title + head.base + head.meta + head.link + head.script);
|
||||||
|
},
|
||||||
|
preRender: function(req, res) {
|
||||||
|
ReactCookie.plugToRequest(req, res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Meteor.isClient) {
|
||||||
|
// Load Google Analytics
|
||||||
|
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||||
|
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||||
|
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||||
|
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||||
|
|
||||||
|
ga('create', 'UA-XXXXXXXX-X', 'auto');
|
||||||
|
ga('send', 'pageview');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example with Redux
|
||||||
|
|
||||||
|
ReactRouterSSR supports applications that use Redux, using the `rehydrateHook` and `dehydrateHook` options in clientOptions and serverOptions respectively.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import React from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
|
import routes from './routes';
|
||||||
|
import configureStore from './store';
|
||||||
|
|
||||||
|
// Data that is populated by hooks during startup
|
||||||
|
let history;
|
||||||
|
let store;
|
||||||
|
let initialState;
|
||||||
|
|
||||||
|
// Use history hook to get a reference to the history object
|
||||||
|
const historyHook = newHistory => history = newHistory;
|
||||||
|
|
||||||
|
// Pass the state of the store as the object to be dehydrated server side
|
||||||
|
const dehydrateHook = () => store.getState();
|
||||||
|
|
||||||
|
// Take the rehydrated state and use it as the initial state client side
|
||||||
|
const rehydrateHook = state => initialState = state;
|
||||||
|
|
||||||
|
// Create a redux store and pass into the redux Provider wrapper
|
||||||
|
const wrapperHook = app => {
|
||||||
|
store = configureStore(initialState, history);
|
||||||
|
return <Provider store={store}>{app}</Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientOptions = { historyHook, rehydrateHook, wrapperHook };
|
||||||
|
const serverOptions = { historyHook, dehydrateHook };
|
||||||
|
|
||||||
|
ReactRouterSSR.Run(routes, clientOptions, serverOptions);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client-side data rehydration
|
||||||
|
ReactRouterSSR provides hooks to make use of client-side data rehydration:
|
||||||
|
|
||||||
|
- On server side, once rendering is done, the data returned from dehydrateHook is serialized (using `JSON.stringify()`) and sent to the client as part of the generated HTML.
|
||||||
|
- On the client side, that serialized data is rehydrated and passed to the client via rehydrateHook.
|
||||||
|
|
||||||
|
#### Data serialization
|
||||||
|
The `JSON.stringify()` serialization means that, if your data holds "rich" domain objects with methods attached though prototypes or ES6 classes (for example documents fetched from Mongo collections with an associated transform, or [ImmutableJS](https://facebook.github.io/immutable-js) structures...), the client receives them downcasted to Plain Old Javascript Objects (without prototypes or methods) in the 'data'.
|
||||||
|
|
||||||
|
It is then the responsibility of the client code to "upcast" them back to the expected domain objects. In the case of redux it is recommended to handle that in each of the relevant reducers, by taking advantage of the fact that redux's `createStore()` dispatches an internal action with the 'initialState' it has been passed (which, in our case, is the unserialized state coming from the server rendering.)
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
- for a reducer that stores a document read from a collection that has a transform attached :
|
||||||
|
```js
|
||||||
|
function myReducer(state = {}, action) {
|
||||||
|
// If needed, upcast the raw state passed by the server SSR.
|
||||||
|
if (typeof state.expectedHelper === 'undefined') { // Or some other check for MyDomainClass ?
|
||||||
|
state = transform(state); // Where transform is the same transform you assigned to your collection
|
||||||
|
}
|
||||||
|
// Then the usual action matching :
|
||||||
|
switch (action.type) {
|
||||||
|
... return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- for a reducer that stores ImmutableJS structures, [redux-immutablejs](https://github.com/indexiatech/redux-immutablejs)'s createReducer() helper accepts an optional 'constructor' argument that does exactly that (defaults to `Immutable.fromJS()`).
|
||||||
|
|
||||||
|
### Server-side pre-render data fetching (optional)
|
||||||
|
On the server-side, ReactRouterSSR implements the "fetchData" mechanism mentioned at the bottom of [the Redux doc on Server-Side Rendering](http://rackt.org/redux/docs/recipes/ServerRendering.html):
|
||||||
|
|
||||||
|
The route components (e.g. `App`, `HomePage`, `LoginPage`... in the example above) can optionally specify a static fetchData() method to pre-populate the store with external data before rendering happens.
|
||||||
|
That fetchData() method, if present, will be automatically called for the components of the matched route (e.g. on `App` and `HomePage` for the url `'/'` in the example above).
|
||||||
|
|
||||||
|
The fetchData() method receives:
|
||||||
|
|
||||||
|
- the store's `getState` function,
|
||||||
|
- the store's `dispatch` function,
|
||||||
|
- the routing props for the resolved route (notably including `location` and `params`)
|
||||||
|
|
||||||
|
and can dispatch async actions for external data fetching, returning the corresponding Promise. Rendering is then deferred until all Promises are resolved.
|
76
packages/_react-router-ssr/lib/client.jsx
Normal file
76
packages/_react-router-ssr/lib/client.jsx
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { Router, browserHistory } from 'react-router';
|
||||||
|
|
||||||
|
const ReactRouterSSR = {
|
||||||
|
Run(routes, clientOptions) {
|
||||||
|
if (!clientOptions) {
|
||||||
|
clientOptions = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let history = browserHistory;
|
||||||
|
|
||||||
|
if(typeof clientOptions.historyHook === 'function') {
|
||||||
|
history = clientOptions.historyHook(history);
|
||||||
|
}
|
||||||
|
|
||||||
|
Meteor.startup(function() {
|
||||||
|
const rootElementName = clientOptions.rootElement || 'react-app';
|
||||||
|
const rootElementType = clientOptions.rootElementType || 'div';
|
||||||
|
const attributes = clientOptions.rootElementAttributes instanceof Array ? clientOptions.rootElementAttributes : [];
|
||||||
|
let rootElement = document.getElementById(rootElementName);
|
||||||
|
|
||||||
|
// In case the root element doesn't exist, let's create it
|
||||||
|
if (!rootElement) {
|
||||||
|
rootElement = document.createElement(rootElementType);
|
||||||
|
rootElement.id = rootElementName;
|
||||||
|
|
||||||
|
// check if a 2-dimensional array was passed... if not, be nice and handle it anyway
|
||||||
|
if(attributes[0] instanceof Array) {
|
||||||
|
// set attributes
|
||||||
|
for(var i = 0; i < attributes.length; i++) {
|
||||||
|
rootElement.setAttribute(attributes[i][0], attributes[i][1]);
|
||||||
|
}
|
||||||
|
} else if (attributes.length > 0){
|
||||||
|
rootElement.setAttribute(attributes[0], attributes[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(rootElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rehydrate data client side, if desired.
|
||||||
|
if(typeof clientOptions.rehydrateHook === 'function') {
|
||||||
|
InjectData.getData('dehydrated-initial-data', data => {
|
||||||
|
const rehydratedData = data ? JSON.parse(data) : undefined;
|
||||||
|
clientOptions.rehydrateHook(rehydratedData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let app = (
|
||||||
|
<Router
|
||||||
|
history={history}
|
||||||
|
children={routes}
|
||||||
|
{...clientOptions.props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typeof clientOptions.wrapperHook === 'function') {
|
||||||
|
app = clientOptions.wrapperHook(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof clientOptions.renderHook === 'function') {
|
||||||
|
clientOptions.renderHook(app, rootElement);
|
||||||
|
} else {
|
||||||
|
ReactDOM.render(app, rootElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
let collectorEl = document.getElementById(clientOptions.styleCollectorId || 'css-style-collector-data')
|
||||||
|
|
||||||
|
if (collectorEl) {
|
||||||
|
collectorEl.parentNode.removeChild(collectorEl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ReactRouterSSR };
|
||||||
|
export default ReactRouterSSR;
|
12
packages/_react-router-ssr/lib/react-router-ssr.js
vendored
Normal file
12
packages/_react-router-ssr/lib/react-router-ssr.js
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { checkNpmVersions } from 'meteor/tmeasday:check-npm-versions';
|
||||||
|
checkNpmVersions({
|
||||||
|
'react': '15.x',
|
||||||
|
'react-dom': '15.x',
|
||||||
|
'react-router': '3.x'
|
||||||
|
}, 'reactrouter:react-router-ssr');
|
||||||
|
|
||||||
|
if (Meteor.isClient) {
|
||||||
|
ReactRouterSSR = require('./client.jsx').default;
|
||||||
|
} else {
|
||||||
|
ReactRouterSSR = require('./server.jsx').default;
|
||||||
|
}
|
268
packages/_react-router-ssr/lib/server.jsx
Normal file
268
packages/_react-router-ssr/lib/server.jsx
Normal file
|
@ -0,0 +1,268 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
match as ReactRouterMatch,
|
||||||
|
RouterContext,
|
||||||
|
createMemoryHistory
|
||||||
|
} from 'react-router';
|
||||||
|
|
||||||
|
import SsrContext from './ssr_context';
|
||||||
|
import patchSubscribeData from './ssr_data';
|
||||||
|
|
||||||
|
import ReactDOMServer from 'react-dom/server';
|
||||||
|
import cookieParser from 'cookie-parser';
|
||||||
|
import Cheerio from 'cheerio';
|
||||||
|
|
||||||
|
function IsAppUrl(req) {
|
||||||
|
var url = req.url;
|
||||||
|
if(url === '/favicon.ico' || url === '/robots.txt') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(url === '/app.manifest') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid serving app HTML for declared routes such as /sockjs/.
|
||||||
|
if(RoutePolicy.classify(url)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let webpackStats;
|
||||||
|
|
||||||
|
const ReactRouterSSR = {};
|
||||||
|
export default ReactRouterSSR;
|
||||||
|
|
||||||
|
// creating some EnvironmentVariables that will be used later on
|
||||||
|
ReactRouterSSR.ssrContext = new Meteor.EnvironmentVariable();
|
||||||
|
ReactRouterSSR.inSubscription = new Meteor.EnvironmentVariable(); // <-- needed in ssr_data.js
|
||||||
|
|
||||||
|
ReactRouterSSR.LoadWebpackStats = function(stats) {
|
||||||
|
webpackStats = stats;
|
||||||
|
};
|
||||||
|
|
||||||
|
ReactRouterSSR.Run = function(routes, clientOptions, serverOptions) {
|
||||||
|
// this line just patches Subscribe and find mechanisms
|
||||||
|
patchSubscribeData(ReactRouterSSR);
|
||||||
|
|
||||||
|
if (!clientOptions) {
|
||||||
|
clientOptions = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!serverOptions) {
|
||||||
|
serverOptions = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!serverOptions.webpackStats) {
|
||||||
|
serverOptions.webpackStats = webpackStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
Meteor.bindEnvironment(function() {
|
||||||
|
WebApp.rawConnectHandlers.use(cookieParser());
|
||||||
|
|
||||||
|
WebApp.connectHandlers.use(Meteor.bindEnvironment(function(req, res, next) {
|
||||||
|
if (!IsAppUrl(req)) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global.__CHUNK_COLLECTOR__ = [];
|
||||||
|
|
||||||
|
var loginToken = req.cookies['meteor_login_token'];
|
||||||
|
var headers = req.headers;
|
||||||
|
var context = new FastRender._Context(loginToken, { headers });
|
||||||
|
|
||||||
|
|
||||||
|
FastRender.frContext.withValue(context, function() {
|
||||||
|
let history = createMemoryHistory(req.url);
|
||||||
|
|
||||||
|
if (typeof serverOptions.historyHook === 'function') {
|
||||||
|
history = serverOptions.historyHook(history);
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactRouterMatch({ history, routes, location: req.url }, Meteor.bindEnvironment((err, redirectLocation, renderProps) => {
|
||||||
|
if (err) {
|
||||||
|
res.writeHead(500);
|
||||||
|
res.write(err.messages);
|
||||||
|
res.end();
|
||||||
|
} else if (redirectLocation) {
|
||||||
|
res.writeHead(302, { Location: redirectLocation.pathname + redirectLocation.search });
|
||||||
|
res.end();
|
||||||
|
} else if (renderProps) {
|
||||||
|
sendSSRHtml(clientOptions, serverOptions, req, res, next, renderProps);
|
||||||
|
} else {
|
||||||
|
res.writeHead(404);
|
||||||
|
res.write('Not found');
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
|
||||||
|
function sendSSRHtml(clientOptions, serverOptions, req, res, next, renderProps) {
|
||||||
|
const { css, html } = generateSSRData(clientOptions, serverOptions, req, res, renderProps);
|
||||||
|
res.write = patchResWrite(clientOptions, serverOptions, res.write, css, html);
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchResWrite(clientOptions, serverOptions, originalWrite, css, html) {
|
||||||
|
return function(data) {
|
||||||
|
if(typeof data === 'string' && data.indexOf('<!DOCTYPE html>') === 0) {
|
||||||
|
if (!serverOptions.dontMoveScripts) {
|
||||||
|
data = moveScripts(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (css) {
|
||||||
|
data = data.replace('</head>', '<style id="' + (clientOptions.styleCollectorId || 'css-style-collector-data') + '">' + css + '</style></head>');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof serverOptions.htmlHook === 'function') {
|
||||||
|
data = serverOptions.htmlHook(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rootElementAttributes = '';
|
||||||
|
const attributes = clientOptions.rootElementAttributes instanceof Array ? clientOptions.rootElementAttributes : [];
|
||||||
|
if(attributes[0] instanceof Array) {
|
||||||
|
for(var i = 0; i < attributes.length; i++) {
|
||||||
|
rootElementAttributes = rootElementAttributes + ' ' + attributes[i][0] + '="' + attributes[i][1] + '"';
|
||||||
|
}
|
||||||
|
} else if (attributes.length > 0){
|
||||||
|
rootElementAttributes = ' ' + attributes[0] + '="' + attributes[1] + '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
data = data.replace('<body>', '<body><' + (clientOptions.rootElementType || 'div') + ' id="' + (clientOptions.rootElement || 'react-app') + '"' + rootElementAttributes + '>' + html + '</' + (clientOptions.rootElementType || 'div') + '>');
|
||||||
|
|
||||||
|
if (typeof serverOptions.webpackStats !== 'undefined') {
|
||||||
|
data = addAssetsChunks(serverOptions, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
originalWrite.call(this, data);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAssetsChunks(serverOptions, data) {
|
||||||
|
const chunkNames = serverOptions.webpackStats.assetsByChunkName;
|
||||||
|
const publicPath = serverOptions.webpackStats.publicPath;
|
||||||
|
|
||||||
|
if (typeof chunkNames.common !== 'undefined') {
|
||||||
|
var chunkSrc = (typeof chunkNames.common === 'string')?
|
||||||
|
chunkNames.common :
|
||||||
|
chunkNames.common[0];
|
||||||
|
|
||||||
|
data = data.replace('<head>', '<head><script type="text/javascript" src="' + publicPath + chunkSrc + '"></script>');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < global.__CHUNK_COLLECTOR__.length; ++i) {
|
||||||
|
if (typeof chunkNames[global.__CHUNK_COLLECTOR__[i]] !== 'undefined') {
|
||||||
|
chunkSrc = (typeof chunkNames[global.__CHUNK_COLLECTOR__[i]] === 'string')?
|
||||||
|
chunkNames[global.__CHUNK_COLLECTOR__[i]] :
|
||||||
|
chunkNames[global.__CHUNK_COLLECTOR__[i]][0];
|
||||||
|
|
||||||
|
data = data.replace('</head>', '<script type="text/javascript" src="' + publicPath + chunkSrc + '"></script></head>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSSRData(clientOptions, serverOptions, req, res, renderProps) {
|
||||||
|
let html, css;
|
||||||
|
|
||||||
|
// we're stealing all the code from FlowRouter SSR
|
||||||
|
// https://github.com/kadirahq/flow-router/blob/ssr/server/route.js#L61
|
||||||
|
const ssrContext = new SsrContext();
|
||||||
|
|
||||||
|
ReactRouterSSR.ssrContext.withValue(ssrContext, () => {
|
||||||
|
try {
|
||||||
|
const frData = InjectData.getData(res, 'fast-render-data');
|
||||||
|
if (frData) {
|
||||||
|
ssrContext.addData(frData.collectionData);
|
||||||
|
}
|
||||||
|
if (serverOptions.preRender) {
|
||||||
|
serverOptions.preRender(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncomment these two lines if you want to easily trigger
|
||||||
|
// multiple client requests from different browsers at the same time
|
||||||
|
|
||||||
|
// console.log('sarted sleeping');
|
||||||
|
// Meteor._sleepForMs(5000);
|
||||||
|
// console.log('ended sleeping');
|
||||||
|
|
||||||
|
global.__STYLE_COLLECTOR_MODULES__ = [];
|
||||||
|
global.__STYLE_COLLECTOR__ = '';
|
||||||
|
|
||||||
|
renderProps = {
|
||||||
|
...renderProps,
|
||||||
|
...serverOptions.props
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchComponentData(serverOptions, renderProps);
|
||||||
|
let app = <RouterContext {...renderProps} />;
|
||||||
|
|
||||||
|
if (typeof clientOptions.wrapperHook === 'function') {
|
||||||
|
app = clientOptions.wrapperHook(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!serverOptions.disableSSR){
|
||||||
|
html = ReactDOMServer.renderToString(app);
|
||||||
|
} else if (serverOptions.loadingScreen){
|
||||||
|
html = serverOptions.loadingScreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
css = global.__STYLE_COLLECTOR__;
|
||||||
|
|
||||||
|
if (typeof serverOptions.dehydrateHook === 'function') {
|
||||||
|
InjectData.pushData(res, 'dehydrated-initial-data', JSON.stringify(serverOptions.dehydrateHook()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverOptions.postRender) {
|
||||||
|
serverOptions.postRender(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// I'm pretty sure this could be avoided in a more elegant way?
|
||||||
|
const context = FastRender.frContext.get();
|
||||||
|
const data = context.getData();
|
||||||
|
InjectData.pushData(res, 'fast-render-data', data);
|
||||||
|
}
|
||||||
|
catch(err) {
|
||||||
|
console.error(new Date(), 'error while server-rendering', err.stack);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { html, css };
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchComponentData(serverOptions, renderProps) {
|
||||||
|
const componentsWithFetch = renderProps.components
|
||||||
|
.filter(component => !!component)
|
||||||
|
.filter(component => component.fetchData);
|
||||||
|
|
||||||
|
if (!componentsWithFetch.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Package.promise) {
|
||||||
|
console.error("react-router-ssr: Support for fetchData() static methods on route components requires the 'promise' package.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promises = serverOptions.fetchDataHook(componentsWithFetch);
|
||||||
|
Promise.awaitAll(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveScripts(data) {
|
||||||
|
const $ = Cheerio.load(data, {
|
||||||
|
decodeEntities: false
|
||||||
|
});
|
||||||
|
const heads = $('head script');
|
||||||
|
$('body').append(heads);
|
||||||
|
$('head').html($('head').html().replace(/(^[ \t]*\n)/gm, ''));
|
||||||
|
|
||||||
|
return $.html();
|
||||||
|
}
|
51
packages/_react-router-ssr/lib/ssr_context.js
Normal file
51
packages/_react-router-ssr/lib/ssr_context.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
// server/ssr_context.js
|
||||||
|
// stolen from https://github.com/kadirahq/flow-router/blob/ssr/server/ssr_context.js
|
||||||
|
|
||||||
|
import deepMerge from 'deepmerge';
|
||||||
|
|
||||||
|
export default class SsrContext {
|
||||||
|
constructor() {
|
||||||
|
this._collections = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
getCollection(collName) {
|
||||||
|
let collection = this._collections[collName];
|
||||||
|
if (!collection) {
|
||||||
|
const minimongo = Package.minimongo;
|
||||||
|
collection = this._collections[collName] = new minimongo.LocalCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
addSubscription(name, params) {
|
||||||
|
const fastRenderContext = FastRender.frContext.get();
|
||||||
|
if (!fastRenderContext) {
|
||||||
|
throw new Error(
|
||||||
|
`Cannot add a subscription: ${name} without FastRender Context`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = [name].concat(params);
|
||||||
|
const data = fastRenderContext.subscribe(...args);
|
||||||
|
this.addData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
addData(data) {
|
||||||
|
_.each(data, (collDataCollection, collectionName) => {
|
||||||
|
const collection = this.getCollection(collectionName);
|
||||||
|
collDataCollection.forEach((collData) => {
|
||||||
|
collData.forEach((item) => {
|
||||||
|
const existingDoc = collection.findOne(item._id);
|
||||||
|
if (existingDoc) {
|
||||||
|
const newDoc = deepMerge(existingDoc, item);
|
||||||
|
delete newDoc._id;
|
||||||
|
collection.update(item._id, newDoc);
|
||||||
|
} else {
|
||||||
|
collection.insert(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
89
packages/_react-router-ssr/lib/ssr_data.js
Normal file
89
packages/_react-router-ssr/lib/ssr_data.js
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
// server/ssr_data.js
|
||||||
|
// stolen from https://github.com/kadirahq/flow-router/blob/ssr/server/ssr_data.js
|
||||||
|
|
||||||
|
export default function patchSubscribeData (ReactRouterSSR) {
|
||||||
|
const originalSubscribe = Meteor.subscribe;
|
||||||
|
|
||||||
|
Meteor.subscribe = function(pubName) {
|
||||||
|
const params = Array.prototype.slice.call(arguments, 1);
|
||||||
|
|
||||||
|
const ssrContext = ReactRouterSSR.ssrContext.get();
|
||||||
|
if (ssrContext) {
|
||||||
|
ReactRouterSSR.inSubscription.withValue(true, () => {
|
||||||
|
ssrContext.addSubscription(pubName, params);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalSubscribe) {
|
||||||
|
originalSubscribe.apply(this, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ready: () => true
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const Mongo = Package.mongo.Mongo;
|
||||||
|
const originalFind = Mongo.Collection.prototype.find;
|
||||||
|
|
||||||
|
Mongo.Collection.prototype.find = function(selector = {}, options = {}) {
|
||||||
|
selector = selector || {};
|
||||||
|
const ssrContext = ReactRouterSSR.ssrContext.get();
|
||||||
|
if (ssrContext && !ReactRouterSSR.inSubscription.get()) {
|
||||||
|
const collName = this._name;
|
||||||
|
|
||||||
|
// this line is added just to make sure this works CollectionFS
|
||||||
|
if (typeof this._transform === 'function') {
|
||||||
|
options.transform = this._transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
const collection = ssrContext.getCollection(collName);
|
||||||
|
const cursor = collection.find(selector, options);
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalFind.call(this, selector, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
// We must implement this. Otherwise, it'll call the origin prototype's
|
||||||
|
// find method
|
||||||
|
Mongo.Collection.prototype.findOne = function(selector, options) {
|
||||||
|
options = options || {};
|
||||||
|
options.limit = 1;
|
||||||
|
return this.find(selector, options).fetch()[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
const originalAutorun = Tracker.autorun;
|
||||||
|
|
||||||
|
Tracker.autorun = (fn) => {
|
||||||
|
// if autorun is in the ssrContext, we need fake and run the callback
|
||||||
|
// in the same eventloop
|
||||||
|
if (ReactRouterSSR.ssrContext.get()) {
|
||||||
|
const c = { firstRun: true, stop: () => {} };
|
||||||
|
fn(c);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalAutorun.call(Tracker, fn);
|
||||||
|
};
|
||||||
|
|
||||||
|
// By default, Meteor[call,apply] also inherit SsrContext
|
||||||
|
// So, they can't access the full MongoDB dataset because of that
|
||||||
|
// Then, we need to remove the SsrContext within Method calls
|
||||||
|
['call', 'apply'].forEach((methodName) => {
|
||||||
|
const original = Meteor[methodName];
|
||||||
|
Meteor[methodName] = (...args) => {
|
||||||
|
const response = ReactRouterSSR.ssrContext.withValue(null, () => {
|
||||||
|
return original.apply(this, args);
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// This is not available in the server. But to make it work with SSR
|
||||||
|
// We need to have it.
|
||||||
|
Meteor.loggingIn = () => {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
42
packages/_react-router-ssr/package.js
Normal file
42
packages/_react-router-ssr/package.js
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
Package.describe({
|
||||||
|
name: 'reactrouter:react-router-ssr',
|
||||||
|
version: '3.1.6-nova-patch',
|
||||||
|
summary: 'Server-side rendering for react-router and react-meteor-data rehydratating Meteor subscriptions',
|
||||||
|
git: 'https://github.com/thereactivestack/meteor-react-router-ssr.git',
|
||||||
|
documentation: 'README.md'
|
||||||
|
});
|
||||||
|
|
||||||
|
Npm.depends({
|
||||||
|
'cookie-parser': '1.4.1',
|
||||||
|
'cheerio': '0.20.0',
|
||||||
|
'deepmerge': '0.2.10'
|
||||||
|
});
|
||||||
|
|
||||||
|
Package.onUse(function(api) {
|
||||||
|
api.versionsFrom('1.3');
|
||||||
|
api.use([
|
||||||
|
'ecmascript',
|
||||||
|
'tracker',
|
||||||
|
'minimongo@1.0.0',
|
||||||
|
'meteorhacks:fast-render@2.16.0',
|
||||||
|
'meteorhacks:inject-data@2.0.1-nova-patch',
|
||||||
|
'tmeasday:check-npm-versions@0.2.0'
|
||||||
|
]);
|
||||||
|
|
||||||
|
api.use([
|
||||||
|
'underscore@1.0.3',
|
||||||
|
'webapp@1.2.0',
|
||||||
|
'mongo@1.0.0',
|
||||||
|
'routepolicy@1.0.5',
|
||||||
|
'url@1.0.9'
|
||||||
|
], 'server');
|
||||||
|
|
||||||
|
api.use([
|
||||||
|
'autopublish@1.0.0',
|
||||||
|
'tmeasday:publish-counts@0.7.0',
|
||||||
|
'promise@0.5.1'
|
||||||
|
], 'server', {weak: true})
|
||||||
|
|
||||||
|
api.export('ReactRouterSSR');
|
||||||
|
api.mainModule('lib/react-router-ssr.js');
|
||||||
|
});
|
36
packages/_react-router-ssr/package.json
Normal file
36
packages/_react-router-ssr/package.json
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"name": "react-router-ssr",
|
||||||
|
"version": "3.0.0",
|
||||||
|
"description": "Server-side rendering for react-router and react-meteor-data rehydratating Meteor subscriptions",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/thereactivestack/meteor-react-router-ssr.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"meteor",
|
||||||
|
"react",
|
||||||
|
"reactrouter",
|
||||||
|
"react-router",
|
||||||
|
"ssr",
|
||||||
|
"server-rendering"
|
||||||
|
],
|
||||||
|
"author": "Benoit Tremblay <benoit@thereactivestack.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/thereactivestack/meteor-react-router-ssr/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/thereactivestack/meteor-react-router-ssr#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"cheerio": "^0.20.0",
|
||||||
|
"cookie-parser": "^1.4.1",
|
||||||
|
"promise": "^7.1.1",
|
||||||
|
"react": "^15.3.0",
|
||||||
|
"react-dom": "^15.3.0",
|
||||||
|
"react-helmet": "^3.1.0",
|
||||||
|
"react-router": "^2.0.1",
|
||||||
|
"underscore": "^1.8.3"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue