Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SSR The config module #11352

Merged
merged 9 commits into from
Mar 6, 2017
Merged

SSR The config module #11352

merged 9 commits into from
Mar 6, 2017

Conversation

samouri
Copy link
Contributor

@samouri samouri commented Feb 10, 2017

Calypso currently has different configuration for its different stages and the way that we handle it causes some inefficiencies. We have almost the same exact modules for wpcalypso, horizon, staging, and production. The only difference between them is the config module. This single module difference essentially forces us to separate out bundles for each environment and call the bundler 4 times within a docker build.

This PR seeks to move the config module to be sent by the server as part of the initial payload which:

  1. cuts build times in a down because we don't need to rebuild docker image 4 times (and have 4 different versions of the bundler: bundle-wpcalypso.js, bundle-production.js, etc.
  2. No longer need to generate a config before running unit tests

Speed tests on my machine:

  • Building the docker image took 4:44.62 in this branch /try/framework/ssr-config
  • 11:44.25 on master

To Test

  • start calypso using make run
  • run all the unit tests (and also circle ci)
  • Open https://calypso.live/?branch=try/framework/ssr-config and make sure it still works
  • build the docker image. then run it in the 4 different environments and make sure it works as expected

related issues this should solve as well:

@matticbot
Copy link
Contributor

@samouri samouri force-pushed the try/framework/ssr-config branch 2 times, most recently from 1f3dc51 to 74a0d6c Compare February 11, 2017 08:12
@@ -0,0 +1,11 @@
let config;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo: add comment or readme explaining whats going on

@gziolo
Copy link
Member

gziolo commented Feb 11, 2017

Cool idea! Thanks for working on it. It would be great to improve config handling, in particular in the context of unit tests.

I'll double check this proposal on Monday. There might be security concern with this approach, because at least on desktop there are secret keys loaded in the server build. This code would probably expose those keys to the client. It needs to be verified.

@samouri samouri force-pushed the try/framework/ssr-config branch from 9fa2c85 to e9c32ff Compare February 12, 2017 12:39
config.isEnabled = window.isEnabled;
} else {
throw new Error( 'config not available' );
}
Copy link
Member

@dmsnell dmsnell Feb 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can flip this conditional and then use a constant value…

/**
 * Manages config flags for various deployment builds
 * @module config/index
 */

if ( 'undefined' === typeof window ||  ! window.config ) {
	throw new ReferenceError( `No configuration was found: please see client/config/README.md for more information` );
}

/**
 * @typedef {Object} CalypsoConfig
 * @property {Function} anyEnabled whether one of a set of properties is enabled
 * @property {Function} isEnabled whether a specific property is enabled
 */
export default config = {
	...window.config,
	anyEnabled: window.anyEnabled,
	isEnabled: window.isEnabled,
};

Another thing to think about that may or may not come into play here is that Object.assign() sets values while destructuring assignment defines them. These are small semantic differences and if we have any properties defined on config then we may want to make sure we carry those over (and not run or evaluate them while copying).

http://www.2ality.com/2016/10/rest-spread-properties.html

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the docs :). I'm definitely going to put those in. Also TIL about ReferenceError!

Another thing to think about that may or may not come into play here is that Object.assign() sets values while destructuring assignment defines them.

window.config is a function here. I don't think spread works for those...does it? I usually prefer spread/object literal when possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

window.config is a function here. I don't think spread works for those

Just tested it out in the online babel repl

a = () => 5;
b = { ...a }

console.log( a() );
console.log( b() );

transpiled to:

"use strict";

var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };

a = function a() {
  return 5;
};
b = _extends({}, a);

console.log(a());
console.log(b());

which printed out:

Uncaught TypeError: b is not a function
    at <anonymous>:1:1

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

absolutely - I didn't realize/think about that it was a function. that's too bad, but thanks for verifying that and adjusting accordingly

Copy link
Member

@dmsnell dmsnell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@samouri thanks for making this change. If we can get this ready to go it's going to be a major boon for development.

I'm not that familiar with this part of the build process so I don't have much to offer. However, I would definitely implore you to spend some time understanding the scope of the improvements you are bringing and the system they change and then write up some guides for the benefit of others (README.md is fair enough)

This would significantly help further efforts to improve the build process.

👍

@alisterscott
Copy link
Contributor

I spun this up locally in docker and ran some e2e tests against it, couldn't find any issues.
Noticed it was faster than usually spinning up in Docker, well done.

@ehg
Copy link
Member

ehg commented Feb 14, 2017

One problem with this that comes to mind is that we won't have the correct build time config for each environment. For example, when building the sections we use config all over the place: https://github.com/Automattic/wp-calypso/blob/master/client/wordpress-com.js#L160

