Skip to content

Comments

Refactor fastboot build to be more performant#369

Merged
stefanpenner merged 1 commit intoember-fastboot:masterfrom
kratiahuja:build-test
May 17, 2017
Merged

Refactor fastboot build to be more performant#369
stefanpenner merged 1 commit intoember-fastboot:masterfrom
kratiahuja:build-test

Conversation

@kratiahuja
Copy link
Contributor

@kratiahuja kratiahuja commented Apr 2, 2017

TL;DR: This unblocks FastBoot 1.0 release with a breaking change. Resolves most of the issues mentioned in #360, #292, #264, #246 .

Sending out a PR since substantial work is done that it can be reviewed for directional purposes.

NOTE: I took a slightly different approach in how the fastboot initializers will be written per the spec

Fixes in this PR currently are

  • Fixes fingerprinted assets to be properly loaded in FastBoot sandbox
  • Build fastboot overrides using treeForPublic hook per the new build spec. It will works as follows:
    • Collect the fastboot-app tree from the project addons and create a funnel for every addon tree with destDir as appName-fastboot
    • Collect the fastboot-app tree from the root app and create a funnel for the app fastboot tree with destDir as appName-fastboot
    • Merge the trees collected from the addons and app and create a new Funnel
    • Run the JS transpilation on the new funneled tree using ember-cli-preprocess-registry/preprocessors API. Use the preprocessJs method from the preprocessors with providing the registry
    • Concat the output of transpilation with outputFile set to assets/appName-fastboot.js
    • Merge the above output tree with the tree returned from treeForPublic
  • Make sure the fastboot-app tree in app is a watched directory tree. Most likely it will be otherwise follow treeForGenerator does here
  • Update FastBootConfig broccoli plugin to add assets/appName-fastboot.js in appFiles manifest list of package.json. The corresponding code is here
  • Update existing intializers of ember-cli-fastboot per the new spec
  • Remove the old fastboot double build spec
    • Remove usage of private ember-cli patching from ember-cli-fastboot. See example here
    • Remove invoking fastboot-filter-initializers in ember-cli-fastboot. See relevant example changes here
    • Remove config function from ember-cli-fastboot/index.js
    • Set app-boot contentFor correctly. See example here
    • Fix config-module contentFor hook to not rely on __is_building_fastboot__ flag. See example fix here
    • Remove preconcat function as it will be unused in new build spec
    • In postprocessTree hook, we should invoke FastBootConfig broccoli plugin directly instead of invoking FastBootBuild broccoli plugin. See example changes here
    • Remove FastBootBuild broccoli plugin as it will be unused
  • Update tests
  • Kill usage of process.env.EMBER_CLI_FASTBOOT flag as it will be unavailable in new build spec
  • Expose a hook to add to manifest files
  • Migration path for apps relying on fastboot-filter-initializers or app/(instance-)?initializers/fastboot
  • Throw build error if addon has app/[instance-]initializers/browser/*.js
  • Create a hook for addons to add additional things in fastboot-asset (example app-lt-2.9.0) --> treeForFastBoot
  • Clean/reorganize code
  • Expose process.env.FASTBOOT_NEW_BUILD so that addons like ember-cli-head can support both versions.

TODO (In subsquent PRs):

  • Unskip tests using ember-cli-head once that addon embraces new format.
  • Remove TODOs from index.js
  • README updates
  • Doc updates
  • Kill ember fastboot command 🔪
  • Update some of the exisiting addons to not rely on process.env.EMBER_CLI_FASTBOOT flag or not contain app/[instance-]initializers/[browser|fastboot]/*.js folder structure (owner: @simonihmig since he has already started this work)

cc: @rwjblue @tomdale @danmcclain @ryanone @simonihmig @arjansingh @stefanpenner

@kratiahuja
Copy link
Contributor Author

kratiahuja commented Apr 2, 2017

Please feel free to try this out in your app without any other fastboot complaint addons.

@kratiahuja
Copy link
Contributor Author

kratiahuja commented Apr 15, 2017

#374 resolves Expose a hook to add to manifest files TODO. Created a separate PR since it will be needed before this PR can land. Once it is merged, will require some minor refactoring in this PR since buildFastbootTree is no longer present.


if (isBrowserInitializersPresent) {
var errorMsg = `FastBoot build no longer supports ${rootPath}/app/(instance-)?initializers/browser structure. ` +
`Please refer to www.ember-fastboot.com for a migration path.`;
Copy link
Contributor

Choose a reason for hiding this comment

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

@kratiahuja I have picked up migrating an addon as we have discussed on Slack, and after updating ember-cli-fastboot to the latest version of your PR, I immediately ran into this error message, being caused by some other (unknown) addon.

The problem I see here is that a) probably every existing app will run into this error once trying to migrate to FastBoot 1.0, and b) it gives no clue where that offending initializer actually comes from, which could be quite frustrating. It could be even some nested addon dependency the user does not see in his package.json.

I think it would be cool (in this PR or a follow up) to have some better tracing available, like which addon is causing this, or at least what the initializers actual name is, to be able to effectively investigate into this!

Copy link
Contributor

Choose a reason for hiding this comment

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

Please forget it! ;) I was not paying enough attention to see it actually does exactly this. 🙄
Need some more sleep probably...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah we can't auto-migrate browser based initializers, hence we will need to fail the build. I am happy to refactor the error messages to be more better. Probably sometime this week i'll come up with a migration cheat sheet so that it is easier for addons 😄

Copy link
Contributor

Choose a reason for hiding this comment

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

That would be awesome! Would be nice to have a link in the above mentioned error message, maybe a dedicated migration page on www.ember-fastboot.com?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry I missed replying to this. Yes would be nice to have a dedicated migration page. I think before this PR is merged, we should send in a warning (but getting another PR) and link to the migration page.

*
* @param {Object} project
*/
function migrateInitializers(project) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I just noticed that this works on the original files. So in my app after running ember s the files were modifed, the old instance-initializer/fastboot/foo.js was physically moved to fastboot/instance-initializer/foo.js (from POV of git a new, not yet added file). The same for addons, some yarn linked addons got silently modified (which I did not want as that would have broken them for pre 1.0 FastBoot).

