├── .editorconfig
├── .gitignore
├── .npmignore
├── .nvmrc
├── .travis.yml
├── LICENSE.md
├── README.md
├── config
├── .babelrc
└── .eslintrc.json
├── coverage.lcov
├── examples
└── simple-server
│ ├── .babelrc
│ ├── .gitignore
│ ├── README.md
│ ├── docs
│ └── preview.png
│ ├── package-lock.json
│ ├── package.json
│ ├── server
│ ├── assets
│ │ └── bg.png
│ ├── data.json
│ ├── index.js
│ └── render.js
│ └── src
│ ├── App.jsx
│ ├── Components
│ ├── Body.jsx
│ ├── Head.jsx
│ ├── Heading.jsx
│ ├── PostCard.jsx
│ └── SocialShare.jsx
│ └── globalStyles.js
├── package-lock.json
├── package.json
├── src
├── Components
│ ├── Head.jsx
│ ├── Link.jsx
│ ├── Meta.jsx
│ ├── Script.jsx
│ ├── Tag.jsx
│ ├── Title.jsx
│ └── index.js
├── constants.js
├── index.jsx
└── store.js
└── test
├── Components
├── Components.test.js
├── Head.test.js
└── snapshots
│ ├── Components.test.js.md
│ ├── Components.test.js.snap
│ ├── index.test.js.md
│ └── index.test.js.snap
├── index.test.js
└── snapshots
├── index.test.js.md
└── index.test.js.snap
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 6
2 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
14 |
15 |
16 |
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 `` tag required by AMP.
113 |
114 | - The following components could be used in case of need to ad elements into `` element
115 |
116 | ```javascript
117 |
118 | ```
119 |
120 | - By default every amp-script address to version 0.1. However it can be customized.
121 |
122 | ```javascript
123 | renderToString( , {
124 | extensions: {
125 | default: 0.2,
126 | 'amp-sticky-unit': 1.0,
127 | }
128 | })
129 | ```
130 |
131 | #### LD+JSON
132 |
133 | ```javascript
134 |
138 | ```
139 |
140 |
141 | ## Configuration
142 |
143 | ### Babel
144 | - Setup the environment as recomends React and Styled-Components server rendering.
145 |
146 | #### React | Styled Components
147 |
148 | `npm i -D babel-plugin-styled-components babel-preset-react`
149 |
150 | ```json
151 | {
152 | "presets": [
153 | "stage-0",
154 | "react"
155 | ],
156 | "plugins": [
157 | ["babel-plugin-styled-components", { "ssr": true }]
158 | ]
159 | }
160 | ```
161 |
162 |
163 | ## Contributing
164 |
165 | - Fork the repository
166 | - `npm install`
167 | - `npm run dev`
168 | - Create pull request
169 |
170 | ### Build examples
171 |
172 | - `cd examples/simple-server`
173 | - `npm install && npm start`
174 |
175 | ## License
176 |
177 | This project is licensed under the Apache License, Version 2.0. Copyright (c) 2016 Ariel Fernando Rodriguez. For more information see `LICENSE.md`.
178 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/examples/simple-server/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | **/node_modules
3 | public
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/examples/simple-server/docs/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ariel-Rodriguez/react-amp-template/cb3fba4e2dd274383dc4f5dcf89ed903c71df3bc/examples/simple-server/docs/preview.png
--------------------------------------------------------------------------------
/examples/simple-server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rampt-demo",
3 | "version": "1.0.0",
4 | "description": "",
5 | "dependencies": {
6 | "live-server": "^1.2.0",
7 | "react-amp-template": "file:../..",
8 | "react": "^16.6.0",
9 | "react-dom": "^16.6.0",
10 | "styled-components": "^3.4.10"
11 | },
12 | "engines": {
13 | "node": "^8.x.x",
14 | "yarn": "^1.x.x"
15 | },
16 | "devDependencies": {
17 | "babel-cli": "^6.26.0",
18 | "babel-core": "^6.26.0",
19 | "babel-plugin-styled-components": "^1.8.0",
20 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
21 | "babel-preset-env": "^1.6.1",
22 | "babel-preset-react": "^6.24.1"
23 | },
24 | "scripts": {
25 | "start": "npm run build && npm run serve",
26 | "test": "echo \"Error: no test specified\" && exit 1",
27 | "build": "babel src -d dist -s && cd server && node render.js",
28 | "dev": "npm run serve & nodemon --watch ./src --ext js,jsx --exec 'npm run build'",
29 | "serve": "cd server && node ./index.js"
30 | },
31 | "author": "Ariel Rodriguez",
32 | "license": "ISC"
33 | }
34 |
--------------------------------------------------------------------------------
/examples/simple-server/server/assets/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ariel-Rodriguez/react-amp-template/cb3fba4e2dd274383dc4f5dcf89ed903c71df3bc/examples/simple-server/server/assets/bg.png
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
18 |
24 | You have updates
25 |
26 |
31 |
38 |
39 |
40 | )
41 |
42 | export default Body
43 |
--------------------------------------------------------------------------------
/examples/simple-server/src/Components/Head.jsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react'
2 | import { AMP } from 'react-amp-template'
3 |
4 | const {
5 | Title, Tag, Link, Meta, Script,
6 | } = AMP
7 |
8 | const metaContent = {
9 | httpEquiv: 'origin-trial',
10 | 'data-expires': '2020-01-01',
11 | 'data-feature': 'Web Share',
12 | content: 'Ajcrk411RcpUCQ3ovgC8le4e7Te/1kARZsW5Hd/OCnW6vIHTs5Kcq1PaABs7SzcrtfvT0TIlFh9Vdb5xWi9LiQsAAABSeyJvcmlnaW4iOiJodHRwczovL2FtcGJ5ZXhhbXBsZS5jb206NDQzIiwiZmVhdHVyZSI6IldlYlNoYXJlIiwiZXhwaXJ5IjoxNDkxMzM3MDEwfQ==',
13 | }
14 |
15 | const Head = ({ title, json }) => (
16 |
17 | {title}
18 |
19 |
20 |
21 |
25 |
26 | )
27 |
28 | export default Head
29 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/examples/simple-server/src/Components/PostCard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import { AMP } from 'react-amp-template'
4 | import SocialShare from './SocialShare'
5 |
6 | const Container = styled.div`
7 | padding: 0;
8 | min-width: 70%;
9 | background: #fff;
10 | `
11 |
12 | const Title = styled.h4`
13 | color: white;
14 | position: relative;
15 | top: -48px;
16 | margin: 0 16px;
17 | height: 0;
18 | padding-bottom: 16px;
19 | `
20 |
21 | const Text = styled.h4`
22 | color: black;
23 | padding-top: 0;
24 | `
25 | const Date = styled.p`
26 | color: #a8a3ae;
27 | font-size: 12px
28 | `
29 |
30 | const PostCard = () => (
31 |
32 |
38 | Green landscape
39 | 00:21:45
40 | A green landscape with trees.
41 |
42 |
43 | )
44 |
45 | export default PostCard
46 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
15 | {state.elements}
16 |
17 | {/** eslint-disable-next-line react/no-danger */}
18 |
19 |
20 | )
21 | }
22 |
23 | )
24 |
25 | Head.defaultProps = {
26 | css: '',
27 | }
28 | Head.propTypes = {
29 | css: PropTypes.string,
30 | }
31 |
32 | export default Head
33 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/Components/Script.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Consumer } from '../store'
4 |
5 |
6 | const Script = ({
7 | _name: name,
8 | _version: version,
9 | children,
10 | ...props
11 | }) => (
12 |
13 | {({ store }) => {
14 | if (name) {
15 | const attrs = { async: true, 'custom-element': name, src: store.getScriptSRC(name) }
16 | store.registerUniqueElement('script', attrs, name)
17 | return React.createElement(name, props, children)
18 | }
19 | store.registerElement('script', props)
20 | return children
21 | }}
22 |
23 | )
24 |
25 | Script.defaultProps = {
26 | _name: '',
27 | _version: '',
28 | children: null,
29 | src: null,
30 | }
31 |
32 | Script.propTypes = {
33 | _name: PropTypes.string,
34 | _version: PropTypes.string,
35 | children: PropTypes.node,
36 | src: PropTypes.string,
37 | }
38 |
39 | export default Script
40 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/Components/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Link from './Link'
3 | import Meta from './Meta'
4 | import Script from './Script'
5 | import Tag from './Tag'
6 | import Title from './Title'
7 |
8 | /**
9 | * Transforms input to hyphened lowercase for each upper case coincidence.
10 | * @param {String} str React custom-element name e.g StickyAd
11 | * @returns {String} the input as kebabCase with leaded by a hyphen e.g. sticky-ad
12 | */
13 | const toKebabCase = str =>
14 | str.replace(/([A-Z])/g, '-$1').slice(1).toLowerCase()
15 |
16 | function derivatedScriptComponent(name) {
17 | if (!/^[A-Z]/.test(name)) {
18 | throw Error(`Failed to fallback component name ${name}. Component to import must begin with uppercase.`)
19 | }
20 | return function scriptComponent(props) {
21 | return React.createElement(Script, {
22 | _name: `amp-${toKebabCase(name)}`,
23 | ...props,
24 | })
25 | }
26 | }
27 |
28 | /**
29 | * @description Exposes a factory to generate AMP components in runtime.
30 | * @returns {Object} React component
31 | */
32 | export default new Proxy({
33 | Link, Meta, Script, Tag, Title, toKebabCase,
34 | }, {
35 | get(thisModule, componentName) {
36 | return thisModule[componentName] || derivatedScriptComponent(componentName)
37 | },
38 | })
39 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | export const CDN = 'https://cdn.ampproject.org/v0'
2 | export const RUNTIME = `${CDN}.js`
3 | export const BOILERPLATE = ' '
4 | export const EXTENSION_VERSION = {
5 | default: '0.1',
6 | 'amp-sticky-ad': '1.0',
7 | }
8 |
--------------------------------------------------------------------------------
/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(<\/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 |
--------------------------------------------------------------------------------
/test/snapshots/index.test.js.md:
--------------------------------------------------------------------------------
1 | # Snapshot report for `test/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 | ## it renders all AMP node element
8 |
9 | > Snapshot 1
10 |
11 | `␊
12 | ␊
13 | ␊
14 | ␊
15 | ␊
16 | title ␊
17 | ␊
18 | ␊
19 | ␊
20 | ␊
21 | ␊
22 | `
23 |
--------------------------------------------------------------------------------
/test/snapshots/index.test.js.snap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ariel-Rodriguez/react-amp-template/cb3fba4e2dd274383dc4f5dcf89ed903c71df3bc/test/snapshots/index.test.js.snap
--------------------------------------------------------------------------------