├── .editorconfig ├── .ember-cli ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .template-lintrc.js ├── .travis.yml ├── .watchmanconfig ├── Procfile ├── README.md ├── app.json ├── app ├── app.js ├── components │ ├── .gitkeep │ ├── command-prompt │ │ ├── component.js │ │ └── template.hbs │ ├── fastboot-logo │ │ ├── component.js │ │ └── template.hbs │ ├── main-hero-terminal │ │ ├── component.js │ │ └── template.hbs │ ├── main-hero │ │ ├── component.js │ │ ├── meetup │ │ │ └── template.hbs │ │ └── template.hbs │ ├── main-nav.js │ ├── nav-bar.js │ └── route-error.js ├── controllers │ └── .gitkeep ├── helpers │ └── .gitkeep ├── index.html ├── models │ └── .gitkeep ├── resolver.js ├── router.js ├── routes │ ├── .gitkeep │ ├── application.js │ └── page.js ├── styles │ ├── app.css │ ├── base │ │ └── reboot.css │ ├── components │ │ ├── command-prompt.css │ │ ├── fastboot-logo.css │ │ ├── main-hero-meetup.css │ │ ├── main-hero-terminal.css │ │ ├── main-hero.css │ │ ├── main-nav.css │ │ └── syntax.css │ ├── nav-bar.css │ └── old.css ├── templates │ ├── application.hbs │ ├── components │ │ ├── .gitkeep │ │ ├── main-nav.hbs │ │ ├── nav-bar.hbs │ │ └── route-error.hbs │ ├── head.hbs │ ├── index.hbs │ └── page.hbs └── transitions.js ├── config ├── deploy.js ├── environment.js ├── optional-features.json └── targets.js ├── ember-cli-build.js ├── fastboot-server.js ├── markdown ├── docs │ ├── addon-author-guide.md │ ├── deploying.md │ └── user-guide.md ├── intro.md └── quickstart.md ├── package-lock.json ├── package.json ├── public ├── images │ ├── addon-author-guide │ │ └── stack-trace-example.png │ ├── background.png │ ├── contacts-example.png │ ├── favicon.png │ ├── logo.svg │ ├── nebula.png │ ├── quickstart │ │ ├── github-fastboot-example-empty-source.png │ │ ├── github-fastboot-example-populated-source.png │ │ └── github-fastboot-example-screenshot.png │ └── site-preview.png └── robots.txt ├── testem.js ├── tests ├── helpers │ └── .gitkeep ├── index.html ├── integration │ └── .gitkeep ├── test-helper.js └── unit │ ├── .gitkeep │ └── routes │ └── application-test.js └── vendor └── .gitkeep /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | 19 | [*.{diff,md}] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | 17 | # ember-try 18 | /.node_modules.ember-try/ 19 | /bower.json.ember-try 20 | /package.json.ember-try 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | ecmaVersion: 2017, 5 | sourceType: 'module' 6 | }, 7 | plugins: [ 8 | 'ember' 9 | ], 10 | extends: [ 11 | 'eslint:recommended', 12 | 'plugin:ember/recommended' 13 | ], 14 | env: { 15 | browser: true 16 | }, 17 | rules: { 18 | }, 19 | overrides: [ 20 | // node files 21 | { 22 | files: [ 23 | 'fastboot-server.js', 24 | '.eslintrc.js', 25 | '.template-lintrc.js', 26 | 'ember-cli-build.js', 27 | 'testem.js', 28 | 'blueprints/*/index.js', 29 | 'config/**/*.js', 30 | 'lib/*/index.js', 31 | 'server/**/*.js' 32 | ], 33 | parserOptions: { 34 | sourceType: 'script', 35 | ecmaVersion: 2015 36 | }, 37 | env: { 38 | browser: false, 39 | node: true 40 | } 41 | } 42 | ] 43 | }; 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /fastboot-dist 6 | /tmp 7 | 8 | # dependencies 9 | /bower_components/ 10 | /node_modules/ 11 | 12 | # misc 13 | /.env* 14 | /.pnp* 15 | /.sass-cache 16 | /connect.lock 17 | /coverage/ 18 | /libpeerconnection.log 19 | /npm-debug.log* 20 | /testem.log 21 | /yarn-error.log 22 | 23 | # ember-try 24 | /.node_modules.ember-try/ 25 | /bower.json.ember-try 26 | /package.json.ember-try 27 | -------------------------------------------------------------------------------- /.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'recommended' 5 | }; 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | sudo: false 5 | dist: trusty 6 | 7 | addons: 8 | chrome: stable 9 | 10 | cache: 11 | yarn: true 12 | directories: 13 | - $HOME/.npm 14 | 15 | env: 16 | global: 17 | # See https://git.io/vdao3 for details. 18 | - JOBS=1 19 | 20 | 21 | install: 22 | - npm install 23 | 24 | script: 25 | - npm run lint:js 26 | - npm test 27 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node fastboot-server.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/ember-fastboot/fastboot-website.svg?branch=master)](https://travis-ci.org/ember-fastboot/fastboot-website) 2 | 3 | # FastBoot Website 4 | 5 | This is the Ember.js app that powers the FastBoot website, . 6 | 7 | Most of the content is authored in Markdown and can be found in the `markdown` directory. 8 | 9 | ## Contributing 10 | 11 | You don't need this repository to use FastBoot. However, if you'd like to contribute documentation or correct errors, you can submit a pull request. 12 | 13 | To run the website locally: 14 | 15 | - `git clone https://github.com/ember-fastboot/fastboot-website` 16 | - `cd fastboot-website` 17 | - `npm install` 18 | - `ember serve` 19 | - Visit the app at . 20 | 21 | ### Running Tests 22 | 23 | * `ember test` 24 | * `ember test --server` 25 | 26 | ### Building 27 | 28 | * `ember build` (development) 29 | * `ember build --environment production` (production) 30 | 31 | ### Fastboot Express Server 32 | 33 | This site is served in production by an express.js application 34 | [fastboot-server](https://github.com/ember-fastboot/fastboot-website/blob/master/fastboot-server.js). To serve the app using the `fastboot-server` use the following command which will 35 | build the app with production `env` and serve the app at [localhost:3000](http://localhost:300) 36 | 37 | ```sh 38 | npm run server:node 39 | ``` 40 | 41 | ### Deploying 42 | 43 | Pull requests merged into master are automatically deployed to Heroku. 44 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "addons": [ 3 | "loaderio" 4 | ], 5 | "buildpacks": [ 6 | { 7 | "url": "https://buildpack-registry.s3.amazonaws.com/buildpacks/aedev/emberjs.tgz" 8 | } 9 | ], 10 | "env": { 11 | "ACME_KEY_1": { 12 | "required": false 13 | }, 14 | "ACME_KEY_2": { 15 | "required": false 16 | }, 17 | "ACME_TOKEN_1": { 18 | "required": false 19 | }, 20 | "ACME_TOKEN_2": { 21 | "required": false 22 | }, 23 | "LOADERIO_API_KEY": { 24 | "required": false 25 | }, 26 | "WORKER_COUNT": { 27 | "required": true 28 | }, 29 | "EXPERIMENTAL_RENDER_MODE_SERIALIZE": { 30 | "required": true 31 | } 32 | }, 33 | "formation": { 34 | "web": { 35 | "quantity": 1 36 | } 37 | }, 38 | "name": "fastboot-website", 39 | "scripts": { 40 | }, 41 | "stack": "heroku-20" 42 | } 43 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from './resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | const App = Application.extend({ 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix, 9 | Resolver 10 | }); 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-fastboot/fastboot-website/53704f71cb854113ad03192a57fb15bc5529daa3/app/components/.gitkeep -------------------------------------------------------------------------------- /app/components/command-prompt/component.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | 3 | export default Component.extend({ 4 | classNames: ['command-prompt'] 5 | }); 6 | -------------------------------------------------------------------------------- /app/components/command-prompt/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |
{{yield}}
3 |
4 | -------------------------------------------------------------------------------- /app/components/fastboot-logo/component.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | 3 | export default Component.extend({ 4 | classNames: ['fastboot-logo'] 5 | }); 6 | -------------------------------------------------------------------------------- /app/components/fastboot-logo/template.hbs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/components/main-hero-terminal/component.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | 3 | export default Component.extend({ 4 | classNames: ['main-hero-terminal'] 5 | }); 6 | -------------------------------------------------------------------------------- /app/components/main-hero-terminal/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | 7 |
8 |
 ember install ember-cli-fastboot
9 |
10 | -------------------------------------------------------------------------------- /app/components/main-hero/component.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | 3 | export default Component.extend({ 4 | classNames: ['main-hero'] 5 | }); 6 | -------------------------------------------------------------------------------- /app/components/main-hero/meetup/template.hbs: -------------------------------------------------------------------------------- 1 | 6 |
7 |

8 | Join us 9 |  Wednesday April 14th 10 |  at 4:30pm pst 11 |

12 | Ember Fastboot SSR & Beyond Meetup 13 |
14 |
15 | Register 16 |
17 |
18 | -------------------------------------------------------------------------------- /app/components/main-hero/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{fastboot-logo}} 4 | 5 |

6 | Progressive enhancement for
7 | ambitious web apps. 8 |