I wonder if that migration magic should not happen inside some broccoli tree rather than operating on the original files of the app and in node_modules/someAddon?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why will it break for pre fastboot 1.0? This migration will not apply for pre fastboot 1.0 but only after.

Reason for moving is because if we don't, these initializers will end up running in browser too. Hence it needs to be moved. I am hoping reading the message, addon owners would move the fastboot intiializers themselves post fastboot 1.0. This migration would be dropped after a few releases of fastboot 1.0

Copy link
Contributor

Choose a reason for hiding this comment

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

The breaking pre FastBoot 1.0 thing was related to the changes in ember-useragent, that you also reviewed. There we left the instance-initializer/fastboot/ember-useragent in place, so it works in pre 1.0 as well as in post 1.0 (because of the automatic migration). But because it was symlinked, and the migration works on the original files, it modified the original addon code, moving the initializer to a the new place...

So this is the point I was trying to make, that this migration did not only move files in a temporary state while building the app like a broccoli plugin would do (take the previous approach of fastboot-filter-initializers as an example, which filters one of the browser|fastboot initializers in a broccoli tree), but it modified the original in-repo files of the app, as well as any symlinked addon (in fact any addon in node_modules). I would expect something like a codemod to do that, but certainly not ember s.

Copy link
Contributor Author

@kratiahuja kratiahuja Apr 26, 2017

Choose a reason for hiding this comment

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

I am like the idea of a codemod doing something like this. But what if they forget to run the codemod tool post 1.0 before serving? It will break for them more. I guess we can detect if they ran codemod too and not serve.

Copy link
Contributor

@simonihmig simonihmig Apr 26, 2017

Choose a reason for hiding this comment

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

I am actually not arguing in favor of a codemod, I am just arguing that ember s should not modify any original file in my app/addons. It can move things around in a broccoli tree when building, but not the original in-repo files IMHO.

Copy link
Contributor Author

@kratiahuja kratiahuja Apr 27, 2017

Choose a reason for hiding this comment

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

@simonihmig My earlier comment was me just musing on the idea.

Wouldn't moving in broccoli-tree be very expensive? Are you talking about after the treeForApp is generated? I don't think we will solve the build performance problem then (at least in a temporary sense).

I totally agree it is not the best way and I am happy to explore new ways to do it. Do you have an example that you can explain with?

Reason for changing the original in-repo files is (maybe moving in broccoli tree solves the below issue):
the problem not moving in-repo files is (as I mentioned earlier) that if we don't move you will end up with two AMD modules after the build:

  1. <app-name>/initializers/fastboot/x.js <-- This one will get loaded in browser and FastBoot
  2. <app-name>/initializers/x.js <--- this one is only loaded in FastBoot

