├── .babelrc.js ├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── README.md ├── codegen ├── componentCode │ ├── index.ts │ ├── lib │ │ ├── camelCase.ts │ │ └── propsCodeReducer.ts │ ├── preamble.tsx │ └── templates │ │ ├── componentOverrideTemplate.ts │ │ ├── componentTemplate.ts │ │ └── mandatoryComponentOverrideTemplate.ts ├── constants.ts ├── duplicateWrapperComponentCode.ts ├── index.ts ├── rules.ts └── tagNameToComponentName.ts ├── helpers.d.ts ├── helpers.js ├── index.d.ts ├── index.js ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── setup.d.ts ├── setup.js ├── setupTest.ts ├── src ├── __tests__ │ ├── __snapshots__ │ │ └── react-amphtml.spec.tsx.snap │ └── react-amphtml.spec.tsx ├── amphtml │ └── components │ │ ├── AmpState.tsx │ │ ├── Html.tsx │ │ ├── Script.tsx │ │ ├── __tests__ │ │ ├── AmpState.spec.tsx │ │ ├── Html.spec.tsx │ │ └── Script.spec.tsx │ │ └── jsx.d.ts ├── constants.ts ├── helpers │ ├── Action.ts │ ├── Bind.ts │ └── helpers.ts ├── lib │ ├── __tests__ │ │ └── getScriptSource.spec.ts │ ├── contextHelper.ts │ └── getScriptSource.ts └── setup │ ├── AmpScripts.tsx │ ├── AmpScriptsManager.tsx │ ├── headerBoilerplate.tsx │ └── setup.ts ├── tsconfig.declarations.json └── tsconfig.json /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-typescript', 4 | [ 5 | '@babel/preset-env', 6 | { 7 | targets: { 8 | node: '8', 9 | }, 10 | }, 11 | ], 12 | '@babel/preset-react', 13 | ], 14 | plugins: [ 15 | '@babel/plugin-proposal-export-default-from', 16 | 'babel-plugin-codegen', 17 | ], 18 | }; 19 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:8.10.0 6 | working_directory: ~/react-amphtml 7 | steps: 8 | - checkout 9 | - restore_cache: 10 | keys: 11 | - v1-node-{{ arch }}-{{ .Branch }}-{{ checksum "package-lock.json" }} 12 | - v1-node-{{ arch }}-{{ .Branch }}- 13 | - v1-node-{{ arch }}- 14 | - run: npm install 15 | - run: npm run codegen 16 | - run: npm run build 17 | - run: npm run typecheck 18 | - run: npm run lint 19 | - run: npm run test 20 | - save_cache: 21 | key: v1-node-{{ arch }}-{{ .Branch }}-{{ checksum "package-lock.json" }} 22 | paths: 23 | - node_modules 24 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /dist 3 | /src/amphtml/amphtml.tsx 4 | !.eslintrc.js 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const typescriptEslintRecommended = require('@typescript-eslint/eslint-plugin/dist/configs/recommended.json'); 3 | const typescriptEslintPrettier = require('eslint-config-prettier/@typescript-eslint'); 4 | 5 | module.exports = { 6 | env: { 7 | node: true, 8 | }, 9 | parser: 'babel-eslint', 10 | extends: [ 11 | 'airbnb', 12 | 'plugin:@typescript-eslint/recommended', 13 | 'prettier', 14 | 'prettier/react', 15 | 'prettier/@typescript-eslint', 16 | ], 17 | plugins: ['prettier'], 18 | rules: { 19 | 'prettier/prettier': 'error', 20 | 'react/jsx-filename-extension': 'off', 21 | 'import/no-extraneous-dependencies': [ 22 | 'error', 23 | { 24 | devDependencies: [ 25 | '.eslintrc.js', 26 | 'rollup.config.js', 27 | '**/__tests__/**/*', 28 | 'setupTest.ts', 29 | 'codegen/**/*', 30 | ], 31 | }, 32 | ], 33 | '@typescript-eslint/no-unused-vars': [ 34 | 'error', 35 | { 36 | devDependencies: [ 37 | 'rollup.config.js', 38 | '.eslintrc.js', 39 | '**/__tests__/**/*', 40 | ], 41 | }, 42 | ], 43 | }, 44 | settings: { 45 | 'import/resolver': { 46 | node: { 47 | extensions: ['.js', '.ts', '.tsx'], 48 | }, 49 | }, 50 | }, 51 | overrides: [ 52 | { 53 | files: ['*.ts', '*.tsx'], 54 | parser: '@typescript-eslint/parser', 55 | // NOTE: Workaround for no nested extends possible. 56 | // See https://github.com/eslint/eslint/issues/8813. 57 | // Working solution would be following, if we had nested extends: 58 | // ``` 59 | // extends: [ 60 | // 'airbnb-base', 61 | // 'plugin:@typescript-eslint/recommended', 62 | // 'prettier/@typescript-eslint', 63 | // 'prettier', 64 | // ], 65 | // ``` 66 | plugins: ['@typescript-eslint', 'prettier'], 67 | rules: Object.assign( 68 | typescriptEslintRecommended.rules, 69 | typescriptEslintPrettier.rules, 70 | { 71 | '@typescript-eslint/explicit-function-return-type': 'error', 72 | }, 73 | ), 74 | }, 75 | { 76 | files: [ 77 | 'setupTest.js', 78 | 'setupTest.ts', 79 | '*.spec.js', 80 | '*.spec.ts', 81 | '*.spec.tsx', 82 | ], 83 | env: { 84 | jest: true, 85 | }, 86 | }, 87 | ], 88 | }; 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules 3 | /dist 4 | /coverage 5 | /src/amphtml/amphtml.tsx 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /dist 3 | !.eslintrc.js 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'all', 3 | singleQuote: true, 4 | }; 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-amphtml 2 | 3 | Use [`amphtml`][amp repo] components inside your React apps easily! 4 | 5 | ## Usage 6 | 7 | `react-amphtml` exports React components and functions to easily create AMP HTML 8 | pages. Each exported React component has a TypeScript interface and PropTypes 9 | derived from AMP HTML's own validator rules to speed up development and make it 10 | safer. Boilerplate and the inclusion of AMP directive-specific scripts is all 11 | handled for you! 12 | 13 | ```js 14 | // All AMP elements 15 | import * as Amp from 'react-amphtml'; 16 | 17 | // Helper render props for actions and bindings 18 | import * as AmpHelpers from 'react-amphtml/helpers'; 19 | 20 | // Components and functions to render pages 21 | import { 22 | AmpScripts, 23 | AmpScriptsManager, 24 | headerBoilerplate, 25 | } from 'react-amphtml/setup'; 26 | ``` 27 | 28 | ### Amp Components 29 | 30 | ```js 31 | import * as Amp from 'react-amphtml'; 32 | // ... 33 | 34 | ``` 35 | 36 | The main file exported by `react-amphtml` contains all of the AMP HTML 37 | directives as React components. This includes the custom element `amp-*` 38 | directives, normal HTML directives with validations required by AMP, and some 39 | components with added functionality: `Html`, `AmpState` (`amp-state` directive) 40 | and `Script`. 41 | 42 | To see a list of available components and their relative documentation see the 43 | official AMP components documentation: [The AMP component catalogue][]. 44 | 45 | [The AMP component catalogue]: https://amp.dev/documentation/components/ 46 | 47 | ### Amp Helpers 48 | 49 | ```js 50 | import * as Amp from 'react-amphtml'; 51 | import * as AmpHelpers from 'react-amphtml/helpers'; 52 | 53 | // Example of attaching actions to elements 54 | 55 | {(props) => ( 56 | 59 | )} 60 | 61 | 62 | // Example of using state and bindings together 63 | const defaultHeading = { 64 | text: 'Hello, World!', 65 | }; 66 | // ... 67 | 68 | {defaultHeading} 69 | 70 | 71 | {(props): ReactElement =>