9 |
10 | 11 |
12 | {{main-hero-terminal}} 13 | 14 |
15 | Ember.js 2.3+ required (Ember.js 2.4+ recommended)
16 | Node.js 4.0+ 17 |
18 |
19 | 20 |
21 | {{#link-to "page" "quickstart" class="main-hero-button"}} 22 | Quickstart 23 | {{/link-to}} 24 | 25 | {{#link-to "page" "docs/user-guide" class="main-hero-button"}} 26 | User Guide 27 | {{/link-to}} 28 |
29 |
30 | -------------------------------------------------------------------------------- /app/components/main-nav.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | 3 | export default Component.extend({ 4 | tagName: 'nav', 5 | classNames: [ 6 | 'main-nav', 7 | 'max-width-3', 8 | 'mx-auto' 9 | ] 10 | }); 11 | -------------------------------------------------------------------------------- /app/components/nav-bar.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import Component from "@ember/component"; 3 | 4 | export default Component.extend({ 5 | classNames: 'nav-bar', 6 | classNameBindings: 'isOffTop', 7 | 8 | didInsertElement() { 9 | this._super(...arguments); 10 | 11 | let $window = $(window); 12 | 13 | $window.on('scroll', () => { 14 | requestAnimationFrame(() => { 15 | let $section = $('section').first(); 16 | let sectionTop = $section[0].getBoundingClientRect().top; 17 | this.set('isOffTop', sectionTop < 0); 18 | }); 19 | }); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /app/components/route-error.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { computed } from '@ember/object'; 3 | import markdownFiles from 'ember-fr-markdown-file/markdownFiles'; 4 | 5 | export default Component.extend({ 6 | _parseObj(obj, dir) { 7 | let prefix = dir || ''; 8 | let result = []; 9 | let that = this; 10 | for (let prop in obj) { 11 | let value = obj[prop]; 12 | if (typeof value === 'object') { 13 | result.push(that._parseObj(value, prop)); 14 | } else { 15 | if (prop !== 'intro') { 16 | result.push(`${prefix}/${prop}`.replace(/^\//, '')); 17 | } 18 | } 19 | } 20 | return result; 21 | }, 22 | availablePages: computed(function() { 23 | return [].concat.apply([], this._parseObj(markdownFiles)); 24 | }) 25 | }); 26 | -------------------------------------------------------------------------------- /app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-fastboot/fastboot-website/53704f71cb854113ad03192a57fb15bc5529daa3/app/controllers/.gitkeep -------------------------------------------------------------------------------- /app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-fastboot/fastboot-website/53704f71cb854113ad03192a57fb15bc5529daa3/app/helpers/.gitkeep -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ember FastBoot 7 | 8 | 9 | 10 | 11 | {{content-for "head"}} 12 | 13 | 14 | 15 | 16 | {{content-for "head-footer"}} 17 | 18 | 19 | {{content-for "body"}} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {{content-for "body-footer"}} 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-fastboot/fastboot-website/53704f71cb854113ad03192a57fb15bc5529daa3/app/models/.gitkeep -------------------------------------------------------------------------------- /app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /app/router.js: -------------------------------------------------------------------------------- 1 | import { get } from '@ember/object'; 2 | import { run } from '@ember/runloop'; 3 | import EmberRouter from '@ember/routing/router'; 4 | import config from './config/environment'; 5 | import { inject as injectService } from '@ember/service'; 6 | 7 | const Router = EmberRouter.extend({ 8 | location: config.locationType, 9 | rootURL: config.rootURL, 10 | metrics: injectService(), 11 | fastboot: injectService(), 12 | 13 | didTransition() { 14 | this._super(...arguments); 15 | if (!get(this, 'fastboot.isFastBoot')) { 16 | this._scrollPage(); 17 | this._trackPage(); 18 | } 19 | }, 20 | 21 | _scrollPage() { 22 | if (this.fastboot.isFastBoot) { 23 | return; 24 | } 25 | 26 | run.scheduleOnce('afterRender', this, () => { 27 | let position = 0; 28 | let hash = window.location.hash; 29 | if (hash) { 30 | let id = hash.split('#')[1]; 31 | let el = document.getElementById(id); 32 | if (el) { 33 | position = el.getBoundingClientRect().top; 34 | } 35 | } 36 | window.scrollTo(0, position); 37 | }); 38 | }, 39 | 40 | _trackPage() { 41 | if (this.fastboot.isFastBoot) { 42 | return; 43 | } 44 | 45 | run.scheduleOnce('afterRender', this, () => { 46 | let page = document.location.pathname; 47 | let title = this.getWithDefault('currentRouteName', 'unknown'); 48 | 49 | this.metrics.trackPage({ page, title }); 50 | }); 51 | } 52 | }); 53 | 54 | Router.map(function() { 55 | this.route('page', { path: '*path' }); 56 | }); 57 | 58 | export default Router; 59 | -------------------------------------------------------------------------------- /app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-fastboot/fastboot-website/53704f71cb854113ad03192a57fb15bc5529daa3/app/routes/.gitkeep -------------------------------------------------------------------------------- /app/routes/application.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import { get } from '@ember/object'; 3 | import markdownFiles from 'ember-fr-markdown-file/markdownFiles'; 4 | 5 | export default Route.extend({ 6 | model() { 7 | return get(markdownFiles, 'intro'); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /app/routes/page.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import { get } from '@ember/object'; 3 | import markdownFiles from 'ember-fr-markdown-file/markdownFiles'; 4 | 5 | export default Route.extend({ 6 | model(params) { 7 | return get(markdownFiles, params.path.replace(/\//g, '.')) || null; 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /app/styles/app.css: -------------------------------------------------------------------------------- 1 | @import "normalize.css"; 2 | @import "basscss"; 3 | 4 | @import "base/reboot"; 5 | 6 | @import "components/command-prompt"; 7 | @import "components/fastboot-logo"; 8 | @import "components/main-hero"; 9 | @import "components/main-hero-meetup"; 10 | @import "components/main-hero-terminal"; 11 | @import "components/main-nav"; 12 | @import "components/syntax"; 13 | 14 | :root { 15 | --dark-gray: #111; 16 | --white: white; 17 | --green: #52ECC7; 18 | --purple: #8692FF; 19 | 20 | --link-color: #7972CE; 21 | 22 | --easeOutQuint: cubic-bezier(0.230, 1.000, 0.320, 1.000); 23 | 24 | --font-family: "tablet-gothic", "Helvetica Neue", Arial, sans-serif; 25 | --monospace-font-family: Menlo, Meslo, Monaco, monospace; 26 | --font-size: 18px; 27 | } 28 | -------------------------------------------------------------------------------- /app/styles/base/reboot.css: -------------------------------------------------------------------------------- 1 | *, 2 | *:after, 3 | *:before { 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | color: #6F7C82; 9 | font-family: var(--font-family); 10 | line-height: 1.5; 11 | font-size: var(--font-size); 12 | } 13 | 14 | a { 15 | border-bottom: 2px solid #EFF3F6; 16 | color: var(--link-color); 17 | text-decoration: none; 18 | transition: color 0.3s var(--easeOutQuint); 19 | } 20 | a:hover, 21 | a:focus { 22 | color: var(--green); 23 | transition-duration: 0.1s; 24 | } 25 | 26 | h1, 27 | h2, 28 | h3, 29 | h4, 30 | h5, 31 | h6 { 32 | color: #292E31; 33 | margin: 2em 0 0.5em; 34 | } 35 | 36 | h1 { 37 | font-size: var(--h1); 38 | font-weight: 300; 39 | } 40 | 41 | h2, 42 | h3, 43 | h4, 44 | h5, 45 | h6 { 46 | font-weight: 600; 47 | } 48 | 49 | h2 { 50 | font-size: var(--h2); 51 | } 52 | 53 | h3 { 54 | font-size: var(--h3); 55 | } 56 | 57 | h4 { 58 | font-size: var(--h4); 59 | } 60 | 61 | h5 { 62 | font-size: var(--h4); 63 | } 64 | 65 | h6 { 66 | font-size: var(--h6); 67 | } 68 | 69 | code { 70 | font-family: var(--monospace-font-family); 71 | } 72 | 73 | img, 74 | svg { 75 | max-width: 100%; 76 | } 77 | -------------------------------------------------------------------------------- /app/styles/components/command-prompt.css: -------------------------------------------------------------------------------- 1 | .command-prompt, pre, code { 2 | background: #EFF3F6; 3 | border-radius: 0.25em; 4 | font-family: var(--monospace-font-family); 5 | } 6 | 7 | pre { 8 | overflow: auto; 9 | padding: 1rem; 10 | white-space: pre; 11 | } 12 | 13 | .command-prompt-body:before, .language-sh:before { 14 | color: #C6D1DA; 15 | content: '$ '; 16 | } 17 | -------------------------------------------------------------------------------- /app/styles/components/fastboot-logo.css: -------------------------------------------------------------------------------- 1 | .fastboot-logo { 2 | fill: #ffffff; 3 | margin: 0 auto; 4 | width: 150px; 5 | } 6 | -------------------------------------------------------------------------------- /app/styles/components/main-hero-meetup.css: -------------------------------------------------------------------------------- 1 | .main-hero-meetup { 2 | background: rgba(0, 0, 0, 0.33); 3 | border-radius: 0.25em; 4 | color: var(--white); 5 | display: flex; 6 | font-size: 1.125rem; 7 | align-items: center; 8 | padding: 1rem; 9 | display: grid; 10 | grid-gap: 2rem; 11 | margin: 1rem 0 2rem; 12 | grid-template-columns: repeat(3, 1fr); 13 | cursor: pointer; 14 | text-decoration: none; 15 | border: none; 16 | } 17 | 18 | .main-hero-meetup:hover, .main-hero-meetup:focus { 19 | color: var(--white); 20 | } 21 | 22 | .main-hero-meetup .content { 23 | grid-column: 1 / span 2; 24 | font-size: 110%; 25 | } 26 | 27 | .main-hero-meetup .small { 28 | font-size: 90%; 29 | } 30 | 31 | .main-hero-meetup .invite { 32 | margin: 0; 33 | font-size: 1rem; 34 | margin-bottom: 0.25rem; 35 | } 36 | 37 | .main-hero-meetup .button { 38 | align-items: center; 39 | text-align: center; 40 | border-radius: 0.25rem; 41 | border: 2px solid var(--white); 42 | min-width: 10em; 43 | padding: 0.55em 2em 0.75em; 44 | text-align: center; 45 | text-decoration: none; 46 | } 47 | 48 | .orange { 49 | color: #FFA91B; 50 | } 51 | -------------------------------------------------------------------------------- /app/styles/components/main-hero-terminal.css: -------------------------------------------------------------------------------- 1 | .main-hero-terminal { 2 | background: rgba(0, 0, 0, 0.33); 3 | border-radius: 0.25em; 4 | color: #8A9DBF; 5 | } 6 | 7 | .main-hero-terminal .faded { 8 | color: rgba(255, 255, 255, 0.1); 9 | } 10 | 11 | .main-hero-terminal .faded:before { 12 | content: '$'; 13 | } 14 | 15 | .main-hero-terminal .orange { 16 | color: #FFA91B; 17 | } 18 | 19 | .main-terminal .purple { 20 | color: var(--purple); 21 | } 22 | 23 | .main-hero-terminal-traffic-light { 24 | background: rgba(255, 255, 255, 0.05); 25 | border-radius: 50%; 26 | height: 14px; 27 | margin-right: 0.25rem; 28 | width: 14px; 29 | } 30 | -------------------------------------------------------------------------------- /app/styles/components/main-hero.css: -------------------------------------------------------------------------------- 1 | .main-hero { 2 | align-items: center; 3 | background: linear-gradient(135deg, #182848, #4B6CB7); 4 | border: 12px solid #102235; 5 | color: var(--white); 6 | display: flex; 7 | font-size: 1.125rem; 8 | justify-content: center; 9 | min-height: 100vh; 10 | } 11 | 12 | .main-hero-logo { 13 | color: inherit; 14 | font-weight: 300; 15 | font-style: italic; 16 | margin: 0; 17 | } 18 | 19 | .main-hero .fastboot-logo { 20 | padding: 50px 0; 21 | } 22 | 23 | .main-hero-slogan { 24 | color: var(--purple); 25 | font-size: var(--h3); 26 | font-style: italic; 27 | font-weight: normal; 28 | margin: 0; 29 | text-transform: lowercase; 30 | } 31 | 32 | .main-hero-requirements { 33 | color: rgba(255, 255, 255, 0.25); 34 | font-size: var(--h6); 35 | text-align: center; 36 | } 37 | 38 | .main-hero-actions { 39 | text-align: center; 40 | } 41 | 42 | .main-hero-button { 43 | margin: 0 13px; 44 | border: 2px solid; 45 | border-radius: 0.25em; 46 | color: var(--green); 47 | display: inline-block; 48 | font-weight: 600; 49 | min-width: 10em; 50 | padding: 0.55em 2em 0.75em; 51 | text-align: center; 52 | text-decoration: none; 53 | margin-bottom: 30px; 54 | } 55 | 56 | .main-hero-button.small { 57 | padding: 0.4em 1.3em 0.6em; 58 | font-size: 90%; 59 | } 60 | 61 | .main-hero-terminal pre, 62 | .main-hero-terminal code { 63 | background: none; 64 | border-radius: 0; 65 | font-family: monospace; 66 | } 67 | .main-hero-terminal pre { 68 | padding: 0; 69 | white-space: normal; 70 | } 71 | -------------------------------------------------------------------------------- /app/styles/components/main-nav.css: -------------------------------------------------------------------------------- 1 | .main-nav a.active { 2 | color: var(--green); 3 | } 4 | .main-nav .fastboot-logo { 5 | margin: 20px 0; 6 | fill: #292E31; 7 | } 8 | -------------------------------------------------------------------------------- /app/styles/components/syntax.css: -------------------------------------------------------------------------------- 1 | /* Base16 Atelier Lakeside Light - Theme */ 2 | /* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/lakeside) */ 3 | /* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ 4 | 5 | /* Atelier-Lakeside Comment */ 6 | .hljs-comment, 7 | .hljs-quote { 8 | color: #5a7b8c; 9 | } 10 | 11 | /* Atelier-Lakeside Red */ 12 | .hljs-variable, 13 | .hljs-template-variable, 14 | .hljs-attribute, 15 | .hljs-tag, 16 | .hljs-name, 17 | .hljs-regexp, 18 | .hljs-link, 19 | .hljs-name, 20 | .hljs-selector-id, 21 | .hljs-selector-class { 22 | color: #d22d72; 23 | } 24 | 25 | /* Atelier-Lakeside Orange */ 26 | .hljs-number, 27 | .hljs-meta, 28 | .hljs-built_in, 29 | .hljs-builtin-name, 30 | .hljs-literal, 31 | .hljs-type, 32 | .hljs-params { 33 | color: #935c25; 34 | } 35 | 36 | /* Atelier-Lakeside Green */ 37 | .hljs-string, 38 | .hljs-symbol, 39 | .hljs-bullet { 40 | color: #568c3b; 41 | } 42 | 43 | /* Atelier-Lakeside Blue */ 44 | .hljs-title, 45 | .hljs-section { 46 | color: #257fad; 47 | } 48 | 49 | /* Atelier-Lakeside Purple */ 50 | .hljs-keyword, 51 | .hljs-selector-tag { 52 | color: #6b6bb8; 53 | } 54 | 55 | .hljs { 56 | display: block; 57 | overflow-x: auto; 58 | background: #EFF3F6; 59 | color: #516d7b; 60 | padding: 0.5em; 61 | } 62 | 63 | .hljs-emphasis { 64 | font-style: italic; 65 | } 66 | 67 | .hljs-strong { 68 | font-weight: bold; 69 | } 70 | -------------------------------------------------------------------------------- /app/styles/nav-bar.css: -------------------------------------------------------------------------------- 1 | .nav-bar { 2 | z-index: 10; 3 | position: fixed; 4 | height: 52px; 5 | background-color: rgba(255, 255, 255, 0.2); 6 | width: 100%; 7 | padding-left: 18px; 8 | top: 0; 9 | 10 | font-size: 24px; 11 | display: flex; 12 | align-items: center; 13 | transition: background-color 0.1s linear, opacity 0.1s linear; 14 | backdrop-filter: blur(10px); 15 | opacity: 0; 16 | } 17 | 18 | .nav-bar h1 { 19 | font-size: 22px; 20 | } 21 | 22 | @media screen and (min-width: 768px) { 23 | .nav-bar { 24 | opacity: 1; 25 | } 26 | 27 | .nav-bar h1 { 28 | color: white; 29 | } 30 | } 31 | 32 | .nav-bar.is-off-top { 33 | opacity: 1; 34 | background-color: rgba(255,255,255, 0.85); 35 | } 36 | 37 | .nav-bar.is-off-top h1 { 38 | color: #8668bf; 39 | } 40 | -------------------------------------------------------------------------------- /app/styles/old.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, body { 6 | margin: 0; 7 | padding: 0; 8 | font-family: Helvetica, Arial, sans-serif; 9 | font-weight: 400; 10 | color: #555; 11 | } 12 | 13 | h1, h2, h3 { 14 | font-family: Bitter; 15 | } 16 | 17 | h1 { 18 | font-size: 40px; 19 | font-weight: 400; 20 | } 21 | 22 | a { 23 | color: #6E569C; 24 | text-decoration: none; 25 | font-weight: 600; 26 | } 27 | 28 | a:visited { 29 | color: #6F5F8C; 30 | } 31 | 32 | .intro { 33 | background: url(/images/nebula.png); 34 | background-size: cover; 35 | width: 100%; 36 | height: 90vh; 37 | } 38 | 39 | .instructions { 40 | width: 100%; 41 | padding: 0 28px; 42 | background: rgba(0,0,0,0.65); 43 | text-align: center; 44 | height: 90vh; 45 | display: flex; 46 | flex-direction: column; 47 | justify-content: space-around; 48 | } 49 | 50 | .instructions h1 { 51 | margin: 0; 52 | font-size: 10vmin; 53 | color: white; 54 | text-align: center; 55 | } 56 | 57 | .instructions h2 { 58 | margin: 0; 59 | font-size: 8vmin; 60 | color: #8668bf; 61 | font-weight: 400; 62 | } 63 | 64 | .buttons { 65 | display: flex; 66 | flex-direction: column; 67 | align-items: center; 68 | } 69 | 70 | img { 71 | max-width: 100%; 72 | } 73 | 74 | pre { 75 | max-width: 100%; 76 | white-space: pre-wrap; 77 | overflow-wrap: break-word; 78 | } 79 | 80 | .buttons button, .buttons a { 81 | display: flex; 82 | justify-content: center; 83 | align-items: center; 84 | font-family: Helvetica; 85 | text-decoration: none; 86 | font-weight: 800; 87 | text-transform: uppercase; 88 | height: 55px; 89 | width: 80vw; 90 | min-width: 150px; 91 | font-size: 14px; 92 | margin: 7px 0; 93 | background-color: transparent; 94 | border-radius: 8px; 95 | border: 4px solid #8668bf; 96 | color: #cfb7f8; 97 | } 98 | 99 | .buttons button:active, .buttons a:active { 100 | border-color: #8668bf; 101 | background-color: #8668bf; 102 | color: #000; 103 | } 104 | 105 | section { 106 | width: 80%; 107 | max-width: 700px; 108 | margin: 32px auto; 109 | font-size: 18px; 110 | line-height: 1.3; 111 | position: relative; 112 | } 113 | 114 | .nav-bar + section { 115 | margin-top: 0; 116 | } 117 | 118 | h3 { 119 | margin: 16px 0; 120 | font-size: 28px; 121 | color: #8668bf; 122 | } 123 | 124 | .command-prompt { 125 | max-width: 600px; 126 | width: 100%; 127 | border: 1px solid #999; 128 | border-radius: 4px; 129 | padding: 14px; 130 | font-family: Monaco, fixed; 131 | } 132 | 133 | p.question { 134 | font-weight: bold; 135 | } 136 | 137 | p.note { 138 | font-size: 70%; 139 | background-color: #E4D6FF; 140 | padding: 13px; 141 | } 142 | 143 | @media screen and (min-width: 1024px) { 144 | h1 { 145 | font-size: 60px; 146 | } 147 | 148 | h3 { 149 | font-size: 40px; 150 | line-height: 1; 151 | } 152 | 153 | section { 154 | max-width: 650px; 155 | line-height: 1.6; 156 | } 157 | 158 | p.note { 159 | margin-right: -200px; 160 | position: absolute; 161 | right: 0; 162 | top: auto; /* default */ 163 | width: 180px; 164 | } 165 | 166 | .intro { 167 | display: flex; 168 | align-items: center; 169 | justify-content: center; 170 | height: 100vh; 171 | } 172 | 173 | .instructions { 174 | width: 60vw; 175 | height: auto; 176 | padding: 28px; 177 | margin-top: 52px; 178 | box-shadow: rgba(0,0,0,0.3) 0 0 18px 7px; 179 | } 180 | 181 | .instructions h1 { 182 | margin: 16px 0; 183 | font-size: 3.4vw; 184 | font-weight: 700; 185 | } 186 | 187 | .instructions h2 { 188 | margin: 16px 0; 189 | font-size: 2vw; 190 | } 191 | 192 | .buttons { 193 | margin: 16px 0; 194 | flex-direction: row; 195 | justify-content: center; 196 | } 197 | 198 | .buttons button, .buttons a { 199 | font-size: 1.2vmax; 200 | height: 4vmax; 201 | min-height: 40px; 202 | max-height: 100px; 203 | width: 40%; 204 | margin: 0 1vmax; 205 | } 206 | } 207 | 208 | @media screen and (min-width: 2000px) { 209 | p.note { 210 | font-size: 80%; 211 | margin-right: -320px; 212 | position: absolute; 213 | right: 0; 214 | top: auto; /* default */ 215 | width: 300px; 216 | } 217 | 218 | h3 { 219 | font-size: 60px; 220 | } 221 | 222 | section { 223 | max-width: 1024px; 224 | font-size: 24px; 225 | margin: 64px auto; 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | {{liquid-outlet}} 2 | -------------------------------------------------------------------------------- /app/templates/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-fastboot/fastboot-website/53704f71cb854113ad03192a57fb15bc5529daa3/app/templates/components/.gitkeep -------------------------------------------------------------------------------- /app/templates/components/main-nav.hbs: -------------------------------------------------------------------------------- 1 | {{fastboot-logo}} 2 | 9 | -------------------------------------------------------------------------------- /app/templates/components/nav-bar.hbs: -------------------------------------------------------------------------------- 1 | {{#link-to "index"}} 2 |

FastBoot

3 | {{/link-to}} 4 | -------------------------------------------------------------------------------- /app/templates/components/route-error.hbs: -------------------------------------------------------------------------------- 1 |

Oops, that page doesn't exist!

2 |

Try one of these:

3 | 8 | -------------------------------------------------------------------------------- /app/templates/head.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/templates/index.hbs: -------------------------------------------------------------------------------- 1 | {{main-hero}} 2 | 3 |
4 | {{main-nav}} 5 |
6 | {{md-text text=model html=true typographer=true linkify=true}} 7 |
8 |
9 | -------------------------------------------------------------------------------- /app/templates/page.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{main-nav}} 3 | {{#liquid-bind model as |currentModel|}} 4 |
5 | {{#if currentModel}} 6 | {{md-text text=currentModel html=true typographer=true linkify=true}} 7 | {{else}} 8 | {{route-error}} 9 | {{/if}} 10 |
11 | {{/liquid-bind}} 12 |
13 | -------------------------------------------------------------------------------- /app/transitions.js: -------------------------------------------------------------------------------- 1 | export default function() { 2 | this.transition( 3 | this.use('crossFade') 4 | ); 5 | } 6 | -------------------------------------------------------------------------------- /config/deploy.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | module.exports = function(deployTarget) { 4 | var ENV = { 5 | build: {}, 6 | gzip: { 7 | ignorePattern: '{fastboot/*.js,*.json}', 8 | keep: true 9 | } 10 | // include other plugin configuration that applies to all deploy targets here 11 | }; 12 | 13 | if (deployTarget === 'development') { 14 | ENV.build.environment = 'development'; 15 | // configure other plugins for development deploy target here 16 | } 17 | 18 | if (deployTarget === 'staging') { 19 | ENV.build.environment = 'production'; 20 | // configure other plugins for staging deploy target here 21 | } 22 | 23 | if (deployTarget === 'production') { 24 | ENV.build.environment = 'production'; 25 | // configure other plugins for production deploy target here 26 | } 27 | 28 | // Note: if you need to build some configuration asynchronously, you can return 29 | // a promise that resolves with the ENV object instead of returning the 30 | // ENV object synchronously. 31 | return ENV; 32 | }; 33 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(environment) { 4 | let ENV = { 5 | modulePrefix: 'fastboot-website', 6 | environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | metricsAdapters: [ 10 | { 11 | name: 'GoogleAnalytics', 12 | environments: ['all'], 13 | config: { 14 | id: 'UA-75816692-1' 15 | } 16 | } 17 | ], 18 | EmberENV: { 19 | FEATURES: { 20 | // Here you can enable experimental features on an ember canary build 21 | // e.g. 'with-controller': true 22 | }, 23 | EXTEND_PROTOTYPES: { 24 | // Prevent Ember Data from overriding Date.parse. 25 | Date: false 26 | } 27 | }, 28 | 29 | APP: { 30 | // Here you can pass flags/options to your application instance 31 | // when it is created 32 | } 33 | }; 34 | 35 | if (environment === 'development') { 36 | // ENV.APP.LOG_RESOLVER = true; 37 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 38 | // ENV.APP.LOG_TRANSITIONS = true; 39 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 40 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 41 | } 42 | 43 | if (environment === 'test') { 44 | // Testem prefers this... 45 | ENV.locationType = 'none'; 46 | 47 | // keep test console output quieter 48 | ENV.APP.LOG_ACTIVE_GENERATION = false; 49 | ENV.APP.LOG_VIEW_LOOKUPS = false; 50 | 51 | ENV.APP.rootElement = '#ember-testing'; 52 | ENV.APP.autoboot = false; 53 | } 54 | 55 | if (environment === 'production') { 56 | // here you can enable a production-specific feature 57 | } 58 | 59 | return ENV; 60 | }; 61 | -------------------------------------------------------------------------------- /config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "jquery-integration": true 3 | } 4 | -------------------------------------------------------------------------------- /config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions' 7 | ]; 8 | 9 | const isCI = !!process.env.CI; 10 | const isProduction = process.env.EMBER_ENV === 'production'; 11 | 12 | if (isCI || isProduction) { 13 | browsers.push('ie 11'); 14 | } 15 | 16 | module.exports = { 17 | browsers 18 | }; 19 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | /* global require, module */ 3 | const EmberApp = require('ember-cli/lib/broccoli/ember-app'); 4 | 5 | module.exports = function(defaults) { 6 | let prepend = ''; 7 | 8 | if ('FASTLY_CDN_URL' in process.env) { 9 | prepend = 'https://' + process.env.FASTLY_CDN_URL + '/'; 10 | } 11 | 12 | const app = new EmberApp(defaults, { 13 | postcssOptions: { 14 | compile: { 15 | enabled: true, 16 | plugins: [ 17 | { 18 | module: require('postcss-import'), 19 | options: { 20 | glob: true 21 | } 22 | }, 23 | { 24 | module: require('postcss-custom-properties') 25 | }, 26 | { 27 | module: require('postcss-custom-media') 28 | }, 29 | { 30 | module: require('autoprefixer'), 31 | options: { 32 | browsers: 'last 2 versions' 33 | } 34 | } 35 | ] 36 | } 37 | }, 38 | fingerprint: { 39 | prepend: prepend 40 | } 41 | }); 42 | 43 | // Use `app.import` to add additional libraries to the generated 44 | // output files. 45 | // 46 | // If you need to use different assets in different 47 | // environments, specify an object as the first parameter. That 48 | // object's keys should be the environment name and the values 49 | // should be the asset to use in that environment. 50 | // 51 | // If the library that you are including contains AMD or ES6 52 | // modules that you would like to import into your application 53 | // please specify an object with the list of modules as keys 54 | // along with the exports of each module as its value. 55 | 56 | return app.toTree(); 57 | }; 58 | -------------------------------------------------------------------------------- /fastboot-server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const express = require('express'); 5 | const cluster = require('express-cluster'); 6 | const fastbootMiddleware = require('fastboot-express-middleware'); 7 | const staticGzip = require('express-serve-static-gzip'); 8 | const sabayon = require('express-sabayon'); 9 | 10 | const assetPath = 'tmp/deploy-dist' 11 | const port = process.env.PORT || 3000; 12 | 13 | // eslint-disable-next-line no-console 14 | console.log('Booting Ember app...'); 15 | 16 | try { 17 | fs.accessSync(assetPath, fs.F_OK); 18 | } catch (e) { 19 | // eslint-disable-next-line no-console 20 | console.error(`The asset path ${assetPath} does not exist.`); 21 | process.exit(1); 22 | } 23 | 24 | 25 | // FastFail™: this is not mandatory; the first call to visit would 26 | // also boot the app anyway. This is just to provide useful feedback 27 | // instead of booting a server that keeps serving 500. 28 | // 29 | // Note that Application#buildApp is still a private API atm, so it might 30 | // go through more churn in the near term. 31 | // eslint-disable-next-line no-console 32 | console.log('Ember app booted successfully.'); 33 | cluster(function() { 34 | let app = express(); 35 | 36 | let fastboot = fastbootMiddleware(assetPath); 37 | 38 | if (assetPath) { 39 | app.get('/', fastboot); 40 | app.use(staticGzip(assetPath)); 41 | app.use(express.static(assetPath)); 42 | } 43 | 44 | app.get(sabayon.path, sabayon.middleware()); 45 | app.get('/*', fastboot); 46 | 47 | let listener = app.listen(port, function() { 48 | let host = listener.address().address; 49 | let port = listener.address().port; 50 | let family = listener.address().family; 51 | 52 | if (family === 'IPv6') { host = '[' + host + ']'; } 53 | 54 | // eslint-disable-next-line no-console 55 | console.log('Ember FastBoot running at http://' + host + ":" + port); 56 | }); 57 | }, { verbose: true }); 58 | -------------------------------------------------------------------------------- /markdown/docs/addon-author-guide.md: -------------------------------------------------------------------------------- 1 | # Addon Author Guide 2 | 3 | If you're the author of an Ember addon, you might need to make some 4 | changes to ensure that your addon runs correctly in the FastBoot 5 | environment. 6 | 7 | Remember that when updating your addons for FastBoot compatibility, your 8 | code now must run in both a browser *and* Node.js. Most importantly, 9 | that means that you cannot assume that DOM APIs will always be 10 | available. 11 | 12 | ## Don't Break the Boot 13 | 14 | The first step towards FastBoot support is ensuring that your addon 15 | doesn't cause the app to crash when it is run in Node. You can verify this by 16 | adding your addon to a new Ember app, adding the FastBoot addon, and 17 | running the FastBoot server with `ember serve` (provided your app is running 18 | `ember-cli` 2.12.0 and above). 19 | 20 | If that process results in an error and the server crashing, you know 21 | that your addon is trying to do something that may work in the browser 22 | but not in Node.js. 23 | 24 | ### Common Causes 25 | 26 | The most common cause of failure on boot is initialization code that 27 | relies on the following global objects that aren't available in Node.js: 28 | 29 | * `document` 30 | * `navigator` 31 | * `window` 32 | 33 | For example, many addons may detect features by creating an element via 34 | `document.createElement()` and checking whether certain properties on it 35 | exist. Or an addon may try to detect the height of the viewport via 36 | `window.innerHeight`, but because there is no viewport conceptually in 37 | Node, this value is not set. 38 | 39 | ### Tracking It Down 40 | 41 | FastBoot works by running a singular build of your application, leveraging 42 | the common `app.js` and `vendor.js` files in both browser and Node. 43 | However, in Node an additional asset `app-fastboot.js` will also be 44 | leveraged. This asset contains FastBoot specific overrides defined by the 45 | application or other addons. 46 | 47 | What this means in practice is that errors will include stack traces 48 | with very large line numbers. The code from your addon will be intermingled 49 | with other addons, and Ember itself, in the `vendor.js` file. 50 | 51 | To find the cause of the exception, look at the stack trace shown when 52 | running the app. Note the line number, then open 53 | `dist/assets/vendor.js` in your text editor. Scroll down to the 54 | provided line number (or, better, use your editor's "Go to Line" 55 | functionality) to pinpoint exactly which line is causing the issue. 56 | 57 | ![Example error message and stack trace from an incompatible addon](/images/addon-author-guide/stack-trace-example.png) 58 | 59 | ### How to Fix It 60 | 61 | There are two common approaches to fixing code that accesses browser 62 | APIs. 63 | 64 | First, you can check for the existence of the API you need before 65 | accessing it. For example, imagine we have an addon that checks the user 66 | agent of the browser to detect the platform. That code looks something 67 | like this: 68 | 69 | ```js 70 | var ua = window.navigator.userAgent || ''; 71 | ``` 72 | 73 | This line will throw an exception, because `window.navigator` doesn't 74 | exist in Node. Instead, we can add an extra guard to verify it exists, 75 | and default to an empty string if not: 76 | 77 | ```js 78 | var ua = window && window.navigator ? window.navigator.userAgent : ''; 79 | ``` 80 | 81 | The second option is to move any code that relies on browser-only APIs to 82 | hooks that only get invoked in the browser. 83 | 84 | Independent from FastBoot compatibility, it's generally a good idea to 85 | do as little work as possible during the evaluation of your addon's 86 | JavaScript files. Doing upfront work can have a negative impact on load 87 | times. If you can defer doing work (such as feature detection) until the 88 | last possible moment, you can improve the performance of applications 89 | using your addon. Adding FastBoot compatibility is a good opportunity to 90 | make these improvements to your addon. 91 | 92 | Here's a concrete example. Imagine our addon has a component that needs 93 | to listen to `resize` events on the window. We set up a single listener in 94 | our component file that's shared across all instances of the component: 95 | 96 | ```js 97 | // addon/components/resizable-component.js 98 | import Component from '@ember/component'; 99 | import $ from 'jquery'; 100 | 101 | let componentsToNotify = []; 102 | $(window).on('resize', () => { 103 | componentsToNotify.forEach(c => c.windowDidResize()); 104 | }); 105 | 106 | export default class ResizableComponent extends Component { 107 | init() { 108 | componentsToNotify.push(this); 109 | }, 110 | 111 | windowDidResize() { 112 | // ... do some work 113 | }, 114 | 115 | willDestroy() { 116 | for (let i = 0; i < componentsToNotify.length; i++) { 117 | if (componentsToNotify[i] === this) { 118 | componentsToNotify.splice(i, 1); 119 | break; 120 | } 121 | } 122 | } 123 | } 124 | ``` 125 | 126 | In this approach, we set up an event listener on the window even if the 127 | app author never uses our component. It also means that our addon won't 128 | work in FastBoot, because it tries to access the DOM while defining the 129 | component. 130 | 131 | Instead, we can move this setup into the component's 132 | `didInsertElement()` hook, which is only invoked once the component is 133 | inserted into the DOM (i.e., it's only invoked in the browser 134 | and never in Node). 135 | 136 | ```js 137 | // addon/components/resizable-component.js 138 | import Component from '@ember/component'; 139 | import $ from 'jquery'; 140 | 141 | let componentsToNotify = []; 142 | let didSetupListener = false; 143 | 144 | function setupListener() { 145 | didSetupListener = true; 146 | $(window).on('resize', () => { 147 | componentsToNotify.forEach(c => c.windowDidResize()); 148 | }); 149 | } 150 | 151 | export default class ResizableComponent extends Component { 152 | didInsertElement() { 153 | if (!didSetupListener) { setupListener(); } 154 | componentsToNotify.push(this); 155 | }, 156 | 157 | windowDidResize() { 158 | // ... do some work 159 | }, 160 | 161 | willDestroy() { 162 | for (let i = 0; i < componentsToNotify.length; i++) { 163 | if (componentsToNotify[i] === this) { 164 | componentsToNotify.splice(i, 1); 165 | break; 166 | } 167 | } 168 | } 169 | } 170 | ``` 171 | 172 | This lazy approach moves setup to the first time the component is used, 173 | improving boot time for routes in the application that don't use this 174 | component. It also means the component is FastBoot-compatible, because 175 | all of the code that touches the DOM is contained in the 176 | `didInsertElement()` hook, which is not invoked in FastBoot. 177 | 178 | ### Third-Party Dependencies 179 | 180 | Many Ember addons wrap other JavaScript libraries in a way that makes 181 | them easier to use from an Ember app. For example, the 182 | [ivy-codemirror](https://github.com/IvyApp/ivy-codemirror) addon wraps 183 | the [CodeMirror code editor](https://codemirror.net/) in a component 184 | that makes it easy to drop into an Ember app. 185 | 186 | Sometimes the library your addon wraps is itself incompatible with 187 | Node.js. When you include the library in your `index.js` file, you 188 | include code that will prevent the app from running in FastBoot. 189 | 190 | If your addon imports third-party code and you are unable to make 191 | changes to it to add Node compatibility, you can add a guard to your 192 | `index.js` file to only include it in the browser build. 193 | 194 | The FastBoot addon (`ember-cli-fastboot`) provides the FastBoot server a manifest of 195 | assets to load in the Node server. If your third-party library is not 196 | Node compatible, you can wrap it with a check as below in an addon: 197 | 198 | ```js 199 | var map = require('broccoli-stew').map; 200 | 201 | treeForVendor(defaultTree) { 202 | var browserVendorLib = new Funnel(); 203 | 204 | browserVendorLib = map(browserVendorLib, (content) => `if (typeof FastBoot === 'undefined') { ${content} }`); 205 | 206 | return new mergeTrees([defaultTree, browserVendorLib]); 207 | } 208 | 209 | included() { 210 | // this file will be loaded in FastBoot but will not be eval'd 211 | app.import('vendor/.js'); 212 | } 213 | ``` 214 | 215 | Note that not running the library in the FastBoot build means that any 216 | modules it exports will not be available inside the app when running in 217 | the FastBoot environment. Make sure to guard against missing 218 | dependencies in that case. 219 | 220 | For apps trying to import third party libraries that are not compatible in Node, should 221 | create an in-repo addon within their app and follow the above guideline. 222 | 223 | ## Loading additional assets in FastBoot 224 | 225 | Often your addon may require to load third party libraries that are 226 | specific to Node.js and only need to be loaded on the server side. 227 | This can include loading libraries before or after the vendor file is loaded 228 | in the sandbox and/or before or after the app file is loaded in the sandbox. 229 | Since the FastBoot manifest defines an array of vendor and app files to load 230 | in the sandbox, an addon can define additional vendor/app files to 231 | load in the sandbox as well. 232 | 233 | If your addon requires to load something in the sandbox, you can define 234 | the `updateFastBootManifest` hook from your addon (in `index.js`) as an example 235 | below: 236 | 237 | ```js 238 | included(app) { 239 | // this will copy contents of foo.js to dist/assets/foo-fastboot.js 240 | app.import('node_modules/foo-package/dist/foo.js', { 241 | outputFile: 'assets/foo-fastboot.js' 242 | }); 243 | 244 | // this will copy contents bar.js to dist/assets/bar-fastboot.js 245 | app.import('node_modules/bar-package/dist/bar.js', { 246 | outputFile: 'assets/bar-fastboot.js' 247 | }); 248 | }, 249 | 250 | updateFastBootManifest(manifest) { 251 | /** 252 | * manifest is an object containing: 253 | * { 254 | * vendorFiles: [, ...], 255 | * appFiles: [, ...], 256 | * htmlFile: '' 257 | * } 258 | */ 259 | 260 | // This will load the foo.js after vendor.js is loaded in Node 261 | manifest.vendorFiles.push('assets/foo-fastboot.js'); 262 | // This will load bar.js after app-fastboot.js is loaded in the Node 263 | manifest.appFiles.push('assets/bar-fastboot.js'); 264 | 265 | // remember to return the updated manifest, otherwise your build will fail. 266 | return manifest; 267 | } 268 | ``` 269 | 270 | The above code snippet loads `foo.js` before `vendor.js` and `bar.js` after 271 | `app-fastboot.js` is loaded in the sandbox. You could chose to decide when you 272 | want to load your library. Typically you would want to load third party libraries 273 | `vendor.js` is loaded in Node. In such cases, you can use 274 | `manifest.vendorFiles.push(...)`. 275 | 276 | ## Conditionally include assets in FastBoot asset 277 | 278 | Often your addon may need to conditionally include additional app trees based on ember version. Example, Ember changed an API and in order to have your addon be backward compatible for the API changes you want to include an asset when the ember version is x. For such usecases you could define the `treeForFastBoot` hook in your addon's `index.js` as below: 279 | 280 | ```js 281 | treeForFastBoot: function(tree) { 282 | let fastbootHtmlBarsTree; 283 | 284 | // check the ember version and conditionally patch the DOM api 285 | if (this._getEmberVersion().lt('2.10.0-alpha.1')) { 286 | fastbootHtmlBarsTree = this.treeGenerator(path.resolve(__dirname, 'fastboot-app-lt-2-9')); 287 | return tree ? new MergeTrees([tree, fastbootHtmlBarsTree]) : fastbootHtmlBarsTree; 288 | } 289 | 290 | return tree; 291 | }, 292 | ``` 293 | 294 | The `tree` is the additional fastboot asset that gets generated and contains the fastboot overrides. 295 | 296 | ## Browser-Only or Node-Only Initializers 297 | 298 | If your addon requires browser-specific or Node-specific initialization 299 | code to be run, you would need to define them seperately as follows: 300 | 301 | ### Browser-Only initializers 302 | 303 | You should define an initializer under `app/initializers` or `app/instance-initializers` as normally 304 | you would do. The only addition would to wrap the initializer with a FastBoot environment check. This is 305 | to make sure the initializer does not run in FastBoot. An example is as follows: 306 | 307 | ```js 308 | function initialize(app) { 309 | if (typeof FastBoot === 'undefined') { 310 | // only execute this in browser 311 | } 312 | } 313 | 314 | export default { 315 | name: 'my-initializer', 316 | initialize: initializer 317 | } 318 | ``` 319 | 320 | ### Node-Only initializer 321 | 322 | Often you want to define Node specific behavior for your app at boot time. You should define the Node-Only 323 | initializer under `fastboot/initializers` or `fastboot/instance-initializers`. Note the `fastboot` directory is a sibling of the `app` or `addon` directory. The FastBoot addon (`ember-cli-fastboot`) will read the `fastboot` 324 | directories from all addons and your parent app and create `app-fastboot.js` which is included in the FastBoot manifest and loaded in FastBoot only. As an addon author, you just need to define the initializer 325 | under `fastboot` directory. An example of directory structure is as follows: 326 | 327 | ```js 328 | -+ app/ 329 | ----+ initializers/ 330 | --------+ foo1.js 331 | --------+ ... 332 | ----+ instance-initializers/ 333 | --------+ foo.js 334 | --------+ ... 335 | -+ fastboot/ 336 | ----+ initializers/ 337 | --------+ foo-fastboot.js 338 | --------+ bar.js 339 | ``` 340 | 341 | An example of how `bar.js` from the above example will look as: 342 | 343 | ```js 344 | function initialize(app) { 345 | // do stuff that will only run in Node 346 | } 347 | 348 | export default { 349 | name: 'bar', 350 | initialize: initializer 351 | } 352 | ``` 353 | 354 | In the Node only initializer, you don't need to wrap them with any FastBoot check since the above initializer is never 355 | sent to the browser. 356 | 357 | **Note**: You could define initializers for browser and Node with the same filename and `name` property. For example, you have an initializer `foo.js` under `app/initializers/foo.js` and `fastboot/initializers/foo.js` with the initializer `name` set to `foo`. When running the app in browser, `app/initializers/foo.js` will run. When running the app in Node, `fastboot/initializers/foo.js` will run. 358 | 359 | ## Requiring Node Modules 360 | 361 | Some addons may need functionality when running in FastBoot that is 362 | normally provided by the browser. Addons can load Node modules (either 363 | built-in or from npm) when running in FastBoot, but only if they've been 364 | explicitly whitelisted first. 365 | 366 | To whitelist a dependency, you'll need to edit your addon's 367 | `package.json`. You probably already have an `ember-addon` field there. 368 | Edit this this hash to add a property called `fastbootDependencies` that 369 | contains an array of Node modules that may be used. 370 | 371 | Make sure that any modules you want to access are also included in your 372 | `package.json`'s `dependencies` field. 373 | 374 | For example, if I have an addon where I need to use both the built-in 375 | `path` module as well as `redis`, my `package.json` might look like 376 | this: 377 | 378 | ```js 379 | { 380 | "name": "my-ember-addon", 381 | "version": "1.0.0", 382 | // ... 383 | "dependencies": { 384 | "redis": "^2.6.0" 385 | }, 386 | "ember-addon": { 387 | "configPath": "tests/dummy/config", 388 | "fastbootDependencies": [ 389 | "path", 390 | "redis" 391 | ] 392 | } 393 | } 394 | ``` 395 | 396 | Because `path` is built-in, I don't need to add it to my dependencies. 397 | But because `redis` comes from npm, I need to add it as a dependency. In 398 | both cases, I must explicitly whitelist that both are available to the 399 | Ember app. 400 | 401 | Now that we've configured `path` and `redis` to be available from 402 | within our Ember app, addon code can require it using the 403 | `FastBoot.require()` method: 404 | 405 | ```js 406 | const redis = FastBoot.require('redis'); 407 | 408 | let client = redis.createClient(); 409 | ``` 410 | 411 | Note that the `FastBoot` global is _only_ available when running in 412 | FastBoot and won't exist when your app is running in the browser. If you 413 | have some code that runs in both environments, make sure to check 414 | whether `FastBoot` exists before you access it. (And, of course, someone 415 | may be using your addon in an app that doesn't have FastBoot installed.) 416 | 417 | ## Accessing the FastBoot Service 418 | 419 | As discussed in the [User Guide](user-guide), you can access information 420 | about the current request via the `fastboot` service, accessible like 421 | any other service in an Ember app. 422 | 423 | One thing to note is that someone may use your addon in an app that 424 | doesn't have FastBoot installed. In that case, if your addon tries to 425 | inject the `fastboot` service, they'll get an exception saying the 426 | `fastboot` service cannot be found. 427 | 428 | Instead, you can write a getter that uses the low-level 429 | `getOwner` functionality to lookup the `fastboot` service directly: 430 | 431 | ```js 432 | import Service from '@ember/service'; 433 | import { getOwner } from '@ember/application'; 434 | 435 | export default class SomeService extends Service { 436 | doSomething() { 437 | let fastboot = this.fastboot; 438 | if (!fastboot) { return; } 439 | // do something that requires FastBoot 440 | }, 441 | 442 | get fastboot() { 443 | let owner = getOwner(this); 444 | 445 | return owner.lookup('service:fastboot'); 446 | } 447 | } 448 | ``` 449 | 450 | ## Examples 451 | 452 | It can be helpful to look at examples of other real-world addons that 453 | have made tweaks for FastBoot compatibility. Here is a list of pull 454 | requests to various addons that have fixed FastBoot compatibility 455 | issues: 456 | 457 | * [Fix IE check on FastBoot - 458 | ember-power-select](https://github.com/cibernox/ember-power-select/pull/543) 459 | * [Make it Fastboot compatible - 460 | ember-basic-dropdown](https://github.com/cibernox/ember-basic-dropdown/pull/42/files) 461 | * [Lazy Fastboot compatibility - 462 | ivy-codemirror](https://github.com/IvyApp/ivy-codemirror/pull/18) 463 | * [Fastboot compatibility - 464 | ember-cli-head](https://github.com/ronco/ember-cli-head/pull/1) 465 | * [Fastboot compatibility - 466 | ember-gestures](https://github.com/runspired/ember-gestures/pull/56) 467 | * [Fastboot compatibility - ember-wormhole](https://github.com/yapplabs/ember-wormhole/pull/54) 468 | 469 | ## Getting Help 470 | 471 | If you're still having trouble figuring out how to make your addon 472 | FastBoot compatible, feel free to join the `#fastboot` channel on the 473 | [Ember Community Discord](https://discordapp.com/invite/zT3asNS) 474 | to get help. 475 | -------------------------------------------------------------------------------- /markdown/docs/deploying.md: -------------------------------------------------------------------------------- 1 | # Deploying FastBoot 2 | 3 | **NOTE**: This page is out of date, [FastBoot App Server is the current 4 | recommended way](https://github.com/ember-fastboot/fastboot-app-server). This section will 5 | be updated soon, in the meantime, please check FastBoot App Server's readme. 6 | 7 | When it comes to deploying FastBoot, there are many off-the-shelf 8 | options that make it easy to get it up and running in production. If 9 | you need something maximally flexible, it's easy to integrate the 10 | FastBoot server into your existing Node.js infrastructure. 11 | 12 | ## Heroku 13 | 14 | Heroku's buildpack for Ember.js supports FastBoot out of the box. With 15 | Heroku, deploying a new version of your app is as simple as typing `git 16 | push heroku`. 17 | 18 | To add the Heroku buildpack to your project, see 19 | [heroku-buildpack-emberjs](https://github.com/heroku/heroku-buildpack-emberjs). 20 | 21 | ## AWS Elastic Beanstalk 22 | 23 | Elastic Beanstalk is a service from Amazon Web Services that allows you 24 | to provision Node.js apps and run them in production with minimal ops 25 | overhead. 26 | 27 | For more about deploying to Elastic Beanstalk, see 28 | [ember-cli-deploy-elastic-beanstalk](https://github.com/tomdale/ember-cli-deploy-elastic-beanstalk). 29 | 30 | ## AWS Lambda 31 | 32 | Lambda is an AWS service for running Node.js functions without a server. 33 | You can deploy your Ember app as a FastBoot Lambda function, so you 34 | don't have to worry about provisioning servers at all; Amazon will 35 | autoscale based on load. You can serve FastBoot results over HTTP by 36 | combining Lambda with API Gateway. 37 | 38 | Lambda is not recommended for serving directly to users, due to 39 | unpredictable response times, but is a perfect fit for pre-rendering or 40 | serving to search crawlers. 41 | 42 | For more about deploying FastBoot to Lambda, see either [Bustle Labs' 43 | Lambda ember-cli-deploy plugin](https://github.com/bustlelabs/ember-cli-deploy-fastboot-lambda). Alternatively, see [ember-cli-deploy-fastboot-api-lambda](https://github.com/wytlytningNZ/ember-cli-deploy-fastboot-api-lambda) for an end-to-end Lambda/API Gateway hosting solution that serves both Fastboot pages and static assets. 44 | 45 | 46 | ## Custom Server 47 | 48 | If one of the out-of-the-box deployment strategies doesn't work for you, 49 | you can adapt [the standalone FastBoot 50 | server](https://github.com/ember-fastboot/ember-fastboot-server) to your 51 | needs. 52 | 53 | You can use the FastBoot server in one of two ways: as an HTTP server 54 | from the command line, or if you need more customization, as an Express 55 | middleware you can add to your own HTTP server. 56 | 57 | ### Deploying Your App 58 | 59 | When you build your Ember app via `ember build`, it will compile 60 | everything into the `dist` directory. Upload this `dist` directory to your 61 | server. It is the "build artifact" that contains the current version of 62 | your application ready to run in both Node.js and the browser. 63 | 64 | Once you've uploaded `dist` to your server, you need to run `npm 65 | install` inside. The `dist` directory contains a `package.json` that 66 | contains all of the dependencies needed to run your app in Node.js. Running 67 | `npm install` makes sure they get installed correctly. 68 | 69 | Make sure that you install npm modules in `dist` **after** uploading 70 | `dist` to your server. Any npm packages that have native dependencies 71 | are unlikely to work if compiled ahead of time, unless you're running 72 | the same OS and CPU architecture on both your local machine and server. 73 | 74 | ### From the Command Line 75 | 76 | On your server, install the FastBoot server package globally: 77 | 78 | ```sh 79 | npm install -g ember-fastboot-server 80 | ``` 81 | 82 | Once installed, you can start the HTTP server by running: 83 | 84 | ```sh 85 | ember-fastboot path/to/dist --port 80 86 | ``` 87 | 88 | ### As a Middleware 89 | 90 | If you need maximum customization of your FastBoot server, or if you 91 | just have an existing Node.js infrastructure that you'd like to 92 | integrate FastBoot into, you can use the `ember-fastboot-server` module 93 | as a middleware: 94 | 95 | ```js 96 | var server = new FastBootServer({ 97 | distPath: 'path/to/dist' 98 | }); 99 | 100 | var app = express(); 101 | 102 | app.get('/*', server.middleware()); 103 | 104 | var listener = app.listen(process.env.PORT || 3000, function() { 105 | var host = listener.address().address; 106 | var port = listener.address().port; 107 | 108 | console.log('FastBoot running at http://' + host + ":" + port); 109 | }); 110 | ``` 111 | 112 | For more information, see the [FastBoot server 113 | README](https://github.com/ember-fastboot/ember-fastboot-server). 114 | -------------------------------------------------------------------------------- /markdown/docs/user-guide.md: -------------------------------------------------------------------------------- 1 | # User Guide 2 | 3 | ## Introduction 4 | 5 | FastBoot brings server-side rendering to Ember.js applications. 6 | By performing initial renders on the server, your Ember app is accessible to search engines, `curl`, and other scrapers. 7 | Users see content faster, and can view your app even if they have JavaScript disabled or the JavaScript files fail to load. 8 | 9 | ## How It Works 10 | 11 | FastBoot works by creating a Node.js process and executing your Ember application within it. 12 | 13 | Most Ember applications should work out of the box. 14 | However, there are some patterns that you should be sure to follow to guarantee that your application is fully FastBoot compatible. 15 | See [Tips and Tricks](#tips-and-tricks) below for a full list. 16 | 17 | ## Installation 18 | 19 | FastBoot is an addon you can add to existing Ember CLI apps. 20 | To FastBoot-enable your application, run: 21 | 22 | ```sh 23 | ember install ember-cli-fastboot 24 | ``` 25 | 26 | This will install the `ember-cli-fastboot` addon via npm and save it to your application's `package.json`. 27 | 28 | ## Testing Locally 29 | 30 | You can start a FastBoot server on your development machine by running: 31 | 32 | ```sh 33 | ember serve 34 | ``` 35 | 36 | This starts a FastBoot server listening on port 4200\. 37 | You can verify it's working by curling from `localhost`: 38 | 39 | ```sh 40 | curl 'http://localhost:4200/' -H 'Accept: text/html' 41 | ``` 42 | 43 | You should see the content of your application's index template rendered in the output, instead of the empty HTML most client-side rendered apps show. 44 | 45 | To stop the server, press `Ctrl-C` on your keyboard to kill the process. 46 | 47 | You can run the development server on a different port by passing the `--port` argument or use any of `ember-cli` options: 48 | 49 | ```sh 50 | ember serve --port 4567 51 | ``` 52 | 53 | ### Disabling FastBoot 54 | 55 | You can also turn off the server side rendering on a per request basis using `fastboot` query parameter. To disable FastBoot rendered content, visit [localhost:4200/?fastboot=false](http://localhost:4200/?fastboot=false). You can enable FastBoot rendered content again by visiting [localhost:4200/?fastboot=true](http://localhost:4200/?fastboot=true). 56 | 57 | 58 | ## Building for Production 59 | 60 | Once you've installed the FastBoot addon, a Node-compatible build of your app is automatically included when you run `ember build`. 61 | As with any other Ember build, you'll want to build for the production environment when deploying for your users to use: 62 | 63 | ```sh 64 | ember build --environment production 65 | ``` 66 | 67 | **You are strongly encouraged to automate deploys using [Ember CLI Deploy][ember-cli-deploy].** 68 | There are a number of FastBoot-compatible deploy plugins, with more being authored every day. 69 | 70 | Manual deployment is slow and error-prone, and even if you have a custom deployment process, writing an Ember CLI Deploy plugin is straightforward and will save you time and energy in the future. 71 | 72 | As with a standard Ember build for the browser, compiled assets by default go in your application's `dist` directory. 73 | FastBoot adds two additions: 74 | 75 | 1. `dist/package.json`, which contains metadata about your app for consumption by the FastBoot server. 76 | 2. `dist/assets/`, which contains your app's compiled JavaScript that is evaluated and run by the FastBoot server (`vendor.js`, `app.js`, `app-fastboot.js`). 77 | 78 | For more information, see the [Architecture](#architecture) section. 79 | 80 | ## The FastBoot Service 81 | 82 | FastBoot registers the `fastboot` [service](https://guides.emberjs.com/v2.7.0/applications/services/) which you can inject into your application: 83 | 84 | ```javascript 85 | import Route from '@ember/routing/route'; 86 | import { service } from '@ember/service'; 87 | 88 | export default class Application extends Route { 89 | @service fastboot; 90 | 91 | get isFastBoot() { 92 | return this.fastboot.isFastBoot; 93 | } 94 | 95 | // ... Application code 96 | } 97 | ``` 98 | 99 | This service contains several useful objects and methods you can use to manage an application's state when it is being FastBooted. 100 | 101 | Property | Description 102 | ---------------- | --------------------------------------------------------------------------------------------------------- 103 | `isFastBoot` | Equals `true` if your application is running in FastBoot 104 | `response` | The FastBoot server's response 105 | `request` | The request sent to the FastBoot server 106 | `shoebox` | A key/value store for passing data acquired server-side to the client 107 | `deferRendering` | A function that takes a `Promise` that you can use to defer sending the response to allow asynchronous rerendering 108 | 109 | ### Deferring Response Rendering 110 | 111 | By default, FastBoot waits for the `beforeModel`, `model`, and `afterModel` hooks to resolve before sending a response back to the client. 112 | You can use these hooks to defer the initial rendering of your application. 113 | See [Use Model Hooks to Defer Rendering](#use-model-hooks-to-defer-rendering). 114 | 115 | If you have asynchronous code that runs outside of these lifecycle hooks, you will want to use `deferRendering` to block the response. `deferRendering` function accepts a `Promise` and will chain all promises passed to it. 116 | FastBoot will wait for these promises to resolve, allowing the app to rerender itself based on new data coming in asynchronously before sending the response to the client. 117 | 118 | You must call `deferRendering` before these model hooks complete. 119 | For example, if you made an asynchronous call in a Component, you would use `deferRendering` in the `init` lifecycle hook. 120 | 121 | ### FastBoot Request 122 | 123 | The `fastboot.request` key allows you access to the request sent to the FastBoot server. 124 | 125 | #### Access Request Headers 126 | 127 | You can access the current request headers via `fastboot.request`. 128 | The `headers` object implements part of the [Fetch API's Headers class](https://developer.mozilla.org/en-US/docs/Web/API/Headers), the functions available are [`has`](https://developer.mozilla.org/en-US/docs/Web/API/Headers/has), [`get`](https://developer.mozilla.org/en-US/docs/Web/API/Headers/get), and [`getAll`](https://developer.mozilla.org/en-US/docs/Web/API/Headers/getAll). 129 | For more information about HTTP headers see [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers). 130 | 131 | ```javascript 132 | import Route from '@ember/routing/route'; 133 | import { service } from '@ember/service'; 134 | 135 | export default class Application extends Route { 136 | @service fastboot; 137 | 138 | model() { 139 | let headers = this.fastboot.request?.headers; 140 | let xRequestHeader = headers.get('X-Request'); 141 | // ... 142 | } 143 | } 144 | ``` 145 | 146 | #### Request Cookies 147 | 148 | You can access cookies for the current request via `fastboot.request` in the `fastboot` service. 149 | 150 | ```javascript 151 | import Route from '@ember/routing/route'; 152 | import { service } from '@ember/service'; 153 | 154 | export default class Application extends Route { 155 | @service fastboot; 156 | 157 | model() { 158 | let authToken = this.fastboot.request?.cookies.auth; 159 | // ... 160 | } 161 | } 162 | ``` 163 | 164 | The FastBoot service's `cookies` property is an object containing the request's cookies as key/value pairs. 165 | 166 | #### Request Host 167 | 168 | You can access the host of the request that the current FastBoot server is responding to via `fastboot.request` in the `fastboot` service. 169 | The `host` property will return the full `hostname` and `port` (`example.com` or `localhost:3000`). 170 | For example, when requesting `http://myapp.example.com/photos` from your browser, `fastboot.request.host` would equal `myapp.example.com` 171 | 172 | ```javascript 173 | import Route from '@ember/routing/route'; 174 | import { service } from '@ember/service'; 175 | 176 | export default class Application extends Route { 177 | @service fastboot; 178 | 179 | model() { 180 | let host = this.fastboot.request?.host; 181 | // ... 182 | } 183 | } 184 | ``` 185 | 186 | Retrieving `host` will error on 2 conditions: 187 | 188 | 1. You do not have a `hostWhitelist` defined. 189 | 2. The `Host` header does not match an entry in your `hostWhitelist`. 190 | 191 | ##### The Host Whitelist 192 | 193 | For security, you must specify a `hostWhitelist` of expected hosts in your application's `config/environment.js`: 194 | 195 | ```javascript 196 | module.exports = function(environment) { 197 | var ENV = { 198 | modulePrefix: 'host', 199 | environment: environment, 200 | baseURL: '/', 201 | locationType: 'auto', 202 | EmberENV: { 203 | // ... 204 | }, 205 | APP: { 206 | // ... 207 | }, 208 | 209 | fastboot: { 210 | hostWhitelist: ['example.com', 'subdomain.example.com', /^localhost:\d+$/] 211 | } 212 | }; 213 | // ... 214 | }; 215 | ``` 216 | 217 | `hostWhitelist` entries can be a `String` or `RegExp` to match multiple hosts. 218 | 219 | ##### Security 220 | 221 | Be careful with `RegExp` entries because host names are checked against the `Host` HTTP header, which can be forged. 222 | An improperly constructed `RegExp` could open your FastBoot servers and any backend they use to malicious requests. 223 | 224 | #### Query Parameters 225 | 226 | You can access query parameters for the current request via `fastboot.request` in the `fastboot` service. 227 | 228 | ```javascript 229 | import Route from '@ember/routing/route'; 230 | import { service } from '@ember/service'; 231 | 232 | export default class Application extends Route { 233 | @service fastboot; 234 | 235 | model() { 236 | let authToken = this.fastboot.request?.queryParams.auth; 237 | // ... 238 | } 239 | } 240 | ``` 241 | 242 | The service's `queryParams` property is an object containing the request's query parameters as key/value pairs. 243 | 244 | #### Path 245 | 246 | You can access the path (`/` or `/some-path`) of the request that the current FastBoot server is responding to via `fastboot.request` in the `fastboot` service. 247 | 248 | ```javascript 249 | import Route from '@ember/routing/route'; 250 | import { service } from '@ember/service'; 251 | 252 | export default class Application extends Route { 253 | @service fastboot; 254 | 255 | model() { 256 | let path = this.fastboot.request.path; 257 | // ... 258 | } 259 | } 260 | ``` 261 | 262 | #### Protocol 263 | 264 | You can access the protocol (`http:` or `https:`) of the request that the current FastBoot server is responding to via `fastboot.request` in the `fastboot` service. 265 | 266 | ```javascript 267 | import Route from '@ember/routing/route'; 268 | import { service } from '@ember/service'; 269 | 270 | export default class Application extends Route { 271 | @service fastboot; 272 | 273 | model() { 274 | let protocol = this.fastboot.request.protocol; 275 | // ... 276 | } 277 | } 278 | ``` 279 | 280 | #### Request Method 281 | 282 | You can also access the method of the request via `fastboot.request` in the `fastboot` service. 283 | The `method` property will return the method name (`GET`, `POST`, `PATCH`...) of the request. 284 | 285 | ```javascript 286 | import Route from '@ember/routing/route'; 287 | import { service } from '@ember/service'; 288 | 289 | export default class Application extends Route { 290 | @service fastboot; 291 | 292 | model() { 293 | let method = this.fastboot.request.method; 294 | // ... 295 | } 296 | } 297 | ``` 298 | 299 | #### Request Body 300 | 301 | On `POST`, `PUT` and `PATCH` request you will probably be interested in the body of the request. 302 | You can access the `body` of the request via `fastboot.request` in the `fastboot` service, but 303 | only if you setup a middleware to extract the body of the request in your express server. 304 | 305 | You can use `body-parser` and `fastboot-express-middleware` and to create an in-repo addon that 306 | contains a middleware. 307 | 308 | 309 | ```javascript 310 | // your-app/lib/fastboot-middleware/index.js 311 | var bodyParser = require('body-parser'); 312 | var FastBootExpressMiddleware = require('fastboot-express-middleware'); 313 | 314 | module.exports = { 315 | name: 'fastboot-middleware', 316 | 317 | serverMiddleware(options) { 318 | var app = options.app; 319 | app.use(bodyParser.text()); // or bodyParser.json() 320 | app.use(function(req, resp, next) { 321 | var outputPath = process.env['EMBER_DIST_FOLDER']; 322 | 323 | if (req.serveUrl) { 324 | var fastbootMiddleware = FastBootExpressMiddleware({ 325 | distPath: outputPath 326 | }); 327 | 328 | fastbootMiddleware(req, resp, next); 329 | } else { 330 | next(); 331 | } 332 | }); 333 | } 334 | }; 335 | ``` 336 | 337 | ```javascript 338 | // your-app/lib/fastboot-middleware/package.json 339 | { 340 | "name": "post-middlware", 341 | "keywords": [ 342 | "ember-addon" 343 | ] 344 | } 345 | ``` 346 | 347 | Then on the `package.json` of your app you can the folder of this in-repo addon to the autodiscovery 348 | paths: 349 | 350 | ```javascript 351 | // your-app/package.json 352 | { 353 | // ... 354 | "ember-addon": { 355 | "paths": [ 356 | "lib/fastboot-middleware" 357 | ] 358 | } 359 | } 360 | ``` 361 | 362 | Now that this middleware is parsing the body of request, you can access it on the `fastboot` service. 363 | Depending on what method on `body-parser` you used, the body can be a parse JSON object, a string 364 | or even a raw Buffer. 365 | 366 | ```javascript 367 | import Route from '@ember/routing/route'; 368 | import { service } from '@ember/service'; 369 | 370 | export default class Application extends Route { 371 | @service fastboot; 372 | 373 | model() { 374 | let method = this.fastboot.request.body; 375 | // ... 376 | } 377 | } 378 | ``` 379 | 380 | ### FastBoot Response 381 | 382 | FastBoot Response gives you access to the response metadata that FastBoot will send back the client. 383 | 384 | #### Response Headers 385 | 386 | You can access the current response headers via `fastboot.response.headers`. 387 | The `headers` object implements part of the [Fetch API's Headers class](https://developer.mozilla.org/en-US/docs/Web/API/Headers), the functions available are [`has`](https://developer.mozilla.org/en-US/docs/Web/API/Headers/has), [`get`](https://developer.mozilla.org/en-US/docs/Web/API/Headers/get), and [`getAll`](https://developer.mozilla.org/en-US/docs/Web/API/Headers/getAll). 388 | 389 | ```javascript 390 | import Route from '@ember/routing/route'; 391 | import { service } from '@ember/service'; 392 | 393 | export default class Application extends Route { 394 | @service fastboot; 395 | 396 | model() { 397 | let isFastBoot = this.fastboot.isFastBoot; 398 | 399 | if (isFastBoot) { 400 | let resHeaders = this.fastboot.response.headers; 401 | resHeaders.set('X-Debug-Response-Type', 'fastboot'); 402 | } 403 | // ... 404 | } 405 | } 406 | ``` 407 | 408 | #### Status Code 409 | 410 | You can access the status code of the current response via `fastboot.response.statusCode`. 411 | This is useful if you want your application to return a non-default (`200`) status code to the client. 412 | For example if you want a route of your application to be `401 - Unauthorized` if it accessed without OAuth credentials, you could use `statusCode` to do that. 413 | 414 | ```javascript 415 | import Route from '@ember/routing/route'; 416 | import { service } from '@ember/service'; 417 | 418 | export default class Application extends Route { 419 | @service fastboot; 420 | 421 | beforeModel() { 422 | let isFastBoot = this.fastboot.isFastBoot; 423 | 424 | if (!isFastBoot) { 425 | return; 426 | } 427 | 428 | let reqHeaders = this.fastboot.request.headers; 429 | let authHeaders = reqHeaders.Authorization; 430 | 431 | if (authHeaders === null) { 432 | this.set('fastboot.response.statusCode', 401); 433 | } 434 | // ... 435 | } 436 | } 437 | ``` 438 | 439 | FastBoot handles `200`, [`204` and `3xx`](https://github.com/ember-fastboot/fastboot/blob/b62e795c8c21c4a5dca09f2cf20e4367c843fc7b/src/result.js#L27-L43) by default. 440 | For other custom responses you will want to modify your application and FastBoot server implementation to act accordingly. 441 | 442 | ### Using a different index.html file 443 | 444 | FastBoot's default behavior is to inject HTML that your application has rendered into the `` and `` sections of the application's `index.html` file. 445 | In some cases you may want to use different HTML boilerplate for your application. For instance, [AMP](https://www.ampproject.org/) pages have a rigid structure 446 | that they must adhere to, so if you are using Ember to generate a valid AMP page you will need to change the `index.html` file that FastBoot uses. 447 | To do so, specify an `htmlFile` in your application's `config/environment.js`: 448 | 449 | ```javascript 450 | module.exports = function(environment) { 451 | var ENV = { 452 | // ... 453 | 454 | fastboot: { 455 | htmlFile: 'custom-index.html' 456 | } 457 | }; 458 | }; 459 | ``` 460 | 461 | FastBoot will look for this file in your `dist/` directory when serving your application. 462 | You can put your custom html file in your application's `public/` directory and `ember-cli` will 463 | copy it to the correct location in the `dist/` directory when building. This should be a standard HTML file 464 | (no handlebars) with comment placeholders `` and 465 | `` that FastBoot will replace with your application's `` and `` content, respectively. 466 | 467 | ### The Shoebox 468 | 469 | The Shoebox lets you pass application state from your FastBoot rendered application to the browser for client-side rendering. 470 | For example if your FastBoot server makes an API request, you can use the Shoebox to pass data to the client's browser. 471 | When the application resumes rendering on the client-side, it will be able to use that data, eliminating the need for it to make an API request of its own. 472 | 473 | The contents of the Shoebox are written to the HTML as strings within ` 488 | . 489 | . 490 | ``` 491 | 492 | #### Putting and Retrieving 493 | 494 | `shoebox.put` lets you add items to the Shoebox. 495 | 496 | `shoebox.retrieve` lets you remove items from the Shoebox. 497 | 498 | In the example below, we find and store our data in a `shoeboxStore` object, when the application is rendered in FastBoot. 499 | When the same code is then executed by the client browser, we retrieve the items from the `shoeboxStore` rather than redoing the find (and triggering a network request). 500 | 501 | ```javascript 502 | import Route from '@ember/routing/route'; 503 | import { service } from '@ember/service'; 504 | 505 | export default class Application extends Route { 506 | @service fastboot; 507 | @service store; 508 | 509 | model(params) { 510 | let shoebox = fastboot.shoebox; 511 | let shoeboxStore = shoebox.retrieve('my-store'); 512 | let isFastBoot = fastboot.isFastBoot; 513 | 514 | if (isFastBoot) { 515 | return this.store.findRecord('post', params.post_id).then(post => { 516 | if (!shoeboxStore) { 517 | shoeboxStore = {}; 518 | shoebox.put('my-store', shoeboxStore); 519 | } 520 | shoeboxStore[post.id] = post.toJSON(); 521 | }); 522 | } 523 | 524 | return shoeboxStore && shoeboxStore[params.post_id]; 525 | } 526 | } 527 | ``` 528 | 529 | ## Useful Ember Addons for FastBoot 530 | 531 | ### ember-fetch: Fetch Resources Over HTTP (AJAX) 532 | 533 | JavaScript running in the browser relies on the `XMLHttpRequest` interface to retrieve resources, while Node offers the `http` module. 534 | What do we do if we want to write a single app that can fetch data from our API server when running in both environments? 535 | 536 | One option is to use the [ember-fetch](https://github.com/stefanpenner/ember-fetch) addon, which provides an implementation of [Fetch API][fetch-api] standard that works seamlessly in both environments. 537 | 538 | To use `ember-fetch`, install it as you would any addon: 539 | 540 | ```sh 541 | ember install ember-fetch 542 | ``` 543 | 544 | Once installed, you can import it using the JavaScript module syntax, wherever you need to make a network request: 545 | 546 | ```javascript 547 | import fetch from 'ember-fetch/ajax'; 548 | ``` 549 | 550 | The `fetch()` method returns a promise and is very similar to jQuery's `getJSON()` method that you are likely already using. 551 | For example, here's an Ember route that uses `fetch()` to access the GitHub JSON API and use it as the route's model: 552 | 553 | ```javascript 554 | import Route from '@ember/routing/route'; 555 | import fetch from 'ember-fetch/ajax'; 556 | 557 | export default class Application extends Route { 558 | model() { 559 | return fetch('https://api.github.com/users/tomdale/events') 560 | .then(function(response) { 561 | return response; 562 | }); 563 | } 564 | } 565 | ``` 566 | 567 | For more information, see [ember-fetch](https://github.com/stefanpenner/ember-fetch) and the [Fetch documentation on MDN][fetch-api]. 568 | 569 | ### ember-cli-document-title: Specify the Page Title 570 | 571 | To control the title that gets displayed for a route (i.e., the `` tag), install the FastBoot-compatible `ember-cli-document-title` addon: 572 | 573 | ```sh 574 | ember install ember-cli-document-title 575 | ``` 576 | 577 | This addon allows you to specify the page title by adding a `title` property to your application's routes. 578 | 579 | ```javascript 580 | // routes/post.js 581 | import Route from '@ember/routing/route'; 582 | import { service } from '@ember/service'; 583 | 584 | export default class Application extends Route { 585 | titleToken(model) { 586 | return model.name; 587 | } 588 | } 589 | ``` 590 | 591 | See [ember-cli-document-title](https://github.com/kimroen/ember-cli-document-title) for more information. 592 | 593 | ### ember-cli-head: Enable Open Graph, Twitter Cards and Other `<head>` Tags 594 | 595 | FastBoot makes your Ember application accessible to services that embed content, such as Twitter's cards and Facebook posts. 596 | You can customize how your site appears in these contexts with `<meta>` tags driven dynamically by your Ember app. 597 | 598 | To get started, install the `ember-cli-head` addon: 599 | 600 | ```sh 601 | ember install ember-cli-head 602 | ``` 603 | 604 | The `ember-cli-head` addon works by giving you a Handlebars template that is rendered into your application's `<head>` tag. 605 | To configure what gets rendered, edit the newly-created `app/templates/head.hbs` file. 606 | 607 | For example, to set the title used when embedding a route in Facebook (or any other Open Graph-compatible app), add the appropriate `<meta>` tag to `head.hbs`: 608 | 609 | ```hbs 610 | {{!-- app/templates/head.hbs --}} 611 | <meta property="og:title" content={{model.title}} /> 612 | ``` 613 | 614 | This creates a `<meta>` tag whose `property` attribute is `og:title`. 615 | Now we just need to tell it what the title is, which is typically determined dynamically by the current route's model: 616 | 617 | ```javascript 618 | import Route from '@ember/routing/route'; 619 | import { service } from '@ember/service'; 620 | 621 | export default class Application extends Route { 622 | // This service is the `model` in the head.hbs template, 623 | // so any properties you set on it are accessible via 624 | // `model.whatever`. 625 | headData: service(), 626 | 627 | afterModel(model) { 628 | let title = model.title || "Untitled"; 629 | this.set('headData.title', title); 630 | } 631 | } 632 | ``` 633 | 634 | For more information, see [ember-cli-head](https://github.com/ronco/ember-cli-head). 635 | 636 | ## Tips and Tricks 637 | 638 | ### Using Whitelisted Node Dependencies 639 | 640 | The browser and Node are two different environments, and functionality in one is not always available in the other. 641 | As you develop your app, you may need to pull in additional dependencies to provide for functionality not available by default in Node. 642 | 643 | For example, `localStorage` is not available in Node and you may instead want to save information to a Redis server by using the `radredis` npm packge. 644 | 645 | For security reasons, your Ember app running in FastBoot can only access packages (both built-in and from npm) that you have explicitly whitelisted. 646 | 647 | To allow your app to require a package, add it to the `fastbootDependencies` array in your app's `package.json`: 648 | 649 | ```javascript 650 | { 651 | "name": "my-sweet-app", 652 | "version": "0.4.2", 653 | "devDependencies": { 654 | // ... 655 | }, 656 | "dependencies": { 657 | "radredis": "0.1.1" 658 | }, 659 | "fastbootDependencies": [ 660 | "radredis", 661 | "path" 662 | ] 663 | } 664 | ``` 665 | 666 | The `fastbootDependencies` in the above example means the only modules your Ember app can use are `radredis` (provided from npm) and `path` (a built-in Node module). 667 | 668 | If the package you are using is not built in to Node, **you must also specify the package and a version in the `package.json` `dependencies` hash.** Built-in modules (`path`, `fs`, etc.) only need to be added to `fastbootDependencies`. 669 | 670 | From your Ember app, you can run `FastBoot.require()` to require a package. 671 | This is identical to the CommonJS `require` except it checks all requests against the whitelist first. 672 | 673 | ```javascript 674 | if (typeof FastBoot !== 'undefined') { 675 | let path = FastBoot.require('path'); 676 | let filePath = path.join('tmp', session.getID()); 677 | } 678 | ``` 679 | 680 | If you attempt to require a package that is not in the whitelist, FastBoot will raise an exception. 681 | 682 | Note that the `FastBoot` global is **only** available when running in FastBoot mode. 683 | You should either guard against its presence or only use it in FastBoot-only initializers. 684 | 685 | ### Use Model Hooks to Defer Rendering 686 | 687 | An Ember app running in the browser is long-lived: as soon as the page loads, you can start rendering parts of the UI as soon as the data driving that UI becomes available. 688 | 689 | Ember starts by collecting the models for each route on screen (via `Route`'s `model()`, `beforeModel()`, and `afterModel()` hooks). 690 | If any of your routes return a promise from one of these hooks, Ember will wait until those promises resolve before rendering the templates. 691 | This avoids a jarring "pop-in" effect. 692 | 693 | Sometimes, though, you don't want secondary UI to block the main content. 694 | For example, imagine an app for managing contacts. 695 | It shows information for one contact, as well as a list of all contacts in the sidebar. 696 | 697 | You may not want to wait for the entire list of contacts to load before showing the primary information the user is after: details about a specific contact. 698 | So, once the `model()` hook finishes loading for the route, the template is rendered, and the sidebar shows a loading spinner while the full list is loaded. 699 | 700 | ![](images/contacts-example.png) 701 | 702 | In the browser, the router's promise chain controls when the template is rendered. 703 | But you can always render immediately (by not returning a promise) and have each component on the page update once its backing data becomes available. 704 | 705 | FastBoot is different. 706 | Because it's sending a static page of HTML to the user's browser, it needs to know when the page is "done." As soon as FastBoot thinks the page is done rendering, it converts the DOM into HTML, sends it to the browser, and destroys the application instance. 707 | 708 | Like when deciding when to render a route's templates, FastBoot uses the promise chain built by routes' `beforeModel()`, `model()`, and `afterModel()` hooks. 709 | But if some components are responsible for marshalling their own data, they may render too late for the HTML response and the user (or the search crawler!) may see the loading state instead of the content you intended. 710 | 711 | To work around this, you can add additional promises to the `afterModel()` hook when in FastBoot mode that block rendering until all of the data is loaded. 712 | Use services to coordinate between the route and the components on the page. 713 | 714 | For example, let's imagine we're building a news page that also has a weather widget. 715 | In the browser, we load the primary model (the article) in the `model()` hook. 716 | The weather component fetches data from a `weather` service. 717 | 718 | In the browser, we let the weather component render a loading spinner if the article loads before the weather data. 719 | But in FastBoot, we block rendering until the weather data is available. 720 | 721 | Here's what the `Route` for that page might look like: 722 | 723 | ```javascript 724 | // app/routes/article.js 725 | import Route from '@ember/routing/route'; 726 | import { service } from '@ember/service'; 727 | 728 | export default class Application extends Route { 729 | @service fastboot; 730 | @service store; 731 | @service weather; 732 | 733 | model({ article_id }) { 734 | return this.store.find('article', article_id); 735 | }, 736 | 737 | afterModel() { 738 | if (this.fastboot.isFastBoot) { 739 | return this.weather.fetch(); 740 | } 741 | } 742 | } 743 | ``` 744 | 745 | In this example, the `Route` always returns the article model from the model hook. 746 | The template won't render in the browser or in FastBoot until the article finishes loading. 747 | 748 | We detect if the app is running in FastBoot via the call to `this.get('fastboot.isFastBoot')` (and do notice that we've injected the `fastboot` service!). 749 | 750 | If we're in FastBoot mode, we add an additional promise to the promise chain in `afterModel()`: we ask the `weather` service for a promise for the weather data. 751 | 752 | With this setup, the page in the browser will render as soon as the article has finished loading. 753 | But in FastBoot, we wait until both the article _and_ weather have loaded to send the HTML back to the browser. 754 | 755 | ### Designing Components 756 | 757 | #### Use `didInsertElement` for client-side DOM manipulation 758 | 759 | In FastBoot, we do not invoke either `didInsertElement` or `willInsertElement` hooks. 760 | If your components have any direct DOM manipulation you would do it in those hooks. 761 | 762 | #### Lifecycle Hooks in FastBoot 763 | 764 | FastBoot calls the `init`, `didReceiveAttrs`, `didUpdateAttrs`, `willRender`, `didRender`, and `willDestroy` hooks. 765 | Any code in these hooks will be run inside of FastBoot and should be free of references to browser APIs or DOM manipulation. 766 | 767 | ### Avoid jQuery 768 | 769 | FastBoot relies on [`Ember.ApplicationInstance`](http://emberjs.com/api/classes/Ember.ApplicationInstance.html) to execute your Ember application on the server. 770 | jQuery is [disabled](https://github.com/emberjs/ember.js/blob/v2.7.0/packages/ember-application/lib/system/application-instance.js#L370) by default for these instances because most of jQuery depends on having full DOM access. 771 | 772 | ### Use `ember-fetch` for XHR requests 773 | 774 | If you are depending on jQuery for XHR requests, use [ember-fetch](https://github.com/stefanpenner/ember-fetch) and replace your `$.ajax` calls with `fetch` calls. 775 | 776 | ## Troubleshooting in Node 777 | 778 | Because your app is now running in Node, not the browser, you will need a new set of tools to help diagnose problems. 779 | 780 | ### Verbose Logging 781 | 782 | Enable verbose logging by running the FastBoot server with the following environment variables set: 783 | 784 | ```sh 785 | DEBUG=ember-cli-fastboot:* ember serve 786 | ``` 787 | 788 | Pull requests for adding or improving logging facilities are very welcome. 789 | 790 | ### Using Node Inspector with Developer Tools 791 | 792 | You can get a debugging environment similar to the Chrome developer tools running with a FastBoot app, although it's not (yet) as easy as in the browser. 793 | 794 | First, install the Node Inspector: 795 | 796 | ```sh 797 | npm install node-inspector -g 798 | ``` 799 | 800 | Next, start the inspector server. 801 | Using the `--no-preload` flag is recommended. 802 | It waits to fetch the source code for a given file until it's actually needed. 803 | 804 | ```sh 805 | node-inspector --no-preload 806 | ``` 807 | 808 | Once the debug server is running, you'll want to start up the FastBoot server with Node in debug mode. 809 | One thing about debug mode: it makes everything much slower. 810 | Since the `ember serve` command does a full build when launched, this becomes agonizingly slow in debug mode. 811 | 812 | ### Speeding up Server-side Debugging 813 | 814 | Avoid the slowness by manually running the build in normal mode, then run FastBoot in debug mode without doing a build: 815 | 816 | ```sh 817 | ember build && node --debug-brk ./node_modules/.bin/ember serve 818 | ``` 819 | 820 | This does a full rebuild and then starts the FastBoot server in debug mode. 821 | 822 | Note that the `--debug-brk` flag will cause your app to start paused to give you a chance to open the debugger. 823 | 824 | Once you see the output `debugger listening on port 5858`, visit <http://127.0.0.1:8080/debug?port=5858> in your browser. 825 | Once it loads, click the "Resume script execution" button (it has a ▶︎ icon) to let FastBoot continue loading. 826 | 827 | Assuming your app loads without an exception, after a few seconds you will see a message that FastBoot is listening on port 3000\. 828 | Once you see that, you can open a connection. 829 | Any exceptions should be logged in the console, and you can use the tools you'd expect such as `console.log`, `debugger` statements, etc. 830 | 831 | ## Architecture 832 | 833 | ### Introduction to Server-Side Rendering 834 | 835 | FastBoot implements server-side rendering, which means it runs your JavaScript Ember app on the server so it can send HTML to the user, not an empty white screen while the JavaScript loads. 836 | 837 | Server-side rendering, or SSR, is a relatively new idea. 838 | Because it's so new, there are often misconceptions about how it works. 839 | Let's start by defining what SSR is not. 840 | 841 | **FastBoot does not replace your existing API server.** Instead, FastBoot drops in on top of your existing API server to improve startup performance and make your Ember app more accessible to user agents without JavaScript. 842 | 843 | Perhaps the best way to think about FastBoot is that it is like a browser running in a data center instead of on a user's device. 844 | This browser already has your Ember application loaded and running. 845 | 846 | When a request comes in from an end user, the browser in the datacenter is told to visit the same URL. 847 | Because it is already running, there is little startup time. 848 | And because it's in the same datacenter as your API server, network requests are very low latency. 849 | It's like a turbocharged version of the app running in the user's browser. 850 | 851 | Once the browser on the server finishes loading the requested URL, we take its DOM, serialize it to HTML, and send it to the user's browser running on their device. 852 | They don't need to download a bunch of JavaScript, wait for it to start up, then wait some more while it makes API requests for the data needed to render. 853 | 854 | Instead, the very first thing the user's browser downloads is the rendered HTML. 855 | Only once the HTML and CSS have finished loading does the browser start to download the Ember app's JavaScript. 856 | 857 | For users with slow connections or slow devices, this means the very first thing they see is the content they were after. 858 | No more waiting around for multi-hundred-kilobyte payloads to download and parse just to read a blog post. 859 | 860 | ### Packaging Your App 861 | 862 | When you run `ember build` at the command line, Ember compiles your application into the `dist` directory. 863 | That directory contains everything you need to make your application work in the browser. 864 | You can upload it to a static hosting service like S3 or Firebase, where browsers can download and run the JavaScript on the user's device. 865 | 866 | FastBoot is a little different because, instead of being purely static, it renders HTML on the server and therefore needs more than just static hosting. 867 | We need to build additional assets that's designed to work in Node rather than the browser. 868 | 869 | When you run `ember build`, your FastBooted app will produce `app.js` and `vendor.js` in `dist/assets` that are used in the browser and Node and will produce an additive asset containing FastBoot overrides for your app in `app-fastboot.js` in `dist/assets`. 870 | The artifacts in `dist/assets` are client-side copies of your application that will be sent to all browsers that request your application. 871 | The artifacts for your FastBoot server are defined by a `manifest` key entry in `dist/package.json` which 872 | will minimally contain `vendor.js`, `app.js` and `app-fastboot.js`. 873 | 874 | You can test that the process is working (and that your app is FastBoot-compatible) by running `ember serve`, which builds your app then starts up a local server and renders your app on server side. 875 | 876 | Once you've confirmed everything looks good, it's ready to hand off to the FastBoot server running in the production environment. 877 | The good news is that this process is usually handled for you by a deployment plugin for `ember-cli-deploy`; see [Deploying](/docs/deploying) for more information about different deployment strategies. 878 | 879 | ### The FastBoot Server 880 | 881 | The [FastBoot server][fastboot-server] is the heart of the FastBoot architecture. 882 | 883 | The server offers an [Express middleware][express] that can be integrated into an existing Node infrastructure, or run standalone. 884 | 885 | ```javascript 886 | var server = new FastBootServer({ 887 | appFiles: [appFile, appFastBootFile], 888 | vendorFiles: [vendorFile], 889 | htmlFile: htmlFile, 890 | ui: ui 891 | }); 892 | 893 | var app = express(); 894 | 895 | app.get('/*', server.middleware()); 896 | 897 | var listener = app.listen(options.port, function() { 898 | var host = listener.address().address; 899 | var port = listener.address().port; 900 | 901 | console.log('FastBoot running at http://' + host + ":" + port); 902 | }); 903 | ``` 904 | 905 | ## Frequently Asked Questions 906 | 907 | _Does this mean I need to rewrite my API server in Ember or JavaScript?_ 908 | 909 | No. 910 | FastBoot works with your existing API rather than replacing it. 911 | That means you can drop in FastBoot no matter what backend you use, whether it's Node, Rails, PHP, .NET, Java, or any other stack. 912 | 913 | That said, the FastBoot server _does_ require Node. 914 | So even though you don't need to replace your backend, you do need to have the ability to deploy Node apps. 915 | 916 | _If the app is running in FastBoot and not the user's browser, how do I access things like `localStorage` where I keep authentication tokens?_ 917 | 918 | The only information about the user requesting the page is any HTTP cookies you have set. 919 | To work with FastBoot, you should store critical information in cookies. 920 | 921 | Alternatively, you can store session data for users in stateful persistence on the server, such as a Redis instance. 922 | When the request comes in, you will need to exchange a cookie for the full user session. 923 | We hope that the community can work together to build a robust solution to this scenario. 924 | 925 | ## Security Issues 926 | 927 | If you discover a security vulnerability in FastBoot, we ask that you follow Ember' [responsible disclosure security policy](http://www.emberjs.com/security). 928 | 929 | ## Useful Links 930 | 931 | * [ember-cli-deploy](http://ember-cli-deploy.github.io/ember-cli-deploy/) 932 | * [ember-cli-document-title](https://github.com/kimroen/ember-cli-document-title) 933 | * [ember-cli-head](https://github.com/ronco/ember-cli-head) 934 | * [ember-fetch](https://github.com/ember-cli/ember-fetch) 935 | * [express](http://expressjs.com) 936 | * [fastboot-server](https://github.com/ember-fastboot/ember-fastboot-server) 937 | * [fetch-api](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) 938 | -------------------------------------------------------------------------------- /markdown/intro.md: -------------------------------------------------------------------------------- 1 | ### How Does It Work? 2 | 3 | FastBoot works by running your Ember application in Node.js. When a user visits your site, the initial HTML is rendered and served to the user. The very first thing they see is your content. 4 | 5 | Only after the content has loaded do they start downloading JavaScript. Once finished, your app takes over. You get the same snappy performance you're used to from Ember apps. 6 | 7 | And yes, this means the content in your Ember application is accessible to everyone, even if they have JavaScript turned off. It's even accessible to cURL—try it yourself: 8 | 9 | ```sh 10 | curl 'http://localhost:4200/' -H 'Accept: text/html' 11 | ``` 12 | 13 | For more information, see [the User Guide](/docs/user-guide) 14 | 15 | ### Running FastBoot 16 | 17 | To start the FastBoot server during development: 18 | 19 | ```sh 20 | ember serve 21 | ``` 22 | 23 | For more information, see the [Quickstart](/quickstart). 24 | 25 | ### Get Help & Contribute 26 | 27 | The FastBoot source code is [available on GitHub](https://github.com/tildeio/ember-cli-fastboot). If you run into an error, please [file an issue](https://github.com/emberjs/ember.js/issues) 28 | 29 | The best way to get help is via the [Ember Community Discord](https://discordapp.com/invite/zT3asNS). Once you've signed up, join the `#fastboot` channel. 30 | -------------------------------------------------------------------------------- /markdown/quickstart.md: -------------------------------------------------------------------------------- 1 | # FastBoot Quickstart 2 | 3 | ## Creating a New Ember App 4 | 5 | This quickstart guide will walk you through creating a simple Ember app that fetches data from GitHub, then renders it using FastBoot. 6 | 7 | To start, make sure you've got Node.js and npm installed. If you've never used Ember before, you may want to run through the [Ember quickstart guide](https://guides.emberjs.com/v2.7.0/getting-started/quick-start/). 8 | 9 | Install Ember by running: 10 | 11 | ```sh 12 | npm install ember-cli -g 13 | ``` 14 | 15 | Create a new application by running: 16 | 17 | ```sh 18 | ember new github-fastboot-example 19 | ``` 20 | 21 | Once created, `cd` into the `github-fastboot-example` directory. 22 | 23 | We need to fetch data from the GitHub API, but there's a small problem: the browser uses `XMLHttpRequest` to fetch JSON data, while in Node.js you'd use the `http` library. We'll be running the same Ember.js app in both environments, so we need some way of making it work in both. 24 | 25 | Let's install a tool that will let us write the same code whether our app is running in the browser or on the server. Code that runs in both places is sometimes called _universal_ or _isomorphic_. 26 | 27 | Run the following command to install `ember-fetch`: 28 | 29 | ```sh 30 | ember install ember-fetch 31 | ``` 32 | 33 | Under the hood, the `ember install` command is just like `npm install`, but automatically saves the addon to your `package.json` file. 34 | 35 | ## Rendering the Model 36 | 37 | In Ember, routes are objects responsible for fetching model data. Let's make a route that fetches information about you from GitHub. 38 | 39 | Run the following command in your terminal: 40 | 41 | ```sh 42 | ember generate route index 43 | ``` 44 | 45 | This will generate a new route called `index`. By convention, `index` is the name of the route for when the user visits the root path, or `/`. Think of it like `index.html`. 46 | 47 | Open the newly-created `app/routes/index.js` file in your code editor. Let's add a method called `model()` to the route that fetches information about your user from GitHub. 48 | 49 | We'll use the `fetch` polyfill exposed by the `ember-fetch` addon that lets us write universal fetching code for both the browser and Node.js. A _polyfill_ is a library that provides a standardized API that isn't available in all environments yet. In this case, it's polyfilling the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). 50 | 51 | Implement the model hook like I did below. You might want to change the username in the URL from mine to yours. 52 | 53 | ```javascript 54 | import Route from '@ember/routing/route'; 55 | import fetch from 'ember-fetch/ajax'; 56 | 57 | export default class Application extends Route { 58 | model() { 59 | return fetch('https://api.github.com/users/tomdale') 60 | .then(function(response) { 61 | return response; 62 | }); 63 | } 64 | } 65 | ``` 66 | 67 | Next, let's render that model in the `index` template. Open `app/templates/index.hbs` and type the following Handlebars template: 68 | 69 | ```hbs 70 | <p> 71 | Username: {{model.login}} 72 | </p> 73 | <p> 74 | Avatar: <img src={{model.avatar_url}} width="32" height="32"> 75 | </p> 76 | <p> 77 | # of Public Repos: {{model.public_repos}} 78 | </p> 79 | ``` 80 | 81 | Once you've made those changes (don't forget to save!), switch back to the terminal and run the development server: 82 | 83 | ```sh 84 | ember serve 85 | ``` 86 | 87 | This runs a local development server you can use to verify your program is running correctly. Visit [localhost:4200/](http://localhost:4200/) in your browser. You should see your GitHub profile rendered below a title that says "Welcome to Ember.js". 88 | 89 | ![Screenshot of completed app](/images/quickstart/github-fastboot-example-screenshot.png) 90 | 91 | We've verified we've got a working application, but there's one problem. If you use your browser's View Source feature, you'll see that there's no HTML being rendered: just an empty body and some `<script>` tags. Let's fix that. 92 | 93 | ![Screenshot showing that the HTML does not contain the application contents](/images/quickstart/github-fastboot-example-empty-source.png) 94 | 95 | ## Install FastBoot 96 | 97 | Back in the terminal, stop the development server by hitting `Ctrl-C` on your keyboard. 98 | 99 | Next, install FastBoot: 100 | 101 | ```sh 102 | ember install ember-cli-fastboot 103 | ``` 104 | 105 | Let's turn on server-side rendering and make sure it works. Start the FastBoot server like this if you have `ember-cli` 2.12.0 and above in your app: 106 | 107 | ```sh 108 | ember serve 109 | ``` 110 | 111 | *Note*: `ember fastboot` command is soon going to be deprecated and removed in FastBoot 1.0. Please refrain from using it as it has live-reload and other issues 112 | 113 | View the FastBoot-rendered content by visiting [localhost:4200/](http://localhost:4200/). Note the same port! `ember serve` can serve render your app on server side as well. Moreover, all options of `ember serve` will work with FastBoot (example `--proxy`, `--port` etc). 114 | 115 | Everything should look just the same as before. The only difference is that, this time when you View Source, the `<body>` tag is populated with the rendered HTML content. 116 | 117 | ![Screenshot showing that application content is now rendered as HTML](/images/quickstart/github-fastboot-example-populated-source.png) 118 | 119 | Congratulations! You've just built your first FastBoot application. 120 | 121 | Let's review what we've accomplished here: 122 | 123 | 1. We created a new app in one command with `ember new` 124 | 2. We used the universal library `ember-fetch` to request AJAX data 125 | 3. When rendering in Node.js, the FastBoot server requested data from GitHub _on the user's behalf_ 126 | 4. We wrote an app that has the benefits of traditional server-side rendering **and** the benefits of client-side JavaScript, in a single codebase. No hacks, just installing an addon. 127 | 128 | Now that you've got your first FastBoot app, it's time to start adding FastBoot to your existing apps. Or, learn how to deploy your new app by learning about [Deploying](/docs/deploying). 129 | 130 | ### Disabling FastBoot with `ember serve` 131 | 132 | If your app is running ember-cli 2.12.0-beta.1 and above, you can now serve your FastBoot rendered content with `ember serve` as well. 133 | 134 | You can also turn off the server side rendering on a per request basis using `fastboot` query parameter. To disable FastBoot rendered content, visit [localhost:4200/?fastboot=false](http://localhost:4200/?fastboot=false). You can enable FastBoot rendered content again by visiting [localhost:4200/?fastboot=true](http://localhost:4200/?fastboot=true). 135 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastboot-website", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "Small description for fastboot-website goes here", 6 | "repository": "https://github.com/ember-fastboot/fastboot-website", 7 | "license": "MIT", 8 | "author": "", 9 | "directories": { 10 | "doc": "doc", 11 | "test": "tests" 12 | }, 13 | "scripts": { 14 | "build": "ember build", 15 | "lint:hbs": "ember-template-lint .", 16 | "lint:js": "eslint .", 17 | "start": "ember serve", 18 | "test": "ember test", 19 | "server:node": "ember build --prod --output-path='tmp/deploy-dist' && EXPERIMENTAL_RENDER_MODE_SERIALIZE=true node fastboot-server.js" 20 | }, 21 | "devDependencies": { 22 | "@ember/jquery": "^0.6.0", 23 | "@ember/optional-features": "^0.7.0", 24 | "autoprefixer": "^9.5.1", 25 | "basscss": "^8.0.10", 26 | "broccoli-asset-rev": "^3.0.0", 27 | "ember-ajax": "^5.0.0", 28 | "ember-browserify": "^1.2.2", 29 | "ember-cli": "~3.9.0", 30 | "ember-cli-app-version": "^3.2.0", 31 | "ember-cli-autoprefixer": "0.8.1", 32 | "ember-cli-babel": "^7.1.2", 33 | "ember-cli-dependency-checker": "^3.1.0", 34 | "ember-cli-deploy": "1.0.2", 35 | "ember-cli-deploy-build": "1.1.1", 36 | "ember-cli-deploy-gzip": "1.0.1", 37 | "ember-cli-eslint": "^5.1.0", 38 | "ember-cli-fastboot": "^2.0.4", 39 | "ember-cli-head": "^0.4.1", 40 | "ember-cli-htmlbars": "^3.0.0", 41 | "ember-cli-htmlbars-inline-precompile": "^2.1.0", 42 | "ember-cli-inject-live-reload": "^2.0.1", 43 | "ember-cli-postcss": "^4.2.0", 44 | "ember-cli-sri": "^2.1.1", 45 | "ember-cli-template-lint": "^1.0.0-beta.1", 46 | "ember-cli-uglify": "^3.0.0", 47 | "ember-disable-proxy-controllers": "^1.0.2", 48 | "ember-export-application-global": "^2.0.0", 49 | "ember-fr-markdown-file": "0.0.0", 50 | "ember-href-to": "^3.1.0", 51 | "ember-load-initializers": "^2.0.0", 52 | "ember-markedjs": "^0.1.2", 53 | "ember-maybe-import-regenerator": "^0.1.6", 54 | "ember-metrics": "0.13.0", 55 | "ember-qunit": "^4.4.1", 56 | "ember-resolver": "^5.0.1", 57 | "ember-source": "~3.9.0", 58 | "eslint-plugin-ember": "^6.3.0", 59 | "highlight.js": "^9.15.6", 60 | "liquid-fire": "^0.29.5", 61 | "loader.js": "^4.7.0", 62 | "markdown-code-highlighting": "^0.2.1", 63 | "marked": "^0.6.2", 64 | "normalize.css": "^8.0.1", 65 | "postcss-custom-media": "^7.0.8", 66 | "postcss-custom-properties": "^8.0.10", 67 | "postcss-import": "^12.0.1", 68 | "qunit-dom": "^0.8.0" 69 | }, 70 | "engines": { 71 | "node": "14.x" 72 | }, 73 | "dependencies": { 74 | "express": "^4.16.4", 75 | "express-cluster": "0.0.5", 76 | "express-sabayon": "hone/express-sabayon", 77 | "express-serve-static-gzip": "hone/express-serve-static-gzip", 78 | "fastboot-express-middleware": "^1.2.0" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /public/images/addon-author-guide/stack-trace-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-fastboot/fastboot-website/53704f71cb854113ad03192a57fb15bc5529daa3/public/images/addon-author-guide/stack-trace-example.png -------------------------------------------------------------------------------- /public/images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-fastboot/fastboot-website/53704f71cb854113ad03192a57fb15bc5529daa3/public/images/background.png -------------------------------------------------------------------------------- /public/images/contacts-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-fastboot/fastboot-website/53704f71cb854113ad03192a57fb15bc5529daa3/public/images/contacts-example.png -------------------------------------------------------------------------------- /public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-fastboot/fastboot-website/53704f71cb854113ad03192a57fb15bc5529daa3/public/images/favicon.png -------------------------------------------------------------------------------- /public/images/logo.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <svg width="535px" height="127px" viewBox="0 0 535 127" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 3 | <!-- Generator: Sketch 3.6.1 (26313) - http://www.bohemiancoding.com/sketch --> 4 | <title>logo 5 | Created with Sketch. 6 | 7 | 8 | 15 | 16 | -------------------------------------------------------------------------------- /public/images/nebula.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-fastboot/fastboot-website/53704f71cb854113ad03192a57fb15bc5529daa3/public/images/nebula.png -------------------------------------------------------------------------------- /public/images/quickstart/github-fastboot-example-empty-source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-fastboot/fastboot-website/53704f71cb854113ad03192a57fb15bc5529daa3/public/images/quickstart/github-fastboot-example-empty-source.png -------------------------------------------------------------------------------- /public/images/quickstart/github-fastboot-example-populated-source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-fastboot/fastboot-website/53704f71cb854113ad03192a57fb15bc5529daa3/public/images/quickstart/github-fastboot-example-populated-source.png -------------------------------------------------------------------------------- /public/images/quickstart/github-fastboot-example-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-fastboot/fastboot-website/53704f71cb854113ad03192a57fb15bc5529daa3/public/images/quickstart/github-fastboot-example-screenshot.png -------------------------------------------------------------------------------- /public/images/site-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-fastboot/fastboot-website/53704f71cb854113ad03192a57fb15bc5529daa3/public/images/site-preview.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test_page: 'tests/index.html?hidepassed', 3 | disable_watching: true, 4 | launch_in_ci: [ 5 | 'Chrome' 6 | ], 7 | launch_in_dev: [ 8 | 'Chrome' 9 | ], 10 | browser_args: { 11 | Chrome: { 12 | ci: [ 13 | // --no-sandbox is needed when running Chrome inside a container 14 | process.env.CI ? '--no-sandbox' : null, 15 | '--headless', 16 | '--disable-gpu', 17 | '--disable-dev-shm-usage', 18 | '--disable-software-rasterizer', 19 | '--mute-audio', 20 | '--remote-debugging-port=0', 21 | '--window-size=1440,900' 22 | ].filter(Boolean) 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /tests/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-fastboot/fastboot-website/53704f71cb854113ad03192a57fb15bc5529daa3/tests/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | FastbootWebsite Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{content-for "body-footer"}} 31 | {{content-for "test-body-footer"}} 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/integration/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-fastboot/fastboot-website/53704f71cb854113ad03192a57fb15bc5529daa3/tests/integration/.gitkeep -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from '../app'; 2 | import config from '../config/environment'; 3 | import { setApplication } from '@ember/test-helpers'; 4 | import { start } from 'ember-qunit'; 5 | 6 | setApplication(Application.create(config.APP)); 7 | 8 | start(); 9 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-fastboot/fastboot-website/53704f71cb854113ad03192a57fb15bc5529daa3/tests/unit/.gitkeep -------------------------------------------------------------------------------- /tests/unit/routes/application-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupTest } from 'ember-qunit'; 3 | 4 | module('Unit | Route | application', function(hooks) { 5 | setupTest(hooks); 6 | 7 | test('it exists', function(assert) { 8 | assert.ok(true); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-fastboot/fastboot-website/53704f71cb854113ad03192a57fb15bc5529daa3/vendor/.gitkeep --------------------------------------------------------------------------------