When the app will boot in the browser, it will run app/initializers/fastboot/x.js (this because we no longer filter the fastboot initializers) which will cause issues. Hence there is a need to move the in-repo files so that we don't generate <app-name>/initializers/fastboot/x.js.

Maybe there is a better way to solve this problem that I am not seeing?

const fs = require('fs-extra');

const fastbootInitializerTypes = [ 'initializers', 'instance-initializers'];
const FASTBOOT_DIR = 'fastboot';
Copy link
Contributor

Choose a reason for hiding this comment

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

Given the description of the PR I was expecting the new FastBoot folder to be named fastboot-app rather than fastboot, but this might be just some misunderstanding?

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 prefer calling it fastboot instead of fastboot-app since it is more generic

@kratiahuja
Copy link
Contributor Author

Draft and WIP cheatsheet on how fastboot compatible addons can continue to be backward compatible (with this build changes) if they do these changes: https://gist.github.com/kratiahuja/d22de0fb1660cf0ef58f07a6bcbf1a1c

This may change subject to the feedback on this PR and feedback on the cheatsheet. I'll try bringing this up in the next FastBoot meeting.

cc: @simonihmig

@kratiahuja kratiahuja changed the title [WIP]: Refactor fastboot build to be more performant Refactor fastboot build to be more performant Apr 27, 2017
@kratiahuja
Copy link
Contributor Author

kratiahuja commented Apr 27, 2017

Removing WIP since this is ready to review. The review has got pretty large that we can do the remaining TODOs in a subsquent PR.

index.js Outdated

// invoke addToFastBootTree for every addon
if (addon.addToFastBootTree) {
var additionalFastBootTree = addon.addToFastBootTree();
Copy link
Contributor

Choose a reason for hiding this comment

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

Related to this problem: ember-fastboot/ember-cli-head#21 (comment)
Is it actually required that addons return a tree that was created by lib/build-utilities/fastboot-builder.js? Isn't it possible that the addon just return a simple tree (like this.treeGenerator(path)), and the mergedFastBootTree (Line 152) is then moved to appName (Funnel destDir)?

Copy link
Contributor Author

@kratiahuja kratiahuja Apr 28, 2017

Choose a reason for hiding this comment

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

I think we could do that that. Will need to try it. This would also enable linting automatically if I remember correctly (cc: @tsubomii).

Copy link
Contributor

Choose a reason for hiding this comment

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

how does this differ from treeForFastboot and would the following cover this use-case?

treeForFastboot(tree) {
  return mergeTrees([
    tree, // `<addon-root>/fastboot/`
    additionalTree // from whereve you would want.
  ]);
}

note: at first glance ^ would feel more aligned to existing API's, but let me know if I am missing someting

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@simonihmig Fixed. You can just use this.treeGenerator(path) and return that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@stefanpenner I actually like the idea of having treeForFastBoot which is more aligned to other existing APIs. And yes it would cover the usecase I wanted it to cover as well. Will update it to providing treeForFastBoot :)

index.js Outdated
this._super.init && this._super.init.apply(this, arguments);
// set a environment variable to allow addons to use `fastboot-filter-initializers`
// for old versions.
process.env.FASTBOOT_NEW_BUILD = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

would it make more sense to set this when this module is included rather than later when it is initialized? That may reduce the window where this variable appears to be unset but will soon be set.

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 wanted this variable to be set early on. Isn't included called after init?

index.js Outdated

// invoke addToFastBootTree for every addon
if (addon.addToFastBootTree) {
var additionalFastBootTree = addon.addToFastBootTree();
Copy link
Contributor

Choose a reason for hiding this comment

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

how does this differ from treeForFastboot and would the following cover this use-case?

treeForFastboot(tree) {
  return mergeTrees([
    tree, // `<addon-root>/fastboot/`
    additionalTree // from whereve you would want.
  ]);
}

note: at first glance ^ would feel more aligned to existing API's, but let me know if I am missing someting

index.js Outdated
});