{defaultHeading.text}

} 72 |
73 | ``` 74 | 75 | The `helpers` file contains render prop components that help add AMP attribute 76 | directives for actions and bindings. Wondering what actions and bindings are all 77 | about? Check out these official guides on the subjects: 78 | 79 | * [Actions and events][] 80 | * [Create interactive AMP pages][] 81 | 82 | [Create interactive AMP pages]: https://amp.dev/documentation/guides-and-tutorials/develop/interactivity/ 83 | [Actions and events]: https://amp.dev/documentation/guides-and-tutorials/learn/amp-actions-and-events 84 | 85 | ### Amp Setup 86 | 87 | ```js 88 | import * as Amp from 'react-amphtml'; 89 | import { 90 | AmpScripts, 91 | AmpScriptsManager, 92 | headerBoilerplate, 93 | } from 'react-amphtml/setup'; 94 | 95 | const ampScripts = new AmpScripts(); 96 | 97 | const bodyContent = renderToStaticMarkup( 98 | 99 |
100 | 108 | 109 |
110 |
, 111 | ); 112 | 113 | /* eslint-disable react/no-danger */ 114 | const html = renderToStaticMarkup( 115 | 116 | 117 | {headerBoilerplate('/')} 118 | react-amphtml 119 | {ampScripts.getScriptElements()} 120 | 121 | 122 | , 123 | ); 124 | /* eslint-enable */ 125 | 126 | const htmlPage = ` 127 | 128 | ${html} 129 | `; 130 | ``` 131 | 132 | The `setup` file makes creating pages for AMP HTML a breeze. It helps insert all 133 | the necessary boilerplate and also the scripts needed for AMP directives. 134 | 135 | The code is based on the requirements from AMP documented in 136 | [Create your AMP HTML page: Required mark-up][]. 137 | 138 | [Create your AMP HTML page: Required mark-up]: https://amp.dev/documentation/guides-and-tutorials/start/create/basic_markup#required-mark-up 139 | 140 | ## Examples 141 | 142 | ### Full Example 143 | 144 | **Go checkout [`ampreact`][]!** 145 | 146 | If you are looking for an example that is in combination with one or more of 147 | these tools: 148 | 149 | * [AMP HTML][] 150 | * [Next.js][] 151 | * [React][] 152 | * [styled-components][] 153 | * [GraphQL][] 154 | * [TypeScript][] 155 | 156 | [`ampreact`][] gives a very nice setup to get started with or learn from! 157 | 158 | [AMP HTML]: https://github.com/ampproject/amphtml/ 159 | [Next.js]: https://github.com/zeit/next.js/ 160 | [React]: https://github.com/facebook/react/ 161 | [styled-components]: https://github.com/styled-components/styled-components/ 162 | [GraphQL]: https://github.com/graphql/graphql-js 163 | [TypeScript]: https://github.com/microsoft/TypeScript 164 | [`ampreact`]: https://github.com/dfrankland/ampreact 165 | 166 | ### Simple Example 167 | 168 | For simple usage examples of `react-amphtml`, check the Jest unit tests in 169 | [`react-amphtml/src/__tests__/react-amphtml.spec.tsx`][]. The best test to look 170 | at is `can server-side render valid html` for a good complete usage of 171 | `react-amphtml`. 172 | 173 | [`react-amphtml/src/__tests__/react-amphtml.spec.tsx`]: https://github.com/dfrankland/react-amphtml/blob/master/src/__tests__/react-amphtml.spec.tsx 174 | 175 | ## Development 176 | 177 | ### About 178 | 179 | The code for `react-amphtml` is generated from [AMP HTML's own validator][] via 180 | [`amphtml-validator-rules`][]. 181 | 182 | Want to learn about AMP HTML validation? See the guide: [Validate AMP pages][]. 183 | 184 | Need to run the validator? Use either the online tool [The AMP Validator][] or 185 | the npm package [`amphtml-validator`][]. 186 | 187 | [AMP HTML's own validator]: https://amp.dev/documentation/guides-and-tutorials/learn/validation-workflow/validate_amp 188 | [Validate AMP pages]: https://github.com/ampproject/amphtml/tree/master/validator#amp-html--validator 189 | [The AMP Validator]: https://validator.ampproject.org/ 190 | [`amphtml-validator`]: https://www.npmjs.com/package/amphtml-validator 191 | 192 | ### Commands 193 | 194 | Use the following commands to develop on `react-amphtml`. 195 | 196 | * `npm run codegen`: Create components based on AMP HTML's validator. This 197 | must be done at least once prior to running `npm run build`, and can be done 198 | afterwards anytime code in `codegen` is modified. 199 | 200 | * `npm run build`: Bundles the source files into `dist`. 201 | 202 | * `npm run typecheck`: Uses TypeScript to ensure type safety. Should be run 203 | after running `npm run build` to check the files in `dist` that are bundled. 204 | 205 | * `npm run lint`: Use ESLint to check source files. 206 | 207 | * `npm run test`: Use Jest to run tests. 208 | 209 | ## Resources 210 | 211 | - [`amphtml-validator-rules`][]: the rules that get used to generate 212 | components 213 | 214 | - AMP Project's [`amphtml` repo][amp repo] 215 | 216 | - [Builtins][] 217 | 218 | - [Extensions][] 219 | 220 | [`amphtml-validator-rules`]: https://github.com/dfrankland/amphtml-validator-rules 221 | [Builtins]: https://github.com/ampproject/amphtml/tree/master/builtins 222 | [Extensions]: https://github.com/ampproject/amphtml/tree/master/extensions 223 | 224 | [amp repo]: https://github.com/ampproject/amphtml 225 | -------------------------------------------------------------------------------- /codegen/componentCode/index.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { resolve as resolvePath } from 'path'; 3 | import newRules, { NewTag } from '../rules'; 4 | import { 5 | MANDATORY_COMPONENT_OVERRIDES, 6 | COMPONENT_OVERRIDES, 7 | BLACKLIST, 8 | } from '../constants'; 9 | import tagNameToComponentName from '../tagNameToComponentName'; 10 | import propsCodeReducer from './lib/propsCodeReducer'; 11 | import mandatoryComponentOverrideTemplate from './templates/mandatoryComponentOverrideTemplate'; 12 | import componentOverrideTemplate from './templates/componentOverrideTemplate'; 13 | import componentTemplate from './templates/componentTemplate'; 14 | 15 | const EXTENSION_TYPE_CUSTOM_TEMPLATE = 'CUSTOM_TEMPLATE'; 16 | 17 | export default newRules.tags.reduce( 18 | ( 19 | code: string, 20 | { 21 | tagName, 22 | dupeName, 23 | attrs, 24 | attrLists, 25 | requiresExtension, 26 | extensionSpec, 27 | mandatoryAncestorSuggestedAlternative, 28 | }: NewTag, 29 | ): string => { 30 | if (BLACKLIST[tagName] || mandatoryAncestorSuggestedAlternative) 31 | return code; 32 | 33 | const componentName = tagNameToComponentName(dupeName || tagName); 34 | 35 | if (MANDATORY_COMPONENT_OVERRIDES[tagNameToComponentName(tagName)]) { 36 | return mandatoryComponentOverrideTemplate({ 37 | code, 38 | tagName, 39 | componentName, 40 | }); 41 | } 42 | 43 | const propsCode = propsCodeReducer({ tagName, attrs, attrLists }); 44 | 45 | const requiresExtensionContext = (Array.isArray(requiresExtension) 46 | ? requiresExtension 47 | : [] 48 | ).reduce( 49 | (requiresExtensionContextCode, requiredExtension): string => ` 50 | ${requiresExtensionContextCode} 51 | contextHelper({ context, extension: '${requiredExtension}', version: props.version }); 52 | `, 53 | '', 54 | ); 55 | 56 | const [extensionPropsGiven, extensionProps] = 57 | extensionSpec && typeof extensionSpec === 'object' 58 | ? [ 59 | true, 60 | { 61 | extension: extensionSpec.name, 62 | isCustomTemplate: 63 | extensionSpec.extensionType === EXTENSION_TYPE_CUSTOM_TEMPLATE, 64 | }, 65 | ] 66 | : [false, {}]; 67 | 68 | const contextArgument = requiresExtensionContext 69 | ? ', context: AmpScriptsManagerContext' 70 | : ''; 71 | 72 | if (COMPONENT_OVERRIDES[tagNameToComponentName(tagName)]) { 73 | return componentOverrideTemplate({ 74 | code, 75 | tagName, 76 | componentName, 77 | dupeName, 78 | extensionSpec, 79 | extensionPropsGiven, 80 | extensionProps, 81 | requiresExtensionContext, 82 | contextArgument, 83 | propsCode, 84 | }); 85 | } 86 | 87 | return componentTemplate({ 88 | code, 89 | tagName, 90 | componentName, 91 | dupeName, 92 | requiresExtensionContext, 93 | contextArgument, 94 | propsCode, 95 | }); 96 | }, 97 | readFileSync(resolvePath(__dirname, './preamble.tsx')).toString('utf8'), 98 | ); 99 | -------------------------------------------------------------------------------- /codegen/componentCode/lib/camelCase.ts: -------------------------------------------------------------------------------- 1 | export default (tagName: string): string => 2 | tagName.toLowerCase().replace(/-(.)/, (_, m): string => m.toUpperCase()); 3 | -------------------------------------------------------------------------------- /codegen/componentCode/lib/propsCodeReducer.ts: -------------------------------------------------------------------------------- 1 | import { TagAttr, AttrListAttr } from 'amphtml-validator-rules'; 2 | import newRules, { NewTag } from '../../rules'; 3 | import camelCase from './camelCase'; 4 | import { JSX_INTRINSICS } from '../../constants'; 5 | 6 | export interface PropsCode { 7 | propTypesCode: { [key: string]: string }; 8 | defaultPropsCode: { [key: string]: string }; 9 | propsInterfaceCode: { [key: string]: string }; 10 | } 11 | 12 | interface TypeProp { 13 | propertyName: string; 14 | type: 'string' | 'boolean'; 15 | propType: string; 16 | interfaceProperty: string; 17 | defaultProp?: string; 18 | } 19 | 20 | export default ({ 21 | tagName, 22 | attrs, 23 | attrLists, 24 | }: Pick): PropsCode => 25 | [ 26 | ...(attrs || []), 27 | ...(attrLists || []).reduce( 28 | ( 29 | allAttrFromLists: AttrListAttr[], 30 | attrListName: string, 31 | ): AttrListAttr[] => [ 32 | ...allAttrFromLists, 33 | ...( 34 | newRules.attrLists.find( 35 | ({ name }): boolean => name === attrListName, 36 | ) || { attrs: [] } 37 | ).attrs, 38 | ], 39 | [], 40 | ), 41 | ].reduce( 42 | ( 43 | { propTypesCode, defaultPropsCode, propsInterfaceCode }, 44 | attr: TagAttr | AttrListAttr, 45 | ): PropsCode => { 46 | const { 47 | name, 48 | value, 49 | // TODO: Use these as well 50 | // valueCasei, 51 | // blacklistedValueRegex, 52 | // valueUrl, 53 | // valueRegex, 54 | // valueProperties, 55 | // valueRegexCasei, 56 | mandatory: mandatoryAttr, 57 | } = attr; 58 | 59 | const isCustomElement = !JSX_INTRINSICS[camelCase(tagName)]; 60 | if ((isCustomElement && name === 'style') || name === 'version') { 61 | return { propTypesCode, defaultPropsCode, propsInterfaceCode }; 62 | } 63 | 64 | const type = ((): TypeProp => { 65 | const propertyName = JSON.stringify(name); 66 | const mandatoryPropType = mandatoryAttr ? '.isRequired' : ''; 67 | const mandatoryType = mandatoryAttr ? '' : ' | undefined'; 68 | if (!value) { 69 | return { 70 | propertyName, 71 | type: 'string', 72 | interfaceProperty: `string${mandatoryType}`, 73 | propType: `PropTypes.string${mandatoryPropType}`, 74 | }; 75 | } 76 | 77 | if (value.length === 1 && value[0] === '') { 78 | return { 79 | propertyName, 80 | type: 'boolean', 81 | interfaceProperty: `boolean${mandatoryType}`, 82 | propType: `PropTypes.bool${mandatoryPropType}`, 83 | defaultProp: JSON.stringify(false), 84 | }; 85 | } 86 | 87 | const [firstValue] = value; 88 | return { 89 | propertyName, 90 | type: 'string', 91 | interfaceProperty: `'${value.join("' | '")}'${mandatoryType}`, 92 | propType: `PropTypes.oneOf<'${value.join("' | '")}'>(${JSON.stringify( 93 | value, 94 | )})${mandatoryPropType}`, 95 | defaultProp: JSON.stringify(firstValue), 96 | }; 97 | })(); 98 | 99 | const newPropTypesCode = { 100 | ...propTypesCode, 101 | [type.propertyName]: type.propType, 102 | }; 103 | 104 | const newDefaultPropsCode = 105 | mandatoryAttr || !type.defaultProp 106 | ? defaultPropsCode 107 | : { 108 | ...defaultPropsCode, 109 | [type.propertyName]: type.defaultProp, 110 | }; 111 | 112 | const newPropsInterfaceCode = { 113 | ...propsInterfaceCode, 114 | [type.propertyName]: type.interfaceProperty, 115 | }; 116 | 117 | return { 118 | propTypesCode: newPropTypesCode, 119 | defaultPropsCode: newDefaultPropsCode, 120 | propsInterfaceCode: newPropsInterfaceCode, 121 | }; 122 | }, 123 | { 124 | propTypesCode: {}, 125 | defaultPropsCode: {}, 126 | propsInterfaceCode: {}, 127 | }, 128 | ); 129 | -------------------------------------------------------------------------------- /codegen/componentCode/preamble.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars, import/no-unresolved, import/extensions */ 2 | 3 | // TODO: Remove `{ Component }` when Rollup fixes its code splitting. 4 | // Currently, this fixes an `React__default is undefined` error. 5 | // @ts-ignore 6 | import React, { Component, ReactNode } from 'react'; 7 | // @ts-ignore 8 | import PropTypes from 'prop-types'; 9 | 10 | // These are relative to the `src/amphtml/amphtml.js` file 11 | // @ts-ignore 12 | import { CONTEXT_KEY } from '../constants'; 13 | // @ts-ignore 14 | import contextHelper from '../lib/contextHelper'; 15 | // @ts-ignore 16 | import { AmpScriptsManagerContext } from '../setup/AmpScriptsManager'; 17 | 18 | // @ts-ignore 19 | type Omit = Pick>; 20 | 21 | // @ts-ignore 22 | const REACT_AMPHTML_CONTEXT = { 23 | [CONTEXT_KEY]: PropTypes.shape({ 24 | addExtension: PropTypes.func.isRequired, 25 | }), 26 | }; 27 | 28 | // The following were all copied from the global JSX IntrinsicElements 29 | // interface. 30 | // The properties of the interface were copied here and then had a regex replace 31 | // run over all of them to make them into actual types: 32 | // 33 | // Find: ^"?([a-z].*?)"?:.*; 34 | // 35 | // Replace: // @ts-ignore\nexport type JSXIntrinsicElements$1 = JSX.IntrinsicElements['$1']; 36 | 37 | // HTML 38 | // @ts-ignore 39 | export type JSXIntrinsicElementsa = JSX.IntrinsicElements['a']; 40 | // @ts-ignore 41 | export type JSXIntrinsicElementsabbr = JSX.IntrinsicElements['abbr']; 42 | // @ts-ignore 43 | export type JSXIntrinsicElementsaddress = JSX.IntrinsicElements['address']; 44 | // @ts-ignore 45 | export type JSXIntrinsicElementsarea = JSX.IntrinsicElements['area']; 46 | // @ts-ignore 47 | export type JSXIntrinsicElementsarticle = JSX.IntrinsicElements['article']; 48 | // @ts-ignore 49 | export type JSXIntrinsicElementsaside = JSX.IntrinsicElements['aside']; 50 | // @ts-ignore 51 | export type JSXIntrinsicElementsaudio = JSX.IntrinsicElements['audio']; 52 | // @ts-ignore 53 | export type JSXIntrinsicElementsb = JSX.IntrinsicElements['b']; 54 | // @ts-ignore 55 | export type JSXIntrinsicElementsbase = JSX.IntrinsicElements['base']; 56 | // @ts-ignore 57 | export type JSXIntrinsicElementsbdi = JSX.IntrinsicElements['bdi']; 58 | // @ts-ignore 59 | export type JSXIntrinsicElementsbdo = JSX.IntrinsicElements['bdo']; 60 | // @ts-ignore 61 | export type JSXIntrinsicElementsbig = JSX.IntrinsicElements['big']; 62 | // @ts-ignore 63 | export type JSXIntrinsicElementsblockquote = JSX.IntrinsicElements['blockquote']; 64 | // @ts-ignore 65 | export type JSXIntrinsicElementsbody = JSX.IntrinsicElements['body']; 66 | // @ts-ignore 67 | export type JSXIntrinsicElementsbr = JSX.IntrinsicElements['br']; 68 | // @ts-ignore 69 | export type JSXIntrinsicElementsbutton = JSX.IntrinsicElements['button']; 70 | // @ts-ignore 71 | export type JSXIntrinsicElementscanvas = JSX.IntrinsicElements['canvas']; 72 | // @ts-ignore 73 | export type JSXIntrinsicElementscaption = JSX.IntrinsicElements['caption']; 74 | // @ts-ignore 75 | export type JSXIntrinsicElementscite = JSX.IntrinsicElements['cite']; 76 | // @ts-ignore 77 | export type JSXIntrinsicElementscode = JSX.IntrinsicElements['code']; 78 | // @ts-ignore 79 | export type JSXIntrinsicElementscol = JSX.IntrinsicElements['col']; 80 | // @ts-ignore 81 | export type JSXIntrinsicElementscolgroup = JSX.IntrinsicElements['colgroup']; 82 | // @ts-ignore 83 | export type JSXIntrinsicElementsdata = JSX.IntrinsicElements['data']; 84 | // @ts-ignore 85 | export type JSXIntrinsicElementsdatalist = JSX.IntrinsicElements['datalist']; 86 | // @ts-ignore 87 | export type JSXIntrinsicElementsdd = JSX.IntrinsicElements['dd']; 88 | // @ts-ignore 89 | export type JSXIntrinsicElementsdel = JSX.IntrinsicElements['del']; 90 | // @ts-ignore 91 | export type JSXIntrinsicElementsdetails = JSX.IntrinsicElements['details']; 92 | // @ts-ignore 93 | export type JSXIntrinsicElementsdfn = JSX.IntrinsicElements['dfn']; 94 | // @ts-ignore 95 | export type JSXIntrinsicElementsdialog = JSX.IntrinsicElements['dialog']; 96 | // @ts-ignore 97 | export type JSXIntrinsicElementsdiv = JSX.IntrinsicElements['div']; 98 | // @ts-ignore 99 | export type JSXIntrinsicElementsdl = JSX.IntrinsicElements['dl']; 100 | // @ts-ignore 101 | export type JSXIntrinsicElementsdt = JSX.IntrinsicElements['dt']; 102 | // @ts-ignore 103 | export type JSXIntrinsicElementsem = JSX.IntrinsicElements['em']; 104 | // @ts-ignore 105 | export type JSXIntrinsicElementsembed = JSX.IntrinsicElements['embed']; 106 | // @ts-ignore 107 | export type JSXIntrinsicElementsfieldset = JSX.IntrinsicElements['fieldset']; 108 | // @ts-ignore 109 | export type JSXIntrinsicElementsfigcaption = JSX.IntrinsicElements['figcaption']; 110 | // @ts-ignore 111 | export type JSXIntrinsicElementsfigure = JSX.IntrinsicElements['figure']; 112 | // @ts-ignore 113 | export type JSXIntrinsicElementsfooter = JSX.IntrinsicElements['footer']; 114 | // @ts-ignore 115 | export type JSXIntrinsicElementsform = JSX.IntrinsicElements['form']; 116 | // @ts-ignore 117 | export type JSXIntrinsicElementsh1 = JSX.IntrinsicElements['h1']; 118 | // @ts-ignore 119 | export type JSXIntrinsicElementsh2 = JSX.IntrinsicElements['h2']; 120 | // @ts-ignore 121 | export type JSXIntrinsicElementsh3 = JSX.IntrinsicElements['h3']; 122 | // @ts-ignore 123 | export type JSXIntrinsicElementsh4 = JSX.IntrinsicElements['h4']; 124 | // @ts-ignore 125 | export type JSXIntrinsicElementsh5 = JSX.IntrinsicElements['h5']; 126 | // @ts-ignore 127 | export type JSXIntrinsicElementsh6 = JSX.IntrinsicElements['h6']; 128 | // @ts-ignore 129 | export type JSXIntrinsicElementshead = JSX.IntrinsicElements['head']; 130 | // @ts-ignore 131 | export type JSXIntrinsicElementsheader = JSX.IntrinsicElements['header']; 132 | // @ts-ignore 133 | export type JSXIntrinsicElementshgroup = JSX.IntrinsicElements['hgroup']; 134 | // @ts-ignore 135 | export type JSXIntrinsicElementshr = JSX.IntrinsicElements['hr']; 136 | // @ts-ignore 137 | export type JSXIntrinsicElementshtml = JSX.IntrinsicElements['html']; 138 | // @ts-ignore 139 | export type JSXIntrinsicElementsi = JSX.IntrinsicElements['i']; 140 | // @ts-ignore 141 | export type JSXIntrinsicElementsiframe = JSX.IntrinsicElements['iframe']; 142 | // @ts-ignore 143 | export type JSXIntrinsicElementsimg = JSX.IntrinsicElements['img']; 144 | // @ts-ignore 145 | export type JSXIntrinsicElementsinput = JSX.IntrinsicElements['input']; 146 | // @ts-ignore 147 | export type JSXIntrinsicElementsins = JSX.IntrinsicElements['ins']; 148 | // @ts-ignore 149 | export type JSXIntrinsicElementskbd = JSX.IntrinsicElements['kbd']; 150 | // @ts-ignore 151 | export type JSXIntrinsicElementskeygen = JSX.IntrinsicElements['keygen']; 152 | // @ts-ignore 153 | export type JSXIntrinsicElementslabel = JSX.IntrinsicElements['label']; 154 | // @ts-ignore 155 | export type JSXIntrinsicElementslegend = JSX.IntrinsicElements['legend']; 156 | // @ts-ignore 157 | export type JSXIntrinsicElementsli = JSX.IntrinsicElements['li']; 158 | // @ts-ignore 159 | export type JSXIntrinsicElementslink = JSX.IntrinsicElements['link']; 160 | // @ts-ignore 161 | export type JSXIntrinsicElementsmain = JSX.IntrinsicElements['main']; 162 | // @ts-ignore 163 | export type JSXIntrinsicElementsmap = JSX.IntrinsicElements['map']; 164 | // @ts-ignore 165 | export type JSXIntrinsicElementsmark = JSX.IntrinsicElements['mark']; 166 | // @ts-ignore 167 | export type JSXIntrinsicElementsmenu = JSX.IntrinsicElements['menu']; 168 | // @ts-ignore 169 | export type JSXIntrinsicElementsmenuitem = JSX.IntrinsicElements['menuitem']; 170 | // @ts-ignore 171 | export type JSXIntrinsicElementsmeta = JSX.IntrinsicElements['meta']; 172 | // @ts-ignore 173 | export type JSXIntrinsicElementsmeter = JSX.IntrinsicElements['meter']; 174 | // @ts-ignore 175 | export type JSXIntrinsicElementsnav = JSX.IntrinsicElements['nav']; 176 | // @ts-ignore 177 | export type JSXIntrinsicElementsnoindex = JSX.IntrinsicElements['noindex']; 178 | // @ts-ignore 179 | export type JSXIntrinsicElementsnoscript = JSX.IntrinsicElements['noscript']; 180 | // @ts-ignore 181 | export type JSXIntrinsicElementsobject = JSX.IntrinsicElements['object']; 182 | // @ts-ignore 183 | export type JSXIntrinsicElementsol = JSX.IntrinsicElements['ol']; 184 | // @ts-ignore 185 | export type JSXIntrinsicElementsoptgroup = JSX.IntrinsicElements['optgroup']; 186 | // @ts-ignore 187 | export type JSXIntrinsicElementsoption = JSX.IntrinsicElements['option']; 188 | // @ts-ignore 189 | export type JSXIntrinsicElementsoutput = JSX.IntrinsicElements['output']; 190 | // @ts-ignore 191 | export type JSXIntrinsicElementsp = JSX.IntrinsicElements['p']; 192 | // @ts-ignore 193 | export type JSXIntrinsicElementsparam = JSX.IntrinsicElements['param']; 194 | // @ts-ignore 195 | export type JSXIntrinsicElementspicture = JSX.IntrinsicElements['picture']; 196 | // @ts-ignore 197 | export type JSXIntrinsicElementspre = JSX.IntrinsicElements['pre']; 198 | // @ts-ignore 199 | export type JSXIntrinsicElementsprogress = JSX.IntrinsicElements['progress']; 200 | // @ts-ignore 201 | export type JSXIntrinsicElementsq = JSX.IntrinsicElements['q']; 202 | // @ts-ignore 203 | export type JSXIntrinsicElementsrp = JSX.IntrinsicElements['rp']; 204 | // @ts-ignore 205 | export type JSXIntrinsicElementsrt = JSX.IntrinsicElements['rt']; 206 | // @ts-ignore 207 | export type JSXIntrinsicElementsruby = JSX.IntrinsicElements['ruby']; 208 | // @ts-ignore 209 | export type JSXIntrinsicElementss = JSX.IntrinsicElements['s']; 210 | // @ts-ignore 211 | export type JSXIntrinsicElementssamp = JSX.IntrinsicElements['samp']; 212 | // @ts-ignore 213 | export type JSXIntrinsicElementsscript = JSX.IntrinsicElements['script']; 214 | // @ts-ignore 215 | export type JSXIntrinsicElementssection = JSX.IntrinsicElements['section']; 216 | // @ts-ignore 217 | export type JSXIntrinsicElementsselect = JSX.IntrinsicElements['select']; 218 | // @ts-ignore 219 | export type JSXIntrinsicElementssmall = JSX.IntrinsicElements['small']; 220 | // @ts-ignore 221 | export type JSXIntrinsicElementssource = JSX.IntrinsicElements['source']; 222 | // @ts-ignore 223 | export type JSXIntrinsicElementsspan = JSX.IntrinsicElements['span']; 224 | // @ts-ignore 225 | export type JSXIntrinsicElementsstrong = JSX.IntrinsicElements['strong']; 226 | // @ts-ignore 227 | export type JSXIntrinsicElementsstyle = JSX.IntrinsicElements['style']; 228 | // @ts-ignore 229 | export type JSXIntrinsicElementssub = JSX.IntrinsicElements['sub']; 230 | // @ts-ignore 231 | export type JSXIntrinsicElementssummary = JSX.IntrinsicElements['summary']; 232 | // @ts-ignore 233 | export type JSXIntrinsicElementssup = JSX.IntrinsicElements['sup']; 234 | // @ts-ignore 235 | export type JSXIntrinsicElementstable = JSX.IntrinsicElements['table']; 236 | // @ts-ignore 237 | export type JSXIntrinsicElementstemplate = JSX.IntrinsicElements['template']; 238 | // @ts-ignore 239 | export type JSXIntrinsicElementstbody = JSX.IntrinsicElements['tbody']; 240 | // @ts-ignore 241 | export type JSXIntrinsicElementstd = JSX.IntrinsicElements['td']; 242 | // @ts-ignore 243 | export type JSXIntrinsicElementstextarea = JSX.IntrinsicElements['textarea']; 244 | // @ts-ignore 245 | export type JSXIntrinsicElementstfoot = JSX.IntrinsicElements['tfoot']; 246 | // @ts-ignore 247 | export type JSXIntrinsicElementsth = JSX.IntrinsicElements['th']; 248 | // @ts-ignore 249 | export type JSXIntrinsicElementsthead = JSX.IntrinsicElements['thead']; 250 | // @ts-ignore 251 | export type JSXIntrinsicElementstime = JSX.IntrinsicElements['time']; 252 | // @ts-ignore 253 | export type JSXIntrinsicElementstitle = JSX.IntrinsicElements['title']; 254 | // @ts-ignore 255 | export type JSXIntrinsicElementstr = JSX.IntrinsicElements['tr']; 256 | // @ts-ignore 257 | export type JSXIntrinsicElementstrack = JSX.IntrinsicElements['track']; 258 | // @ts-ignore 259 | export type JSXIntrinsicElementsu = JSX.IntrinsicElements['u']; 260 | // @ts-ignore 261 | export type JSXIntrinsicElementsul = JSX.IntrinsicElements['ul']; 262 | // @ts-ignore 263 | export type JSXIntrinsicElementsvar = JSX.IntrinsicElements['var']; 264 | // @ts-ignore 265 | export type JSXIntrinsicElementsvideo = JSX.IntrinsicElements['video']; 266 | // @ts-ignore 267 | export type JSXIntrinsicElementswbr = JSX.IntrinsicElements['wbr']; 268 | // @ts-ignore 269 | export type JSXIntrinsicElementswebview = JSX.IntrinsicElements['webview']; 270 | 271 | // SVG 272 | // @ts-ignore 273 | export type JSXIntrinsicElementssvg = JSX.IntrinsicElements['svg']; 274 | 275 | // @ts-ignore 276 | export type JSXIntrinsicElementsanimate = JSX.IntrinsicElements['animate']; // TODO: It is SVGAnimateElement but is not in TypeScript's lib.dom.d.ts for now. 277 | // @ts-ignore 278 | export type JSXIntrinsicElementsanimateMotion = JSX.IntrinsicElements['animateMotion']; 279 | // @ts-ignore 280 | export type JSXIntrinsicElementsanimateTransform = JSX.IntrinsicElements['animateTransform']; // TODO: It is SVGAnimateTransformElement but is not in TypeScript's lib.dom.d.ts for now. 281 | // @ts-ignore 282 | export type JSXIntrinsicElementscircle = JSX.IntrinsicElements['circle']; 283 | // @ts-ignore 284 | export type JSXIntrinsicElementsclipPath = JSX.IntrinsicElements['clipPath']; 285 | // @ts-ignore 286 | export type JSXIntrinsicElementsdefs = JSX.IntrinsicElements['defs']; 287 | // @ts-ignore 288 | export type JSXIntrinsicElementsdesc = JSX.IntrinsicElements['desc']; 289 | // @ts-ignore 290 | export type JSXIntrinsicElementsellipse = JSX.IntrinsicElements['ellipse']; 291 | // @ts-ignore 292 | export type JSXIntrinsicElementsfeBlend = JSX.IntrinsicElements['feBlend']; 293 | // @ts-ignore 294 | export type JSXIntrinsicElementsfeColorMatrix = JSX.IntrinsicElements['feColorMatrix']; 295 | // @ts-ignore 296 | export type JSXIntrinsicElementsfeComponentTransfer = JSX.IntrinsicElements['feComponentTransfer']; 297 | // @ts-ignore 298 | export type JSXIntrinsicElementsfeComposite = JSX.IntrinsicElements['feComposite']; 299 | // @ts-ignore 300 | export type JSXIntrinsicElementsfeConvolveMatrix = JSX.IntrinsicElements['feConvolveMatrix']; 301 | // @ts-ignore 302 | export type JSXIntrinsicElementsfeDiffuseLighting = JSX.IntrinsicElements['feDiffuseLighting']; 303 | // @ts-ignore 304 | export type JSXIntrinsicElementsfeDisplacementMap = JSX.IntrinsicElements['feDisplacementMap']; 305 | // @ts-ignore 306 | export type JSXIntrinsicElementsfeDistantLight = JSX.IntrinsicElements['feDistantLight']; 307 | // @ts-ignore 308 | export type JSXIntrinsicElementsfeDropShadow = JSX.IntrinsicElements['feDropShadow']; 309 | // @ts-ignore 310 | export type JSXIntrinsicElementsfeFlood = JSX.IntrinsicElements['feFlood']; 311 | // @ts-ignore 312 | export type JSXIntrinsicElementsfeFuncA = JSX.IntrinsicElements['feFuncA']; 313 | // @ts-ignore 314 | export type JSXIntrinsicElementsfeFuncB = JSX.IntrinsicElements['feFuncB']; 315 | // @ts-ignore 316 | export type JSXIntrinsicElementsfeFuncG = JSX.IntrinsicElements['feFuncG']; 317 | // @ts-ignore 318 | export type JSXIntrinsicElementsfeFuncR = JSX.IntrinsicElements['feFuncR']; 319 | // @ts-ignore 320 | export type JSXIntrinsicElementsfeGaussianBlur = JSX.IntrinsicElements['feGaussianBlur']; 321 | // @ts-ignore 322 | export type JSXIntrinsicElementsfeImage = JSX.IntrinsicElements['feImage']; 323 | // @ts-ignore 324 | export type JSXIntrinsicElementsfeMerge = JSX.IntrinsicElements['feMerge']; 325 | // @ts-ignore 326 | export type JSXIntrinsicElementsfeMergeNode = JSX.IntrinsicElements['feMergeNode']; 327 | // @ts-ignore 328 | export type JSXIntrinsicElementsfeMorphology = JSX.IntrinsicElements['feMorphology']; 329 | // @ts-ignore 330 | export type JSXIntrinsicElementsfeOffset = JSX.IntrinsicElements['feOffset']; 331 | // @ts-ignore 332 | export type JSXIntrinsicElementsfePointLight = JSX.IntrinsicElements['fePointLight']; 333 | // @ts-ignore 334 | export type JSXIntrinsicElementsfeSpecularLighting = JSX.IntrinsicElements['feSpecularLighting']; 335 | // @ts-ignore 336 | export type JSXIntrinsicElementsfeSpotLight = JSX.IntrinsicElements['feSpotLight']; 337 | // @ts-ignore 338 | export type JSXIntrinsicElementsfeTile = JSX.IntrinsicElements['feTile']; 339 | // @ts-ignore 340 | export type JSXIntrinsicElementsfeTurbulence = JSX.IntrinsicElements['feTurbulence']; 341 | // @ts-ignore 342 | export type JSXIntrinsicElementsfilter = JSX.IntrinsicElements['filter']; 343 | // @ts-ignore 344 | export type JSXIntrinsicElementsforeignObject = JSX.IntrinsicElements['foreignObject']; 345 | // @ts-ignore 346 | export type JSXIntrinsicElementsg = JSX.IntrinsicElements['g']; 347 | // @ts-ignore 348 | export type JSXIntrinsicElementsimage = JSX.IntrinsicElements['image']; 349 | // @ts-ignore 350 | export type JSXIntrinsicElementsline = JSX.IntrinsicElements['line']; 351 | // @ts-ignore 352 | export type JSXIntrinsicElementslinearGradient = JSX.IntrinsicElements['linearGradient']; 353 | // @ts-ignore 354 | export type JSXIntrinsicElementsmarker = JSX.IntrinsicElements['marker']; 355 | // @ts-ignore 356 | export type JSXIntrinsicElementsmask = JSX.IntrinsicElements['mask']; 357 | // @ts-ignore 358 | export type JSXIntrinsicElementsmetadata = JSX.IntrinsicElements['metadata']; 359 | // @ts-ignore 360 | export type JSXIntrinsicElementsmpath = JSX.IntrinsicElements['mpath']; 361 | // @ts-ignore 362 | export type JSXIntrinsicElementspath = JSX.IntrinsicElements['path']; 363 | // @ts-ignore 364 | export type JSXIntrinsicElementspattern = JSX.IntrinsicElements['pattern']; 365 | // @ts-ignore 366 | export type JSXIntrinsicElementspolygon = JSX.IntrinsicElements['polygon']; 367 | // @ts-ignore 368 | export type JSXIntrinsicElementspolyline = JSX.IntrinsicElements['polyline']; 369 | // @ts-ignore 370 | export type JSXIntrinsicElementsradialGradient = JSX.IntrinsicElements['radialGradient']; 371 | // @ts-ignore 372 | export type JSXIntrinsicElementsrect = JSX.IntrinsicElements['rect']; 373 | // @ts-ignore 374 | export type JSXIntrinsicElementsstop = JSX.IntrinsicElements['stop']; 375 | // @ts-ignore 376 | export type JSXIntrinsicElementsswitch = JSX.IntrinsicElements['switch']; 377 | // @ts-ignore 378 | export type JSXIntrinsicElementssymbol = JSX.IntrinsicElements['symbol']; 379 | // @ts-ignore 380 | export type JSXIntrinsicElementstext = JSX.IntrinsicElements['text']; 381 | // @ts-ignore 382 | export type JSXIntrinsicElementstextPath = JSX.IntrinsicElements['textPath']; 383 | // @ts-ignore 384 | export type JSXIntrinsicElementstspan = JSX.IntrinsicElements['tspan']; 385 | // @ts-ignore 386 | export type JSXIntrinsicElementsuse = JSX.IntrinsicElements['use']; 387 | // @ts-ignore 388 | export type JSXIntrinsicElementsview = JSX.IntrinsicElements['view']; 389 | -------------------------------------------------------------------------------- /codegen/componentCode/templates/componentOverrideTemplate.ts: -------------------------------------------------------------------------------- 1 | import { NewTag } from '../../rules'; 2 | import tagNameToComponentName from '../../tagNameToComponentName'; 3 | import { PropsCode } from '../lib/propsCodeReducer'; 4 | 5 | const propsInterfaceReducer = ({ 6 | componentName, 7 | dupeName, 8 | propsCode, 9 | extensionSpec, 10 | }: { 11 | componentName: string; 12 | dupeName?: string; 13 | propsCode: PropsCode; 14 | extensionSpec: NewTag['extensionSpec']; 15 | }): string => { 16 | const propsInterfaceProperties = Object.entries( 17 | propsCode.propsInterfaceCode, 18 | ).reduce((acc, [key, value]): string => { 19 | const optional = /undefined/.test(value) ? '?' : ''; 20 | return ` 21 | ${acc} 22 | ${key}${optional}: ${value}; 23 | `; 24 | }, ''); 25 | const versionProperty = extensionSpec 26 | ? extensionSpec.version.map((v): string => JSON.stringify(v)).join('|') 27 | : "ScriptProps['version']"; 28 | const exportInterface = dupeName ? '' : 'export'; 29 | return ` 30 | ${exportInterface} interface ${componentName} { 31 | ${propsInterfaceProperties} 32 | version?: ${versionProperty}; 33 | on?: string; 34 | } 35 | `; 36 | }; 37 | 38 | const propTypesReducer = ({ 39 | componentName, 40 | propsCode, 41 | extensionSpec, 42 | }: { 43 | componentName: string; 44 | propsCode: PropsCode; 45 | extensionSpec: NewTag['extensionSpec']; 46 | }): string => { 47 | const propTypesEntries = Object.entries(propsCode.propTypesCode); 48 | if (propTypesEntries.length === 0 && !extensionSpec) return ''; 49 | const versionProperty = extensionSpec 50 | ? ` 51 | PropTypes.oneOf<'${extensionSpec.version.join( 52 | "' | '", 53 | )}'>(${JSON.stringify(extensionSpec.version)}) 54 | ` 55 | : "PropTypes.string as PropTypes.Requireable"; 56 | const propTypesProperties = propTypesEntries.reduce( 57 | (acc, [key, value]): string => ` 58 | ${acc} 59 | ${key}: ${value}, 60 | `, 61 | '', 62 | ); 63 | return ` 64 | ${componentName}.propTypes = { 65 | ${propTypesProperties} 66 | version: ${versionProperty}, 67 | on: PropTypes.string, 68 | }; 69 | `; 70 | }; 71 | 72 | const defaultPropsReducer = ({ 73 | componentName, 74 | propsCode, 75 | extensionSpec, 76 | }: { 77 | componentName: string; 78 | propsCode: PropsCode; 79 | extensionSpec: NewTag['extensionSpec']; 80 | }): string => { 81 | const defaultPropsEntries = Object.entries({ 82 | ...propsCode.defaultPropsCode, 83 | [JSON.stringify('version')]: JSON.stringify( 84 | extensionSpec ? extensionSpec.version.slice().pop() : 'latest', 85 | ), 86 | }); 87 | if (defaultPropsEntries.length === 0) return ''; 88 | const defaultPropsProperties = defaultPropsEntries.reduce( 89 | (acc, [key, value]): string => ` 90 | ${acc} 91 | ${key}: ${value}, 92 | `, 93 | '', 94 | ); 95 | return ` 96 | ${componentName}.defaultProps = { 97 | ${defaultPropsProperties} 98 | }; 99 | `; 100 | }; 101 | 102 | export default ({ 103 | code, 104 | tagName, 105 | componentName, 106 | dupeName, 107 | extensionSpec, 108 | extensionPropsGiven, 109 | extensionProps, 110 | requiresExtensionContext, 111 | contextArgument, 112 | propsCode, 113 | }: { 114 | code: string; 115 | tagName: string; 116 | componentName: string; 117 | dupeName?: string; 118 | extensionSpec: NewTag['extensionSpec']; 119 | extensionPropsGiven: boolean; 120 | extensionProps: { 121 | extension?: string; 122 | isCustomTemplate?: boolean; 123 | }; 124 | requiresExtensionContext: string; 125 | contextArgument: string; 126 | propsCode: PropsCode; 127 | }): string => { 128 | const componentOverrideName = `${componentName}Override`; 129 | const componentOverrideFileName = tagNameToComponentName(tagName); 130 | const exportComponent = dupeName ? '' : 'export'; 131 | const propsArgument = 'props'; 132 | const propsSpread = extensionPropsGiven 133 | ? `{...${propsArgument}, ...${JSON.stringify(extensionProps)}}` 134 | : propsArgument; 135 | return ` 136 | ${code} 137 | import ${componentOverrideName} from './components/${componentOverrideFileName}'; 138 | ${propsInterfaceReducer({ 139 | componentName, 140 | dupeName, 141 | propsCode, 142 | extensionSpec, 143 | })} 144 | // @ts-ignore 145 | ${exportComponent} const ${componentName}: React.FunctionComponent<${componentName}> = (${propsArgument}: ${componentName}${contextArgument}): ReactNode => { 146 | ${requiresExtensionContext} 147 | return ( 148 | <${componentOverrideName} {...${propsSpread}} /> 149 | ); 150 | }; 151 | ${propTypesReducer({ componentName, propsCode, extensionSpec })} 152 | ${defaultPropsReducer({ componentName, propsCode, extensionSpec })} 153 | `; 154 | }; 155 | -------------------------------------------------------------------------------- /codegen/componentCode/templates/componentTemplate.ts: -------------------------------------------------------------------------------- 1 | import { PropsCode } from '../lib/propsCodeReducer'; 2 | import camelCase from '../lib/camelCase'; 3 | import { JSX_INTRINSICS } from '../../constants'; 4 | 5 | const propsInterfaceReducer = ({ 6 | isCustomElement, 7 | camelCasedTagName, 8 | componentPropsName, 9 | dupeName, 10 | propsCode, 11 | }: { 12 | isCustomElement: boolean; 13 | camelCasedTagName: string; 14 | componentPropsName: string; 15 | dupeName?: string; 16 | propsCode: PropsCode; 17 | }): string => { 18 | const classProperty = isCustomElement ? 'class?: string | undefined;' : ''; 19 | const propsInterfaceCode = { ...propsCode.propsInterfaceCode }; 20 | const propsInterfaceProperties = Object.entries(propsInterfaceCode).reduce( 21 | (acc, [key, value]): string => { 22 | const optional = /undefined/.test(value) ? '?' : ''; 23 | return ` 24 | ${acc} 25 | ${key}${optional}: ${value}; 26 | `; 27 | }, 28 | '', 29 | ); 30 | const exportTypeOrInterface = dupeName ? '' : 'export'; 31 | if (isCustomElement) { 32 | return ` 33 | ${exportTypeOrInterface} interface ${componentPropsName} extends React.DetailedHTMLProps, HTMLElement> { 34 | ${propsInterfaceProperties} 35 | ${classProperty} 36 | version?: ScriptProps['version']; 37 | on?: string; 38 | } 39 | `; 40 | } 41 | 42 | return ` 43 | ${exportTypeOrInterface} type ${componentPropsName} = { 44 | ${propsInterfaceProperties} 45 | ${classProperty} 46 | version?: ScriptProps['version']; 47 | on?: string; 48 | } & JSXIntrinsicElements${camelCasedTagName}; 49 | `; 50 | }; 51 | 52 | const propTypesReducer = ({ 53 | isCustomElement, 54 | componentName, 55 | propsCode, 56 | }: { 57 | isCustomElement: boolean; 58 | componentName: string; 59 | propsCode: PropsCode; 60 | }): string => { 61 | if (!isCustomElement) return ''; 62 | const propTypesCode = { ...propsCode.propTypesCode }; 63 | const propTypesEntries = Object.entries(propTypesCode); 64 | if (propTypesEntries.length === 0) return ''; 65 | const propTypesProperties = propTypesEntries.reduce( 66 | (acc, [key, value]): string => ` 67 | ${acc} 68 | ${key}: ${value}, 69 | `, 70 | '', 71 | ); 72 | return ` 73 | ${componentName}.propTypes = { 74 | ${propTypesProperties} 75 | version: PropTypes.string as PropTypes.Requireable, 76 | on: PropTypes.string, 77 | }; 78 | `; 79 | }; 80 | 81 | const defaultPropsReducer = ({ 82 | componentName, 83 | propsCode, 84 | }: { 85 | componentName: string; 86 | propsCode: PropsCode; 87 | }): string => { 88 | const defaultPropsEntries = Object.entries(propsCode.defaultPropsCode); 89 | if (defaultPropsEntries.length === 0) return ''; 90 | const defaultPropsProperties = defaultPropsEntries.reduce( 91 | (acc, [key, value]): string => ` 92 | ${acc} 93 | ${key}: ${value}, 94 | `, 95 | '', 96 | ); 97 | return ` 98 | ${componentName}.defaultProps = { 99 | ${defaultPropsProperties} 100 | }; 101 | `; 102 | }; 103 | 104 | export default ({ 105 | code, 106 | tagName, 107 | componentName, 108 | dupeName, 109 | requiresExtensionContext, 110 | contextArgument, 111 | propsCode, 112 | }: { 113 | code: string; 114 | tagName: string; 115 | componentName: string; 116 | dupeName?: string; 117 | requiresExtensionContext: string; 118 | contextArgument: string; 119 | propsCode: PropsCode; 120 | }): string => { 121 | const componentPropsName = `${componentName}Props`; 122 | const contextTypes = requiresExtensionContext 123 | ? ` 124 | ${componentName}.contextTypes = REACT_AMPHTML_CONTEXT; 125 | ` 126 | : ''; 127 | const propsArgument = 'props'; 128 | const newPropsVar = 'newProps'; 129 | const camelCasedTagName = camelCase(tagName); 130 | const isCustomElement = !JSX_INTRINSICS[camelCasedTagName]; 131 | const swapClassNameForClass = isCustomElement 132 | ? ` 133 | let ${newPropsVar} = ${propsArgument}; 134 | if (typeof ${newPropsVar}.className === 'string') { 135 | const { className, ...restProps } = ${newPropsVar}; 136 | ${newPropsVar} = restProps; 137 | ${newPropsVar}.class = className; 138 | } 139 | ` 140 | : ''; 141 | const propsSpread = isCustomElement ? newPropsVar : propsArgument; 142 | const exportComponent = dupeName ? '' : 'export'; 143 | const ignoreCustomElement = isCustomElement ? '// @ts-ignore' : ''; 144 | const customElementBoolean = isCustomElement 145 | ? ` 146 | // Add or remove attributes based on if they are boolean true or false 147 | ${newPropsVar} = Object.entries(${newPropsVar}).reduce( 148 | ( 149 | // @ts-ignore 150 | acc: any, 151 | [key, value], 152 | ): 153 | // @ts-ignore 154 | any => { 155 | if (value === true) { 156 | acc[key] = ''; 157 | } else if (value !== false) { 158 | acc[key] = value; 159 | } 160 | 161 | return acc; 162 | }, 163 | {}, 164 | ); 165 | ` 166 | : ''; 167 | return ` 168 | ${code} 169 | ${propsInterfaceReducer({ 170 | isCustomElement, 171 | camelCasedTagName, 172 | componentPropsName, 173 | dupeName, 174 | propsCode, 175 | })} 176 | ${exportComponent} const ${componentName}: React.FunctionComponent<${componentPropsName}> = (${propsArgument}: ${componentPropsName}${contextArgument}) => { 177 | ${requiresExtensionContext} 178 | ${swapClassNameForClass} 179 | ${customElementBoolean} 180 | return ( 181 | ${ignoreCustomElement} 182 | <${tagName.toLowerCase()} {...${propsSpread}} /> 183 | ); 184 | }; 185 | ${propTypesReducer({ isCustomElement, componentName, propsCode })} 186 | ${defaultPropsReducer({ componentName, propsCode })} 187 | ${contextTypes} 188 | `; 189 | }; 190 | -------------------------------------------------------------------------------- /codegen/componentCode/templates/mandatoryComponentOverrideTemplate.ts: -------------------------------------------------------------------------------- 1 | import tagNameToComponentName from '../../tagNameToComponentName'; 2 | 3 | export default ({ 4 | code, 5 | tagName, 6 | componentName, 7 | }: { 8 | code: string; 9 | tagName: string; 10 | componentName: string; 11 | }): string => { 12 | const componentOverrideName = `${componentName}Override`; 13 | const componentOverrideFileName = tagNameToComponentName(tagName); 14 | return ` 15 | ${code} 16 | import ${componentOverrideName} from './components/${componentOverrideFileName}'; 17 | export const ${componentName} = ${componentOverrideName}; 18 | `; 19 | }; 20 | -------------------------------------------------------------------------------- /codegen/constants.ts: -------------------------------------------------------------------------------- 1 | export const MANDATORY_COMPONENT_OVERRIDES: { 2 | [key: string]: boolean; 3 | } = { 4 | Html: true, 5 | }; 6 | 7 | export const COMPONENT_OVERRIDES: { 8 | [key: string]: boolean; 9 | } = { 10 | AmpState: true, 11 | Script: true, 12 | }; 13 | 14 | export const BLACKLIST: { 15 | [key: string]: boolean; 16 | } = { 17 | '!DOCTYPE': true, 18 | $REFERENCE_POINT: true, 19 | 'O:P': true, 20 | 'I-AMPHTML-SIZER': true, 21 | }; 22 | 23 | export const DUPES_BLACKLIST: { 24 | [key: string]: boolean; 25 | } = { 26 | HTML: true, 27 | }; 28 | 29 | export const MISSING_SCRIPT_EXTENSIONS = [ 30 | 'amp-video', 31 | 'amp-ad', 32 | 'amp-ad-custom', 33 | ]; 34 | 35 | // The following were all copied from the global JSX IntrinsicElements 36 | // interface. 37 | // The properties of the interface were copied here and then had a regex replace 38 | // run over all of them to make them into actual properties: 39 | // 40 | // Find: ^"?([a-z].*?)"?:.*; 41 | // 42 | // Replace: $1: true, 43 | 44 | export const JSX_INTRINSICS: { 45 | [key: string]: boolean; 46 | } = { 47 | // HTML 48 | a: true, 49 | abbr: true, 50 | address: true, 51 | area: true, 52 | article: true, 53 | aside: true, 54 | audio: true, 55 | b: true, 56 | base: true, 57 | bdi: true, 58 | bdo: true, 59 | big: true, 60 | blockquote: true, 61 | body: true, 62 | br: true, 63 | button: true, 64 | canvas: true, 65 | caption: true, 66 | cite: true, 67 | code: true, 68 | col: true, 69 | colgroup: true, 70 | data: true, 71 | datalist: true, 72 | dd: true, 73 | del: true, 74 | details: true, 75 | dfn: true, 76 | dialog: true, 77 | div: true, 78 | dl: true, 79 | dt: true, 80 | em: true, 81 | embed: true, 82 | fieldset: true, 83 | figcaption: true, 84 | figure: true, 85 | footer: true, 86 | form: true, 87 | h1: true, 88 | h2: true, 89 | h3: true, 90 | h4: true, 91 | h5: true, 92 | h6: true, 93 | head: true, 94 | header: true, 95 | hgroup: true, 96 | hr: true, 97 | html: true, 98 | i: true, 99 | iframe: true, 100 | img: true, 101 | input: true, 102 | ins: true, 103 | kbd: true, 104 | keygen: true, 105 | label: true, 106 | legend: true, 107 | li: true, 108 | link: true, 109 | main: true, 110 | map: true, 111 | mark: true, 112 | menu: true, 113 | menuitem: true, 114 | meta: true, 115 | meter: true, 116 | nav: true, 117 | noindex: true, 118 | noscript: true, 119 | object: true, 120 | ol: true, 121 | optgroup: true, 122 | option: true, 123 | output: true, 124 | p: true, 125 | param: true, 126 | picture: true, 127 | pre: true, 128 | progress: true, 129 | q: true, 130 | rp: true, 131 | rt: true, 132 | ruby: true, 133 | s: true, 134 | samp: true, 135 | script: true, 136 | section: true, 137 | select: true, 138 | small: true, 139 | source: true, 140 | span: true, 141 | strong: true, 142 | style: true, 143 | sub: true, 144 | summary: true, 145 | sup: true, 146 | table: true, 147 | template: true, 148 | tbody: true, 149 | td: true, 150 | textarea: true, 151 | tfoot: true, 152 | th: true, 153 | thead: true, 154 | time: true, 155 | title: true, 156 | tr: true, 157 | track: true, 158 | u: true, 159 | ul: true, 160 | var: true, 161 | video: true, 162 | wbr: true, 163 | webview: true, 164 | 165 | // SVG 166 | svg: true, 167 | 168 | animate: true, // TODO: It is SVGAnimateElement but is not in TypeScript's lib.dom.d.ts for now. 169 | animateMotion: true, 170 | animateTransform: true, // TODO: It is SVGAnimateTransformElement but is not in TypeScript's lib.dom.d.ts for now. 171 | circle: true, 172 | clipPath: true, 173 | defs: true, 174 | desc: true, 175 | ellipse: true, 176 | feBlend: true, 177 | feColorMatrix: true, 178 | feComponentTransfer: true, 179 | feComposite: true, 180 | feConvolveMatrix: true, 181 | feDiffuseLighting: true, 182 | feDisplacementMap: true, 183 | feDistantLight: true, 184 | feDropShadow: true, 185 | feFlood: true, 186 | feFuncA: true, 187 | feFuncB: true, 188 | feFuncG: true, 189 | feFuncR: true, 190 | feGaussianBlur: true, 191 | feImage: true, 192 | feMerge: true, 193 | feMergeNode: true, 194 | feMorphology: true, 195 | feOffset: true, 196 | fePointLight: true, 197 | feSpecularLighting: true, 198 | feSpotLight: true, 199 | feTile: true, 200 | feTurbulence: true, 201 | filter: true, 202 | foreignObject: true, 203 | g: true, 204 | image: true, 205 | line: true, 206 | linearGradient: true, 207 | marker: true, 208 | mask: true, 209 | metadata: true, 210 | mpath: true, 211 | path: true, 212 | pattern: true, 213 | polygon: true, 214 | polyline: true, 215 | radialGradient: true, 216 | rect: true, 217 | stop: true, 218 | switch: true, 219 | symbol: true, 220 | text: true, 221 | textPath: true, 222 | tspan: true, 223 | use: true, 224 | view: true, 225 | }; 226 | -------------------------------------------------------------------------------- /codegen/duplicateWrapperComponentCode.ts: -------------------------------------------------------------------------------- 1 | import newRules from './rules'; 2 | import tagNameToComponentName from './tagNameToComponentName'; 3 | import { MISSING_SCRIPT_EXTENSIONS } from './constants'; 4 | 5 | export default Object.entries(newRules.dupes).reduce( 6 | ( 7 | code: string, 8 | [tagName, dupes]: [string, { [dupeTagName: string]: string }], 9 | ): string => { 10 | const componentName = tagNameToComponentName(tagName); 11 | 12 | const { 13 | dupeCode: dupeComponentCode, 14 | dupeVersions: dupeComponentVersions, 15 | } = Object.entries(dupes).reduce( 16 | ( 17 | { 18 | dupeCode, 19 | dupeVersions, 20 | }: { 21 | dupeCode: string; 22 | dupeVersions: Set; 23 | }, 24 | [dupeTagName, specName]: [string, string], 25 | ): { 26 | dupeCode: string; 27 | dupeVersions: Set; 28 | } => { 29 | const tag = newRules.tags.find( 30 | ({ dupeName: t }: { dupeName?: string }): boolean => 31 | t === dupeTagName, 32 | ); 33 | if ( 34 | tag && 35 | tag.extensionSpec && 36 | Array.isArray(tag.extensionSpec.version) 37 | ) { 38 | tag.extensionSpec.version.forEach( 39 | (version: string): void => { 40 | dupeVersions.add(version); 41 | }, 42 | ); 43 | } 44 | 45 | return { 46 | dupeCode: ` 47 | ${dupeCode} 48 | if (props.specName === '${specName}') { 49 | const { specName: _, ...restProps } = props; 50 | return ( 51 | // @ts-ignore 52 | <${tagNameToComponentName(dupeTagName)} 53 | {...restProps} 54 | /> 55 | ); 56 | }; 57 | `, 58 | dupeVersions, 59 | }; 60 | }, 61 | { dupeCode: '', dupeVersions: new Set() }, 62 | ); 63 | 64 | const specNames = Object.values(dupes); 65 | 66 | return ` 67 | ${code} 68 | export interface ${componentName}Props { 69 | specName: ${Object.values(dupes) 70 | .concat(componentName === 'Script' ? MISSING_SCRIPT_EXTENSIONS : []) 71 | .map((v): string => JSON.stringify(v)) 72 | .join('|')}; 73 | ${ 74 | dupeComponentVersions.size > 0 75 | ? ` 76 | version?: ${[...dupeComponentVersions.values()] 77 | .map((v): string => JSON.stringify(v)) 78 | .join('|')}; 79 | ` 80 | : '' 81 | } 82 | [prop: string]: any; 83 | } 84 | 85 | // @ts-ignore 86 | const ${componentName}: React.FunctionComponent<${componentName}Props> = (props): ReactNode => { 87 | ${dupeComponentCode} 88 | return null; 89 | }; 90 | 91 | // @ts-ignore 92 | ${componentName}.propTypes = { 93 | specName: PropTypes.oneOf<'${specNames.join( 94 | "' | '", 95 | )}'>(${JSON.stringify(specNames)}).isRequired, 96 | ${ 97 | dupeComponentVersions.size > 0 98 | ? `version: PropTypes.oneOf<${[...dupeComponentVersions.values()] 99 | .map((v): string => JSON.stringify(v)) 100 | .join('|')}>(${JSON.stringify([ 101 | ...dupeComponentVersions.values(), 102 | ])}),` 103 | : '' 104 | } 105 | }; 106 | 107 | // @ts-ignore 108 | ${componentName}.defaultProps = { 109 | ${dupeComponentVersions.size > 0 ? "version: 'latest'," : ''} 110 | }; 111 | 112 | export { ${componentName} }; 113 | `; 114 | }, 115 | '', 116 | ); 117 | -------------------------------------------------------------------------------- /codegen/index.ts: -------------------------------------------------------------------------------- 1 | import prettier from 'prettier'; 2 | import componentCode from './componentCode'; 3 | import duplicateWrapperComponentCode from './duplicateWrapperComponentCode'; 4 | 5 | // For debugging purposes. 6 | // console.log( 7 | // ` 8 | // ${componentCode} 9 | // ${duplicateWrapperComponentCode} 10 | // ` 11 | // .split('\n') 12 | // .map((line, index): string => `${index + 1}${line}`) 13 | // .join('\n'), 14 | // ); 15 | 16 | const code = prettier.format( 17 | ` 18 | ${componentCode} 19 | ${duplicateWrapperComponentCode} 20 | `, 21 | { parser: 'typescript' }, 22 | ); 23 | 24 | process.stdout.write(code); 25 | -------------------------------------------------------------------------------- /codegen/rules.ts: -------------------------------------------------------------------------------- 1 | import * as amphtmlRules from 'amphtml-validator-rules'; 2 | import { BLACKLIST, DUPES_BLACKLIST } from './constants'; 3 | 4 | const DUPLICATE_SPEC_NAME = 'default'; 5 | 6 | interface DuplicateTags { 7 | [tag: string]: number; 8 | } 9 | 10 | const duplicateTags: DuplicateTags = amphtmlRules.tags.reduce( 11 | (cache: DuplicateTags, { tagName }: amphtmlRules.Tag): DuplicateTags => ({ 12 | ...cache, 13 | [tagName]: typeof cache[tagName] === 'number' ? cache[tagName] + 1 : 0, 14 | }), 15 | {}, 16 | ); 17 | 18 | export interface NewTag extends amphtmlRules.Tag { 19 | dupeName?: string; 20 | } 21 | 22 | export interface NewAmphtmlRules extends amphtmlRules.IndexD { 23 | dupes: { 24 | [tagName: string]: { 25 | [newTagName: string]: string; 26 | }; 27 | }; 28 | tags: NewTag[]; 29 | version: string; 30 | } 31 | 32 | const newAmphtmlRules: NewAmphtmlRules = amphtmlRules.tags.reduce( 33 | (acc: NewAmphtmlRules, tag: amphtmlRules.Tag): NewAmphtmlRules => { 34 | const { 35 | tagName, 36 | specName: possibleSpecName, 37 | extensionSpec: possibleExtensionSpec, 38 | } = tag; 39 | 40 | if (BLACKLIST[tagName]) { 41 | return acc; 42 | } 43 | 44 | if (DUPES_BLACKLIST[tagName] || !duplicateTags[tagName]) { 45 | acc.tags = acc.tags 46 | .filter(({ tagName: t }: { tagName: string }): boolean => t !== tagName) 47 | .concat(tag); 48 | return acc; 49 | } 50 | 51 | const extensionSpec = possibleExtensionSpec || { name: '' }; 52 | const specName = 53 | possibleSpecName || extensionSpec.name || DUPLICATE_SPEC_NAME; 54 | 55 | const newTagName = `${tagName}_${Buffer.from(specName).toString('hex')}`; 56 | 57 | acc.dupes[tagName] = acc.dupes[tagName] || {}; 58 | acc.dupes[tagName][newTagName] = specName; 59 | acc.tags.push({ ...tag, dupeName: newTagName, specName }); 60 | 61 | return acc; 62 | }, 63 | { 64 | ...amphtmlRules, 65 | dupes: {}, 66 | tags: [], 67 | }, 68 | ); 69 | 70 | export default newAmphtmlRules; 71 | -------------------------------------------------------------------------------- /codegen/tagNameToComponentName.ts: -------------------------------------------------------------------------------- 1 | export default (tagName: string): string => 2 | tagName 3 | .toLowerCase() 4 | .replace( 5 | /(^.|-.)/g, 6 | (_: string, p1: string): string => p1.replace('-', '').toUpperCase(), 7 | ); 8 | -------------------------------------------------------------------------------- /helpers.d.ts: -------------------------------------------------------------------------------- 1 | import Action, { ActionProps, ActionOnProps } from './dist/helpers/Action'; 2 | import Bind, { AmpBindProps, BindProps } from './dist/helpers/Bind'; 3 | 4 | export { Action, ActionProps, ActionOnProps, AmpBindProps, BindProps, Bind }; 5 | -------------------------------------------------------------------------------- /helpers.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/helpers/helpers'); 2 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist/amphtml/amphtml'; 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/amphtml/amphtml'); 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFilesAfterEnv: ['./setupTest.ts'], 3 | collectCoverage: true, 4 | collectCoverageFrom: [ 5 | '/src/**/*.js', 6 | '/src/**/*.ts', 7 | '/src/**/*.tsx', 8 | '!/src/amphtml/amphtml.tsx', 9 | ], 10 | coverageThreshold: { 11 | global: { 12 | branches: 90, 13 | functions: 90, 14 | lines: 90, 15 | statements: 90, 16 | }, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-amphtml", 3 | "version": "4.0.2", 4 | "description": "Use amphtml components inside your React apps easily!", 5 | "main": "./index.js", 6 | "typings": "./index.d.ts", 7 | "files": [ 8 | "dist/*", 9 | "index.*", 10 | "helpers.*", 11 | "setup.*" 12 | ], 13 | "scripts": { 14 | "codegen": "babel-node --extensions='.js,.ts,.tsx' ./codegen/index.ts > ./src/amphtml/amphtml.tsx", 15 | "prebuild": "rimraf ./dist", 16 | "build": "rollup -c ./rollup.config.js", 17 | "postbuild": "npm run ts-declarations", 18 | "prepublishOnly": "npm run build", 19 | "lint": "eslint --ext .ts --ext .tsx --ext .js .", 20 | "test": "jest --no-cache", 21 | "format": "prettier --write '**/*.js' '**/*.jsx' '**/*.ts' '**/*.tsx' '**/*.md'", 22 | "typecheck": "tsc -p ./tsconfig.json --noEmit", 23 | "ts-declarations": "tsc -p ./tsconfig.declarations.json --emitDeclarationOnly --declaration" 24 | }, 25 | "keywords": [ 26 | "react", 27 | "amphtml" 28 | ], 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/dfrankland/react-amphtml.git" 32 | }, 33 | "author": "Dylan Frankland", 34 | "license": "MIT", 35 | "devDependencies": { 36 | "@babel/core": "^7.4.5", 37 | "@babel/node": "^7.4.5", 38 | "@babel/plugin-proposal-export-default-from": "^7.2.0", 39 | "@babel/preset-env": "^7.4.5", 40 | "@babel/preset-react": "^7.0.0", 41 | "@babel/preset-typescript": "^7.3.3", 42 | "@betit/rollup-plugin-rename-extensions": "0.0.4", 43 | "@types/amphtml-validator": "^1.0.0", 44 | "@types/enzyme": "^3.9.3", 45 | "@types/enzyme-adapter-react-16": "^1.0.5", 46 | "@types/jest": "^24.0.13", 47 | "@types/prettier": "^1.16.4", 48 | "@types/react": "^16.8.19", 49 | "@types/react-dom": "^16.8.4", 50 | "@typescript-eslint/eslint-plugin": "^1.9.0", 51 | "@typescript-eslint/parser": "^1.9.0", 52 | "amphtml-validator": "^1.0.23", 53 | "amphtml-validator-rules": "^3.0.0", 54 | "babel-eslint": "^10.0.1", 55 | "babel-jest": "^24.8.0", 56 | "babel-plugin-codegen": "^3.0.0", 57 | "enzyme": "^3.9.0", 58 | "enzyme-adapter-react-16": "^1.13.2", 59 | "eslint": "^5.16.0", 60 | "eslint-config-airbnb": "^17.1.0", 61 | "eslint-config-prettier": "^4.3.0", 62 | "eslint-plugin-import": "^2.17.3", 63 | "eslint-plugin-jest": "^22.6.4", 64 | "eslint-plugin-jsx-a11y": "^6.2.1", 65 | "eslint-plugin-prettier": "^3.1.0", 66 | "eslint-plugin-react": "^7.13.0", 67 | "globby": "^9.2.0", 68 | "jest": "^24.8.0", 69 | "prettier": "^1.17.1", 70 | "react": "^16.8.6", 71 | "react-dom": "^16.8.6", 72 | "react-test-renderer": "^16.8.6", 73 | "rimraf": "^2.6.3", 74 | "rollup": "^1.13.0", 75 | "rollup-plugin-babel": "^4.3.2", 76 | "rollup-plugin-node-resolve": "^5.0.1", 77 | "typescript": "^3.5.1" 78 | }, 79 | "dependencies": { 80 | "prop-types": "^15.7.2" 81 | }, 82 | "peerDependencies": { 83 | "react": "^15.0.0 || ^16.0.0" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from 'rollup-plugin-node-resolve'; 2 | import babel from 'rollup-plugin-babel'; 3 | import renameExtensions from '@betit/rollup-plugin-rename-extensions'; 4 | import { dependencies, peerDependencies } from './package.json'; 5 | 6 | process.env.BABEL_DISABLE_CACHE = 1; 7 | 8 | export default { 9 | preserveModules: true, 10 | input: [ 11 | './src/amphtml/amphtml.tsx', 12 | './src/helpers/helpers.ts', 13 | './src/setup/setup.ts', 14 | ], 15 | output: { 16 | dir: './dist', 17 | format: 'cjs', 18 | sourcemap: true, 19 | }, 20 | plugins: [ 21 | nodeResolve({ 22 | extensions: ['.js', '.ts', '.tsx'], 23 | }), 24 | babel({ 25 | extensions: ['.js', '.ts', '.tsx'], 26 | exclude: 'node_modules/**', 27 | }), 28 | renameExtensions({ 29 | include: ['**/*.ts', '**/*.tsx'], 30 | mappings: { 31 | '.ts': '.js', 32 | '.tsx': '.js', 33 | }, 34 | }), 35 | ], 36 | external: [...Object.keys(dependencies), ...Object.keys(peerDependencies)], 37 | }; 38 | -------------------------------------------------------------------------------- /setup.d.ts: -------------------------------------------------------------------------------- 1 | import AmpScripts from './dist/setup/AmpScripts'; 2 | import AmpScriptsManager, { 3 | AmpScriptsManagerContext, 4 | AmpScriptsManagerProps, 5 | } from './dist/setup/AmpScriptsManager'; 6 | import headerBoilerplate from './dist/setup/headerBoilerplate'; 7 | 8 | export { 9 | AmpScripts, 10 | AmpScriptsManagerContext, 11 | AmpScriptsManagerProps, 12 | AmpScriptsManager, 13 | headerBoilerplate, 14 | }; 15 | -------------------------------------------------------------------------------- /setup.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/setup/setup'); 2 | -------------------------------------------------------------------------------- /setupTest.ts: -------------------------------------------------------------------------------- 1 | import Adapter from 'enzyme-adapter-react-16'; 2 | import Enzyme from 'enzyme'; 3 | 4 | Enzyme.configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/react-amphtml.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`react-amphtml can server-side render valid html 1`] = ` 4 | " 5 | 6 | react-amphtml
11 | " 12 | `; 13 | -------------------------------------------------------------------------------- /src/__tests__/react-amphtml.spec.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | import { mount } from 'enzyme'; 3 | import { renderToStaticMarkup } from 'react-dom/server'; 4 | import amphtmlValidator from 'amphtml-validator'; 5 | import * as Amp from '../amphtml/amphtml'; 6 | import * as AmpHelpers from '../helpers/helpers'; 7 | import { ActionOnProps } from '../helpers/Action'; 8 | import { 9 | AmpScripts, 10 | AmpScriptsManager, 11 | headerBoilerplate, 12 | } from '../setup/setup'; 13 | 14 | describe('react-amphtml', (): void => { 15 | it('renders amp-html built-ins, and does not generate extra script tags', (): void => { 16 | const ampScripts = new AmpScripts(); 17 | mount( 18 | 19 |
20 | 21 | 22 |
23 |
, 24 | ); 25 | 26 | const ampScriptElements = ampScripts.getScriptElements(); 27 | expect(ampScriptElements.length).toBe(1); 28 | }); 29 | 30 | it('renders amp-html extensions, and generates script tags', (): void => { 31 | const ampScripts = new AmpScripts(); 32 | mount( 33 | 34 |
35 | 36 | 37 | 38 | Hello, {'{{world}}'}! 39 | 40 |
41 |
, 42 | ); 43 | 44 | const ampScriptElements = ampScripts.getScriptElements(); 45 | const wrapper = mount(
{ampScriptElements}
); 46 | 47 | expect(wrapper.find('[custom-element]').length).toBe(2); 48 | expect(wrapper.find('[custom-template]').length).toBe(1); 49 | expect(wrapper.find('script').length).toBe(4); 50 | }); 51 | 52 | it('should be able to statically export script sources', (): void => { 53 | const ampScripts = new AmpScripts(); 54 | mount( 55 | 56 |
57 | 58 | 59 | 60 |
61 |
, 62 | ); 63 | 64 | const ampScriptSources = ampScripts.getScripts(); 65 | 66 | expect(ampScriptSources).toEqual( 67 | expect.arrayContaining( 68 | [ 69 | 'https://cdn.ampproject.org/v0/amp-youtube-latest.js', 70 | 'https://cdn.ampproject.org/v0/amp-script-latest.js', 71 | 'https://cdn.ampproject.org/v0/amp-accordion-latest.js', 72 | ].map((src): any => expect.objectContaining({ src })), 73 | ), 74 | ); 75 | }); 76 | 77 | it('can specify versions of script tags', (): void => { 78 | const ampScripts = new AmpScripts(); 79 | mount( 80 | 81 |
82 | 83 | Hello 84 | 85 |
86 |
, 87 | ); 88 | 89 | const ampScriptsSources = ampScripts.getScripts(); 90 | expect(ampScriptsSources).toEqual( 91 | expect.arrayContaining([ 92 | expect.objectContaining({ 93 | src: 'https://cdn.ampproject.org/v0/amp-mustache-0.2.js', 94 | }), 95 | ]), 96 | ); 97 | }); 98 | 99 | it('warns on invalid versions of script tags', (): void => { 100 | const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); 101 | const ampScripts = new AmpScripts(); 102 | mount( 103 | 104 |
105 | 106 | Hello 107 | 108 |
109 |
, 110 | ); 111 | 112 | mount(
{ampScripts.getScriptElements()}
); 113 | expect(consoleSpy).toHaveBeenCalledTimes(2); 114 | consoleSpy.mockRestore(); 115 | }); 116 | 117 | it('renders amp-html, and works without context from AmpScriptsManager', (): void => { 118 | const wrapper = mount( 119 |
120 | 121 | 122 |
, 123 | ); 124 | 125 | expect(wrapper.find('amp-youtube').length).toBe(1); 126 | expect(wrapper.find('amp-accordion').length).toBe(1); 127 | }); 128 | 129 | it('renders amp-html, and passes `className` prop', (): void => { 130 | const wrapper = mount( 131 | , 132 | ); 133 | 134 | expect(wrapper.find('[class="cool"]').length).toBe(1); 135 | }); 136 | 137 | it('renders amp-form, properly', (): void => { 138 | const ampScripts = new AmpScripts(); 139 | const wrapper = mount( 140 | 141 |
142 | 148 |
149 |
, 150 | ); 151 | 152 | const ampScriptElements = ampScripts.getScriptElements(); 153 | 154 | expect(ampScriptElements.length).toBe(2); 155 | expect(wrapper.find('form').length).toBe(1); 156 | }); 157 | 158 | it('renders amp-state & amp-bind properly, and only appends the amp-bind script', (): void => { 159 | const ampScripts = new AmpScripts(); 160 | const initialState = { text: 'Hello, World!' }; 161 | const wrapper = mount( 162 | 163 |
164 | 165 | {initialState} 166 | 167 | 168 | {(props): ReactElement =>
} 169 | 170 |
171 | , 172 | ); 173 | 174 | const ampScriptElements = ampScripts.getScriptElements(); 175 | 176 | expect(ampScriptElements.length).toBe(2); 177 | expect(wrapper.find('[data-amp-bind-text="myState.text"]').length).toBe(1); 178 | expect(wrapper.find('amp-state').length).toBe(1); 179 | expect(wrapper.find('amp-state').text()).toBe(JSON.stringify(initialState)); 180 | }); 181 | 182 | it('renders amphtml action `on` attribute properly', (): void => { 183 | const wrapper = mount( 184 | 190 | {(props: ActionOnProps): ReactElement => } 191 | , 192 | ); 193 | 194 | expect( 195 | wrapper 196 | .find( 197 | '[on="tap:AMP.setState({ myState: { text: \\"tap!\\" }}),print;change:AMP.setState({ myState: { input: event.value } })"]', 198 | ) 199 | .exists(), 200 | ).toBe(true); 201 | }); 202 | 203 | it('renders amp-action inside amp-bind properly', (): void => { 204 | const myStateText = 'myState.text'; 205 | 206 | const wrapper = mount( 207 | 208 | {(props): ReactElement => ( 209 | 215 | {(props1: ActionOnProps): ReactElement => ( 216 | 217 | )} 218 | 219 | )} 220 | , 221 | ); 222 | 223 | expect(wrapper.find('[on="tap:print"]').exists()).toBe(true); 224 | expect(wrapper.find(`[data-amp-bind-text="${myStateText}"]`).exists()).toBe( 225 | true, 226 | ); 227 | }); 228 | 229 | it('renders amp-bind inside amp-action properly', (): void => { 230 | const myStateText = 'myState.text'; 231 | 232 | const wrapper = mount( 233 | 238 | {(props): ReactElement => ( 239 | 240 | {(props1): ReactElement => } 241 | 242 | )} 243 | , 244 | ); 245 | 246 | expect(wrapper.find('[on="tap:print"]').exists()).toBe(true); 247 | expect(wrapper.find(`[data-amp-bind-text="${myStateText}"]`).exists()).toBe( 248 | true, 249 | ); 250 | }); 251 | 252 | it('renders amp-bind inside amp-bind properly', (): void => { 253 | const myStateClass = 'myState.class'; 254 | const myStateText = 'myState.text'; 255 | 256 | /* eslint-disable react/no-unknown-property */ 257 | const wrapper = mount( 258 | 259 | {(props): ReactElement => ( 260 | 261 | {(props1): ReactElement => } 262 | 263 | )} 264 | , 265 | ); 266 | /* eslint-enable */ 267 | 268 | expect( 269 | wrapper.find(`[data-amp-bind-class="${myStateClass}"]`).exists(), 270 | ).toBe(true); 271 | expect(wrapper.find(`[data-amp-bind-text="${myStateText}"]`).exists()).toBe( 272 | true, 273 | ); 274 | }); 275 | 276 | it( 277 | 'renders non-standard attributes on non-standard elements (this ' + 278 | "shouldn't throw warnings, otherwise this won't work with React " + 279 | 'normally even if this test passes; see ' + 280 | 'https://github.com/facebook/react/pull/12568)', 281 | (): void => { 282 | const myStateClass = 'myState.class'; 283 | const myStateText = 'myState.text'; 284 | 285 | /* eslint-disable react/no-unknown-property */ 286 | const wrapper = mount( 287 | 288 | {(props): ReactElement => ( 289 | 290 | {(props1): ReactElement => ( 291 | 292 | )} 293 | 294 | )} 295 | , 296 | ); 297 | /* eslint-enable */ 298 | 299 | expect( 300 | wrapper.find(`[data-amp-bind-class="${myStateClass}"]`).exists(), 301 | ).toBe(true); 302 | expect( 303 | wrapper.find(`[data-amp-bind-text="${myStateText}"]`).exists(), 304 | ).toBe(true); 305 | }, 306 | ); 307 | 308 | it('can server-side render valid html', async (): Promise => { 309 | expect.assertions(2); 310 | 311 | const ampScripts = new AmpScripts(); 312 | 313 | const bodyContent = renderToStaticMarkup( 314 | 315 |
316 | 324 | 325 |
326 |
, 327 | ); 328 | 329 | /* eslint-disable react/no-danger */ 330 | const html = renderToStaticMarkup( 331 | 332 | 333 | {headerBoilerplate('/')} 334 | react-amphtml 335 | {ampScripts.getScriptElements()} 336 | 337 | 338 | , 339 | ); 340 | /* eslint-enable */ 341 | 342 | const htmlPage = ` 343 | 344 | ${html} 345 | `; 346 | 347 | expect(htmlPage).toMatchSnapshot(); 348 | 349 | const validator = await amphtmlValidator.getInstance(); 350 | const result = validator.validateString(htmlPage); 351 | 352 | result.errors.forEach( 353 | ({ line, col, message, specUrl, severity }): void => { 354 | // eslint-disable-next-line no-console 355 | (severity === 'ERROR' ? console.error : console.warn)( 356 | // eslint-disable-line no-console 357 | `line ${line}, col ${col}: ${message} ${ 358 | specUrl ? ` (see ${specUrl})` : '' 359 | }`, 360 | ); 361 | }, 362 | ); 363 | 364 | expect(result.status).toBe('PASS'); 365 | }); 366 | }); 367 | -------------------------------------------------------------------------------- /src/amphtml/components/AmpState.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import contextHelper from '../../lib/contextHelper'; 4 | import { CONTEXT_KEY } from '../../constants'; 5 | 6 | export interface AmpStateProps { 7 | children?: any; 8 | id?: string; 9 | src?: string; 10 | } 11 | 12 | const AmpState: React.FunctionComponent = ( 13 | { children, id, src }, 14 | context, 15 | ): ReactElement => { 16 | contextHelper({ context, extension: 'amp-bind' }); 17 | 18 | if (src) { 19 | return ; 20 | } 21 | 22 | return ( 23 | 24 |