├── .github
└── FUNDING.yml
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── cjs
├── custom-elements.js
├── index.js
└── package.json
├── esm
├── custom-elements.js
└── index.js
├── heresy.png
├── heresy.svg
├── package.json
└── test
├── TODO.md
├── cjs
├── body.js
├── definitions.js
├── package.json
└── twitter-share.js
├── esm
├── body.js
├── definitions.js
└── twitter-share.js
├── index.js
├── issue-24.js
├── multi-style.js
├── multiple-documents.js
└── twitter-share.js
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # not working due missing www.
5 | open_collective: hyperHTML
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | custom: https://www.patreon.com/webreflection
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | package-lock.json
3 |
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | test/
3 | package-lock.json
4 | .travis.yml
5 | .github/
6 | heresy.png
7 | heresy.svg
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - stable
4 | git:
5 | depth: 1
6 | branches:
7 | only:
8 | - master
9 | - /^greenkeeper/.*$/
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | ISC License
2 |
3 | Copyright (c) 2019, Andrea Giammarchi, @WebReflection
4 |
5 | Permission to use, copy, modify, and/or distribute this software for any
6 | purpose with or without fee is hereby granted, provided that the above
7 | copyright notice and this permission notice appear in all copies.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15 | PERFORMANCE OF THIS SOFTWARE.
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #  heresy SSR
2 | Don't simulate the DOM. Be the DOM.
3 | - - -
4 | **Social Media Photo by [Thomas Kelley](https://unsplash.com/@thkelley) on [Unsplash](https://unsplash.com/)**
5 |
6 |  [](https://opensource.org/licenses/ISC) [](https://travis-ci.com/WebReflection/heresy-ssr) [](https://greenkeeper.io/)
7 |
8 | It's pretty much the same [heresy](https://github.com/WebReflection/heresy#readme), but for the server, and with extra features.
9 |
10 | - - -
11 |
12 | ### 📣 Community Announcement
13 |
14 | Please ask questions in the [dedicated discussions repository](https://github.com/WebReflection/discussions), to help the community around this project grow ♥
15 |
16 | ---
17 |
18 | ### V2 Breaking Changes
19 |
20 | Please be sure you understand the [breaking changes landed in lighterhtml](https://github.com/WebReflection/lighterhtml#v4-breaking-changes).
21 |
22 | - - -
23 |
24 |
25 | ### How To Install With Canvas
26 |
27 | If you need/want to use the `` element, which is a dev dependency of [basicHTML](https://github.com/WebReflection/basicHTML#readme), you explicitly need to type:
28 |
29 | ```sh
30 | # install heresy-ssr with canvas
31 | npm i heresy-ssr canvas
32 | ```
33 |
34 | Otherwise _heresy-ssr_ will ship without canvas via simply typing:
35 |
36 | ```sh
37 | # install heresy-ssr without canvas
38 | npm i heresy-ssr
39 | ```
40 |
41 | ## Extra Features
42 |
43 | * dedicated `onSSRInit/AttributeChanged/Connected/Disconnected` methods to override client side `oninit/attributechanged/connected/disconnected`, in order to fine-tune, whenever necessary, the layout and behavior via SSR
44 | * an already available ` ` tag to put in the header, whenever polyfills for legacy or WebKit/Safari are needed. The component accepts `modern` and `legacy` attributes as pointers to polyfills, loaded only after feature detection to leave Chrome, Firefox, and Edge on Chromium free of bloat.
45 | * components style automatically minified via [csso](https://www.npmjs.com/package/csso)
46 | * global `customElements` or `document`, swappable on the `window` with any local instance of `Document` or `CustomElementRegistry`
47 |
48 |
49 | ### Basic Example
50 |
51 | You can see inside the [test folder](./test) a similar example you can run via `npm run build` or just `npm test`, after the first build.
52 |
53 | ```js
54 | const {document, render, html} = require('heresy-ssr');
55 |
56 | const Body = require('./body.js');
57 | define('Body', Body);
58 |
59 | const lang = 'en';
60 | const {hostname} = require('os');
61 | const {readFileSync} = require('fs');
62 |
63 | render(document, html`
64 |
65 |
66 | 🔥 heresy SSR 🔥
67 |
68 |
69 |
70 |
71 |
72 |
73 | `);
74 | ```
75 |
76 | You can also try `node test/twitter-share.js` to see an example of a component served through the same definition crystal clean via SSR, but still re-hydrated on the client whenever the definition lands on the page.
77 |
78 |
79 | ## Multiple Documents
80 |
81 | The default `document` is ideal for **S**ingle **P**age **A**pplications but not optimal for sites distributed through various pages.
82 |
83 | In latter scenario, you can use a new document per each render.
84 |
85 | ```js
86 | const {Document, render, html} = require('heresy-ssr');
87 |
88 | // create a new document related to this page only
89 | const document = new Document;
90 |
91 | render(document, html`Hello `);
92 | ```
93 |
94 |
95 | ## Project Goals
96 |
97 | * reuse exact same components on client as well as on the server, with the ability to provide cleaner layouts via SSR
98 | * hydration on the fly without even thinking about it, whenever the definition lands on the client, it just works™️
99 | * Custom Elements and built-in extends out of the box for any sort of client/server need
100 | * you work on the server with same DOM primitives you know on the client, but you can also create as many documents you want, so that each page is reflected by a different, always clean document
101 |
102 |
103 | ### Differences from viperHTML
104 |
105 | There are tons of differences with _viperHTML_ at this stage:
106 |
107 | * the template literal parser is exactly the same one used on the client (slower cold bootstrap, but damn fast hot renders)
108 | * the code is exactly the same one used by the client library, with minor tweaks specific for SSR usage only
109 | * _viperHTML_ never really provided proper re-hydration for _hyperHTML_, while here this is provided natively by the Web platform, and it works better than anything else, specially with custom elements builtin
110 |
--------------------------------------------------------------------------------
/cjs/custom-elements.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /*
3 |
7 | */
8 | module.exports = {
9 | extends: 'script',
10 | get legacy() {
11 | return this.getAttribute('legacy') || '//unpkg.com/document-register-element';
12 | },
13 | get modern() {
14 | return this.getAttribute('modern') || '//unpkg.com/@ungap/custom-elements-builtin';
15 | },
16 | onSSRConnected() {
17 | this.attributes = [];
18 | this.textContent = `
19 | (function(w, d, c){
20 | try {
21 | w[c].define('built-in', d.createElement('p').constructor, {'extends':'p'})
22 | }
23 | catch (e) {
24 | d.write(
25 | unescape(
26 | '%3Cscript%20src%3D%22' +
27 | (w[c] ? '${this.modern}' : '${this.legacy}') +
28 | '%22%3E%3C/script%3E'
29 | )
30 | )
31 | }
32 | }(this, document, 'customElements'))
33 | `.replace(/\s+/g, '');
34 | }
35 | };
36 |
--------------------------------------------------------------------------------
/cjs/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const {
3 | init,
4 | Document: PlainDocument,
5 | HTMLElement,
6 | HTMLTemplateElement
7 | } = require('basichtml');
8 | const {customElements, document, window} = init({});
9 |
10 | const csso = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('csso'));
11 |
12 | const {
13 | define: heresyDefine,
14 | render: heresyRender,
15 | ref,
16 | html,
17 | svg
18 | } = require('heresy');
19 |
20 | const CustomElements = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('./custom-elements.js'));
21 |
22 | const {keys} = Object;
23 | const configurable = true;
24 | const documents = new WeakMap;
25 | const classStyle = new Map;
26 | let waiting = new Map;
27 | const cleanWait = $ => {
28 | const doc = window.document;
29 | const styles = documents.get(doc) || documents.set(doc, new Set).get(doc);
30 | classStyle.forEach((css, Class) => {
31 | if (css.length && !styles.has(Class)) {
32 | styles.add(Class);
33 | const {head} = doc;
34 | const style = doc.createElement('style');
35 | style.setAttribute('type', 'text/css');
36 | style.textContent = css;
37 | head.insertBefore(style, head.lastChild);
38 | }
39 | });
40 | waiting.forEach(({render, args}, node) => {
41 | render.apply(node, args);
42 | });
43 | waiting = new Map;
44 | return $;
45 | };
46 |
47 | const setStyle = (Class, styled = new Set) => {
48 | if (styled.has(Class))
49 | return;
50 | styled.add(Class);
51 | if ('style' in Class) {
52 | const {style} = Class;
53 | defineProperty(Class, 'style', {
54 | configurable,
55 | value() {
56 | if (!classStyle.has(Class))
57 | classStyle.set(Class, csso.minify(style.apply(Class, arguments)).css);
58 | return '';
59 | }
60 | });
61 | }
62 | const sub = Class.contains || Class.includes;
63 | if (sub)
64 | keys(sub).forEach(key => setStyle(sub[key]), styled);
65 | };
66 |
67 | const {defineProperty} = Object;
68 | const define = (...args) => {
69 | const Class = args.length < 2 ? args[0] : args[1];
70 | setStyle(Class);
71 | const proto = typeof Class === 'function' ? Class.prototype : Class;
72 | for (const lifecycle of ['Init', 'Connected', 'Disconnected', 'AttributeChanged']) {
73 | const ssr = 'onSSR' + lifecycle;
74 | if (ssr in proto)
75 | defineProperty(proto, 'on' + lifecycle.toLowerCase(), {
76 | configurable,
77 | value: proto[ssr]
78 | });
79 | }
80 | if ('render' in proto) {
81 | const {render} = proto;
82 | defineProperty(proto, 'render', {
83 | configurable,
84 | value() {
85 | waiting.set(this, {render, args});
86 | return this;
87 | }
88 | });
89 | }
90 | return heresyDefine.apply(null, args);
91 | }
92 |
93 | const template = new HTMLTemplateElement;
94 | const render = (where, what) => {
95 | const {document} = window;
96 | let result;
97 | switch (true) {
98 | case where instanceof HTMLElement:
99 | window.document = where.ownerDocument;
100 | try {
101 | result = cleanWait(heresyRender(where, what));
102 | }
103 | catch(error) {
104 | console.error(error);
105 | }
106 | finally {
107 | window.document = document;
108 | }
109 | return result;
110 | case where instanceof PlainDocument:
111 | window.document = where;
112 | try {
113 | heresyRender(template, what);
114 | where.documentElement = template.firstElementChild;
115 | result = cleanWait(where);
116 | }
117 | catch(error) {
118 | console.error(error);
119 | }
120 | finally {
121 | window.document = document;
122 | }
123 | return result;
124 | // RAW fallback, through response.write(...)
125 | // render(response, html`anything`)
126 | // TODO: documentation + way to pass a different document
127 | default:
128 | heresyRender(template, what);
129 | return where.write(cleanWait(template).innerHTML);
130 | }
131 | };
132 |
133 | Document.prototype = PlainDocument.prototype;
134 |
135 | exports.Document = Document;
136 | exports.customElements = customElements;
137 | exports.document = document;
138 | exports.window = window;
139 | exports.define = define;
140 | exports.render = render;
141 | exports.ref = ref;
142 | exports.html = html;
143 | exports.svg = svg;
144 |
145 | // make check available with ease
146 | define('CustomElements', CustomElements);
147 |
148 | function Document() {
149 | return new PlainDocument(customElements);
150 | }
151 |
--------------------------------------------------------------------------------
/cjs/package.json:
--------------------------------------------------------------------------------
1 | {"type":"commonjs"}
--------------------------------------------------------------------------------
/esm/custom-elements.js:
--------------------------------------------------------------------------------
1 | /*
2 |
6 | */
7 | export default {
8 | extends: 'script',
9 | get legacy() {
10 | return this.getAttribute('legacy') || '//unpkg.com/document-register-element';
11 | },
12 | get modern() {
13 | return this.getAttribute('modern') || '//unpkg.com/@ungap/custom-elements-builtin';
14 | },
15 | onSSRConnected() {
16 | this.attributes = [];
17 | this.textContent = `
18 | (function(w, d, c){
19 | try {
20 | w[c].define('built-in', d.createElement('p').constructor, {'extends':'p'})
21 | }
22 | catch (e) {
23 | d.write(
24 | unescape(
25 | '%3Cscript%20src%3D%22' +
26 | (w[c] ? '${this.modern}' : '${this.legacy}') +
27 | '%22%3E%3C/script%3E'
28 | )
29 | )
30 | }
31 | }(this, document, 'customElements'))
32 | `.replace(/\s+/g, '');
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/esm/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | init,
3 | Document as PlainDocument,
4 | HTMLElement, HTMLTemplateElement
5 | } from 'basichtml';
6 | const {customElements, document, window} = init({});
7 |
8 | import csso from 'csso';
9 |
10 | import {
11 | define as heresyDefine,
12 | render as heresyRender,
13 | ref, html, svg
14 | } from 'heresy';
15 |
16 | import CustomElements from './custom-elements.js';
17 |
18 | const {keys} = Object;
19 | const configurable = true;
20 | const documents = new WeakMap;
21 | const classStyle = new Map;
22 | let waiting = new Map;
23 | const cleanWait = $ => {
24 | const doc = window.document;
25 | const styles = documents.get(doc) || documents.set(doc, new Set).get(doc);
26 | classStyle.forEach((css, Class) => {
27 | if (css.length && !styles.has(Class)) {
28 | styles.add(Class);
29 | const {head} = doc;
30 | const style = doc.createElement('style');
31 | style.setAttribute('type', 'text/css');
32 | style.textContent = css;
33 | head.insertBefore(style, head.lastChild);
34 | }
35 | });
36 | waiting.forEach(({render, args}, node) => {
37 | render.apply(node, args);
38 | });
39 | waiting = new Map;
40 | return $;
41 | };
42 |
43 | const setStyle = (Class, styled = new Set) => {
44 | if (styled.has(Class))
45 | return;
46 | styled.add(Class);
47 | if ('style' in Class) {
48 | const {style} = Class;
49 | defineProperty(Class, 'style', {
50 | configurable,
51 | value() {
52 | if (!classStyle.has(Class))
53 | classStyle.set(Class, csso.minify(style.apply(Class, arguments)).css);
54 | return '';
55 | }
56 | });
57 | }
58 | const sub = Class.contains || Class.includes;
59 | if (sub)
60 | keys(sub).forEach(key => setStyle(sub[key]), styled);
61 | };
62 |
63 | const {defineProperty} = Object;
64 | const define = (...args) => {
65 | const Class = args.length < 2 ? args[0] : args[1];
66 | setStyle(Class);
67 | const proto = typeof Class === 'function' ? Class.prototype : Class;
68 | for (const lifecycle of ['Init', 'Connected', 'Disconnected', 'AttributeChanged']) {
69 | const ssr = 'onSSR' + lifecycle;
70 | if (ssr in proto)
71 | defineProperty(proto, 'on' + lifecycle.toLowerCase(), {
72 | configurable,
73 | value: proto[ssr]
74 | });
75 | }
76 | if ('render' in proto) {
77 | const {render} = proto;
78 | defineProperty(proto, 'render', {
79 | configurable,
80 | value() {
81 | waiting.set(this, {render, args});
82 | return this;
83 | }
84 | });
85 | }
86 | return heresyDefine.apply(null, args);
87 | }
88 |
89 | const template = new HTMLTemplateElement;
90 | const render = (where, what) => {
91 | const {document} = window;
92 | let result;
93 | switch (true) {
94 | case where instanceof HTMLElement:
95 | window.document = where.ownerDocument;
96 | try {
97 | result = cleanWait(heresyRender(where, what));
98 | }
99 | catch(error) {
100 | console.error(error);
101 | }
102 | finally {
103 | window.document = document;
104 | }
105 | return result;
106 | case where instanceof PlainDocument:
107 | window.document = where;
108 | try {
109 | heresyRender(template, what);
110 | where.documentElement = template.firstElementChild;
111 | result = cleanWait(where);
112 | }
113 | catch(error) {
114 | console.error(error);
115 | }
116 | finally {
117 | window.document = document;
118 | }
119 | return result;
120 | // RAW fallback, through response.write(...)
121 | // render(response, html`anything`)
122 | // TODO: documentation + way to pass a different document
123 | default:
124 | heresyRender(template, what);
125 | return where.write(cleanWait(template).innerHTML);
126 | }
127 | };
128 |
129 | Document.prototype = PlainDocument.prototype;
130 |
131 | export {
132 | // SSR only - You can have one document per page/end point
133 | Document,
134 | // also for SSR, don't use `document` within components
135 | customElements, document, window,
136 | // specialized for SSR too, not needed within components
137 | define, render,
138 |
139 | // Exact same heresy helpers, usable in any client/server component
140 | ref, html, svg
141 | };
142 |
143 | // make check available with ease
144 | define('CustomElements', CustomElements);
145 |
146 | function Document() {
147 | return new PlainDocument(customElements);
148 | }
149 |
--------------------------------------------------------------------------------
/heresy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WebReflection/heresy-ssr/405e0817a58561f2f65691da3531e8217be304d5/heresy.png
--------------------------------------------------------------------------------
/heresy.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "heresy-ssr",
3 | "version": "2.0.3",
4 | "description": "🔥 heresy 🔥 Server Side Rendering",
5 | "main": "cjs/index.js",
6 | "module": "esm/index.js",
7 | "scripts": {
8 | "build": "npm run cjs",
9 | "cjs": "ascjs --no-default esm cjs",
10 | "test": "npm run build && ascjs --no-default test/esm test/cjs"
11 | },
12 | "keywords": [
13 | "heresy",
14 | "SSR",
15 | "server",
16 | "render"
17 | ],
18 | "author": "Andrea Giammarchi",
19 | "license": "ISC",
20 | "devDependencies": {
21 | "ascjs": "^4.0.3"
22 | },
23 | "dependencies": {
24 | "basichtml": "^2.4.3",
25 | "csso": "^4.2.0",
26 | "heresy": "^1.0.4"
27 | },
28 | "directories": {
29 | "test": "test"
30 | },
31 | "repository": {
32 | "type": "git",
33 | "url": "git+https://github.com/WebReflection/heresy-ssr.git"
34 | },
35 | "bugs": {
36 | "url": "https://github.com/WebReflection/heresy-ssr/issues"
37 | },
38 | "homepage": "https://github.com/WebReflection/heresy-ssr#readme"
39 | }
40 |
--------------------------------------------------------------------------------
/test/TODO.md:
--------------------------------------------------------------------------------
1 | # TODO
2 |
3 | - [ ] _heresy_ and _heresy-ssr_ share the exact same `ref`, `html`, and `svg` utilities. This means that components should always be developed using client side _heresy_ and these will work out of the box in _heresy-ssr_ too, since it uses exact same utilities. Test/validate this with an example.
4 |
5 | - [ ] in a list of items the _html_ or _svg_ helpers could be used, but also _ref_. Beside testing components work isomorphic out of the box, be sure these features are also tested.
6 |
7 | - [ ] how to automate lazy load of components ?
8 |
--------------------------------------------------------------------------------
/test/cjs/body.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const Fragment = {
3 | extends: 'fragment',
4 | mappedAttributes: ['hostname'],
5 | render() {
6 | this.html`Welcome in ${this.hostname} !`;
7 | }
8 | };
9 |
10 | module.exports = {
11 | extends: 'body',
12 | includes: {Fragment},
13 | style: (selector) => `
14 | ${selector} {
15 | font-family: sans-serif;
16 | font-size: 16px;
17 | }
18 | ${selector} > strong {
19 | opacity: .75;
20 | }
21 | `,
22 | render() {
23 | this.html``;
24 | console.log(this.outerHTML);
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/test/cjs/definitions.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const {define} = heresy;
3 |
4 | const Body = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('./body.js'));
5 |
6 | define('Body', Body);
7 |
--------------------------------------------------------------------------------
/test/cjs/package.json:
--------------------------------------------------------------------------------
1 | {"type":"commonjs"}
--------------------------------------------------------------------------------
/test/cjs/twitter-share.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | // original implementation here
3 | // https://svelte.dev/repl/98aa20d4cb3d40dabfef7d8dae183b85?version=3.5.2
4 |
5 | const TwitterShare = {
6 | style: (TS) => `
7 | ${TS} {
8 | background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 400'%3E%3Cpath fill='rgb(29, 161, 242)' class='cls-2' d='M153.62,301.59c94.34,0,145.94-78.16,145.94-145.94,0-2.22,0-4.43-.15-6.63A104.36,104.36,0,0,0,325,122.47a102.38,102.38,0,0,1-29.46,8.07,51.47,51.47,0,0,0,22.55-28.37,102.79,102.79,0,0,1-32.57,12.45,51.34,51.34,0,0,0-87.41,46.78A145.62,145.62,0,0,1,92.4,107.81a51.33,51.33,0,0,0,15.88,68.47A50.91,50.91,0,0,1,85,169.86c0,.21,0,.43,0,.65a51.31,51.31,0,0,0,41.15,50.28,51.21,51.21,0,0,1-23.16.88,51.35,51.35,0,0,0,47.92,35.62,102.92,102.92,0,0,1-63.7,22A104.41,104.41,0,0,1,75,278.55a145.21,145.21,0,0,0,78.62,23'/%3E%3C/svg%3E") 0 50% no-repeat;
9 | background-size: 1.5em;
10 | color: rgb(29, 161, 242);
11 | font-weight: bold;
12 | padding: 0.5em 0.3em 0.5em 1.5em;
13 | text-decoration: none;
14 | }
15 | ${TS}:hover {
16 | border-bottom: 2px solid rgb(29, 161, 242);
17 | }
18 | `,
19 | extends: 'a',
20 | observedAttributes: ['text', 'url', 'hashtags', 'via', 'related'],
21 | onconnected() { this.addEventListener('click', this); },
22 | onattributechanged(event) {
23 | this.setAttribute('href', getHref(this));
24 | this.setAttribute('noreferrer', '');
25 | this.textContent = 'Tweet this';
26 | },
27 | onclick(e) {
28 | e.preventDefault();
29 | const w = 600;
30 | const h = 400;
31 | const x = (screen.width - w) / 2;
32 | const y = (screen.height - h) / 2;
33 | const features = `width=${w},height=${h},left=${x},top=${y}`;
34 | window.open(this.href, '_blank', features);
35 | },
36 |
37 | // overwrites client side onconnected, ignored by heresy client
38 | // it is possible to fine-tune components on the server side
39 | // with onSSR/Init/AttributeChanged/Connected/Disconnected
40 | onSSRConnected() {
41 | // hard remove observed attributes so that client won't have
42 | // onattributechanged ever triggered:
43 | // * faster bootstrap
44 | // * lighter SSR
45 | // 🎉
46 | this.attributes = this.attributes.filter(
47 | attr => !TwitterShare.observedAttributes.includes(attr.name)
48 | );
49 | }
50 | };
51 |
52 | module.exports = TwitterShare;
53 |
54 | function getHref(self) {
55 | return 'https://twitter.com/intent/tweet?' +
56 | TwitterShare.observedAttributes.reduce(
57 | (qs, curr) => {
58 | const val = self.getAttribute(curr);
59 | if (val)
60 | qs.push(curr + '=' + encodeURIComponent(val));
61 | return qs;
62 | },
63 | []
64 | ).join('&');
65 | }
66 |
--------------------------------------------------------------------------------
/test/esm/body.js:
--------------------------------------------------------------------------------
1 | const Fragment = {
2 | extends: 'fragment',
3 | mappedAttributes: ['hostname'],
4 | render() {
5 | this.html`Welcome in ${this.hostname} !`;
6 | }
7 | };
8 |
9 | export default {
10 | extends: 'body',
11 | includes: {Fragment},
12 | style: (selector) => `
13 | ${selector} {
14 | font-family: sans-serif;
15 | font-size: 16px;
16 | }
17 | ${selector} > strong {
18 | opacity: .75;
19 | }
20 | `,
21 | render() {
22 | this.html``;
23 | console.log(this.outerHTML);
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/test/esm/definitions.js:
--------------------------------------------------------------------------------
1 | const {define} = heresy;
2 |
3 | import Body from './body.js';
4 |
5 | define('Body', Body);
6 |
--------------------------------------------------------------------------------
/test/esm/twitter-share.js:
--------------------------------------------------------------------------------
1 | // original implementation here
2 | // https://svelte.dev/repl/98aa20d4cb3d40dabfef7d8dae183b85?version=3.5.2
3 |
4 | const TwitterShare = {
5 | style: (TS) => `
6 | ${TS} {
7 | background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 400'%3E%3Cpath fill='rgb(29, 161, 242)' class='cls-2' d='M153.62,301.59c94.34,0,145.94-78.16,145.94-145.94,0-2.22,0-4.43-.15-6.63A104.36,104.36,0,0,0,325,122.47a102.38,102.38,0,0,1-29.46,8.07,51.47,51.47,0,0,0,22.55-28.37,102.79,102.79,0,0,1-32.57,12.45,51.34,51.34,0,0,0-87.41,46.78A145.62,145.62,0,0,1,92.4,107.81a51.33,51.33,0,0,0,15.88,68.47A50.91,50.91,0,0,1,85,169.86c0,.21,0,.43,0,.65a51.31,51.31,0,0,0,41.15,50.28,51.21,51.21,0,0,1-23.16.88,51.35,51.35,0,0,0,47.92,35.62,102.92,102.92,0,0,1-63.7,22A104.41,104.41,0,0,1,75,278.55a145.21,145.21,0,0,0,78.62,23'/%3E%3C/svg%3E") 0 50% no-repeat;
8 | background-size: 1.5em;
9 | color: rgb(29, 161, 242);
10 | font-weight: bold;
11 | padding: 0.5em 0.3em 0.5em 1.5em;
12 | text-decoration: none;
13 | }
14 | ${TS}:hover {
15 | border-bottom: 2px solid rgb(29, 161, 242);
16 | }
17 | `,
18 | extends: 'a',
19 | observedAttributes: ['text', 'url', 'hashtags', 'via', 'related'],
20 | onconnected() { this.addEventListener('click', this); },
21 | onattributechanged(event) {
22 | this.setAttribute('href', getHref(this));
23 | this.setAttribute('noreferrer', '');
24 | this.textContent = 'Tweet this';
25 | },
26 | onclick(e) {
27 | e.preventDefault();
28 | const w = 600;
29 | const h = 400;
30 | const x = (screen.width - w) / 2;
31 | const y = (screen.height - h) / 2;
32 | const features = `width=${w},height=${h},left=${x},top=${y}`;
33 | window.open(this.href, '_blank', features);
34 | },
35 |
36 | // overwrites client side onconnected, ignored by heresy client
37 | // it is possible to fine-tune components on the server side
38 | // with onSSR/Init/AttributeChanged/Connected/Disconnected
39 | onSSRConnected() {
40 | // hard remove observed attributes so that client won't have
41 | // onattributechanged ever triggered:
42 | // * faster bootstrap
43 | // * lighter SSR
44 | // 🎉
45 | this.attributes = this.attributes.filter(
46 | attr => !TwitterShare.observedAttributes.includes(attr.name)
47 | );
48 | }
49 | };
50 |
51 | export default TwitterShare;
52 |
53 | function getHref(self) {
54 | return 'https://twitter.com/intent/tweet?' +
55 | TwitterShare.observedAttributes.reduce(
56 | (qs, curr) => {
57 | const val = self.getAttribute(curr);
58 | if (val)
59 | qs.push(curr + '=' + encodeURIComponent(val));
60 | return qs;
61 | },
62 | []
63 | ).join('&');
64 | }
65 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | // for testing purpose to use exact same ESM content
2 | global.heresy = require('../cjs');
3 |
4 | const {document, render, html} = heresy;
5 | require('./cjs/definitions.js');
6 |
7 | const lang = 'en';
8 | const {hostname} = require('os');
9 | const {readFileSync} = require('fs');
10 |
11 | render(document, html`
12 |
13 |
14 | 🔥 heresy SSR 🔥
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | `);
24 |
25 | require('http').createServer((req, res) => {
26 | switch (true) {
27 | case /^\/esm\/.+$/.test(req.url):
28 | res.writeHead(200, {"content-type": "application/javascript"});
29 | res.end(readFileSync(`./test${req.url}`));
30 | break;
31 | case req.url === '/favicon.ico':
32 | res.writeHead(404);
33 | res.end();
34 | break;
35 | default:
36 | res.writeHead(200, {"content-type": "text/html;charset=utf-8"});
37 | res.end(document.toString());
38 | break;
39 | }
40 | }).listen(8080);
41 |
42 | console.log('http://localhost:8080/');
43 |
--------------------------------------------------------------------------------
/test/issue-24.js:
--------------------------------------------------------------------------------
1 | const http = require('http');
2 | const {document, render, html, define} = require('../cjs');
3 |
4 | let firstElemInits = 0;
5 | let secondElemInits = 0;
6 | let thirdElement = 0;
7 |
8 | const FirstElement = {
9 | name: 'FirstElement',
10 | extends: 'element',
11 | oninit() {
12 | console.log('FirstElement init:', ++firstElemInits);
13 | },
14 | render() {
15 | this.html`FirstElement: ${ firstElemInits }`;
16 | }
17 | }
18 | const SecondElement = {
19 | name: 'SecondElement',
20 | extends: 'button',
21 | oninit() {
22 | console.log('SecondElement init:', ++secondElemInits);
23 | },
24 | render() {
25 | this.html`SecondElementID: ${ secondElemInits }`;
26 | }
27 | }
28 | const ThirdElement = {
29 | name: 'ThirdElement',
30 | extends: 'element',
31 | oninit() {
32 | console.log('ThirdElement init:', ++thirdElement);
33 | },
34 | render() {
35 | this.html`ThirdElementID: ${ thirdElement }`;
36 | }
37 | }
38 |
39 | define(FirstElement);
40 | define(SecondElement);
41 | define(ThirdElement);
42 |
43 | render(document.body, html`
44 |
45 |
46 |
47 | `);
48 |
49 | const server = http.createServer(function(req, res) {
50 | res.writeHead(200, {"content-type": "text/html;charset=utf-8"});
51 | res.end(document.toString());
52 | }).listen(8080, () => console.log("server running"))
--------------------------------------------------------------------------------
/test/multi-style.js:
--------------------------------------------------------------------------------
1 | const {Document, define, render, html} = require('../cjs');
2 |
3 | define('MyStyle', {
4 | style: MyStyle => `
5 | ${MyStyle} { font-family: sans-serif; }
6 | `
7 | });
8 |
9 | const defaultDocument = render(document, html`
10 |
11 |
12 |
13 | `).toString();
14 |
15 | const newDoc1 = new Document;
16 | const newDocument1 = render(newDoc1, html`
17 |
18 |
19 |
20 | `).toString();
21 |
22 | const newDoc2 = new Document;
23 | const newDocument2 = render(newDoc2, html`
24 |
25 |
26 |
27 | `).toString();
28 |
29 | const newDoc3 = new Document;
30 | const multiPass = doc => render(doc, html`
31 |
32 |
33 |
34 | `).toString();
35 |
36 | console.assert(defaultDocument === newDocument1 && newDocument1 === newDocument2, 'unexpected inline layout');
37 | console.assert(multiPass(newDoc3) === multiPass(newDoc3) && newDocument1 === multiPass(newDoc3), 'unexpected scoped layout');
38 |
39 | console.log('\x1b[1mOK\x1b[0m');
40 |
--------------------------------------------------------------------------------
/test/multiple-documents.js:
--------------------------------------------------------------------------------
1 | const {Document, document, render, html} = require('../cjs');
2 |
3 | const defaultDocument = render(document, html` `).toString();
4 | const newDocument1 = render(new Document, html` `).toString();
5 | const newDocument2 = render(new Document, html` `).toString();
6 |
7 | console.assert(defaultDocument === newDocument1 && newDocument1 === newDocument2, 'unexpected layout');
8 |
9 | console.log('\x1b[1mOK\x1b[0m');
10 | console.log(defaultDocument);
11 |
--------------------------------------------------------------------------------
/test/twitter-share.js:
--------------------------------------------------------------------------------
1 | const {document, define, render, html} = require('../cjs');
2 | const TwitterShare = require('./cjs/twitter-share');
3 |
4 | // generic document head setup
5 | render(document.head, html`
6 | 🔥 heresy SSR 🔥 TwitterShare Example
7 |
8 |
9 |
10 |
11 |
12 |
19 |
23 | `
31 | );
32 |
33 | // define the component like a Custom Element
34 | define('TwitterShare', TwitterShare);
35 |
36 | // and use it whenever you want
37 | render(document.body, html`
38 | `
43 | );
44 |
45 | // spin a server to test this
46 | require('http').createServer((req, res) => {
47 | switch (true) {
48 | case /^\/esm\/.+$/.test(req.url):
49 | res.writeHead(200, {"content-type": "application/javascript"});
50 | res.end(require('fs').readFileSync(`./test${req.url}`));
51 | break;
52 | default:
53 | res.writeHead(200, {"content-type": "text/html;charset=utf-8"});
54 | res.end(document.toString());
55 | break;
56 | }
57 | }).listen(8080);
58 |
59 | console.log('http://localhost:8080/');
60 |
--------------------------------------------------------------------------------