var fileAppName = path.basename(this.app.options.outputPaths.app.js).split('.')[0];
var finalFastbootTree = Concat(processExtraTree, {
Copy link
Contributor

Choose a reason for hiding this comment

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

new Concat

index.js Outdated
}
trees.push(fastbootTree);

let newTree = new mergeTrees(trees);
Copy link
Contributor

Choose a reason for hiding this comment

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

new MergeTrees or no new if lowercase.

index.js Outdated
var fastbootConfig = config.fastboot;
// do not boot the app automatically in fastboot. The instance is booted and
// lives for the lifetime of the request.
if (config.hasOwnProperty('APP')) {
Copy link
Contributor

Choose a reason for hiding this comment

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

'APP' in config


const fastbootDirPath = path.join(rootPath, FASTBOOT_DIR);
// check if fastboot/app exists
if(!existsSync(fastbootDirPath)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

missing space between if and (


// copy over app/initializers/fastboot and app/instance/initializers/fastboot
fastbootInitializerTypes.forEach((fastbootInitializerType) => {
var srcFastbootPath = path.join(rootPath, 'app', fastbootInitializerType, 'fastboot');
Copy link
Contributor

Choose a reason for hiding this comment

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

if we are using const and => could we also use let in-place of var?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup definitely yes :)

package.json Outdated
"dependencies": {
"broccoli-funnel": "^1.0.0",
"broccoli-concat": "^3.2.2",
"broccoli-funnel": "^1.1.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

v1.2.0 is latest

"ember-cli-babel": "^5.1.7",
"ember-cli-eslint": "^3.0.2",
"ember-cli-version-checker": "^1.3.1",
"exists-sync": "0.0.4",
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should just use fs.existsSync as it is provided by the STDLIB, although fs.exists is deprecated the sync variant is not.

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 thought fs.existsSync is deprecated. The readme of exists-sync says this: https://github.com/ember-cli/exists-sync#exists-sync

Copy link
Member

Choose a reason for hiding this comment

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

fs.existsSync was deprecated along with fs.exists a long way back in nodejs/node@7b5ffa4, but then fs.existsSync was undeprecated in nodejs/node@7b5ffa4.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Aah didn't know that. Will use fs.existsSync and update readme for exists-sync too.


describe('head content acceptance', function() {
// TODO: Unskip this test once ember-cli-head is released with new fastboot build spec.
describe.skip('head content acceptance', function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes :)

@stefanpenner
Copy link
Contributor

Left some thoughts, going to test it out manually next :)

@kratiahuja
Copy link
Contributor Author

@stefanpenner Thank you so much for taking time to review a very very long PR :) Really appreciate it. I'll address the comments this weekend!

@stefanpenner
Copy link
Contributor

stefanpenner commented May 5, 2017

Thank you so much for taking time to review a very very long PR

NP! Excited to see progress on this front!

I'll address the comments this weekend!

Some of them are intended more as discussion (don't assume I'm right)

@kratiahuja
Copy link
Contributor Author

Some of them are intended more as discussion (don't assume I'm right)

Yup I meant I'll reply back this weekend :)

@stefanpenner
Copy link
Contributor

stefanpenner commented May 5, 2017

Local testing is looking good, rebuild times feel great. Everyone seems to work.

I was a tad surprised that curl 'http://localhost:4200/' didn't include the fast boot content but
curl 'http://localhost:4200/' -H 'Accept: text/html' did. But I guess this resolves some ambiguity and mimics the browser.

Copy link
Contributor

@stefanpenner stefanpenner left a comment

Choose a reason for hiding this comment

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

LGTM, IMHO lets :shipit:

@tomdale c/d?

@mazondo
Copy link

mazondo commented May 15, 2017

Anything I can help with to push this over the edge - we're keen on using it in our fastboot migration

@stefanpenner
Copy link
Contributor

stefanpenner commented May 15, 2017

Anything I can help with to push this over the edge - we're keen on using it in our fastboot migration

We have a meeting tomorrow afternoon/evening to get this shipped. If delegatable tasks fall out, we will open them as issues :)

Best you can do to help us today, is test out this PR and report issues.

@scottmessinger
Copy link

I tried out this branch and it's working great! The one problem I've encountered is handling app.import. The discussion in this issue seems to focus on addons. In my app, addons aren't really the problem -- vendor libraries are. I'm importing libraries like interact.js, CKEditor, iscroll, etc and I had to go into each file in /vendor and add if (window.document !== undefined) {...}. Upgrading the libraries in the future will be harder since I'll have to copy and paste the library into the if block.

In addition to converting my vendor libs over, I tried getting ember-fetch working with this branch using the solution proposed here https://gist.github.com/kratiahuja/d22de0fb1660cf0ef58f07a6bcbf1a1c and working from the PR here: https://github.com/tomdale/ember-network/pull/20/files. I found the instructions in the gist to be somewhat confusing. I really appreciate @kratiahuja for drafting that up and I acknowledge that some of the complexity might be necessary. I'm hoping it can avoided and tucked behind a more declarative API.

Before this PR, the if (isFastBoot){...} else {....} was clear because it was a simple branch -- do the opposite if Fastboot is there. Now, there's a wildly different way to include a lib for Fastboot vs not including it. For instance, when including for the browser, a file can be included without being evaled if it's filtered out in treeForVendor. A couple steps, but they're logical. However, the opposite -- only include a file for fastboot is a completely different technique, requiring knowledge of the fastboot manifest.

In working with ember-fetch and my own app, what I wanted was a much simpler, more declarative way of filtering libs for Fastboot: app.import('libname.js', {loadInFastBoot: false}). I'm not sure if an API like that is possible, but I think it would be a helpful improvement.

@kratiahuja
Copy link
Contributor Author

Thanks @scottmessinger for your feedback. I appreciate it.

I had to go into each file in /vendor and add if (window.document !== undefined) {...}. Upgrading the libraries in the future will be harder since I'll have to copy and paste the library into the if block.

You could do this using broccoli-stew as well to avoid copy and paste issues in future. You could use an in-repo addon too for such a purpose.

In working with ember-fetch and my own app, what I wanted was a much simpler, more declarative way of filtering libs for Fastboot: app.import('libname.js', {loadInFastBoot: false}). I'm not sure if an API like that is possible, but I think it would be a helpful improvement.

I agree with your thoughts on this. Did you read the build spec here ? There is an unresolved item about app.import that we can address in future. The goal of this PR is to come up with a minimal set of changes required to make the build more performant and make it to cut FastBoot major release version.

Before this PR, the if (isFastBoot){...} else {....} was clear because it was a simple branch -- do the opposite if Fastboot is there. Now, there's a wildly different way to include a lib for Fastboot vs not including it. For instance, when including for the browser, a file can be included without being evaled if it's filtered out in treeForVendor. A couple steps, but they're logical. However, the opposite -- only include a file for fastboot is a completely different technique, requiring knowledge of the fastboot manifest.

I understand that but the new build schema is different than the current one. The idea is instead of forking assets for different environments you want to load the browser assets and additional fastboot assets in fastboot environment. So you will need to load the fastboot assets in a different way. An app that is being server side rendered will nneed to have knowledge of fastboot manifest. I don't think that is wrong assumption.

I found the instructions in the gist to be somewhat confusing. I really appreciate @kratiahuja for drafting that up and I acknowledge that some of the complexity might be necessary. I'm hoping it can avoided and tucked behind a more declarative API.

The gist is very much in early draft stages. It was meant to iron out the different usecases where we need a migration path forward. As mentioned in #360 there needs to be a migration guide to help addon owners.

@scottmessinger
Copy link

@kratiahuja Thanks for the response! I'm excited you're open to letting app.import filter fastboot assets.

An app that is being server side rendered will need to have knowledge of fastboot manifest. I don't think that is wrong assumption.

While I do think it's completely reasonable an app/addon to have knowledge of fastboot, I don't follow why an app/addon needs an understanding of the fastboot manifest. When "you want to load the browser assets and additional fastboot assets in fastboot environment", I think you'd only need a higher level api (like app.import('fileName', {loadInFastboot: false})). While having lower level primitives (involving the fastboot manifest) is a sound idea, in my mind, it's better to keep those primitives private in order to reduce the surface area of the public API one needs to learn just to get their app/addon working in fastboot. That all said, you know way, way more about fastboot than I, so maybe requiring addon/app developers to learn the fastboot manfest and more Broccoli callbacks is important and necessary.

Totally understand about the gist being an early draft -- I figured as much and really appreciate that you wrote it up.

@stefanpenner
Copy link
Contributor

app.import('fileName', {loadInFastboot: false})).

I believe this already exists, or at-least the machinery to make it work.

app.import(thing, { outputFile: 'fastboot/some-file-only-required-in-input.js' }

But this discussion should likely find a new issue / rfc or something.

@stefanpenner stefanpenner merged commit 8cb1542 into ember-fastboot:master May 17, 2017
@stefanpenner
Copy link
Contributor

Awesome job @kratiahuja!

@stefanpenner
Copy link
Contributor

We should cut a beta today, then ask for community help in upgrading existing add-ons.

Burn down issue for addons that need <3 #387

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants