├── .nvmrc ├── examples └── simple-server │ ├── .gitignore │ ├── docs │ └── preview.png │ ├── server │ ├── assets │ │ └── bg.png │ ├── render.js │ ├── index.js │ └── data.json │ ├── src │ ├── globalStyles.js │ ├── App.jsx │ └── Components │ │ ├── Heading.jsx │ │ ├── SocialShare.jsx │ │ ├── Head.jsx │ │ ├── PostCard.jsx │ │ └── Body.jsx │ ├── .babelrc │ ├── README.md │ └── package.json ├── test ├── snapshots │ ├── index.test.js.snap │ └── index.test.js.md ├── Components │ ├── snapshots │ │ ├── index.test.js.snap │ │ ├── Components.test.js.snap │ │ ├── index.test.js.md │ │ └── Components.test.js.md │ ├── Head.test.js │ └── Components.test.js └── index.test.js ├── .npmignore ├── src ├── Components │ ├── Link.jsx │ ├── Meta.jsx │ ├── Title.jsx │ ├── Tag.jsx │ ├── Head.jsx │ ├── Script.jsx │ └── index.js ├── constants.js ├── index.jsx └── store.js ├── .gitignore ├── .travis.yml ├── config ├── .babelrc └── .eslintrc.json ├── .editorconfig ├── LICENSE.md ├── coverage.lcov ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 6 2 | -------------------------------------------------------------------------------- /examples/simple-server/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | **/node_modules 3 | public 4 | -------------------------------------------------------------------------------- /test/snapshots/index.test.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ariel-Rodriguez/react-amp-template/HEAD/test/snapshots/index.test.js.snap -------------------------------------------------------------------------------- /examples/simple-server/docs/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ariel-Rodriguez/react-amp-template/HEAD/examples/simple-server/docs/preview.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .travis.yml 2 | examples 3 | .nyc_output 4 | src 5 | .editorconfig 6 | yarn-error.log 7 | tmp 8 | coverage 9 | test 10 | config 11 | -------------------------------------------------------------------------------- /examples/simple-server/server/assets/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ariel-Rodriguez/react-amp-template/HEAD/examples/simple-server/server/assets/bg.png -------------------------------------------------------------------------------- /test/Components/snapshots/index.test.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ariel-Rodriguez/react-amp-template/HEAD/test/Components/snapshots/index.test.js.snap -------------------------------------------------------------------------------- /test/Components/snapshots/Components.test.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ariel-Rodriguez/react-amp-template/HEAD/test/Components/snapshots/Components.test.js.snap -------------------------------------------------------------------------------- /src/Components/Link.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Tag from './Tag' 3 | 4 | const Link = props => 5 | 6 | 7 | export default Link 8 | -------------------------------------------------------------------------------- /src/Components/Meta.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Tag from './Tag' 3 | 4 | const Meta = props => 5 | 6 | 7 | export default Meta 8 | -------------------------------------------------------------------------------- /examples/simple-server/src/globalStyles.js: -------------------------------------------------------------------------------- 1 | import { injectGlobal } from 'styled-components' 2 | 3 | injectGlobal` 4 | p { 5 | font-size: 1.2rem; 6 | font-family: monospace; 7 | } 8 | ` 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/** 2 | node_modules/** 3 | npm-debug.log 4 | .DS_Store 5 | yarn-error.log 6 | lib 7 | dist 8 | examples/build 9 | examples/simple/node_modules 10 | test-debug.html 11 | .nyc_output 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "stable" 4 | cache: 5 | yarn: false 6 | script: 7 | - npm install 8 | - npm test 9 | after_script: 10 | - npm install -g codecov 11 | - npm run report-coverage 12 | -------------------------------------------------------------------------------- /examples/simple-server/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceMaps": true, 3 | "presets": [ 4 | "react", 5 | ["env", { 6 | "targets": { 7 | "node": "current" 8 | } 9 | }]], 10 | "plugins": [ 11 | ["babel-plugin-styled-components", { "ssr": true }] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /config/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | ["env", { 5 | "targets": { 6 | "node": "current" 7 | } 8 | }] 9 | ], 10 | "plugins": [ 11 | "transform-object-rest-spread" 12 | ], 13 | "env": { 14 | "test": { 15 | "plugins": ["istanbul"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Components/Title.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Tag from './Tag' 4 | 5 | const Title = ({ children }) => 6 | 7 | 8 | Title.propTypes = { 9 | children: PropTypes.string.isRequired, 10 | } 11 | 12 | export default Title 13 | -------------------------------------------------------------------------------- /examples/simple-server/README.md: -------------------------------------------------------------------------------- 1 | ## DEMO 2 | 3 | This is a full boilerplate application. 4 | Demostrates how to implement RAMPT using Babel + React 16 + StyledComponents v3. 5 | 6 | 7 | 8 | ### Install 9 | - `npm install` 10 | - `npm start` 11 | 12 | ### Dev 13 | Launch server live reload. 14 | - `npm run dev` 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | # https://github.com/jokeyrhyme/standard-editorconfig 3 | 4 | # top-most EditorConfig file 5 | root = true 6 | 7 | # Unix-style newlines with a newline ending every file 8 | [*] 9 | end_of_line = lf 10 | insert_final_newline = true 11 | 12 | # Set default charset 13 | [*] 14 | charset = utf-8 15 | 16 | # Other good defaults 17 | [*] 18 | indent_size = 2 19 | indent_style = space 20 | trim_trailing_whitespace = true 21 | -------------------------------------------------------------------------------- /examples/simple-server/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import { renderToString } from 'react-amp-template' 3 | import Head from './Components/Head' 4 | import Body from './Components/Body' 5 | import './globalStyles' 6 | 7 | const App = ({ title, date, json }) => ( 8 | 9 | 10 | 11 | 12 | ) 13 | 14 | export default props => renderToString() 15 | -------------------------------------------------------------------------------- /src/Components/Tag.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Consumer } from '../store' 4 | 5 | 6 | const Tag = ({ 7 | _name: name, 8 | children, 9 | ...props 10 | }) => ( 11 | 12 | {({ store }) => { 13 | store.registerElement(name, props) 14 | return null 15 | }} 16 | 17 | ) 18 | 19 | Tag.defaultProps = { 20 | _name: '', 21 | children: null, 22 | } 23 | 24 | Tag.propTypes = { 25 | _name: PropTypes.string, 26 | children: PropTypes.node, 27 | } 28 | 29 | export default Tag 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Ariel Fernando Rodriguez 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /examples/simple-server/src/Components/Heading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | const Container = styled.div` 5 | padding: 1rem; 6 | ` 7 | 8 | const H1 = styled.h1` 9 | font-size: 1.5em; 10 | font-family: sans-serif; 11 | ` 12 | 13 | const Date = styled.small` 14 | background: yellowgreen; 15 | ` 16 | 17 | const Heading = ({ date }) => ( 18 | 19 |

An AMP Live Blog

20 |

{date}

21 |

by Chiara Chiappini

22 |

This is a sample article demonstrating how to write a live blog in AMP. It demonstrates the usage of amp-live-list component which allows to create live blogs.

23 |
24 | ) 25 | 26 | export default Heading 27 | -------------------------------------------------------------------------------- /test/Components/snapshots/index.test.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/Components/index.test.js` 2 | 3 | The actual snapshot is saved in `index.test.js.snap`. 4 | 5 | Generated by [AVA](https://ava.li). 6 | 7 | ## Custom script ldjson with children 8 | 9 | > Snapshot 1 10 | 11 |
14 | 15 | ## Renders amp-live-list element 16 | 17 | > Snapshot 1 18 | 19 | 25 | 26 | ## Renders amp-live-list with children 27 | 28 | > Snapshot 1 29 | 30 | 33 |
36 | 37 | -------------------------------------------------------------------------------- /config/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | 4 | "parser": "babel-eslint", 5 | 6 | "plugins": [ 7 | "react", 8 | "jsx-a11y", 9 | "import" 10 | ], 11 | 12 | "env": { 13 | "browser": false, 14 | "node": true, 15 | "es6": true 16 | }, 17 | 18 | "rules": { 19 | "semi": ["error", "never"], 20 | "arrow-parens": ["error", "as-needed"], 21 | "jsx-a11y/href-no-hash": "off", 22 | "react/sort-comp": ["error", { 23 | "order": [ 24 | "lifecycle", 25 | "rendering", 26 | "everything-else", 27 | "static-methods" 28 | ], 29 | "groups": { 30 | "rendering": [ 31 | "render", 32 | "/^render.+$/" 33 | ] 34 | } 35 | }] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/simple-server/server/render.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const { resolve } = require('path') 3 | const app = require('../dist/App').default 4 | const json = require('./data.json') 5 | const mkdirp = require('mkdirp') 6 | 7 | const dest = resolve('./public') 8 | 9 | function onError(err) { 10 | throw err 11 | } 12 | 13 | function renderApp(cb) { 14 | const html = app({ 15 | title: 'test', 16 | date: Date().substring(0, 15), 17 | json, 18 | }) 19 | 20 | fs.writeFile(`${dest}/index.html`, html, err => { 21 | if (err) return onError(err) 22 | 23 | console.log(`\n *** AMP generated in ${dest}\n`) 24 | }) 25 | } 26 | 27 | function createDir(cb) { 28 | mkdirp(dest, err => { 29 | if (err) return onError(err) 30 | return cb() 31 | }) 32 | } 33 | 34 | createDir(renderApp) 35 | -------------------------------------------------------------------------------- /test/Components/Head.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import React from 'react' 3 | import TestRenderer from 'react-test-renderer' 4 | import Head from '../../src/Components/Head' 5 | 6 | 7 | test('Head empty css', t => { 8 | const testRenderer = TestRenderer.create(React.createElement(Head)) 9 | const testInstance = testRenderer.root 10 | t.is(typeof testInstance.props.css, 'string', 'css property should be string') 11 | t.is(testInstance.props.css, '', 'css property defaults empty string') 12 | }) 13 | 14 | test('Head with css', t => { 15 | const testRenderer = TestRenderer.create(React.createElement(Head, { css: '.class {}' })) 16 | const testInstance = testRenderer.root 17 | t.is(typeof testInstance.props.css, 'string', 'css property should be string') 18 | t.is(testInstance.props.css, '.class {}', 'css property should match') 19 | }) 20 | -------------------------------------------------------------------------------- /examples/simple-server/server/index.js: -------------------------------------------------------------------------------- 1 | const liveServer = require('live-server') 2 | 3 | const params = { 4 | port: 1337, // Set the server port. Defaults to 8080. 5 | host: '0.0.0.0', // Set the address to bind to. Defaults to 0.0.0.0 or process.env.IP. 6 | root: '.', // Set root directory that's being served. Defaults to cwd. 7 | open: true, // When false, it won't load your browser by default. 8 | file: './public/index.html', // When set, serve this file (server root relative) for every 404 (useful for single-page applications) 9 | wait: 1000, // Waits for all changes, before reloading. Defaults to 0 sec. 10 | logLevel: 2, // 0 = errors only, 1 = some, 2 = lots 11 | middleware: [function (req, res, next) { next() }], // Takes an array of Connect-compatible middleware that are injected into the server middleware stack 12 | } 13 | liveServer.start(params) 14 | -------------------------------------------------------------------------------- /examples/simple-server/src/Components/SocialShare.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { AMP } from 'react-amp-template' 4 | 5 | 6 | const Container = styled.div` 7 | line-height:36px; 8 | display: flex; 9 | padding-bottom: 16px; 10 | padding-top: 16px; 11 | ` 12 | 13 | const SocialShare = () => ( 14 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | 29 | export default SocialShare 30 | -------------------------------------------------------------------------------- /src/Components/Head.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Consumer } from '../store' 4 | 5 | const buildStyleProps = plainCSS => ({ 6 | dangerouslySetInnerHTML: { __html: plainCSS }, 7 | }) 8 | 9 | const Head = ({ css }) => ( 10 | 11 | {({ store: { state } }) => ( 12 | 13 | 14 | ␊ 16 | title␊ 17 | ␊ 18 | ␊ 19 | ␊ 20 | ␊ 21 | ␊ 22 | ` 23 | -------------------------------------------------------------------------------- /examples/simple-server/server/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://schema.org", 3 | "@type": "LiveBlogPosting", 4 | "url": "https://ampbyexample.com/samples_templates/live_blog/", 5 | "articleBody": "This is the initial text in the blog post", 6 | "datePublished": "2016-09-08T23:04:28.24337", 7 | "about": { 8 | "@type": "Event", 9 | "description": "This is my great live blog sample", 10 | "startDate": "2016-07-23T13:00:00-07:00", 11 | "endDate": "2016-07-23T15:00:00-07:00", 12 | "name": "An AMP Live Blog", 13 | "url": "https://ampbyexample.com/samples_templates/live_blog/", 14 | "location": { 15 | "@type": "EventVenue", 16 | "name": "The Venue Name", 17 | "address" : { 18 | "@type": "PostalAddress", 19 | "streetAddress": "701 Mission St", 20 | "addressLocality": "San Francisco", 21 | "addressRegion": "CA", 22 | "postalCode": "94103", 23 | "addressCountry": "US" 24 | } 25 | } 26 | }, 27 | "publisher": { 28 | "@type": "Organization", 29 | "name": "Google", 30 | "logo": { 31 | "@type": "ImageObject", 32 | "url": "https://ampbyexample.com/img/favicon.png", 33 | "width": "512", 34 | "height": "512" 35 | } 36 | }, 37 | "image": { 38 | "@type": "ImageObject", 39 | "url": "https://ampbyexample.com/img/abe_preview.png", 40 | "height": "1532", 41 | "width": "2046" 42 | }, 43 | "coverageStartTime": "2016-07-23T11:30:00-07:00", 44 | "coverageEndTime": "2016-07-23T16:00:00-07:00", 45 | "headline": "An AMP Live Blog", 46 | "description": "A Live Blog implementation with AMP", 47 | "liveBlogUpdate": [] 48 | } 49 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { renderToStaticMarkup } from 'react-dom/server' 3 | import { ServerStyleSheet } from 'styled-components' 4 | import pretty from 'pretty' 5 | 6 | import store, { setOptions } from './store' 7 | import Components from './Components' 8 | import Head from './Components/Head' 9 | 10 | /** 11 | * Transform React component into HTML AMP format. 12 | * @returns {String} html 13 | * @param {Class|Object} body React component to render 14 | * @param {Object} options Settings 15 | * @property {string} options.cdnURI absolute URL to AMP CDN 16 | * @property {string} options.boilerplate HTML string which contains AMP boilerplate styles 17 | * @property {object} options.extensions Key map of references to specify an extension version 18 | * @property {object} options.extensions.default default version for all amp-extensions e.g '0.1' 19 | * @property {object} options.extensions.extension [extension-name] 20 | ** specify custom version for derived extension e.g: 'amp-sticky-ad': '1.0' 21 | */ 22 | export const renderToString = (body, options = {}) => { 23 | setOptions(options) 24 | const sheet = new ServerStyleSheet() 25 | const bodyStyless = pretty(renderToStaticMarkup(sheet.collectStyles(body))) 26 | const styles = sheet.getStyleElement()[0] 27 | // eslint-disable-next-line no-underscore-dangle 28 | const css = styles ? styles.props.dangerouslySetInnerHTML.__html : '' 29 | const head = pretty(renderToStaticMarkup()) 30 | .replace('', store.boilerplate) 31 | 32 | return `\n\n${head}\n${bodyStyless}\n` 33 | } 34 | 35 | export { Components as AMP } 36 | -------------------------------------------------------------------------------- /coverage.lcov: -------------------------------------------------------------------------------- 1 | TN: 2 | SF:/Users/ariel/react-amp-template/src/index.jsx 3 | FN:22,(anonymous_0) 4 | FNF:1 5 | FNH:1 6 | FNDA:4,(anonymous_0) 7 | DA:22,1 8 | DA:23,4 9 | DA:24,4 10 | DA:25,4 11 | DA:26,4 12 | DA:28,4 13 | DA:29,4 14 | DA:32,4 15 | LF:8 16 | LH:8 17 | BRDA:22,0,0,4 18 | BRDA:28,1,0,1 19 | BRDA:28,1,1,3 20 | BRF:3 21 | BRH:3 22 | end_of_record 23 | TN: 24 | SF:/Users/ariel/react-amp-template/src/Components/Head.jsx 25 | FN:5,(anonymous_0) 26 | FN:7,(anonymous_1) 27 | FNF:2 28 | FNH:2 29 | FNDA:6,(anonymous_0) 30 | FNDA:6,(anonymous_1) 31 | DA:5,2 32 | DA:6,6 33 | DA:8,6 34 | DA:20,2 35 | DA:23,2 36 | LF:5 37 | LH:5 38 | BRF:0 39 | BRH:0 40 | end_of_record 41 | TN: 42 | SF:/Users/ariel/react-amp-template/src/Components/Link.jsx 43 | FN:4,(anonymous_0) 44 | FNF:1 45 | FNH:1 46 | FNDA:3,(anonymous_0) 47 | DA:4,2 48 | DA:5,3 49 | LF:2 50 | LH:2 51 | BRF:0 52 | BRH:0 53 | end_of_record 54 | TN: 55 | SF:/Users/ariel/react-amp-template/src/Components/Meta.jsx 56 | FN:4,(anonymous_0) 57 | FNF:1 58 | FNH:1 59 | FNDA:1,(anonymous_0) 60 | DA:4,2 61 | DA:5,1 62 | LF:2 63 | LH:2 64 | BRF:0 65 | BRH:0 66 | end_of_record 67 | TN: 68 | SF:/Users/ariel/react-amp-template/src/Components/Script.jsx 69 | FN:6,(anonymous_0) 70 | FN:13,(anonymous_1) 71 | FNF:2 72 | FNH:2 73 | FNDA:5,(anonymous_0) 74 | FNDA:5,(anonymous_1) 75 | DA:6,2 76 | DA:12,5 77 | DA:14,5 78 | DA:15,3 79 | DA:16,3 80 | DA:17,3 81 | DA:19,2 82 | DA:20,2 83 | DA:25,2 84 | DA:32,2 85 | LF:10 86 | LH:10 87 | BRDA:14,0,0,3 88 | BRDA:14,0,1,2 89 | BRF:2 90 | BRH:2 91 | end_of_record 92 | TN: 93 | SF:/Users/ariel/react-amp-template/src/Components/Tag.jsx 94 | FN:6,(anonymous_0) 95 | FN:12,(anonymous_1) 96 | FNF:2 97 | FNH:2 98 | FNDA:7,(anonymous_0) 99 | FNDA:7,(anonymous_1) 100 | DA:6,2 101 | DA:11,7 102 | DA:13,7 103 | DA:14,7 104 | DA:19,2 105 | DA:24,2 106 | LF:6 107 | LH:6 108 | BRF:0 109 | BRH:0 110 | end_of_record 111 | TN: 112 | SF:/Users/ariel/react-amp-template/src/Components/Title.jsx 113 | FN:5,(anonymous_0) 114 | FNF:1 115 | FNH:1 116 | FNDA:3,(anonymous_0) 117 | DA:5,2 118 | DA:6,3 119 | DA:8,2 120 | LF:3 121 | LH:3 122 | BRF:0 123 | BRH:0 124 | end_of_record 125 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import test from 'ava' 3 | import { renderToString, AMP } from '../src' 4 | import styled from 'styled-components' 5 | 6 | test('it renders simple node element', async t => { 7 | const body = React.createElement('body', {}) 8 | const output = renderToString(body) 9 | t.regex(output, /<\/body>/, 'Renders HTML template with body element.') 10 | }) 11 | 12 | const renderAllAMPComponents = key => { 13 | const Title = React.createElement(AMP.Title, { key: `title-${key}`}, 'title') 14 | const Link = React.createElement(AMP.Link, { src: 'https://link', key: `link-${key}` }, 'link') 15 | const body = React.createElement('body', {}, [Title, Link]) 16 | return renderToString(body) 17 | } 18 | 19 | test('it renders all AMP node element', t => { 20 | const render1 = renderAllAMPComponents() 21 | const render2 = renderAllAMPComponents() 22 | t.true(render1 === render2, 'Each render should not mix the state between each other.') 23 | t.snapshot(render1) 24 | }) 25 | 26 | 27 | test('RAMPT render with styles', async t => { 28 | const styles = "background: url('https://www.somedomain.com');" 29 | const styledBody = styled.body`${styles}` 30 | const body = React.createElement(styledBody, {}) 31 | const output = renderToString(body) 32 | 33 | const unScapedStyles = styles.replace(/\(/g, '\\(').replace(/\)/g, '\\)') 34 | 35 | t.regex(output, new RegExp(unScapedStyles), 'Renders HTML template with body element and styles.') 36 | }) 37 | 38 | test('RAMPT renderToString multiple calls', async t => { 39 | const stylesFirstRender = 'background: red;' 40 | const stylesLastRender = 'background: blue;' 41 | 42 | const styledBody = styled.body`${stylesFirstRender}` 43 | const body = React.createElement(styledBody, {}) 44 | 45 | const styledBody2 = styled.body`${stylesLastRender}` 46 | const body2 = React.createElement(styledBody2, {}) 47 | 48 | // First render with background: red 49 | renderToString(body) 50 | // 2nd render with background: blue 51 | const output2 = renderToString(body2) 52 | 53 | t.regex(output2, new RegExp(stylesLastRender), 'It should render unique styles from each render.') 54 | t.notRegex(output2, new RegExp(stylesFirstRender), 'It should not mix class styles from each render.') 55 | }) 56 | 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-amp-template", 3 | "version": "4.1.0", 4 | "description": "AMP react server rendering.", 5 | "keywords": [ 6 | "preact", 7 | "react", 8 | "amp", 9 | "server-rendering", 10 | "react-amp", 11 | "amp-react", 12 | "template", 13 | "aphrodite", 14 | "styledcomponent" 15 | ], 16 | "main": "lib/index.js", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/Ariel-Rodriguez/react-amp-template.git" 20 | }, 21 | "scripts": { 22 | "build": "rimraf lib && babel src -d lib -s", 23 | "dev": "nodemon --watch ./src --ext js,jsx --exec 'npm run prepare && npm run build && npm run test'", 24 | "prepare": "npm test", 25 | "prepublishOnly": "NODE_ENV=production && npm run build && echo build using $NODE_ENV env", 26 | "test": "npm run -s lint && BABEL_ENV=test nyc ava", 27 | "test:watch": "npm test -- --watch", 28 | "lint": "eslint --fix --ext .jsx --ext .js src/.", 29 | "report-coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov -t $CODECOV_TOKEN" 30 | }, 31 | "author": "Ariel Fernando Rodriguez", 32 | "license": "Apache-2.0", 33 | "ava": { 34 | "require": [ 35 | "babel-register" 36 | ], 37 | "babel": "inherit" 38 | }, 39 | "babel": { 40 | "extends": "./config/.babelrc" 41 | }, 42 | "eslintConfig": { 43 | "extends": "./config/.eslintrc.json" 44 | }, 45 | "engines": { 46 | "node": ">=8.0.0", 47 | "yarn": ">=1.5.0" 48 | }, 49 | "dependencies": { 50 | "pretty": "^2.0.0", 51 | "prop-types": "^15.6.2", 52 | "react": "^16.6.0", 53 | "react-dom": "^16.6.0", 54 | "styled-components": "^3.4.10" 55 | }, 56 | "devDependencies": { 57 | "ava": "^0.25.0", 58 | "babel-cli": "^6.26.0", 59 | "babel-core": "^6.26.3", 60 | "babel-eslint": "^8.2.6", 61 | "babel-plugin-istanbul": "^5.1.0", 62 | "babel-plugin-styled-components": "^1.8.0", 63 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 64 | "babel-preset-env": "^1.7.0", 65 | "babel-preset-react": "^6.24.1", 66 | "babel-runtime": "^6.26.0", 67 | "debug": "^4.1.1", 68 | "eslint": "^4.19.1", 69 | "eslint-config-airbnb": "^16.1.0", 70 | "eslint-config-airbnb-base": "^12.1.0", 71 | "eslint-plugin-import": "^2.14.0", 72 | "eslint-plugin-jsx-a11y": "^6.1.2", 73 | "eslint-plugin-react": "^7.11.1", 74 | "mkdirp": "^0.5.1", 75 | "nyc": "^11.9.0", 76 | "react-test-renderer": "^16.6.0", 77 | "rimraf": "^2.6.2" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import * as AMP from './constants' 3 | 4 | 5 | /** 6 | * Store type definition 7 | * @typedef {Object} state 8 | * @property {string} cdnURI absolute URL to AMP CDN 9 | * @property {string} boilerplate 10 | * @property {object} extensions Key map of references to specify an extension version 11 | * @property {Array} elements List of runtime generated elements to be appended into Head 12 | */ 13 | const state = { 14 | cdnURI: AMP.CDN, 15 | boilerplate: AMP.BOILERPLATE, 16 | extensions: AMP.EXTENSION_VERSION, 17 | runtimeURI: AMP.RUNTIME, 18 | elements: [], 19 | } 20 | 21 | let hashRegister = [] 22 | 23 | const createElement = (tag, attrs, key) => 24 | React.createElement(tag, { ...attrs, key }) 25 | 26 | /** 27 | * Resolves the source URI for the extension script. 28 | * @returns {String} CDN URI of script extension 29 | * @param {String} extensionName script extension e.g "amp-fit-text" 30 | */ 31 | const getScriptSRC = extensionName => 32 | `${state.cdnURI}/${extensionName}-${state.extensions[extensionName] || state.extensions.default}.js` 33 | 34 | /** 35 | * Updates the store with the given element to be appended into Head. 36 | * @param {String} tag html tag e.g "script" 37 | * @param {Object} attrs html element attributes 38 | */ 39 | const registerElement = (tag, attrs) => { 40 | state.elements.push(createElement(tag, attrs, `${tag}-${state.elements.length}`)) 41 | } 42 | 43 | /** 44 | * Same as registerElement but only updates the store if the element was not registered before. 45 | * It is required for managing script in head. 46 | * It is desired to do not load the same script twice. 47 | * @param {String} tag html tag e.g "script" 48 | * @param {Object} attrs html element attributes 49 | * @param {String} name custom-element name e.g "amp-fit-text" 50 | */ 51 | const registerUniqueElement = (tag, attrs, name) => { 52 | if (hashRegister.indexOf(name) === -1) { 53 | hashRegister.push(name) 54 | state.elements.push(createElement(tag, attrs, name)) 55 | } 56 | } 57 | 58 | const setOptions = ({ 59 | boilerplate, extensions, runtimeURI, cdnURI, 60 | }) => { 61 | state.cdnURI = cdnURI || state.cdnURI 62 | state.boilerplate = boilerplate || state.boilerplate 63 | state.runtimeURI = runtimeURI || state.runtimeURI 64 | state.extensions = { ...state.extensions, ...extensions } 65 | state.elements.length = 0 66 | hashRegister = [] 67 | } 68 | 69 | const { Provider, Consumer } = React.createContext({ 70 | store: { 71 | state, 72 | getScriptSRC, 73 | registerElement, 74 | registerUniqueElement, 75 | }, 76 | }) 77 | 78 | export { 79 | Consumer, 80 | createElement, 81 | registerElement, 82 | registerUniqueElement, 83 | getScriptSRC, 84 | Provider, 85 | setOptions, 86 | } 87 | export default state 88 | -------------------------------------------------------------------------------- /test/Components/Components.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import React from 'react' 3 | import TestRenderer from 'react-test-renderer' 4 | import Components from '../../src/Components' 5 | 6 | test('Script component', async t => { 7 | const Script = TestRenderer.create(React.createElement(Components.Script, { _name: 'amp-test' })) 8 | t.is(Script.root.props._name, 'amp-test') 9 | t.snapshot(Script.toJSON()) 10 | }) 11 | 12 | test('Custom script ldjson with children', async t => { 13 | const Div = React.createElement('div', { id: 'child' }) 14 | const Script = TestRenderer.create(React.createElement(Components.Script, { type: 'application/ld+json' }, Div)) 15 | t.is(Script.root.props.type, 'application/ld+json', 'should identify ld+json type to append in head') 16 | // should return the child element to append in body 17 | t.snapshot(Script.toJSON()) 18 | }) 19 | 20 | test('Renders amp-live-list element', async t => { 21 | const LiveList = TestRenderer.create(React.createElement(Components.LiveList, { 22 | layout: 'container', 23 | 'data-poll-interval': '15000', 24 | 'data-max-items-per-page': '5', 25 | })) 26 | t.snapshot(LiveList.toJSON()) 27 | }) 28 | 29 | test('Renders amp-live-list with children', async t => { 30 | const Div = React.createElement('div', { id: 'child' }) 31 | const LiveList = TestRenderer.create(React.createElement(Components.LiveList, null, Div)) 32 | t.is(LiveList.root.findByType('div').props.id, 'child', 'should render a child with respective props.') 33 | t.snapshot(LiveList.toJSON()) 34 | }) 35 | 36 | test('Custom Tag with children', async t => { 37 | const Div = React.createElement('div', { id: 'child' }) 38 | const Script = TestRenderer.create(React.createElement(Components.Script, { type: 'application/ld+json' }, Div)) 39 | t.is(Script.root.props.type, 'application/ld+json', 'should identify ld+json type to append in head') 40 | // should return the child element to append in body 41 | t.snapshot(Script.toJSON()) 42 | }) 43 | 44 | test('Meta component', async t => { 45 | const Child = React.createElement('div', { id: 'child' }) 46 | const testRenderer = TestRenderer.create(React.createElement(Components.Meta, { content: 'test' }, Child)) 47 | t.is(testRenderer.toJSON(), null, 'Meta tag should return null.') 48 | }) 49 | 50 | test('Link component', async t => { 51 | const Child = React.createElement('div', { id: 'child' }) 52 | const testRenderer = TestRenderer.create(React.createElement(Components.Link, { content: 'test' }, Child)) 53 | t.is(testRenderer.toJSON(), null, 'Link tag should return null.') 54 | }) 55 | 56 | test('Title component', async t => { 57 | const Child = 'A title' 58 | const testRenderer = TestRenderer.create(React.createElement(Components.Title, null, Child)) 59 | t.is(testRenderer.toJSON(), null, 'Title tag should return null.') 60 | }) 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

RAMPT v4

3 |

AMP components aliases and shims for React SSR 16+ & styled-components v3

4 |
5 | 6 |
7 | 8 |
9 | 10 | 11 | 12 | 13 | Build Status 14 | 15 | 16 | npm version 17 | 18 | 19 |
20 | 21 | 22 | Write AMP pages using React syntaxt right the way and style with your preferred style manager 23 | 24 |
25 |
:zap: AMP elements
26 |
Ready to render any AMP component
27 |
:nail_care: Modular CSS
28 |
Style with the power of Styled Components or Aphrodite or Your Own custom StyleManager!
29 |
30 | 31 | 32 | 33 | 34 | ## Contents 35 | 36 | - [Usage](#usage) 37 | - [Demo](#demo) 38 | - [API](#api) 39 | - [Configuration](#configuration) 40 | - [Contribute](#contributing) 41 | 42 | 43 | ## Usage 44 | 45 | ### Install 46 | 47 | - `npm i react-amp-template` 48 | 49 | ### Static Render 50 | 51 | ```javascript 52 | import React, { Fragment } from 'react' 53 | import styled from 'styled-components' 54 | import { renderToString, AMP } from 'react-amp-template' 55 | 56 | const { Title, Link, Carousel } = AMP 57 | 58 | const Body = styled.body` 59 | margin: 0 1rem; 60 | ` 61 | 62 | const App = ({ title }) => ( 63 | 64 | {title} 65 | 66 | 67 |

Hello World

68 | 69 | 70 | 71 | 72 | 73 | 74 |
75 | ) 76 | 77 | export default props => renderToString() 78 | ``` 79 | 80 | 81 | ## Demo 82 | [See complete example here](https://github.com/Ariel-Rodriguez/react-amp-template/tree/master/examples/simple-server) 83 | 84 | 85 | ## API 86 | 87 | ### renderToString 88 | 89 | ```javascript 90 | /** 91 | * Transform React component into HTML AMP format. 92 | * 93 | * @returns {String} html 94 | * @param {Class|Object} body React component to render 95 | * @param {Object} options Settings 96 | * @property {string} options.cdnURI absolute URL to AMP CDN 97 | * @property {string} options.boilerplate HTML string which contains AMP boilerplate styles 98 | * @property {object} options.extensions Key map of references to specify an extension version 99 | * @property {object} options.extensions.default default version for all amp-extensions e.g '0.1' 100 | * @property {object} options.extensions.extension [extension-name] 101 | ** specify custom version for derived extension e.g: 'amp-sticky-ad': '1.0' 102 | import { renderToString } from 'react-amp-template' 103 | ``` 104 | 105 | #### AMP components 106 | 107 | ```javascript 108 | import { AMP } from 'react-amp-template' 109 | 110 | const AdUnit = () => 111 | ``` 112 | - RAMPT provides shorthands for amp-custom-elements. A \[ get \] operation on { AMP } module returns Node element and automatically registers the `