Skip to content

Commit

Permalink
Merge pull request #10 from mydea/fn/shoebox
Browse files Browse the repository at this point in the history
Allow to store data between prerender and rehydration in shoebox
  • Loading branch information
mydea authored Dec 7, 2021
2 parents c89b823 + 5a8e4a5 commit 0cf47d7
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 1 deletion.
39 changes: 39 additions & 0 deletions packages/ember-build-prerender/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,31 @@ export default class MyComponent extends Component {
}
```

The `prerender` service also provides a simple shoebox (similar to ember-cli-fastboot) to store & exchange data
between the prerender and the rehydration phases:

```js
export default class MyComponent extends Component {
@service prerender;

async loadData() {
if (this.prerender.isPrerender) {
let data = await loadDataFromApi();
this.prerender.shoebox.set('api-data', data);
return data;
} else {
return this.prerender.shoebox.get('api-data');
}
}
}
```

Any data you set via `shoebox.set()` will be JSON-stringified, and can be retreived with the same key via `shoebox.get()`.
Note that you can _only_ set data in prerender mode. The shoebox will be stored in a meta tag in the document.

Note that in dev mode the shoebox will be empty.
So any non-prerender code should never rely on any shoebox content to exist, but instead treat it as optional.

## How it works

ember-prerender basically works in four steps:
Expand All @@ -90,6 +115,16 @@ ember-prerender basically works in four steps:

The app will rehydrate from the static HTML files, providing a smooth transition from the static page to a fully booted Ember app.

## Difference to ember-cli-fastboot / prember

In contrast to ember-cli-fastboot, this addon will prerender the app using a "regular" browser (Chrome via Puppeteer).
This has the benefit of running all code normally - modifiers etc. will all run just like they do normally.

The restriction is that this _only_ works for prerendering. You can not run this in production like ember-cli-fastboot.
As such, it is only a possible replacement for [prember](https://github.com/ef4/prember), which uses fastboot to prerender your app.

It uses the same serialization/rehydration code under the hood (which lives directly in Glimmer nowadays) as fastboot does.

## Integration into build/deployment process

You can use an [ember-cli-deploy](http://ember-cli-deploy.com/) plugin for easy integration into your pipeline:
Expand All @@ -103,3 +138,7 @@ See the [Contributing](CONTRIBUTING.md) guide for details.
## License

This project is licensed under the [MIT License](LICENSE.md).

```
```
55 changes: 55 additions & 0 deletions packages/ember-build-prerender/addon/services/prerender.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Service from '@ember/service';
import { assert } from '@ember/debug';

export default class PrerenderService extends Service {
/* Set by instance initializer
Expand All @@ -10,7 +11,61 @@ export default class PrerenderService extends Service {
*/
_stage = undefined;

shoebox = new Shoebox();

get isPrerender() {
return this._stage === 'prerender';
}

constructor() {
super(...arguments);

this._loadShoebox();
}

_loadShoebox() {
if (this.isPrerender) {
this.shoebox.empty();
} else {
this.shoebox.load();
}
}
}

class Shoebox {
_content = undefined;
_readOnly = true;

get(item) {
return this._content[item];
}

set(item, value) {
assert(
'You can only set shoebox items in prerender mode!',
!this._readOnly
);

this._content[item] = value;

let element = document.querySelector('meta[name="prerender-shoebox"]');
element.setAttribute('content', JSON.stringify(this._content));
}

load() {
let element = document.querySelector('meta[name="prerender-shoebox"]');
if (!element || !element.getAttribute('content')) {
this._content = {};
return;
}

this._content = JSON.parse(element.getAttribute('content'));
}

empty() {
let element = document.querySelector('meta[name="prerender-shoebox"]');
this._content = {};
this._readOnly = false;
element.setAttribute('content', JSON.stringify({}));
}
}
5 changes: 4 additions & 1 deletion packages/ember-build-prerender/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ module.exports = {
let prerenderEnabled = app.__PRERENDER_ENABLED;

if (type === 'head' && prerenderEnabled) {
return '<meta name="prerender-config" content="should-prerender"></meta>';
return `<meta name="prerender-config" content="should-prerender"></meta>
<meta name="prerender-shoebox" content="{}"></meta>`;
} else if (type === 'head') {
return '<meta name="prerender-shoebox" content="{}"></meta>';
}
},
};
Empty file.
99 changes: 99 additions & 0 deletions packages/test-app/tests/unit/services/prerender-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import PrerenderService from 'ember-build-prerender/services/prerender';

module('Unit | Service | prerender', function (hooks) {
setupTest(hooks);

module('shoebox', function (hooks) {
hooks.beforeEach(function () {
let element = document.querySelector('meta[name="prerender-shoebox"]');
this._originalContent = element.getAttribute('content');
this._element = element;
});

hooks.afterEach(function () {
this._element.setAttribute('content', this._originalContent);
});

test('it allows to read from & write to the shoebox', function (assert) {
class ExtendedPrerenderService extends PrerenderService {
get isPrerender() {
return true;
}

constructor() {
super(...arguments);

this._loadShoebox();
}
}

this.owner.register('service:test-prerender', ExtendedPrerenderService);
let service = this.owner.lookup('service:test-prerender');

assert.strictEqual(service.shoebox.get('itemA'), undefined);

service.shoebox.set('itemA', 'string value');
assert.strictEqual(service.shoebox.get('itemA'), 'string value');

service.shoebox.set('itemA', 9);
assert.strictEqual(service.shoebox.get('itemA'), 9);

service.shoebox.set('itemA', { test: 'AA' });
assert.deepEqual(service.shoebox.get('itemA'), { test: 'AA' });
assert
.dom(this._element)
.hasAttribute('content', JSON.stringify({ itemA: { test: 'AA' } }));

service.shoebox.set('itemA', undefined);
assert.strictEqual(service.shoebox.get('itemA'), undefined);
});

test('it loads shoebox content from DOM', function (assert) {
class ExtendedPrerenderService extends PrerenderService {
get isPrerender() {
return false;
}

constructor() {
super(...arguments);

this._loadShoebox();
}
}

this._element.setAttribute(
'content',
JSON.stringify({ itemA: { test: 'AA' } })
);

this.owner.register('service:test-prerender', ExtendedPrerenderService);
let service = this.owner.lookup('service:test-prerender');

assert.deepEqual(service.shoebox.get('itemA'), { test: 'AA' });
});

test('it disallows setting content in non-prerender mode', function (assert) {
class ExtendedPrerenderService extends PrerenderService {
get isPrerender() {
return false;
}

constructor() {
super(...arguments);

this._loadShoebox();
}
}

this.owner.register('service:test-prerender', ExtendedPrerenderService);
let service = this.owner.lookup('service:test-prerender');

assert.throws(
() => service.shoebox.set('itemA', 'test'),
/You can only set shoebox items in prerender mode!/
);
});
});
});

0 comments on commit 0cf47d7

Please sign in to comment.