@samouri
Copy link
Contributor Author

samouri commented Feb 14, 2017

i think @ehg is right, I can't access devdocs in the docker instance for some reason even though config.isEnabled('devdocs') --> true

I'm testing out putting CALYPSO_ENV env var before the $(BUNDLER) call in the makefile

EDIT: its this line https://github.com/Automattic/wp-calypso/pull/11352/files#diff-3254677a7917c6c01f55212f86c57fbfR42

Since it builds with CALYPSO_ENV=production, the right bundle (devdocs) never gets generated. This just got more complex 🤔

@samouri
Copy link
Contributor Author

samouri commented Feb 14, 2017

A solution i came up with that I think may be pretty valid is:

Always make all the bundles.

The client can request which bundles it needs. There is one big constraint that this idea would create though:

  • We cannot expect/make a bundle contain different things for different environments. I think this is reasonable. If you want code to be different/only run in certain envs, it needs to be in its own bundle

@dmsnell
Copy link
Member

dmsnell commented Feb 14, 2017

The client can request which bundles it needs. There is one big constraint that this idea would create though:

We cannot expect/make a bundle contain different things for different environments. I think this is reasonable. If you want code to be different/only run in certain envs, it needs to be in its own bundle

related is existing work on async loaders and changing the way we request bundles. if we had a more flexible and robust system for loading code asynchronously this would be less of a concern I would imagine since we wouldn't be tied to the sections.

by the way, the section bundles have been serving us incredibly well and props to those who made them work the way they do!

@samouri samouri added [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. and removed [Status] In Progress labels Feb 15, 2017
Dockerfile Outdated
@@ -39,11 +39,7 @@ RUN npm install --production || npm install --production
COPY . /calypso

# Build javascript bundles for each environment and change ownership
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment above becomes no longer valid.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed.

Dockerfile Outdated
CALYPSO_ENV=stage make build-stage && \
CALYPSO_ENV=production make build-production && \
chown -R nobody /calypso
RUN CALYPSO_ENV=production make build && chown -R nobody /calypso
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This need to be further investigated, because we have 3 ways of detecting env in client and server code:

  • config( 'env )
  • NODE_ENV
  • CALYPSO_ENV

secondary: true,
enableLoggedOut: true
} );
sections.push( {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that we need to remove those feature flag checks from files where sections definition are located. At the same time I think we should move existing logic to entry point of each individual section. This means I'm suggesting to bring back those file guard to the first file which isn't generated by Webpack, to make sure we don't change any existing logic. We can remove some flags in separate PRs.

Copy link
Contributor Author

@samouri samouri Feb 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think both are required in this PR:

  1. removing feature flag checks for build (so that all bundles are generated)
  2. adding feature flag checks to controller files (so that routes inaccessible in a specific env stay inaccessible)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean we'll be generating a bundle for devdocs regardless of config settings?

Copy link
Contributor Author

@samouri samouri Feb 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mtias yes, the bundle will be generated regardless of config (this should be bringing us to a single build)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure generating these bundles if they are disabled at the config level is a good path. It makes extending Calypso and using it to build things harder.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does it differ from using feature flags nested deeper into a chunk? I think it is a very good simplification that bring lots of benefits simplifying build process and drastically reducing its time.

However we need to sync it with extensions. I double checked it and it turns out that our code depends on env_id (https://github.com/Automattic/wp-calypso/blob/master/client/sections.js#L17). We plan to use production env for all non-development envs when preparing build targeted for Docker, which means it won't work as expected for staging, wpcalypso and horizon.

Copy link
Contributor Author

@samouri samouri Feb 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mtias: I'm not sure how generating /not-generating bundles can affect extensibility.
Whether or not the client requests the bundle is up to it. For example, theres no reason the devdocs bundle can't exist on the server for NODE_ENV=production, as long as the client doesn't register the /devdocs routes via page

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gziolo I'll fix that now. My current solution will be to move that check to the individual index.js and hopefully eventually we can autowrap the extensions's default exports with an env_id check

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I found solution with 3d98ba8 to this issue. This allows to bundle all sections and extensions, but at the same time never load chunks that are hidden behind the feature flag using env_id in package.json file.

@mtias @mcsf @ehg Let us know if that solution would work :)

@gziolo
Copy link
Member

gziolo commented Feb 16, 2017

Just wanted to summarize what we need to be aware of when working on this PR.

Calypso code depends on the following constructs:

As far as I understand there was intent to have:

  • config( 'env_id' ) === CALYPSO_ENV // staging, horizon, development, production, desktop ...
  • config( 'env' ) === NODE_ENV // production or development

This is how I read it from config files.

All of those different expressions are compared in many places against development and production mostly, but you can also find references to desktop and other envs. In the context of having one docker build based on CALYPSO_ENV=production make build all code checks that target all non production envs are in question.

We also need to keep in mind that we are using here Webpack DefinePlugin which is able to replace all code occurrences like process.env.NODE_ENV !== 'production' into boolean value. If any such check inside if statement is replaced with false Weppack is going to remove also all code that is living inside this control statement. We need to keep in mind that in this case NODE_ENV is derived from CALYPSO_ENV which makes reasoning even more complicated. It's even difficult to explain what can break when we generate build using production CALYPSO_ENV, but run it with staging, but I can imagine there might slip in some subtle changes to the codebase. I guess it isn't a big deal, but we should process such cases and maybe find one common way of accessing current env :)

Much better in code explanation how process.env.NODE_ENV works with Webpack can be found here:

// Webpack can optimize bundles if it can detect that a block will
// never be reached. Since NODE_ENV is defined using DefinePlugin,
// these debugging modules will be excluded from the production build.

I hope this helps reviewing this PR :)

@gziolo
Copy link
Member

gziolo commented Feb 16, 2017

One more important note. We need to make sure we don't break Desktop app introducing those changes. I don't remember how config was consumed in https://github.com/Automattic/wp-desktop but I have a feeling that it could have it's own flavors :)

'<%= anyEnabled %>'
) ( {
data: JSON.stringify( clientData ),
config: config.toString(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't worry about duplicating the config module on the client if I were you, I think we should avoid serializing functions here and depending on several globals.

1 global object containing only the raw configuration should be enough for our needs:

window.configData = <%= data %>;

Moreover, we could easily share the config module between the client and the server.

function loadConfig( configData = null ) {
	data = configData || window && window.configData;
}

Copy link
Member

@gziolo gziolo Feb 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree on this one. At the moment we have this code inlined in HTML file:

function config(key) {
		if (key in data) {
			return data[key];
		}
		throw new Error('config key `' + key + '` does not exist');
	}function isEnabled(feature) {
		return !!data.features[feature];
	}function anyEnabled() {
		var args = Array.prototype.slice.call(arguments);
		return args.some(function (feature) {
			return isEnabled(feature);
		});
	}

We should rather keep config API code in the JS file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tested this out locally. While I'd rather rely on the same module in both client/server, I couldn't figure out how to ensure webpack would exclude the serverConfig from the bundle (without changing what this file exports)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I branched off a PR to test this out: #11454

Copy link
Member

@gziolo gziolo Feb 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Tug @samouri I think I managed to refactor it to use shared config API. See a80d2dc.

I decided to use createConfig higher-order function to make it testable. Let me know if this works for you.

@@ -114,7 +114,7 @@ webpack( webpackConfig, function( error, stats ) {
} ).map( function( chunk ) {
return path.join( process.cwd(), 'public', chunk.file );
} );
files.push( path.join( process.cwd(), 'public', 'vendor.js' ) );

files.unshift( path.join( process.cwd(), 'public', 'vendor.js' ) );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually rebase to avoid those merge commits ;)

git rebase origin/master

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this may not have been the best place to test out the resolve conflicts feature of GitHub. Is there anything different (or worse) about this method?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one at the front, one at the end…

@Tug
Copy link
Contributor

Tug commented Feb 17, 2017

Looks good so far, good job on this @samouri :)
I had a minor comment to slightly improve the config module. Also added a few missing feature checks on the client.

I think this might need extensive testing before we are confident about a merge but I'm optimist, don't hesitate to keep pinging people to have a look!

secondary: true
} );
}
sections.push( {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@scottsweb It looks like you created scaffolding for VIP section. It doesn't look like it is actively developed. Do you use those pages or would it be good idea to remove those files?

Copy link
Contributor

@scottsweb scottsweb Feb 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are not currently using those files and any further development there is at least 3 months out. If you would like to remove them then please do. I imagine a great deal has changed in Calypso since they were put together :) cc @gziolo

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for letting me know. I'll open a follow up PR once this one gets merged ;)

samouri and others added 4 commits March 6, 2017 05:07
…plate literals

1. use es6 template literal instead of lodash template
2. remove anyEnabled from config api since it is never used
3. CALYPSO_ENV --> config( 'env_id' )
4. docs changes thanks @withinboredom / slight doc updates
5. move extensions check to individual extension instead of in sections.js so it happens at runtime
…erences

Build: Move env check for extensions to bundler loader
…, combine server/config with client/lib/config
@samouri samouri force-pushed the try/framework/ssr-config branch from 9670e46 to baae88d Compare March 6, 2017 10:07
@matticbot matticbot added the [Size] XL Probably needs to be broken down into multiple smaller issues label Mar 6, 2017
@@ -157,7 +150,7 @@ const sections = [
name: 'themes',
paths: [ '/design' ],
module: 'my-sites/themes',
enableLoggedOut: config.isEnabled( 'manage/themes/logged-out' ),
enableLoggedOut: true,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since manage/themes/logged-out is true in every environment configuration I think that modifying this should be harmless. Is this true?

cc @seear

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. We should also remove manage/themes/logged-out from the config files if we set this to true, in case anyone tries to turn that flag off in the future expecting it to have any effect.

Copy link
Contributor Author

@samouri samouri Mar 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Darn, I missed this comment earlier. I'll follow up with a PR after this lands

sections.push( {
name: 'post-editor',
paths: editorPaths,
paths: [ '/post', '/page', '/edit' ],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since manage/custom-post-types is true in every environment configuration I think that modifying this should produce no changes

cc @aduth

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since manage/custom-post-types is true in every environment configuration I think that modifying this should produce no changes

Noticing I hadn't replied here. Yes, this should be fine, but we should probably also remove the config flag altogether if we're starting to make assumptions here that it's enabled in all environments.

];

envs.forEach( env => {
process.env.NODE_ENV = env;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to revert process.env.NODE_ENV to value set before this test suite is executed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way we use only two values for NODE_ENV: development and production.

Copy link
Contributor Author

@samouri samouri Mar 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think thats what Line 54 is doing right?

Copy link
Contributor Author

@samouri samouri Mar 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea, that whole section should be modifying CALYPSO_ENV not NODE_ENV. Its a completely separate issue to this PR though, so i'd rather fix that in a smaller future pr

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed that, sorry :)

2nd comment is still valid though.

Copy link
Member

@gziolo gziolo Mar 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's correct as it is in my opinion to use NODE_ENV. The idea here is take advantage of dead code elimination and never execute this check in non-development envs. @dmsnell can you confirm?

group: 'sites'
} );
}
sections.push( {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

manage/drafts isn't enabled on production and staging. We need to add this check in my-sites/drafts

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I missed that ;)


if ( config.isEnabled( 'reader' ) ) {
Copy link
Member

@gziolo gziolo Mar 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's safe to remove. We don't have the same check inside all reader modules, but it is set to true for all envs.

@samouri
Copy link
Contributor Author

samouri commented Mar 6, 2017

There are a couple action items for after this lands:

  1. Calypso.live compatibility
  2. wp-desktop compatibility

Calypso.live

  • this should be easy and is just changing a single string from build-wpcalypso.js --> build.js

wp-desktop

There is nothing fundamental about this PR that is incompatible with wp-desktop. I spun up a branch that makes it compatible and it requires relatively small changes to both both repos. The biggest issue I ran into was when both wp-desktop requires a module that exists in both repos it currently prefers wp-calypso whereas it should probably prefer itself (/lib/config).

@samouri samouri merged commit b2b925c into master Mar 6, 2017
@matticbot matticbot removed the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Mar 6, 2017
@gziolo gziolo mentioned this pull request Mar 7, 2017
2 tasks
} ),
obj = {};

keys.forEach( function( key ) {
Copy link
Member

@gziolo gziolo Mar 22, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@samouri I think we need to bring it back when we expose clientData in server/config/parser.js. It performs filtering based on keys loaded from client.json file.

@aduth
Copy link
Contributor

aduth commented Mar 31, 2017

One subtle change from this pull request is that it's no longer possible to use debug in wpcalypso, horizon, or staging environment because the Webpack configuration will now treat all non-local environments as production.

@samouri samouri deleted the try/framework/ssr-config branch March 31, 2017 21:50
@samouri
Copy link
Contributor Author

samouri commented Mar 31, 2017

@aduth, if it is important to keep debug in other envs, we can modify that module replacement to instead make a wrapper around debug that will no-op if config( 'env_id' ) === 'production'

@aduth
Copy link
Contributor

aduth commented Apr 3, 2017

It's not my use-case, so I can't really speak to whether it's important. Just passing along an observed change from @jmosky who'd expected to be able to use debug logging in staging.

p1490989187543272-slack-calypso

@jmosky
Copy link

jmosky commented Apr 5, 2017

It's extremely useful to be able to see what events fire as I go through WP.com. It doesn't matter to me whether I have to go to https://calypso.live/?branch=master vs. just launching the debugging tool in staging. Also, I think this might be related: #12277 .

@samouri
Copy link
Contributor Author

samouri commented Apr 5, 2017

i'll whip up a pr that allows debug statements to go through in all stages except production with a runtime check to env_id
PR #12841

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Build Framework [Size] XL Probably needs to be broken down into multiple smaller issues [Status] Needs Rebase
Projects
None yet
Development

Successfully merging this pull request may close these issues.