├── .formatter.exs ├── .gitignore ├── LICENSE ├── README.md ├── assets ├── babel.config.js ├── js │ └── react-surface.js ├── package-lock.json ├── package.json └── webpack.config.js ├── config └── config.exs ├── demo ├── .formatter.exs ├── .gitignore ├── README.md ├── assets │ ├── .babelrc │ ├── css │ │ ├── app.scss │ │ └── phoenix.css │ ├── js │ │ ├── app.js │ │ └── components │ │ │ ├── HelloReactSurface.js │ │ │ ├── Simple.js │ │ │ └── index.js │ ├── package-lock.json │ ├── package.json │ ├── ssr.js │ ├── static │ │ ├── favicon.ico │ │ ├── images │ │ │ └── phoenix.png │ │ └── robots.txt │ └── webpack.config.js ├── config │ ├── config.exs │ ├── dev.exs │ ├── prod.exs │ ├── prod.secret.exs │ └── test.exs ├── lib │ ├── demo.ex │ ├── demo │ │ ├── application.ex │ │ └── repo.ex │ ├── demo_web.ex │ └── demo_web │ │ ├── channels │ │ └── user_socket.ex │ │ ├── endpoint.ex │ │ ├── gettext.ex │ │ ├── live │ │ ├── hello_react_surface.ex │ │ ├── page_live.ex │ │ └── simple.ex │ │ ├── router.ex │ │ ├── telemetry.ex │ │ ├── templates │ │ └── layout │ │ │ ├── app.html.eex │ │ │ ├── live.html.leex │ │ │ └── root.html.leex │ │ └── views │ │ ├── error_helpers.ex │ │ ├── error_view.ex │ │ └── layout_view.ex ├── mix.exs ├── mix.lock ├── priv │ ├── gettext │ │ ├── en │ │ │ └── LC_MESSAGES │ │ │ │ └── errors.po │ │ └── errors.pot │ └── repo │ │ ├── migrations │ │ └── .formatter.exs │ │ └── seeds.exs └── test │ ├── demo_web │ ├── live │ │ └── page_live_test.exs │ └── views │ │ ├── error_view_test.exs │ │ └── layout_view_test.exs │ ├── support │ ├── channel_case.ex │ ├── conn_case.ex │ └── data_case.ex │ └── test_helper.exs ├── lib ├── mix │ └── tasks │ │ ├── clean_ssr_script.ex │ │ └── gen_ssr_script.ex ├── react_surface.ex └── react_surface │ ├── live_view_test.ex │ ├── react.ex │ └── ssr.ex ├── mix.exs ├── mix.lock ├── package-lock.json ├── package.json ├── priv ├── react-ssr.js ├── react-surface.js └── ssr.js └── test ├── babel.config.js ├── components └── Hello.js ├── package-lock.json ├── package.json ├── react_surface_test.exs ├── ssr.js ├── support └── conn_case.ex └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | import_deps: [:surface], 4 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 5 | ] 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | react_surface-*.tar 24 | 25 | 26 | # Temporary files for e.g. tests 27 | /tmp 28 | 29 | assets/node_modules/ 30 | 31 | node_modules/ 32 | 33 | .elixir_ls 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Doug 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReactSurface 2 | 3 | Creates a Surface + LiveView container for rendering and updating React components. 4 | 5 | ## Features 6 | - LiveView js hook which performs performs rendering or hydration of the component 7 | - A LiveView React Context that is injected with each render/update providing access to LiveView js functions 8 | - An optional SSR macro that can assist with generating placeholder or static react markup at compile time 9 | - This runs a hydrate when mounting, as it was rendered server side 10 | 11 | In the future: 12 | - Integrate Surface with [React Server Components](https://github.com/josephsavona/rfcs/blob/server-components/text/0000-server-components.md#alternatives) if possible i.e use Surface for writing the React Server Component (Surface AST -> react/jsx?) 13 | 14 | ## Client Setup 15 | 16 | Create a components object to be passed into the hook generation function. 17 | 18 | *It is recommended to create a module that exports all your components used by react-surface.* 19 | `assets/js/components/index.js`: 20 | ```js 21 | import Component1 from "./component1" 22 | import Component2 from "./component2" 23 | 24 | export default { 25 | Component1, 26 | Component2, 27 | } 28 | ``` 29 | 30 | `assets/js/app.js`: 31 | ```js 32 | import components from "./components" 33 | import { buildHook } from "react-surface"; 34 | 35 | // pass component mapping to buildHook function 36 | const reactSurfaceHook = buildHook(components) 37 | // setup liveview as normal, merging your hooks with react-surface hooks. 38 | let liveSocket = new LiveSocket("/live", Socket, { 39 | {...otherhooks, ...reactSurfaceHook}, 40 | params: { _csrf_token: csrfToken }, 41 | }); 42 | ``` 43 | 44 | ## SSR Setup 45 | 46 | If not performing SSR via `use ReactSurface.SSR` this can be skipped. 47 | 48 | ### Run mix task to create SSR script inside your asset directory. 49 | 50 | ``` 51 | mix gen_ssr_script 52 | ``` 53 | 54 | ### Define `node_ssr` config in config.exs 55 | 56 | This is used to configure `node_ssr` at compile time to understand where your components are, and how many instances you are running 57 | 58 | ```elixir 59 | config :node_ssr, 60 | # REQUIRED - This be the folder with assets, and a package.json file - passed to erlexec `:cd` option 61 | assets_path: "#{File.cwd!()}/assets", 62 | # this is the name of the script to be invoked - defaults to "ssr.js", can change it here. 63 | script_name: "ssr.js" 64 | ``` 65 | 66 | Optional requirements: 67 | ``` elixir 68 | component_path: "js/components" # this is the default, relative path to your components directory from assets/. 69 | component_ext: ".js" # this is the default, used with nodejs require statements 70 | count: 1 # this is the number of workers in the nodejs cluster - likely not necessary to have more than 1, unless rendering lots of components 71 | ``` 72 | 73 | ## Example 74 | 75 | ### On the Server in a Surface component 76 | 77 | Where `props` is a map of JSON serializable values. 78 | 79 | ```elixir 80 | 81 | ``` 82 | 83 | Supply an `rid` when rendering multiple of the same component on the same page. (translated to DOM id attrubute) 84 | 85 | ```elixir 86 | 87 | ``` 88 | 89 | This will result in the following DOM being generated in Elixir. 90 | 91 | ```html 92 |
99 |
100 |
101 | ``` 102 | 103 | The props are being base64 encoded (no padding) for the DOM attribute 104 | The ids are generated sha1 hashes, based on the component name, and optional rid prop 105 | 106 | When server rendering it is similar - but with the rendered component contents as a child of the inner div on initial render, and a different hook 107 | 108 | ```html 109 |
116 |
117 |
118 | ``` 119 | 120 | ## LiveView event handling 121 | 122 | LiveView events can be accessed via the `useLiveContext` React hook exported from the javascript package. 123 | This hook returns an object with the functions: `{handleEvent, pushEvent, pushEventTo}` 124 | 125 | See the [HelloReactSurface.js](demo/assets/js/components/HelloReactSurface.js) component for an example. 126 | 127 | 128 | ## Installation 129 | 130 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 131 | by adding `react_surface` to your list of dependencies in `mix.exs`: 132 | 133 | ```elixir 134 | def deps do 135 | [ 136 | {:react_surface, "~> 0.1.0"} 137 | ] 138 | end 139 | ``` 140 | 141 | From github: 142 | 143 | ```elixir 144 | def deps do 145 | [ 146 | {:react_surface, github: "harmon25/react_surface"} 147 | ] 148 | end 149 | ``` 150 | 151 | Add `react-surface` as a dep in your package.json 152 | 153 | ```json 154 | { 155 | "react-surface": "file:../deps/react_surface" 156 | } 157 | ``` 158 | 159 | ## How to add react to your phoenix app 160 | 161 | In your assets dir: 162 | 163 | ```sh 164 | npm install react react-dom --save 165 | npm install @babel/preset-env @babel/preset-react --save-dev 166 | ``` 167 | 168 | Add `"@babel/preset-react"` as a babel preset in `.babelrc` 169 | 170 | Add the following to your `assets/webpack.config.js` file to ensure only a single react + react-dom is included in your bundle: 171 | 172 | ```javascript 173 | module.exports = (env, options) => ({ 174 | // add: 175 | resolve: { 176 | alias: { 177 | react: path.resolve(__dirname, './node_modules/react'), 178 | 'react-dom': path.resolve(__dirname, './node_modules/react-dom') 179 | } 180 | } 181 | // ... 182 | }); 183 | ``` 184 | 185 | ## Inspiration 186 | 187 | This library is inspired by [react-phoenix](https://github.com/geolessel/react-phoenix) and [phoenix_live_react](https://github.com/fidr/phoenix_live_react). 188 | 189 | Check em out if you want to use react components in an eex or leex templates. 190 | -------------------------------------------------------------------------------- /assets/babel.config.js: -------------------------------------------------------------------------------- 1 | // babel.config.js 2 | module.exports = { 3 | presets: [ 4 | "@babel/preset-env", 5 | "@babel/preset-react", 6 | ], 7 | env: { 8 | test: { 9 | presets: [ 10 | [ 11 | "@babel/preset-env", 12 | { 13 | targets: { 14 | esmodules: true, 15 | node: "11", 16 | }, 17 | }, 18 | ], 19 | ], 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /assets/js/react-surface.js: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | // attribute names used on the server. 5 | const COMP_ATTR = "rs-c"; 6 | const PROP_ATTR = "rs-p"; 7 | const METHOD_ATTR = "rs-m"; 8 | 9 | // some exported react utility hooks for accessing + setting up the LiveContext 10 | export const LiveContext = React.createContext({}); 11 | export const useLiveContext = () => React.useContext(LiveContext); 12 | export const LiveContextProvider = ({ children, ...events }) => ( 13 | {children} 14 | ); 15 | 16 | // default options for buildHook function. 17 | const defaultOpts = { 18 | debug: false, 19 | }; 20 | 21 | const logPrefix = "react-surface: "; 22 | 23 | /** 24 | * Builds the LiveView Hooks to be passed to LiveSocket constructor. 25 | * 26 | * @param {Object} components 27 | * @param {Object} opts 28 | */ 29 | export function buildHook(components = {}, opts = defaultOpts) { 30 | opts = { ...defaultOpts, ...opts }; 31 | const hook = { 32 | mounted() { 33 | const [compName, initialProps, method] = extractAttrs(this.el); 34 | if (!components[compName]) { 35 | // this kills LV - needs to be caught early in dev if it arises 36 | throw `${logPrefix}Missing ${compName} in supplied components object (${Object.keys( 37 | components 38 | )})`; 39 | } 40 | 41 | const handleEvent = this.handleEvent.bind(this); 42 | const pushEvent = this.pushEvent.bind(this); 43 | const pushEventTo = this.pushEventTo.bind(this); 44 | this._ReactSurface = { 45 | name: compName, 46 | contextProps: { 47 | handleEvent, 48 | pushEvent, 49 | pushEventTo, 50 | }, 51 | }; 52 | // not sure if this should be a react-surface concern or if this should be something the user should be aware of. (when using react lazy + suspense) 53 | // when this is a full client side render - check if it is lazy, and add a suspense fallback 54 | this._ReactSurface.all = [ 55 | [LiveContextProvider, this._ReactSurface.contextProps], 56 | [components[compName], initialProps], 57 | ]; 58 | 59 | this._ReactSurface.comp = createComponent(this._ReactSurface.all); 60 | 61 | method === "h" 62 | ? ReactDOM.hydrate(this._ReactSurface.comp, this.el.lastChild) 63 | : ReactDOM.render(this._ReactSurface.comp, this.el.lastChild); 64 | 65 | if (opts.debug) 66 | log("mounted " + formatLog([compName, initialProps, method])); 67 | }, 68 | updated() { 69 | const [compName, newProps, method] = extractAttrs(this.el); 70 | const { name } = this._ReactSurface; 71 | 72 | if (compName !== name) 73 | warn("Previous component name differs from updated component name"); 74 | 75 | this._ReactSurface.name = compName; 76 | // update the props of the child component. 77 | this._ReactSurface.all[1][1] = newProps; 78 | 79 | ReactDOM.hydrate( 80 | createComponent(this._ReactSurface.all), 81 | this.el.lastChild 82 | ); 83 | 84 | if (opts.debug) log("updated " + formatLog([compName, newProps, method])); 85 | }, 86 | destroyed() { 87 | ReactDOM.unmountComponentAtNode(this.el.lastChild); 88 | if (opts.debug) log(`Destroyed ${this._ReactSurface.name}`); 89 | }, 90 | }; 91 | 92 | // return the created hook 93 | return { 94 | _RS: hook, 95 | }; 96 | } 97 | 98 | function createComponent([parent_ctx, child_comp]) { 99 | return React.createElement( 100 | parent_ctx[0], 101 | parent_ctx[1], 102 | React.createElement(child_comp[0], child_comp[1]) 103 | ); 104 | } 105 | 106 | // grab component name, and encoded component props from element attributes, and returns the decoded tuple [component, props] 107 | function extractAttrs(el) { 108 | const name = el.attributes[COMP_ATTR].value; 109 | const encodedProps = el.attributes[PROP_ATTR].value; 110 | const method = el.attributes[METHOD_ATTR].value; 111 | return [name, JSON.parse(atob(encodedProps)), method]; 112 | } 113 | 114 | // logging utils 115 | 116 | function formatLog([name, props, method]) { 117 | return `\nname: ${name}\nprops: ${JSON.stringify(props)}\nmethod: ${method}`; 118 | } 119 | 120 | function warn(msg, ...rest) { 121 | console.warn(logPrefix + msg, ...rest); 122 | } 123 | function log(msg, ...rest) { 124 | console.log(logPrefix + msg, ...rest); 125 | } 126 | -------------------------------------------------------------------------------- /assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-surface", 3 | "version": "0.1.0", 4 | "description": "Hook to render react components in a Surface Component.", 5 | "license": "MIT", 6 | "main": "./priv/react-surface.js", 7 | "repository": {}, 8 | "scripts": { 9 | "build": "webpack --mode production", 10 | "watch": "webpack --mode development --watch" 11 | }, 12 | "dependencies": {}, 13 | "devDependencies": { 14 | "@babel/core": "^7.9.0", 15 | "@babel/preset-env": "^7.9.5", 16 | "@babel/preset-react": "^7.12.10", 17 | "babel-loader": "^8.1.0", 18 | "webpack": "4.43.0", 19 | "webpack-cli": "^3.3.11" 20 | }, 21 | "peerDependencies": { 22 | "react": ">=17", 23 | "react-dom": ">=17" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /assets/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | resolve: { 5 | extensions: [".js"], 6 | }, 7 | externals: { 8 | react: { 9 | commonjs: "react", 10 | commonjs2: "react", 11 | amd: "React", 12 | root: "React", 13 | }, 14 | "react-dom": { 15 | commonjs: "react-dom", 16 | commonjs2: "react-dom", 17 | amd: "ReactDOM", 18 | root: "ReactDOM", 19 | }, 20 | }, 21 | entry: "./js/react-surface.js", 22 | output: { 23 | filename: "react-surface.js", 24 | path: path.resolve(__dirname, "../priv"), 25 | library: "react-surface", 26 | libraryTarget: "umd", 27 | }, 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.js$/, 32 | exclude: /node_modules/, 33 | use: { 34 | loader: "babel-loader", 35 | }, 36 | }, 37 | ], 38 | }, 39 | plugins: [], 40 | }; 41 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | config :phoenix, :json_library, Jason 6 | 7 | config :node_ssr, 8 | count: 1, 9 | assets_path: "#{File.cwd!()}/test", 10 | script_name: "ssr.js", 11 | component_path: "components" 12 | # log_prefix: "#{File.cwd!()}/logs" 13 | 14 | # This configuration is loaded before any dependency and is restricted 15 | # to this project. If another project depends on this project, this 16 | # file won't be loaded nor affect the parent project. For this reason, 17 | # if you want to provide default values for your application for 18 | # third-party users, it should be done in your "mix.exs" file. 19 | 20 | # You can configure your application as: 21 | # 22 | # config :surface, key: :value 23 | # 24 | # and access this configuration in your application as: 25 | # 26 | # Application.get_env(:surface, :key) 27 | # 28 | # You can also configure a third-party app: 29 | # 30 | # config :logger, level: :info 31 | # 32 | 33 | # It is also possible to import configuration files, relative to this 34 | # directory. For example, you can emulate configuration per environment 35 | # by uncommenting the line below and defining dev.exs, test.exs and such. 36 | # Configuration from the imported file will override the ones defined 37 | # here (which is why it is important to import them last). 38 | # 39 | # import_config "#{Mix.env()}.exs" 40 | -------------------------------------------------------------------------------- /demo/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto, :phoenix, :surface], 3 | inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | subdirectories: ["priv/*/migrations"] 5 | ] 6 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | demo-*.tar 24 | 25 | # If NPM crashes, it generates a log, let's ignore it too. 26 | npm-debug.log 27 | 28 | # The directory NPM downloads your dependencies sources to. 29 | /assets/node_modules/ 30 | 31 | # Since we are building assets from assets/, 32 | # we ignore priv/static. You may want to comment 33 | # this depending on your deployment strategy. 34 | /priv/static/ 35 | 36 | logs/ -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # Demo 2 | 3 | To start your Phoenix server: 4 | 5 | * Install dependencies with `mix deps.get` 6 | * Create and migrate your database with `mix ecto.setup` 7 | * Install Node.js dependencies with `npm install` inside the `assets` directory 8 | * Start Phoenix endpoint with `mix phx.server` 9 | 10 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 11 | 12 | Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). 13 | 14 | ## Learn more 15 | 16 | * Official website: https://www.phoenixframework.org/ 17 | * Guides: https://hexdocs.pm/phoenix/overview.html 18 | * Docs: https://hexdocs.pm/phoenix 19 | * Forum: https://elixirforum.com/c/phoenix-forum 20 | * Source: https://github.com/phoenixframework/phoenix 21 | -------------------------------------------------------------------------------- /demo/assets/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "esmodules": true 8 | } 9 | } 10 | ], 11 | "@babel/preset-react" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /demo/assets/css/app.scss: -------------------------------------------------------------------------------- 1 | /* This file is for your main application css. */ 2 | @import "./phoenix.css"; 3 | @import "../node_modules/nprogress/nprogress.css"; 4 | 5 | /* LiveView specific classes for your customizations */ 6 | .phx-no-feedback.invalid-feedback, 7 | .phx-no-feedback .invalid-feedback { 8 | display: none; 9 | } 10 | 11 | .phx-click-loading { 12 | opacity: 0.5; 13 | transition: opacity 1s ease-out; 14 | } 15 | 16 | .phx-disconnected{ 17 | cursor: wait; 18 | } 19 | .phx-disconnected *{ 20 | pointer-events: none; 21 | } 22 | 23 | .phx-modal { 24 | opacity: 1!important; 25 | position: fixed; 26 | z-index: 1; 27 | left: 0; 28 | top: 0; 29 | width: 100%; 30 | height: 100%; 31 | overflow: auto; 32 | background-color: rgb(0,0,0); 33 | background-color: rgba(0,0,0,0.4); 34 | } 35 | 36 | .phx-modal-content { 37 | background-color: #fefefe; 38 | margin: 15% auto; 39 | padding: 20px; 40 | border: 1px solid #888; 41 | width: 80%; 42 | } 43 | 44 | .phx-modal-close { 45 | color: #aaa; 46 | float: right; 47 | font-size: 28px; 48 | font-weight: bold; 49 | } 50 | 51 | .phx-modal-close:hover, 52 | .phx-modal-close:focus { 53 | color: black; 54 | text-decoration: none; 55 | cursor: pointer; 56 | } 57 | 58 | 59 | /* Alerts and form errors */ 60 | .alert { 61 | padding: 15px; 62 | margin-bottom: 20px; 63 | border: 1px solid transparent; 64 | border-radius: 4px; 65 | } 66 | .alert-info { 67 | color: #31708f; 68 | background-color: #d9edf7; 69 | border-color: #bce8f1; 70 | } 71 | .alert-warning { 72 | color: #8a6d3b; 73 | background-color: #fcf8e3; 74 | border-color: #faebcc; 75 | } 76 | .alert-danger { 77 | color: #a94442; 78 | background-color: #f2dede; 79 | border-color: #ebccd1; 80 | } 81 | .alert p { 82 | margin-bottom: 0; 83 | } 84 | .alert:empty { 85 | display: none; 86 | } 87 | .invalid-feedback { 88 | color: #a94442; 89 | display: block; 90 | margin: -1rem 0 2rem; 91 | } 92 | -------------------------------------------------------------------------------- /demo/assets/css/phoenix.css: -------------------------------------------------------------------------------- 1 | /* Includes some default style for the starter application. 2 | * This can be safely deleted to start fresh. 3 | */ 4 | 5 | /* Milligram v1.3.0 https://milligram.github.io 6 | * Copyright (c) 2017 CJ Patoilo Licensed under the MIT license 7 | */ 8 | 9 | *,*:after,*:before{box-sizing:inherit}html{box-sizing:border-box;font-size:62.5%}body{color:#000000;font-family:'Helvetica', 'Arial', sans-serif;font-size:1.6em;font-weight:300;line-height:1.6}blockquote{border-left:0.3rem solid #d1d1d1;margin-left:0;margin-right:0;padding:1rem 1.5rem}blockquote *:last-child{margin-bottom:0}.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#0069d9;border:0.1rem solid #0069d9;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.8rem;letter-spacing:.1rem;line-height:3.8rem;padding:0 3.0rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#0069d9;border-color:#0069d9}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#0069d9}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#0069d9}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#0069d9}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#0069d9}code{background:#f4f5f6;border-radius:.4rem;font-size:86%;margin:0 .2rem;padding:.2rem .5rem;white-space:nowrap}pre{background:#f4f5f6;border-left:0.3rem solid #0069d9;overflow-y:hidden}pre>code{border-radius:0;display:block;padding:1rem 1.5rem;white-space:pre}hr{border:0;border-top:0.1rem solid #f4f5f6;margin:3.0rem 0}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #d1d1d1;border-radius:.4rem;box-shadow:none;box-sizing:inherit;height:3.8rem;padding:.6rem 1.0rem;width:100%}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,textarea:focus,select:focus{border-color:#0069d9;outline:0}select{background:url('data:image/svg+xml;utf8,') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,')}textarea{min-height:6.5rem}label,legend{display:block;font-size:1.6rem;font-weight:700;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}.row{display:flex;flex-direction:column;padding:0;width:100%}.row.row-no-padding{padding:0}.row.row-no-padding>.column{padding:0}.row.row-wrap{flex-wrap:wrap}.row.row-top{align-items:flex-start}.row.row-bottom{align-items:flex-end}.row.row-center{align-items:center}.row.row-stretch{align-items:stretch}.row.row-baseline{align-items:baseline}.row .column{display:block;flex:1 1 auto;margin-left:0;max-width:100%;width:100%}.row .column.column-offset-10{margin-left:10%}.row .column.column-offset-20{margin-left:20%}.row .column.column-offset-25{margin-left:25%}.row .column.column-offset-33,.row .column.column-offset-34{margin-left:33.3333%}.row .column.column-offset-50{margin-left:50%}.row .column.column-offset-66,.row .column.column-offset-67{margin-left:66.6666%}.row .column.column-offset-75{margin-left:75%}.row .column.column-offset-80{margin-left:80%}.row .column.column-offset-90{margin-left:90%}.row .column.column-10{flex:0 0 10%;max-width:10%}.row .column.column-20{flex:0 0 20%;max-width:20%}.row .column.column-25{flex:0 0 25%;max-width:25%}.row .column.column-33,.row .column.column-34{flex:0 0 33.3333%;max-width:33.3333%}.row .column.column-40{flex:0 0 40%;max-width:40%}.row .column.column-50{flex:0 0 50%;max-width:50%}.row .column.column-60{flex:0 0 60%;max-width:60%}.row .column.column-66,.row .column.column-67{flex:0 0 66.6666%;max-width:66.6666%}.row .column.column-75{flex:0 0 75%;max-width:75%}.row .column.column-80{flex:0 0 80%;max-width:80%}.row .column.column-90{flex:0 0 90%;max-width:90%}.row .column .column-top{align-self:flex-start}.row .column .column-bottom{align-self:flex-end}.row .column .column-center{-ms-grid-row-align:center;align-self:center}@media (min-width: 40rem){.row{flex-direction:row;margin-left:-1.0rem;width:calc(100% + 2.0rem)}.row .column{margin-bottom:inherit;padding:0 1.0rem}}a{color:#0069d9;text-decoration:none}a:focus,a:hover{color:#606c76}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{font-size:90%;margin:1.5rem 0 1.5rem 3.0rem}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:2.5rem}table{border-spacing:0;width:100%}td,th{border-bottom:0.1rem solid #e1e1e1;padding:1.2rem 1.5rem;text-align:left}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right} 10 | 11 | /* General style */ 12 | h1{font-size: 3.6rem; line-height: 1.25} 13 | h2{font-size: 2.8rem; line-height: 1.3} 14 | h3{font-size: 2.2rem; letter-spacing: -.08rem; line-height: 1.35} 15 | h4{font-size: 1.8rem; letter-spacing: -.05rem; line-height: 1.5} 16 | h5{font-size: 1.6rem; letter-spacing: 0; line-height: 1.4} 17 | h6{font-size: 1.4rem; letter-spacing: 0; line-height: 1.2} 18 | pre{padding: 1em;} 19 | 20 | .container{ 21 | margin: 0 auto; 22 | max-width: 80.0rem; 23 | padding: 0 2.0rem; 24 | position: relative; 25 | width: 100% 26 | } 27 | select { 28 | width: auto; 29 | } 30 | 31 | /* Phoenix promo and logo */ 32 | .phx-hero { 33 | text-align: center; 34 | border-bottom: 1px solid #e3e3e3; 35 | background: #eee; 36 | border-radius: 6px; 37 | padding: 3em 3em 1em; 38 | margin-bottom: 3rem; 39 | font-weight: 200; 40 | font-size: 120%; 41 | } 42 | .phx-hero input { 43 | background: #ffffff; 44 | } 45 | .phx-logo { 46 | min-width: 300px; 47 | margin: 1rem; 48 | display: block; 49 | } 50 | .phx-logo img { 51 | width: auto; 52 | display: block; 53 | } 54 | 55 | /* Headers */ 56 | header { 57 | width: 100%; 58 | background: #fdfdfd; 59 | border-bottom: 1px solid #eaeaea; 60 | margin-bottom: 2rem; 61 | } 62 | header section { 63 | align-items: center; 64 | display: flex; 65 | flex-direction: column; 66 | justify-content: space-between; 67 | } 68 | header section :first-child { 69 | order: 2; 70 | } 71 | header section :last-child { 72 | order: 1; 73 | } 74 | header nav ul, 75 | header nav li { 76 | margin: 0; 77 | padding: 0; 78 | display: block; 79 | text-align: right; 80 | white-space: nowrap; 81 | } 82 | header nav ul { 83 | margin: 1rem; 84 | margin-top: 0; 85 | } 86 | header nav a { 87 | display: block; 88 | } 89 | 90 | @media (min-width: 40.0rem) { /* Small devices (landscape phones, 576px and up) */ 91 | header section { 92 | flex-direction: row; 93 | } 94 | header nav ul { 95 | margin: 1rem; 96 | } 97 | .phx-logo { 98 | flex-basis: 527px; 99 | margin: 2rem 1rem; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /demo/assets/js/app.js: -------------------------------------------------------------------------------- 1 | // We need to import the CSS so that webpack will load it. 2 | // The MiniCssExtractPlugin is used to separate it out into 3 | // its own CSS file. 4 | import "../css/app.scss"; 5 | 6 | // webpack automatically bundles all modules in your 7 | // entry points. Those entry points can be configured 8 | // in "webpack.config.js". 9 | // 10 | // Import deps with the dep name or local files with a relative path, for example: 11 | // 12 | // import {Socket} from "phoenix" 13 | // import socket from "./socket" 14 | // 15 | import "phoenix_html"; 16 | import { Socket } from "phoenix"; 17 | import NProgress from "nprogress"; 18 | import { LiveSocket } from "phoenix_live_view"; 19 | // import components 20 | // import HelloReactSurface from "./components/HelloReactSurface"; 21 | import components from "./components"; 22 | // import buildHook function from `react_surface` 23 | import { buildHook } from "react-surface"; 24 | 25 | // pass components object to buildHook function 26 | // returns an hooks object that can be merged with existing liveview hooks object, 27 | // or passed directly to hooks liveSocket option if it is the only hook. 28 | const hooks = buildHook(components, { debug: true }); 29 | // const hooks = buildHook(components); 30 | 31 | // regular liveview setup. 32 | let csrfToken = document 33 | .querySelector("meta[name='csrf-token']") 34 | .getAttribute("content"); 35 | let liveSocket = new LiveSocket("/live", Socket, { 36 | hooks, 37 | params: { _csrf_token: csrfToken }, 38 | }); 39 | 40 | // Show progress bar on live navigation and form submits 41 | window.addEventListener("phx:page-loading-start", (info) => NProgress.start()); 42 | window.addEventListener("phx:page-loading-stop", (info) => NProgress.done()); 43 | 44 | // connect if there are any LiveViews on the page 45 | liveSocket.connect(); 46 | 47 | // expose liveSocket on window for web console debug logs and latency simulation: 48 | // >> liveSocket.enableDebug() 49 | // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session 50 | // >> liveSocket.disableLatencySim() 51 | window.liveSocket = liveSocket; 52 | -------------------------------------------------------------------------------- /demo/assets/js/components/HelloReactSurface.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useLiveContext } from "react-surface"; 3 | 4 | // props are provided by the server. 5 | const HelloReactSurface = (props) => { 6 | const [count, setCount] = useState(0); 7 | const { pushEvent, handleEvent, pushEventTo } = useLiveContext(); 8 | 9 | useEffect(() => { 10 | handleEvent("from_server", (val) => { 11 | console.log("from server!"); 12 | console.log(val); 13 | }); 14 | }, []); 15 | 16 | const incCount = () => { 17 | setCount((c) => c + 1); 18 | }; 19 | 20 | const handleSubmit = (e) => { 21 | e.preventDefault(); 22 | const data = new FormData(e.target); 23 | pushEvent("update_name", { new_name: data.get("name") }); 24 | }; 25 | 26 | return ( 27 |
28 |

Props:

29 |
{JSON.stringify(props)}
30 |
31 |
32 | 33 | 34 |
35 |
36 |

Count: {count}

37 | 40 |
41 | 44 |
45 | ); 46 | }; 47 | 48 | export default HelloReactSurface -------------------------------------------------------------------------------- /demo/assets/js/components/Simple.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default ({ name }) =>

WHOA! {name}

; 4 | -------------------------------------------------------------------------------- /demo/assets/js/components/index.js: -------------------------------------------------------------------------------- 1 | import Simple from "./Simple"; 2 | import HelloReactSurface from "./HelloReactSurface" 3 | 4 | export default { 5 | Simple, 6 | HelloReactSurface 7 | }; 8 | -------------------------------------------------------------------------------- /demo/assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": {}, 3 | "description": " ", 4 | "license": "MIT", 5 | "scripts": { 6 | "deploy": "webpack --mode production", 7 | "watch": "webpack --mode development --watch" 8 | }, 9 | "dependencies": { 10 | "nprogress": "^0.2.0", 11 | "phoenix": "file:../deps/phoenix", 12 | "phoenix_html": "file:../deps/phoenix_html", 13 | "phoenix_live_view": "file:../deps/phoenix_live_view", 14 | "react-surface": "file:../../../react_surface", 15 | "react": "^17.0.1", 16 | "react-dom": "^17.0.1" 17 | }, 18 | "devDependencies": { 19 | "@babel/core": "^7.0.0", 20 | "@babel/preset-env": "^7.12.11", 21 | "@babel/preset-react": "^7.12.10", 22 | "@babel/register": "^7.12.10", 23 | "babel-loader": "^8.0.0", 24 | "copy-webpack-plugin": "^5.1.1", 25 | "css-loader": "^3.4.2", 26 | "hard-source-webpack-plugin": "^0.13.1", 27 | "mini-css-extract-plugin": "^0.9.0", 28 | "node-sass": "^4.13.1", 29 | "optimize-css-assets-webpack-plugin": "^5.0.1", 30 | "sass-loader": "^8.0.2", 31 | "terser-webpack-plugin": "^2.3.2", 32 | "webpack": "4.41.5", 33 | "webpack-cli": "^3.3.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /demo/assets/ssr.js: -------------------------------------------------------------------------------- 1 | require("@babel/register")({ cwd: __dirname }); 2 | // a render function that takes a component name + props and returns a json response 3 | const { startService, render } = require("react-surface/priv/react-ssr"); 4 | 5 | const opts = { 6 | debug: false, 7 | }; 8 | 9 | // starts listening on a random tcp port for render requests 10 | startService(render, opts); 11 | -------------------------------------------------------------------------------- /demo/assets/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harmon25/react_surface/03c9c5bb8041e5ac8318b9e99447b1ffb47541bb/demo/assets/static/favicon.ico -------------------------------------------------------------------------------- /demo/assets/static/images/phoenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harmon25/react_surface/03c9c5bb8041e5ac8318b9e99447b1ffb47541bb/demo/assets/static/images/phoenix.png -------------------------------------------------------------------------------- /demo/assets/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /demo/assets/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HardSourceWebpackPlugin = require("hard-source-webpack-plugin"); 3 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | const TerserPlugin = require("terser-webpack-plugin"); 5 | const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); 6 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 7 | 8 | module.exports = (env, options) => { 9 | const devMode = options.mode !== "production"; 10 | 11 | return { 12 | optimization: { 13 | minimizer: [ 14 | new TerserPlugin({ cache: true, parallel: true, sourceMap: devMode }), 15 | new OptimizeCSSAssetsPlugin({}), 16 | ], 17 | }, 18 | entry: { 19 | app: "./js/app.js", 20 | }, 21 | output: { 22 | filename: "js/[name].js", 23 | publicPath: "/", 24 | path: path.resolve(__dirname, "../priv/static"), 25 | }, 26 | resolve: { 27 | symlinks: false, 28 | alias: { 29 | react: path.resolve(__dirname, "./node_modules/react"), 30 | "react-dom": path.resolve(__dirname, "./node_modules/react-dom"), 31 | }, 32 | }, 33 | devtool: devMode ? "eval-cheap-module-source-map" : undefined, 34 | module: { 35 | rules: [ 36 | { 37 | test: /\.js$/, 38 | exclude: /node_modules/, 39 | use: { 40 | loader: "babel-loader", 41 | }, 42 | }, 43 | { 44 | test: /\.[s]?css$/, 45 | use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"], 46 | }, 47 | ], 48 | }, 49 | plugins: [ 50 | new MiniCssExtractPlugin({ filename: "./css/app.css" }), 51 | new CopyWebpackPlugin([{ from: "static/", to: "./" }]), 52 | ].concat(devMode ? [new HardSourceWebpackPlugin()] : []), 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /demo/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | 7 | # General application configuration 8 | use Mix.Config 9 | 10 | config :node_ssr, 11 | assets_path: "#{File.cwd!()}/assets", 12 | script_name: "ssr.js" 13 | 14 | config :demo, 15 | ecto_repos: [Demo.Repo] 16 | 17 | # Configures the endpoint 18 | config :demo, DemoWeb.Endpoint, 19 | url: [host: "localhost"], 20 | secret_key_base: "MTHx1F64afPN9FmhbPt5Rssv9YMn4Y9ICw6WuZ2uGGzWgsfII+pbO3aaQSlUSeic", 21 | render_errors: [view: DemoWeb.ErrorView, accepts: ~w(html json), layout: false], 22 | pubsub_server: Demo.PubSub, 23 | live_view: [signing_salt: "Uq0u5SDm"] 24 | 25 | # Configures Elixir's Logger 26 | config :logger, :console, 27 | format: "$time $metadata[$level] $message\n", 28 | metadata: [:request_id] 29 | 30 | # Use Jason for JSON parsing in Phoenix 31 | config :phoenix, :json_library, Jason 32 | 33 | # Import environment specific config. This must remain at the bottom 34 | # of this file so it overrides the configuration defined above. 35 | import_config "#{Mix.env()}.exs" 36 | -------------------------------------------------------------------------------- /demo/config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # Configure your database 4 | config :demo, Demo.Repo, 5 | username: "postgres", 6 | password: "postgres", 7 | database: "demo_dev", 8 | hostname: "localhost", 9 | show_sensitive_data_on_connection_error: true, 10 | pool_size: 10 11 | 12 | # For development, we disable any cache and enable 13 | # debugging and code reloading. 14 | # 15 | # The watchers configuration can be used to run external 16 | # watchers to your application. For example, we use it 17 | # with webpack to recompile .js and .css sources. 18 | config :demo, DemoWeb.Endpoint, 19 | http: [port: 4000], 20 | debug_errors: true, 21 | code_reloader: true, 22 | check_origin: false, 23 | watchers: [ 24 | node: [ 25 | "node_modules/webpack/bin/webpack.js", 26 | "--mode", 27 | "development", 28 | "--watch-stdin", 29 | cd: Path.expand("../assets", __DIR__) 30 | ] 31 | ] 32 | 33 | # ## SSL Support 34 | # 35 | # In order to use HTTPS in development, a self-signed 36 | # certificate can be generated by running the following 37 | # Mix task: 38 | # 39 | # mix phx.gen.cert 40 | # 41 | # Note that this task requires Erlang/OTP 20 or later. 42 | # Run `mix help phx.gen.cert` for more information. 43 | # 44 | # The `http:` config above can be replaced with: 45 | # 46 | # https: [ 47 | # port: 4001, 48 | # cipher_suite: :strong, 49 | # keyfile: "priv/cert/selfsigned_key.pem", 50 | # certfile: "priv/cert/selfsigned.pem" 51 | # ], 52 | # 53 | # If desired, both `http:` and `https:` keys can be 54 | # configured to run both http and https servers on 55 | # different ports. 56 | 57 | # Watch static and templates for browser reloading. 58 | config :demo, DemoWeb.Endpoint, 59 | live_reload: [ 60 | patterns: [ 61 | ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", 62 | ~r"priv/gettext/.*(po)$", 63 | ~r"lib/demo_web/(live|views)/.*(ex|.sface)$", 64 | ~r"lib/demo_web/templates/.*(eex)$" 65 | ] 66 | ] 67 | 68 | # Do not include metadata nor timestamps in development logs 69 | config :logger, :console, format: "[$level] $message\n" 70 | 71 | # Set a higher stacktrace during development. Avoid configuring such 72 | # in production as building large stacktraces may be expensive. 73 | config :phoenix, :stacktrace_depth, 20 74 | 75 | # Initialize plugs at runtime for faster development compilation 76 | config :phoenix, :plug_init_mode, :runtime 77 | -------------------------------------------------------------------------------- /demo/config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For production, don't forget to configure the url host 4 | # to something meaningful, Phoenix uses this information 5 | # when generating URLs. 6 | # 7 | # Note we also include the path to a cache manifest 8 | # containing the digested version of static files. This 9 | # manifest is generated by the `mix phx.digest` task, 10 | # which you should run after static files are built and 11 | # before starting your production server. 12 | config :demo, DemoWeb.Endpoint, 13 | url: [host: "example.com", port: 80], 14 | cache_static_manifest: "priv/static/cache_manifest.json" 15 | 16 | # Do not print debug messages in production 17 | config :logger, level: :info 18 | 19 | # ## SSL Support 20 | # 21 | # To get SSL working, you will need to add the `https` key 22 | # to the previous section and set your `:url` port to 443: 23 | # 24 | # config :demo, DemoWeb.Endpoint, 25 | # ... 26 | # url: [host: "example.com", port: 443], 27 | # https: [ 28 | # port: 443, 29 | # cipher_suite: :strong, 30 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 31 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH"), 32 | # transport_options: [socket_opts: [:inet6]] 33 | # ] 34 | # 35 | # The `cipher_suite` is set to `:strong` to support only the 36 | # latest and more secure SSL ciphers. This means old browsers 37 | # and clients may not be supported. You can set it to 38 | # `:compatible` for wider support. 39 | # 40 | # `:keyfile` and `:certfile` expect an absolute path to the key 41 | # and cert in disk or a relative path inside priv, for example 42 | # "priv/ssl/server.key". For all supported SSL configuration 43 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 44 | # 45 | # We also recommend setting `force_ssl` in your endpoint, ensuring 46 | # no data is ever sent via http, always redirecting to https: 47 | # 48 | # config :demo, DemoWeb.Endpoint, 49 | # force_ssl: [hsts: true] 50 | # 51 | # Check `Plug.SSL` for all available options in `force_ssl`. 52 | 53 | # Finally import the config/prod.secret.exs which loads secrets 54 | # and configuration from environment variables. 55 | import_config "prod.secret.exs" 56 | -------------------------------------------------------------------------------- /demo/config/prod.secret.exs: -------------------------------------------------------------------------------- 1 | # In this file, we load production configuration and secrets 2 | # from environment variables. You can also hardcode secrets, 3 | # although such is generally not recommended and you have to 4 | # remember to add this file to your .gitignore. 5 | use Mix.Config 6 | 7 | database_url = 8 | System.get_env("DATABASE_URL") || 9 | raise """ 10 | environment variable DATABASE_URL is missing. 11 | For example: ecto://USER:PASS@HOST/DATABASE 12 | """ 13 | 14 | config :demo, Demo.Repo, 15 | # ssl: true, 16 | url: database_url, 17 | pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10") 18 | 19 | secret_key_base = 20 | System.get_env("SECRET_KEY_BASE") || 21 | raise """ 22 | environment variable SECRET_KEY_BASE is missing. 23 | You can generate one by calling: mix phx.gen.secret 24 | """ 25 | 26 | config :demo, DemoWeb.Endpoint, 27 | http: [ 28 | port: String.to_integer(System.get_env("PORT") || "4000"), 29 | transport_options: [socket_opts: [:inet6]] 30 | ], 31 | secret_key_base: secret_key_base 32 | 33 | # ## Using releases (Elixir v1.9+) 34 | # 35 | # If you are doing OTP releases, you need to instruct Phoenix 36 | # to start each relevant endpoint: 37 | # 38 | # config :demo, DemoWeb.Endpoint, server: true 39 | # 40 | # Then you can assemble a release by calling `mix release`. 41 | # See `mix help release` for more information. 42 | -------------------------------------------------------------------------------- /demo/config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # Configure your database 4 | # 5 | # The MIX_TEST_PARTITION environment variable can be used 6 | # to provide built-in test partitioning in CI environment. 7 | # Run `mix help test` for more information. 8 | config :demo, Demo.Repo, 9 | username: "postgres", 10 | password: "postgres", 11 | database: "demo_test#{System.get_env("MIX_TEST_PARTITION")}", 12 | hostname: "localhost", 13 | pool: Ecto.Adapters.SQL.Sandbox 14 | 15 | # We don't run a server during test. If one is required, 16 | # you can enable the server option below. 17 | config :demo, DemoWeb.Endpoint, 18 | http: [port: 4002], 19 | server: false 20 | 21 | # Print only warnings and errors during test 22 | config :logger, level: :warn 23 | -------------------------------------------------------------------------------- /demo/lib/demo.ex: -------------------------------------------------------------------------------- 1 | defmodule Demo do 2 | @moduledoc """ 3 | Demo keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /demo/lib/demo/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Demo.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | def start(_type, _args) do 9 | children = [ 10 | # Start the Ecto repository 11 | # Demo.Repo, 12 | # Start the Telemetry supervisor 13 | DemoWeb.Telemetry, 14 | # Start the PubSub system 15 | {Phoenix.PubSub, name: Demo.PubSub}, 16 | # Start the Endpoint (http/https) 17 | DemoWeb.Endpoint 18 | # Start a worker by calling: Demo.Worker.start_link(arg) 19 | # {Demo.Worker, arg} 20 | ] 21 | 22 | # See https://hexdocs.pm/elixir/Supervisor.html 23 | # for other strategies and supported options 24 | opts = [strategy: :one_for_one, name: Demo.Supervisor] 25 | Supervisor.start_link(children, opts) 26 | end 27 | 28 | # Tell Phoenix to update the endpoint configuration 29 | # whenever the application is updated. 30 | def config_change(changed, _new, removed) do 31 | DemoWeb.Endpoint.config_change(changed, removed) 32 | :ok 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /demo/lib/demo/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Demo.Repo do 2 | use Ecto.Repo, 3 | otp_app: :demo, 4 | adapter: Ecto.Adapters.Postgres 5 | end 6 | -------------------------------------------------------------------------------- /demo/lib/demo_web.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, views, channels and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use DemoWeb, :controller 9 | use DemoWeb, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define any helper function in modules 17 | and import those modules here. 18 | """ 19 | 20 | def controller do 21 | quote do 22 | use Phoenix.Controller, namespace: DemoWeb 23 | 24 | import Plug.Conn 25 | import DemoWeb.Gettext 26 | alias DemoWeb.Router.Helpers, as: Routes 27 | end 28 | end 29 | 30 | def view do 31 | quote do 32 | use Phoenix.View, 33 | root: "lib/demo_web/templates", 34 | namespace: DemoWeb 35 | 36 | # Import convenience functions from controllers 37 | import Phoenix.Controller, 38 | only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1] 39 | 40 | # Include shared imports and aliases for views 41 | unquote(view_helpers()) 42 | end 43 | end 44 | 45 | def live_view do 46 | quote do 47 | use Phoenix.LiveView, 48 | layout: {DemoWeb.LayoutView, "live.html"} 49 | 50 | unquote(view_helpers()) 51 | end 52 | end 53 | 54 | def live_component do 55 | quote do 56 | use Phoenix.LiveComponent 57 | 58 | unquote(view_helpers()) 59 | end 60 | end 61 | 62 | def router do 63 | quote do 64 | use Phoenix.Router 65 | 66 | import Plug.Conn 67 | import Phoenix.Controller 68 | import Phoenix.LiveView.Router 69 | end 70 | end 71 | 72 | def channel do 73 | quote do 74 | use Phoenix.Channel 75 | import DemoWeb.Gettext 76 | end 77 | end 78 | 79 | defp view_helpers do 80 | quote do 81 | # Use all HTML functionality (forms, tags, etc) 82 | use Phoenix.HTML 83 | 84 | # Import LiveView helpers (live_render, live_component, live_patch, etc) 85 | import Phoenix.LiveView.Helpers 86 | 87 | # Import basic rendering functionality (render, render_layout, etc) 88 | import Phoenix.View 89 | 90 | import DemoWeb.ErrorHelpers 91 | import DemoWeb.Gettext 92 | alias DemoWeb.Router.Helpers, as: Routes 93 | end 94 | end 95 | 96 | @doc """ 97 | When used, dispatch to the appropriate controller/view/etc. 98 | """ 99 | defmacro __using__(which) when is_atom(which) do 100 | apply(__MODULE__, which, []) 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /demo/lib/demo_web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoWeb.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "room:*", DemoWeb.RoomChannel 6 | 7 | # Socket params are passed from the client and can 8 | # be used to verify and authenticate a user. After 9 | # verification, you can put default assigns into 10 | # the socket that will be set for all channels, ie 11 | # 12 | # {:ok, assign(socket, :user_id, verified_user_id)} 13 | # 14 | # To deny connection, return `:error`. 15 | # 16 | # See `Phoenix.Token` documentation for examples in 17 | # performing token verification on connect. 18 | @impl true 19 | def connect(_params, socket, _connect_info) do 20 | {:ok, socket} 21 | end 22 | 23 | # Socket id's are topics that allow you to identify all sockets for a given user: 24 | # 25 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}" 26 | # 27 | # Would allow you to broadcast a "disconnect" event and terminate 28 | # all active sockets and channels for a given user: 29 | # 30 | # DemoWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) 31 | # 32 | # Returning `nil` makes this socket anonymous. 33 | @impl true 34 | def id(_socket), do: nil 35 | end 36 | -------------------------------------------------------------------------------- /demo/lib/demo_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :demo 3 | 4 | # The session will be stored in the cookie and signed, 5 | # this means its contents can be read but not tampered with. 6 | # Set :encryption_salt if you would also like to encrypt it. 7 | @session_options [ 8 | store: :cookie, 9 | key: "_demo_key", 10 | signing_salt: "mxaoGxYe" 11 | ] 12 | 13 | socket "/socket", DemoWeb.UserSocket, 14 | websocket: true, 15 | longpoll: false 16 | 17 | socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] 18 | 19 | # Serve at "/" the static files from "priv/static" directory. 20 | # 21 | # You should set gzip to true if you are running phx.digest 22 | # when deploying your static files in production. 23 | plug Plug.Static, 24 | at: "/", 25 | from: :demo, 26 | gzip: false, 27 | only: ~w(css fonts images js favicon.ico robots.txt) 28 | 29 | # Code reloading can be explicitly enabled under the 30 | # :code_reloader configuration of your endpoint. 31 | if code_reloading? do 32 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 33 | plug Phoenix.LiveReloader 34 | plug Phoenix.CodeReloader 35 | plug Phoenix.Ecto.CheckRepoStatus, otp_app: :demo 36 | end 37 | 38 | plug Phoenix.LiveDashboard.RequestLogger, 39 | param_key: "request_logger", 40 | cookie_key: "request_logger" 41 | 42 | plug Plug.RequestId 43 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] 44 | 45 | plug Plug.Parsers, 46 | parsers: [:urlencoded, :multipart, :json], 47 | pass: ["*/*"], 48 | json_decoder: Phoenix.json_library() 49 | 50 | plug Plug.MethodOverride 51 | plug Plug.Head 52 | plug Plug.Session, @session_options 53 | plug DemoWeb.Router 54 | end 55 | -------------------------------------------------------------------------------- /demo/lib/demo_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoWeb.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import DemoWeb.Gettext 9 | 10 | # Simple translation 11 | gettext("Here is the string to translate") 12 | 13 | # Plural translation 14 | ngettext("Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3) 17 | 18 | # Domain-based translation 19 | dgettext("errors", "Here is the error message to translate") 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :demo 24 | end 25 | -------------------------------------------------------------------------------- /demo/lib/demo_web/live/hello_react_surface.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoWeb.HelloReactSurface do 2 | @moduledoc """ 3 | Example of a compile time, server rendered component 4 | """ 5 | use ReactSurface.SSR, [default_props: %{name: "Doug"} ] 6 | end 7 | -------------------------------------------------------------------------------- /demo/lib/demo_web/live/page_live.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoWeb.PageLive do 2 | # use DemoWeb, :live_view 3 | use Surface.LiveView 4 | alias ReactSurface.React 5 | alias DemoWeb.{HelloReactSurface, Simple} 6 | 7 | data component_props, :map, default: %{name: "Doug"} 8 | data show_react, :boolean, default: true 9 | 10 | @impl true 11 | def render(assigns) do 12 | ~H""" 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | """ 22 | end 23 | 24 | @impl true 25 | def handle_event("update_name", %{"new_name" => new_name}, socket) do 26 | # passes new component props - hydrating the component with new props - retaining internal state. 27 | {:noreply, assign(socket, :component_props, %{name: new_name})} 28 | end 29 | 30 | @impl true 31 | def handle_event("toggle-react", _, socket) do 32 | # show and hide react element - test react node is 'destroyed' when lv removes parent from dom. 33 | {:noreply, assign(socket, :show_react, !socket.assigns.show_react)} 34 | end 35 | 36 | @impl true 37 | def handle_event("trigger-event", _, socket) do 38 | # receive data from within react component via handleEvent 39 | {:noreply, push_event(socket, "from_server", %{data_from_server: "WHOOA"})} 40 | end 41 | 42 | end 43 | -------------------------------------------------------------------------------- /demo/lib/demo_web/live/simple.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoWeb.Simple do 2 | @moduledoc """ 3 | Example of a compile time, server rendered component 4 | """ 5 | use ReactSurface.SSR, [default_props: %{name: "Doug"} ] 6 | end 7 | -------------------------------------------------------------------------------- /demo/lib/demo_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoWeb.Router do 2 | use DemoWeb, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_live_flash 8 | plug :put_root_layout, {DemoWeb.LayoutView, :root} 9 | plug :protect_from_forgery 10 | plug :put_secure_browser_headers 11 | end 12 | 13 | pipeline :api do 14 | plug :accepts, ["json"] 15 | end 16 | 17 | scope "/", DemoWeb do 18 | pipe_through :browser 19 | 20 | live "/", PageLive, :index 21 | end 22 | 23 | # Other scopes may use custom stacks. 24 | # scope "/api", DemoWeb do 25 | # pipe_through :api 26 | # end 27 | 28 | # Enables LiveDashboard only for development 29 | # 30 | # If you want to use the LiveDashboard in production, you should put 31 | # it behind authentication and allow only admins to access it. 32 | # If your application does not have an admins-only section yet, 33 | # you can use Plug.BasicAuth to set up some basic authentication 34 | # as long as you are also using SSL (which you should anyway). 35 | # if Mix.env() in [:dev, :test] do 36 | # import Phoenix.LiveDashboard.Router 37 | 38 | # scope "/" do 39 | # pipe_through :browser 40 | # live_dashboard "/dashboard", metrics: DemoWeb.Telemetry 41 | # end 42 | # end 43 | end 44 | -------------------------------------------------------------------------------- /demo/lib/demo_web/telemetry.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoWeb.Telemetry do 2 | use Supervisor 3 | import Telemetry.Metrics 4 | 5 | def start_link(arg) do 6 | Supervisor.start_link(__MODULE__, arg, name: __MODULE__) 7 | end 8 | 9 | @impl true 10 | def init(_arg) do 11 | children = [ 12 | # Telemetry poller will execute the given period measurements 13 | # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics 14 | {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} 15 | # Add reporters as children of your supervision tree. 16 | # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} 17 | ] 18 | 19 | Supervisor.init(children, strategy: :one_for_one) 20 | end 21 | 22 | def metrics do 23 | [ 24 | # Phoenix Metrics 25 | summary("phoenix.endpoint.stop.duration", 26 | unit: {:native, :millisecond} 27 | ), 28 | summary("phoenix.router_dispatch.stop.duration", 29 | tags: [:route], 30 | unit: {:native, :millisecond} 31 | ), 32 | 33 | # Database Metrics 34 | summary("demo.repo.query.total_time", unit: {:native, :millisecond}), 35 | summary("demo.repo.query.decode_time", unit: {:native, :millisecond}), 36 | summary("demo.repo.query.query_time", unit: {:native, :millisecond}), 37 | summary("demo.repo.query.queue_time", unit: {:native, :millisecond}), 38 | summary("demo.repo.query.idle_time", unit: {:native, :millisecond}), 39 | 40 | # VM Metrics 41 | summary("vm.memory.total", unit: {:byte, :kilobyte}), 42 | summary("vm.total_run_queue_lengths.total"), 43 | summary("vm.total_run_queue_lengths.cpu"), 44 | summary("vm.total_run_queue_lengths.io") 45 | ] 46 | end 47 | 48 | defp periodic_measurements do 49 | [ 50 | # A module, function and arguments to be invoked periodically. 51 | # This function must call :telemetry.execute/3 and a metric must be added above. 52 | # {DemoWeb, :count_users, []} 53 | ] 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /demo/lib/demo_web/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | <%= @inner_content %> 5 |
6 | -------------------------------------------------------------------------------- /demo/lib/demo_web/templates/layout/live.html.leex: -------------------------------------------------------------------------------- 1 |
2 | 5 | 6 | 9 | 10 | <%= @inner_content %> 11 |
12 | -------------------------------------------------------------------------------- /demo/lib/demo_web/templates/layout/root.html.leex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= csrf_meta_tag() %> 8 | <%= live_title_tag assigns[:page_title] || "Demo", suffix: " · Phoenix Framework" %> 9 | "/> 10 | 11 | 12 | 13 | <%= @inner_content %> 14 | 15 | 16 | -------------------------------------------------------------------------------- /demo/lib/demo_web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoWeb.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | def error_tag(form, field) do 12 | Enum.map(Keyword.get_values(form.errors, field), fn error -> 13 | content_tag(:span, translate_error(error), 14 | class: "invalid-feedback", 15 | phx_feedback_for: input_id(form, field) 16 | ) 17 | end) 18 | end 19 | 20 | @doc """ 21 | Translates an error message using gettext. 22 | """ 23 | def translate_error({msg, opts}) do 24 | # When using gettext, we typically pass the strings we want 25 | # to translate as a static argument: 26 | # 27 | # # Translate "is invalid" in the "errors" domain 28 | # dgettext("errors", "is invalid") 29 | # 30 | # # Translate the number of files with plural rules 31 | # dngettext("errors", "1 file", "%{count} files", count) 32 | # 33 | # Because the error messages we show in our forms and APIs 34 | # are defined inside Ecto, we need to translate them dynamically. 35 | # This requires us to call the Gettext module passing our gettext 36 | # backend as first argument. 37 | # 38 | # Note we use the "errors" domain, which means translations 39 | # should be written to the errors.po file. The :count option is 40 | # set by Ecto and indicates we should also apply plural rules. 41 | if count = opts[:count] do 42 | Gettext.dngettext(DemoWeb.Gettext, "errors", msg, msg, count, opts) 43 | else 44 | Gettext.dgettext(DemoWeb.Gettext, "errors", msg, opts) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /demo/lib/demo_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoWeb.ErrorView do 2 | use DemoWeb, :view 3 | 4 | # If you want to customize a particular status code 5 | # for a certain format, you may uncomment below. 6 | # def render("500.html", _assigns) do 7 | # "Internal Server Error" 8 | # end 9 | 10 | # By default, Phoenix returns the status message from 11 | # the template name. For example, "404.html" becomes 12 | # "Not Found". 13 | def template_not_found(template, _assigns) do 14 | Phoenix.Controller.status_message_from_template(template) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /demo/lib/demo_web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoWeb.LayoutView do 2 | use DemoWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /demo/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Demo.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :demo, 7 | version: "0.1.0", 8 | elixir: "~> 1.7", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | compilers: [:phoenix, :gettext] ++ Mix.compilers(), 11 | start_permanent: Mix.env() == :prod, 12 | aliases: aliases(), 13 | deps: deps() 14 | ] 15 | end 16 | 17 | # Configuration for the OTP application. 18 | # 19 | # Type `mix help compile.app` for more information. 20 | def application do 21 | [ 22 | mod: {Demo.Application, []}, 23 | extra_applications: [:logger, :runtime_tools] 24 | ] 25 | end 26 | 27 | # Specifies which paths to compile per environment. 28 | defp elixirc_paths(:test), do: ["lib", "test/support"] 29 | defp elixirc_paths(_), do: ["lib"] 30 | 31 | # Specifies your project dependencies. 32 | # 33 | # Type `mix help deps` for examples and options. 34 | defp deps do 35 | [ 36 | {:phoenix, "~> 1.5.7"}, 37 | {:phoenix_ecto, "~> 4.1"}, 38 | {:ecto_sql, "~> 3.4"}, 39 | {:postgrex, ">= 0.0.0"}, 40 | {:phoenix_live_view, "~> 0.15.0"}, 41 | {:floki, ">= 0.27.0", only: :test}, 42 | {:phoenix_html, "~> 2.11"}, 43 | {:phoenix_live_reload, "~> 1.2", only: :dev}, 44 | {:phoenix_live_dashboard, "~> 0.4"}, 45 | {:telemetry_metrics, "~> 0.4"}, 46 | {:telemetry_poller, "~> 0.4"}, 47 | {:gettext, "~> 0.11"}, 48 | {:jason, "~> 1.0"}, 49 | {:plug_cowboy, "~> 2.0"}, 50 | {:surface, "~> 0.1.1"}, 51 | {:react_surface, path: "../"} 52 | ] 53 | end 54 | 55 | # Aliases are shortcuts or tasks specific to the current project. 56 | # For example, to install project dependencies and perform other setup tasks, run: 57 | # 58 | # $ mix setup 59 | # 60 | # See the documentation for `Mix` for more info on aliases. 61 | defp aliases do 62 | [ 63 | setup: ["deps.get", "ecto.setup", "cmd npm install --prefix assets"], 64 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 65 | "ecto.reset": ["ecto.drop", "ecto.setup"], 66 | test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"] 67 | ] 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /demo/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "certifi": {:hex, :certifi, "2.5.3", "70bdd7e7188c804f3a30ee0e7c99655bc35d8ac41c23e12325f36ab449b70651", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "ed516acb3929b101208a9d700062d520f3953da3b6b918d866106ffa980e1c10"}, 3 | "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, 4 | "cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"}, 5 | "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"}, 6 | "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"}, 7 | "db_connection": {:hex, :db_connection, "2.3.1", "4c9f3ed1ef37471cbdd2762d6655be11e38193904d9c5c1c9389f1b891a3088e", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "abaab61780dde30301d840417890bd9f74131041afd02174cf4e10635b3a63f5"}, 8 | "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, 9 | "earmark": {:hex, :earmark, "1.4.13", "2c6ce9768fc9fdbf4046f457e207df6360ee6c91ee1ecb8e9a139f96a4289d91", [:mix], [{:earmark_parser, ">= 1.4.12", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "a0cf3ed88ef2b1964df408889b5ecb886d1a048edde53497fc935ccd15af3403"}, 10 | "earmark_parser": {:hex, :earmark_parser, "1.4.12", "b245e875ec0a311a342320da0551da407d9d2b65d98f7a9597ae078615af3449", [:mix], [], "hexpm", "711e2cc4d64abb7d566d43f54b78f7dc129308a63bc103fbd88550d2174b3160"}, 11 | "ecto": {:hex, :ecto, "3.5.5", "48219a991bb86daba6e38a1e64f8cea540cded58950ff38fbc8163e062281a07", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "98dd0e5e1de7f45beca6130d13116eae675db59adfa055fb79612406acf6f6f1"}, 12 | "ecto_sql": {:hex, :ecto_sql, "3.5.3", "1964df0305538364b97cc4661a2bd2b6c89d803e66e5655e4e55ff1571943efd", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.5.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d2f53592432ce17d3978feb8f43e8dc0705e288b0890caf06d449785f018061c"}, 13 | "erlexec": {:git, "https://github.com/saleyn/erlexec.git", "8d423b2abbff82ecfb715191bca887974d558b06", []}, 14 | "exexec": {:hex, :exexec, "0.2.0", "a6ffc48cba3ac9420891b847e4dc7120692fb8c08c9e82220ebddc0bb8d96103", [:mix], [{:erlexec, "~> 1.10", [hex: :erlexec, repo: "hexpm", optional: false]}], "hexpm", "312cd1c9befba9e078e57f3541e4f4257eabda6eb9c348154fe899d6ac633299"}, 15 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 16 | "floki": {:hex, :floki, "0.29.0", "b1710d8c93a2f860dc2d7adc390dd808dc2fb8f78ee562304457b75f4c640881", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "008585ce64b9f74c07d32958ec9866f4b8a124bf4da1e2941b28e41384edaaad"}, 17 | "gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"}, 18 | "hackney": {:hex, :hackney, "1.17.0", "717ea195fd2f898d9fe9f1ce0afcc2621a41ecfe137fae57e7fe6e9484b9aa99", [:rebar3], [{:certifi, "~>2.5", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "64c22225f1ea8855f584720c0e5b3cd14095703af1c9fbc845ba042811dc671c"}, 19 | "html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"}, 20 | "httpoison": {:hex, :httpoison, "1.7.0", "abba7d086233c2d8574726227b6c2c4f6e53c4deae7fe5f6de531162ce9929a0", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "975cc87c845a103d3d1ea1ccfd68a2700c211a434d8428b10c323dc95dc5b980"}, 21 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 22 | "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, 23 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 24 | "mime": {:hex, :mime, "1.5.0", "203ef35ef3389aae6d361918bf3f952fa17a09e8e43b5aa592b93eba05d0fb8d", [:mix], [], "hexpm", "55a94c0f552249fc1a3dd9cd2d3ab9de9d3c89b559c2bd01121f824834f24746"}, 25 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 26 | "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, 27 | "node_ssr": {:git, "https://github.com/harmon25/node_ssr.git", "ab6198738c9c63f3f92a4f6ef98375fef74a2acf", [branch: "main"]}, 28 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 29 | "phoenix": {:hex, :phoenix, "1.5.7", "2923bb3af924f184459fe4fa4b100bd25fa6468e69b2803dfae82698269aa5e0", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "774cd64417c5a3788414fdbb2be2eb9bcd0c048d9e6ad11a0c1fd67b7c0d0978"}, 30 | "phoenix_ecto": {:hex, :phoenix_ecto, "4.2.1", "13f124cf0a3ce0f1948cf24654c7b9f2347169ff75c1123f44674afee6af3b03", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "478a1bae899cac0a6e02be1deec7e2944b7754c04e7d4107fc5a517f877743c0"}, 31 | "phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"}, 32 | "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.4.0", "87990e68b60213d7487e65814046f9a2bed4a67886c943270125913499b3e5c3", [:mix], [{:ecto_psql_extras, "~> 0.4.1 or ~> 0.5", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.14.1 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.15.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.4.0 or ~> 0.5.0 or ~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "8d52149e58188e9e4497cc0d8900ab94d9b66f96998ec38c47c7a4f8f4f50e57"}, 33 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.0", "f35f61c3f959c9a01b36defaa1f0624edd55b87e236b606664a556d6f72fd2e7", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "02c1007ae393f2b76ec61c1a869b1e617179877984678babde131d716f95b582"}, 34 | "phoenix_live_view": {:hex, :phoenix_live_view, "0.15.3", "70c7917e5c421e32d1a1c8ddf8123378bb741748cd8091eb9d557fb4be92a94f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 0.5", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cabcfb6738419a08600009219a5f0d861de97507fc1232121e1d5221aba849bd"}, 35 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, 36 | "plug": {:hex, :plug, "1.11.0", "f17217525597628298998bc3baed9f8ea1fa3f1160aa9871aee6df47a6e4d38e", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2d9c633f0499f9dc5c2fd069161af4e2e7756890b81adcbb2ceaa074e8308876"}, 37 | "plug_cowboy": {:hex, :plug_cowboy, "2.4.1", "779ba386c0915027f22e14a48919a9545714f849505fa15af2631a0d298abf0f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d72113b6dff7b37a7d9b2a5b68892808e3a9a752f2bf7e503240945385b70507"}, 38 | "plug_crypto": {:hex, :plug_crypto, "1.2.0", "1cb20793aa63a6c619dd18bb33d7a3aa94818e5fd39ad357051a67f26dfa2df6", [:mix], [], "hexpm", "a48b538ae8bf381ffac344520755f3007cc10bd8e90b240af98ea29b69683fc2"}, 39 | "postgrex": {:hex, :postgrex, "0.15.7", "724410acd48abac529d0faa6c2a379fb8ae2088e31247687b16cacc0e0883372", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "88310c010ff047cecd73d5ceca1d99205e4b1ab1b9abfdab7e00f5c9d20ef8f9"}, 40 | "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, 41 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, 42 | "surface": {:hex, :surface, "0.1.1", "812249c5832d1b36051101a3fe68eac374758cc4437652bf9bf2cb14ab052fb7", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.15.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "08b2c00ad1a3c6d0f60fa05bda43aa90b112f71e7989870a6e756670fdc1698c"}, 43 | "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, 44 | "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.0", "da9d49ee7e6bb1c259d36ce6539cd45ae14d81247a2b0c90edf55e2b50507f7b", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5cfe67ad464b243835512aa44321cee91faed6ea868d7fb761d7016e02915c3d"}, 45 | "telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"}, 46 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 47 | } 48 | -------------------------------------------------------------------------------- /demo/priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | 13 | ## From Ecto.Changeset.cast/4 14 | msgid "can't be blank" 15 | msgstr "" 16 | 17 | ## From Ecto.Changeset.unique_constraint/3 18 | msgid "has already been taken" 19 | msgstr "" 20 | 21 | ## From Ecto.Changeset.put_change/3 22 | msgid "is invalid" 23 | msgstr "" 24 | 25 | ## From Ecto.Changeset.validate_acceptance/3 26 | msgid "must be accepted" 27 | msgstr "" 28 | 29 | ## From Ecto.Changeset.validate_format/3 30 | msgid "has invalid format" 31 | msgstr "" 32 | 33 | ## From Ecto.Changeset.validate_subset/3 34 | msgid "has an invalid entry" 35 | msgstr "" 36 | 37 | ## From Ecto.Changeset.validate_exclusion/3 38 | msgid "is reserved" 39 | msgstr "" 40 | 41 | ## From Ecto.Changeset.validate_confirmation/3 42 | msgid "does not match confirmation" 43 | msgstr "" 44 | 45 | ## From Ecto.Changeset.no_assoc_constraint/3 46 | msgid "is still associated with this entry" 47 | msgstr "" 48 | 49 | msgid "are still associated with this entry" 50 | msgstr "" 51 | 52 | ## From Ecto.Changeset.validate_length/3 53 | msgid "should be %{count} character(s)" 54 | msgid_plural "should be %{count} character(s)" 55 | msgstr[0] "" 56 | msgstr[1] "" 57 | 58 | msgid "should have %{count} item(s)" 59 | msgid_plural "should have %{count} item(s)" 60 | msgstr[0] "" 61 | msgstr[1] "" 62 | 63 | msgid "should be at least %{count} character(s)" 64 | msgid_plural "should be at least %{count} character(s)" 65 | msgstr[0] "" 66 | msgstr[1] "" 67 | 68 | msgid "should have at least %{count} item(s)" 69 | msgid_plural "should have at least %{count} item(s)" 70 | msgstr[0] "" 71 | msgstr[1] "" 72 | 73 | msgid "should be at most %{count} character(s)" 74 | msgid_plural "should be at most %{count} character(s)" 75 | msgstr[0] "" 76 | msgstr[1] "" 77 | 78 | msgid "should have at most %{count} item(s)" 79 | msgid_plural "should have at most %{count} item(s)" 80 | msgstr[0] "" 81 | msgstr[1] "" 82 | 83 | ## From Ecto.Changeset.validate_number/3 84 | msgid "must be less than %{number}" 85 | msgstr "" 86 | 87 | msgid "must be greater than %{number}" 88 | msgstr "" 89 | 90 | msgid "must be less than or equal to %{number}" 91 | msgstr "" 92 | 93 | msgid "must be greater than or equal to %{number}" 94 | msgstr "" 95 | 96 | msgid "must be equal to %{number}" 97 | msgstr "" 98 | -------------------------------------------------------------------------------- /demo/priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here has no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | 11 | ## From Ecto.Changeset.cast/4 12 | msgid "can't be blank" 13 | msgstr "" 14 | 15 | ## From Ecto.Changeset.unique_constraint/3 16 | msgid "has already been taken" 17 | msgstr "" 18 | 19 | ## From Ecto.Changeset.put_change/3 20 | msgid "is invalid" 21 | msgstr "" 22 | 23 | ## From Ecto.Changeset.validate_acceptance/3 24 | msgid "must be accepted" 25 | msgstr "" 26 | 27 | ## From Ecto.Changeset.validate_format/3 28 | msgid "has invalid format" 29 | msgstr "" 30 | 31 | ## From Ecto.Changeset.validate_subset/3 32 | msgid "has an invalid entry" 33 | msgstr "" 34 | 35 | ## From Ecto.Changeset.validate_exclusion/3 36 | msgid "is reserved" 37 | msgstr "" 38 | 39 | ## From Ecto.Changeset.validate_confirmation/3 40 | msgid "does not match confirmation" 41 | msgstr "" 42 | 43 | ## From Ecto.Changeset.no_assoc_constraint/3 44 | msgid "is still associated with this entry" 45 | msgstr "" 46 | 47 | msgid "are still associated with this entry" 48 | msgstr "" 49 | 50 | ## From Ecto.Changeset.validate_length/3 51 | msgid "should be %{count} character(s)" 52 | msgid_plural "should be %{count} character(s)" 53 | msgstr[0] "" 54 | msgstr[1] "" 55 | 56 | msgid "should have %{count} item(s)" 57 | msgid_plural "should have %{count} item(s)" 58 | msgstr[0] "" 59 | msgstr[1] "" 60 | 61 | msgid "should be at least %{count} character(s)" 62 | msgid_plural "should be at least %{count} character(s)" 63 | msgstr[0] "" 64 | msgstr[1] "" 65 | 66 | msgid "should have at least %{count} item(s)" 67 | msgid_plural "should have at least %{count} item(s)" 68 | msgstr[0] "" 69 | msgstr[1] "" 70 | 71 | msgid "should be at most %{count} character(s)" 72 | msgid_plural "should be at most %{count} character(s)" 73 | msgstr[0] "" 74 | msgstr[1] "" 75 | 76 | msgid "should have at most %{count} item(s)" 77 | msgid_plural "should have at most %{count} item(s)" 78 | msgstr[0] "" 79 | msgstr[1] "" 80 | 81 | ## From Ecto.Changeset.validate_number/3 82 | msgid "must be less than %{number}" 83 | msgstr "" 84 | 85 | msgid "must be greater than %{number}" 86 | msgstr "" 87 | 88 | msgid "must be less than or equal to %{number}" 89 | msgstr "" 90 | 91 | msgid "must be greater than or equal to %{number}" 92 | msgstr "" 93 | 94 | msgid "must be equal to %{number}" 95 | msgstr "" 96 | -------------------------------------------------------------------------------- /demo/priv/repo/migrations/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto_sql], 3 | inputs: ["*.exs"] 4 | ] 5 | -------------------------------------------------------------------------------- /demo/priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # Demo.Repo.insert!(%Demo.SomeSchema{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | -------------------------------------------------------------------------------- /demo/test/demo_web/live/page_live_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DemoWeb.PageLiveTest do 2 | use DemoWeb.ConnCase 3 | 4 | import Phoenix.LiveViewTest 5 | 6 | test "disconnected and connected render", %{conn: conn} do 7 | {:ok, page_live, disconnected_html} = live(conn, "/") 8 | assert disconnected_html =~ "Welcome to Phoenix!" 9 | assert render(page_live) =~ "Welcome to Phoenix!" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /demo/test/demo_web/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DemoWeb.ErrorViewTest do 2 | use DemoWeb.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(DemoWeb.ErrorView, "404.html", []) == "Not Found" 9 | end 10 | 11 | test "renders 500.html" do 12 | assert render_to_string(DemoWeb.ErrorView, "500.html", []) == "Internal Server Error" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /demo/test/demo_web/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DemoWeb.LayoutViewTest do 2 | use DemoWeb.ConnCase, async: true 3 | 4 | # When testing helpers, you may want to import Phoenix.HTML and 5 | # use functions such as safe_to_string() to convert the helper 6 | # result into an HTML string. 7 | # import Phoenix.HTML 8 | end 9 | -------------------------------------------------------------------------------- /demo/test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoWeb.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | we enable the SQL sandbox, so changes done to the database 12 | are reverted at the end of every test. If you are using 13 | PostgreSQL, you can even run database tests asynchronously 14 | by setting `use DemoWeb.ChannelCase, async: true`, although 15 | this option is not recommended for other databases. 16 | """ 17 | 18 | use ExUnit.CaseTemplate 19 | 20 | using do 21 | quote do 22 | # Import conveniences for testing with channels 23 | import Phoenix.ChannelTest 24 | import DemoWeb.ChannelCase 25 | 26 | # The default endpoint for testing 27 | @endpoint DemoWeb.Endpoint 28 | end 29 | end 30 | 31 | setup tags do 32 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Demo.Repo) 33 | 34 | unless tags[:async] do 35 | Ecto.Adapters.SQL.Sandbox.mode(Demo.Repo, {:shared, self()}) 36 | end 37 | 38 | :ok 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /demo/test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoWeb.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | we enable the SQL sandbox, so changes done to the database 12 | are reverted at the end of every test. If you are using 13 | PostgreSQL, you can even run database tests asynchronously 14 | by setting `use DemoWeb.ConnCase, async: true`, although 15 | this option is not recommended for other databases. 16 | """ 17 | 18 | use ExUnit.CaseTemplate 19 | 20 | using do 21 | quote do 22 | # Import conveniences for testing with connections 23 | import Plug.Conn 24 | import Phoenix.ConnTest 25 | import DemoWeb.ConnCase 26 | 27 | alias DemoWeb.Router.Helpers, as: Routes 28 | 29 | # The default endpoint for testing 30 | @endpoint DemoWeb.Endpoint 31 | end 32 | end 33 | 34 | setup tags do 35 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Demo.Repo) 36 | 37 | unless tags[:async] do 38 | Ecto.Adapters.SQL.Sandbox.mode(Demo.Repo, {:shared, self()}) 39 | end 40 | 41 | {:ok, conn: Phoenix.ConnTest.build_conn()} 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /demo/test/support/data_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Demo.DataCase do 2 | @moduledoc """ 3 | This module defines the setup for tests requiring 4 | access to the application's data layer. 5 | 6 | You may define functions here to be used as helpers in 7 | your tests. 8 | 9 | Finally, if the test case interacts with the database, 10 | we enable the SQL sandbox, so changes done to the database 11 | are reverted at the end of every test. If you are using 12 | PostgreSQL, you can even run database tests asynchronously 13 | by setting `use Demo.DataCase, async: true`, although 14 | this option is not recommended for other databases. 15 | """ 16 | 17 | use ExUnit.CaseTemplate 18 | 19 | using do 20 | quote do 21 | alias Demo.Repo 22 | 23 | import Ecto 24 | import Ecto.Changeset 25 | import Ecto.Query 26 | import Demo.DataCase 27 | end 28 | end 29 | 30 | setup tags do 31 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Demo.Repo) 32 | 33 | unless tags[:async] do 34 | Ecto.Adapters.SQL.Sandbox.mode(Demo.Repo, {:shared, self()}) 35 | end 36 | 37 | :ok 38 | end 39 | 40 | @doc """ 41 | A helper that transforms changeset errors into a map of messages. 42 | 43 | assert {:error, changeset} = Accounts.create_user(%{password: "short"}) 44 | assert "password is too short" in errors_on(changeset).password 45 | assert %{password: ["password is too short"]} = errors_on(changeset) 46 | 47 | """ 48 | def errors_on(changeset) do 49 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> 50 | Regex.replace(~r"%{(\w+)}", message, fn _, key -> 51 | opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() 52 | end) 53 | end) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /demo/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | Ecto.Adapters.SQL.Sandbox.mode(Demo.Repo, :manual) 3 | -------------------------------------------------------------------------------- /lib/mix/tasks/clean_ssr_script.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.CleanSsrScript do 2 | @moduledoc "Cleans ssr script from assets directory" 3 | 4 | use Mix.Task 5 | 6 | def run(_args) do 7 | ["#{File.cwd!()}/assets/ssr.js"] 8 | |> Enum.each(&File.rm!(&1)) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/mix/tasks/gen_ssr_script.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.GenSsrScript do 2 | @moduledoc "Generates the ssr script for your assets directory" 3 | 4 | use Mix.Task 5 | 6 | def run(_args) do 7 | source = Application.app_dir(:react_surface, "priv") <> "/ssr.js" 8 | dest = "#{File.cwd!()}/assets/ssr.js" 9 | File.cp!(source, dest) 10 | 11 | # make file executable. 12 | File.chmod(dest, 0o755) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/react_surface.ex: -------------------------------------------------------------------------------- 1 | defmodule ReactSurface do 2 | @moduledoc """ 3 | Documentation for `ReactSurface`. 4 | """ 5 | @spec ssr(String.t(), map()) :: String.t() 6 | def ssr(component_name, props) do 7 | Application.ensure_all_started(:node_ssr) 8 | 9 | {:ok, %{markup: html}} = NodeSsr.render(component_name, props) 10 | html 11 | end 12 | 13 | @spec hash_id(String.t()) :: String.t() 14 | def hash_id(str), do: :crypto.hash(:sha, str) |> Base.encode16() |> String.slice(0,8) 15 | end 16 | -------------------------------------------------------------------------------- /lib/react_surface/live_view_test.ex: -------------------------------------------------------------------------------- 1 | defmodule ReactSurface.LiveViewTest do 2 | @moduledoc """ 3 | Conveniences for testing Surface components. 4 | 5 | Copied from Surface Repo. 6 | """ 7 | 8 | alias Phoenix.LiveView.{Diff, Socket} 9 | 10 | defmodule BlockWrapper do 11 | @moduledoc false 12 | 13 | use Surface.Component 14 | 15 | slot default, required: true 16 | 17 | def render(assigns) do 18 | ~H""" 19 | 20 | """ 21 | end 22 | end 23 | 24 | defmacro __using__(_opts) do 25 | quote do 26 | import Phoenix.ConnTest 27 | import Phoenix.LiveViewTest 28 | import Phoenix.LiveView.Helpers, only: [live_component: 3, live_component: 4] 29 | import Surface, only: [sigil_H: 2] 30 | import ReactSurface.LiveViewTest 31 | end 32 | end 33 | 34 | @doc """ 35 | Render Surface code. 36 | 37 | Use this macro when testing regular rendering of stateless components or live components 38 | that don't require a parent live view during the tests. 39 | 40 | For tests depending on the existence of a parent live view, e.g. testing events on live 41 | components and its side-effects, you need to use either `Phoenix.LiveViewTest.live/2` or 42 | `Phoenix.LiveViewTest.live_isolated/3`. 43 | 44 | ## Example 45 | 46 | html = 47 | render_surface do 48 | ~H"\"" 49 | 50 | "\"" 51 | end 52 | 53 | assert html =~ "\"" 54 | user 55 | "\"" 56 | 57 | """ 58 | defmacro render_surface(do: do_block) do 59 | render_component_call = 60 | quote do 61 | Surface.LiveViewTest.render_component_with_block( 62 | Surface.LiveViewTest.BlockWrapper, 63 | %{__context__: %{}, __surface__: %{provided_templates: [:default]}}, 64 | do: unquote(do_block) 65 | ) 66 | end 67 | 68 | if Macro.Env.has_var?(__CALLER__, {:assigns, nil}) do 69 | quote do 70 | var!(assigns) = Map.merge(var!(assigns), %{__context__: %{}}) 71 | unquote(render_component_call) |> Surface.LiveViewTest.clean_html() 72 | end 73 | else 74 | quote do 75 | var!(assigns) = %{__context__: %{}} 76 | unquote(render_component_call) |> Surface.LiveViewTest.clean_html() 77 | end 78 | end 79 | end 80 | 81 | @doc """ 82 | Compiles Surface code into a new LiveView module. 83 | 84 | This macro should be used sparingly as it always generates and compiles a new module 85 | on-the-fly, which can potentially slow down your test suite. 86 | 87 | Its main use is when testing compile-time errors/warnings. 88 | 89 | ## Example 90 | 91 | code = 92 | quote do 93 | ~H"\"" 94 | 95 | "\"" 96 | end 97 | 98 | message = 99 | ~S(code:1: invalid value for property "prop". Expected a :keyword, got: "some string".) 100 | 101 | assert_raise(CompileError, message, fn -> 102 | compile_surface(code) 103 | end) 104 | 105 | """ 106 | defmacro compile_surface(code, assigns \\ quote(do: %{})) do 107 | env = Map.take(__CALLER__, [:function, :module, :line]) 108 | 109 | quote do 110 | ast = 111 | unquote(__MODULE__).generate_live_view_ast( 112 | unquote(code), 113 | unquote(assigns), 114 | unquote(Macro.escape(env)) 115 | ) 116 | 117 | {{:module, module, _, _}, _} = Code.eval_quoted(ast, [], %{__ENV__ | file: "code", line: 0}) 118 | 119 | module 120 | end 121 | end 122 | 123 | @doc """ 124 | Wraps a test code so it runs using a custom configuration for a given component. 125 | 126 | Tests using this macro should run synchronously. A warning is shown if the test 127 | case is configured as `async: true`. 128 | 129 | ## Example 130 | 131 | using_config TextInput, default_class: "default_class" do 132 | html = 133 | render_surface do 134 | ~H"\"" 135 | 136 | "\"" 137 | end 138 | 139 | assert html =~ ~r/class="default_class"/ 140 | end 141 | 142 | """ 143 | defmacro using_config(component, config, do: block) do 144 | if Module.get_attribute(__CALLER__.module, :ex_unit_async) do 145 | message = """ 146 | Using `using_config` with `async: true` might lead to race conditions. 147 | 148 | Please set `async: false` on the test module. 149 | """ 150 | 151 | Surface.IOHelper.warn(message, __CALLER__, & &1) 152 | end 153 | 154 | quote do 155 | component = unquote(component) 156 | old_config = Application.get_env(:surface, :components, []) 157 | value = unquote(config) 158 | new_config = Keyword.update(old_config, component, value, fn _ -> value end) 159 | Application.put_env(:surface, :components, new_config) 160 | 161 | try do 162 | unquote(block) 163 | after 164 | Application.put_env(:surface, :components, old_config) 165 | end 166 | end 167 | end 168 | 169 | @doc false 170 | def generate_live_view_ast(render_code, props, env) do 171 | {func, _} = env.function 172 | module = Module.concat([env.module, "(#{func}) at line #{env.line}"]) 173 | 174 | props_ast = 175 | for {name, _} <- props do 176 | quote do 177 | prop unquote(Macro.var(name, nil)), :any 178 | end 179 | end 180 | 181 | quote do 182 | defmodule unquote(module) do 183 | use Surface.LiveView 184 | 185 | unquote_splicing(props_ast) 186 | 187 | def render(var!(assigns)) do 188 | var!(assigns) = Map.merge(var!(assigns), unquote(Macro.escape(props))) 189 | unquote(render_code) 190 | end 191 | end 192 | end 193 | end 194 | 195 | # Custom version of phoenix's `render_component` that supports 196 | # passing a inner_block. This should be used until a compatible 197 | # version of `phoenix_live_view` is released. 198 | 199 | @doc false 200 | defmacro render_component_with_block(component, assigns, opts \\ [], do_block \\ []) do 201 | {do_block, opts} = 202 | case {do_block, opts} do 203 | {[do: do_block], _} -> {do_block, opts} 204 | {_, [do: do_block]} -> {do_block, []} 205 | {_, _} -> {nil, opts} 206 | end 207 | 208 | endpoint = 209 | Module.get_attribute(__CALLER__.module, :endpoint) || 210 | raise ArgumentError, 211 | "the module attribute @endpoint is not set for #{inspect(__MODULE__)}" 212 | 213 | socket = 214 | quote do 215 | %Socket{endpoint: unquote(endpoint), router: unquote(opts)[:router]} 216 | end 217 | 218 | if do_block do 219 | quote do 220 | socket = unquote(socket) 221 | var!(assigns) = Map.put(var!(assigns), :socket, socket) 222 | 223 | inner_block = fn _, _args -> 224 | unquote(do_block) 225 | end 226 | 227 | assigns = unquote(assigns) |> Map.new() |> Map.put(:inner_block, inner_block) 228 | Surface.LiveViewTest.__render_component_with_block__(socket, unquote(component), assigns) 229 | end 230 | else 231 | quote do 232 | assigns = Map.new(unquote(assigns)) 233 | 234 | Surface.LiveViewTest.__render_component_with_block__( 235 | unquote(socket), 236 | unquote(component), 237 | assigns 238 | ) 239 | end 240 | end 241 | end 242 | 243 | @doc false 244 | def __render_component_with_block__(socket, component, assigns) do 245 | mount_assigns = if assigns[:id], do: %{myself: %Phoenix.LiveComponent.CID{cid: -1}}, else: %{} 246 | rendered = Diff.component_to_rendered(socket, component, assigns, mount_assigns) 247 | {_, diff, _} = Diff.render(socket, rendered, Diff.new_components()) 248 | diff |> Diff.to_iodata() |> IO.iodata_to_binary() 249 | end 250 | 251 | @doc false 252 | def clean_html(html) do 253 | html 254 | |> String.replace(~r/\n+/, "\n") 255 | |> String.replace(~r/\n\s+\n/, "\n") 256 | end 257 | end 258 | -------------------------------------------------------------------------------- /lib/react_surface/react.ex: -------------------------------------------------------------------------------- 1 | defmodule ReactSurface.React do 2 | @moduledoc """ 3 | Container for rendering and interacting with a react component tree in a LiveView/Surface app. 4 | """ 5 | use Surface.Component 6 | 7 | @doc "React ID - used to generate a unique DOM ID used for container elements, uses component name if not supplied" 8 | prop rid, :string 9 | 10 | @doc "Name of component, should exist as a key in the components object passed to the `buildHook` js function" 11 | prop component, :string 12 | 13 | @doc "Props passed to the react component" 14 | prop props, :map, default: %{} 15 | 16 | @doc "Class for container div" 17 | prop container_class, :css_class, default: [] 18 | 19 | @doc "Passed to container div :attrs" 20 | prop opts, :keyword, default: [] 21 | 22 | @doc "Used by SSR macro, do not set manually" 23 | prop ssr, :boolean, default: false 24 | 25 | @doc "Used by SSR macro, do not set manually" 26 | slot default 27 | 28 | def render(assigns) do 29 | id = build_id(assigns) 30 | ~H"
id] }}>
" 31 | end 32 | 33 | defp build_attrs( 34 | %{component: component, props: props_map, id: id} = props, 35 | other_attrs 36 | ) do 37 | # encode props into a base64 encoded string, should be more efficient on the wire, and better easier diffs than the htmlsafe raw json string. 38 | encoded_props = Jason.encode!(props_map) |> Base.encode64(padding: false) 39 | 40 | Keyword.merge(other_attrs, 41 | id: id, 42 | "rs-c": component, 43 | "rs-p": encoded_props, 44 | "rs-m": method_name(props), 45 | "phx-hook": hook_name(props) 46 | ) 47 | end 48 | 49 | # hydration hook 50 | defp hook_name(_), do: "_RS" 51 | # clientside render hook 52 | # defp hook_name(%{ssr: false}), do: "_RSR" 53 | defp method_name(%{ssr: false}), do: "r" 54 | defp method_name(%{ssr: true}), do: "h" 55 | defp build_id(%{rid: nil, component: comp}), do: ReactSurface.hash_id(comp) 56 | defp build_id(%{rid: id, component: comp}), do: ReactSurface.hash_id("#{id}#{comp}") 57 | 58 | end 59 | -------------------------------------------------------------------------------- /lib/react_surface/ssr.ex: -------------------------------------------------------------------------------- 1 | defmodule ReactSurface.SSR do 2 | @moduledoc """ 3 | Macro to transform a module into a server rendered react component with some default props. 4 | 5 | ```elixir 6 | defmodule ReactComponent do 7 | use ReactSurface.SSR, [default_props: %{name: "Doug"}] 8 | end 9 | ``` 10 | This assumes there is a react component that is the default export of a js module with the same name. 11 | 12 | The expected location is `assets/js/components/` 13 | 14 | The above example would import and generate static markup based on the react component located: 15 | `assets/js/components/ReactComponent.js` with the props `{"name": "Doug"}` 16 | 17 | ```js 18 | export default ({name}) =>

Hi {name}

19 | ``` 20 | 21 | Which can now be used in any surface component 22 | ```elixir 23 | ~H\"\"\" 24 | 25 | \"\"\" 26 | ``` 27 | """ 28 | 29 | defmacro __using__(opts) do 30 | quote bind_quoted: [opts: opts] do 31 | Module.register_attribute(__MODULE__, :rendered_content, accumulate: false) 32 | use Surface.Component 33 | 34 | alias ReactSurface.React 35 | 36 | @doc "React ID - used to generate a unique DOM ID used for container elements, uses component name if not supplied" 37 | prop rid, :string 38 | 39 | @doc "Props passed to the react component" 40 | prop props, :map, default: %{} 41 | 42 | @doc "Class for container div" 43 | prop container_class, :css_class, default: [] 44 | 45 | @doc "Passed to container div :attrs" 46 | prop opts, :keyword, default: [] 47 | 48 | @impl true 49 | def render(var!(assigns)) do 50 | ~H""" 51 | {{ {:safe, get_ssr()} }} 52 | """ 53 | end 54 | 55 | @component_name Module.split(__MODULE__) |> List.last() 56 | 57 | 58 | Module.put_attribute( 59 | __MODULE__, 60 | :rendered_content, 61 | ReactSurface.ssr(@component_name, opts[:default_props] || %{}) 62 | ) 63 | 64 | def get_ssr() do 65 | @rendered_content 66 | end 67 | 68 | def component_name() do 69 | Module.split(__MODULE__) |> List.last() 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ReactSurface.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :react_surface, 7 | version: "0.5.0", 8 | elixir: "~> 1.11", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | package: package(), 11 | compilers: [:phoenix] ++ Mix.compilers(), 12 | description: description(), 13 | start_permanent: Mix.env() == :prod, 14 | aliases: aliases(), 15 | deps: deps() 16 | ] 17 | end 18 | 19 | # Run "mix help compile.app" to learn about applications. 20 | def application do 21 | [ 22 | extra_applications: [:logger] 23 | ] 24 | end 25 | 26 | defp elixirc_paths(:test), do: ["lib", "test/support"] 27 | defp elixirc_paths(_), do: ["lib"] 28 | 29 | # Run "mix help deps" to learn about dependencies. 30 | defp deps do 31 | [ 32 | {:jason, "~> 1.1"}, 33 | {:surface, "~> 0.2.0"}, 34 | {:node_ssr, github: "harmon25/node_ssr", branch: "main", runtime: false}, 35 | {:floki, ">= 0.27.0", only: :test} 36 | ] 37 | end 38 | 39 | def description do 40 | """ 41 | A helper library for rendering React components via [Surface](http://surface-demo.msaraiva.io/) 42 | """ 43 | end 44 | 45 | defp aliases do 46 | [test: "test test/react_surface_test.exs"] 47 | end 48 | 49 | defp package do 50 | [ 51 | name: :react_surface, 52 | files: ["lib", "priv", "mix.exs", "package.json", "README*", "LICENSE*"], 53 | maintainers: ["Doug W."], 54 | licenses: ["MIT"], 55 | links: %{"GitHub" => "https://github.com/harmon25/react_surface"} 56 | ] 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "certifi": {:hex, :certifi, "2.5.3", "70bdd7e7188c804f3a30ee0e7c99655bc35d8ac41c23e12325f36ab449b70651", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "ed516acb3929b101208a9d700062d520f3953da3b6b918d866106ffa980e1c10"}, 3 | "earmark": {:hex, :earmark, "1.4.13", "2c6ce9768fc9fdbf4046f457e207df6360ee6c91ee1ecb8e9a139f96a4289d91", [:mix], [{:earmark_parser, ">= 1.4.12", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "a0cf3ed88ef2b1964df408889b5ecb886d1a048edde53497fc935ccd15af3403"}, 4 | "earmark_parser": {:hex, :earmark_parser, "1.4.12", "b245e875ec0a311a342320da0551da407d9d2b65d98f7a9597ae078615af3449", [:mix], [], "hexpm", "711e2cc4d64abb7d566d43f54b78f7dc129308a63bc103fbd88550d2174b3160"}, 5 | "erlexec": {:git, "https://github.com/saleyn/erlexec.git", "8d423b2abbff82ecfb715191bca887974d558b06", []}, 6 | "floki": {:hex, :floki, "0.29.0", "b1710d8c93a2f860dc2d7adc390dd808dc2fb8f78ee562304457b75f4c640881", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "008585ce64b9f74c07d32958ec9866f4b8a124bf4da1e2941b28e41384edaaad"}, 7 | "hackney": {:hex, :hackney, "1.17.0", "717ea195fd2f898d9fe9f1ce0afcc2621a41ecfe137fae57e7fe6e9484b9aa99", [:rebar3], [{:certifi, "~>2.5", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "64c22225f1ea8855f584720c0e5b3cd14095703af1c9fbc845ba042811dc671c"}, 8 | "html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"}, 9 | "httpoison": {:hex, :httpoison, "1.8.0", "6b85dea15820b7804ef607ff78406ab449dd78bed923a49c7160e1886e987a3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "28089eaa98cf90c66265b6b5ad87c59a3729bea2e74e9d08f9b51eb9729b3c3a"}, 10 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 11 | "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, 12 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 13 | "mime": {:hex, :mime, "1.5.0", "203ef35ef3389aae6d361918bf3f952fa17a09e8e43b5aa592b93eba05d0fb8d", [:mix], [], "hexpm", "55a94c0f552249fc1a3dd9cd2d3ab9de9d3c89b559c2bd01121f824834f24746"}, 14 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 15 | "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, 16 | "node_ssr": {:git, "https://github.com/harmon25/node_ssr.git", "ab6198738c9c63f3f92a4f6ef98375fef74a2acf", [branch: "main"]}, 17 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 18 | "phoenix": {:hex, :phoenix, "1.5.7", "2923bb3af924f184459fe4fa4b100bd25fa6468e69b2803dfae82698269aa5e0", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "774cd64417c5a3788414fdbb2be2eb9bcd0c048d9e6ad11a0c1fd67b7c0d0978"}, 19 | "phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"}, 20 | "phoenix_live_view": {:hex, :phoenix_live_view, "0.15.4", "86908dc9603cc81c07e84725ee42349b5325cb250c9c20d3533856ff18dbb7dc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 0.5", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "35d78f3c35fe10a995dca5f4ab50165b7a90cbe02e23de245381558f821e9462"}, 21 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, 22 | "plug": {:hex, :plug, "1.11.0", "f17217525597628298998bc3baed9f8ea1fa3f1160aa9871aee6df47a6e4d38e", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2d9c633f0499f9dc5c2fd069161af4e2e7756890b81adcbb2ceaa074e8308876"}, 23 | "plug_crypto": {:hex, :plug_crypto, "1.2.0", "1cb20793aa63a6c619dd18bb33d7a3aa94818e5fd39ad357051a67f26dfa2df6", [:mix], [], "hexpm", "a48b538ae8bf381ffac344520755f3007cc10bd8e90b240af98ea29b69683fc2"}, 24 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, 25 | "surface": {:hex, :surface, "0.2.0", "62173cc2cac9d69099efe19a432cb99fd95fb1eac4e46425561c55a8ec120ee6", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.15.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "cb02c969cddf30cba50ef7d4f42db15f4cfe0588feb8740dd786cada6c997d4a"}, 26 | "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, 27 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 28 | } 29 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-surface", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "elixir-node-ssr": { 8 | "version": "github:harmon25/node_ssr#ab6198738c9c63f3f92a4f6ef98375fef74a2acf", 9 | "from": "github:harmon25/node_ssr#main" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-surface", 3 | "version": "0.1.0", 4 | "description": "Hook to render live react components in Phoenix LiveView.", 5 | "license": "MIT", 6 | "main": "./priv/react-surface.js", 7 | "author": "Doug W.", 8 | "dependencies": { 9 | "elixir-node-ssr": "harmon25/node_ssr#main" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/harmon25/react_surface.git" 14 | }, 15 | "files": [ 16 | "README.md", 17 | "LICENSE.md", 18 | "package.json", 19 | "priv/react-surface.js", 20 | "assets/js/react-surface.js", 21 | "priv/react-ssr.js" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /priv/react-ssr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React SSR script, requires a component, and renders to a html string. 3 | * Used by NodeSSR as the render function. 4 | */ 5 | const { renderToString } = require("react-dom/server"); 6 | const { createElement } = require("react"); 7 | const startService = require("elixir-node-ssr"); 8 | 9 | // provide a window object... 10 | if (typeof window === "undefined") { 11 | global.window = {}; 12 | } 13 | 14 | function render(componentName, props) { 15 | // grabs this from the environment - injected via node_ssr 16 | const { COMPONENT_EXT, COMPONENT_PATH } = process.env; 17 | // should just take component name, and from that resolve the path... 18 | try { 19 | const componentPath = componentName.endsWith(COMPONENT_EXT) 20 | ? `${COMPONENT_PATH}/${componentName}` 21 | : `${COMPONENT_PATH}/${componentName}${COMPONENT_EXT}`; 22 | 23 | const component = require(componentPath); 24 | const element = component.default ? component.default : component; 25 | const markup = renderToString(createElement(element, props)); 26 | 27 | return { markup, error: null, props, extra: null }; 28 | } catch (e) { 29 | return { 30 | markup: null, 31 | component: null, 32 | extra: null, 33 | error: { type: e.constructor.name, message: e.message, stack: e.stack }, 34 | }; 35 | } 36 | } 37 | 38 | module.exports = { render, startService }; 39 | -------------------------------------------------------------------------------- /priv/react-surface.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("react"),require("react-dom")):"function"==typeof define&&define.amd?define(["React","ReactDOM"],t):"object"==typeof exports?exports["react-surface"]=t(require("react"),require("react-dom")):e["react-surface"]=t(e.React,e.ReactDOM)}(window,(function(e,t){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=2)}([function(t,r){t.exports=e},function(e,r){e.exports=t},function(e,t,r){"use strict";r.r(t),r.d(t,"LiveContext",(function(){return d})),r.d(t,"useLiveContext",(function(){return b})),r.d(t,"LiveContextProvider",(function(){return y})),r.d(t,"buildHook",(function(){return m}));var n=r(0),o=r.n(n),a=r(1),c=r.n(a);function u(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){if("undefined"==typeof Symbol||!(Symbol.iterator in Object(e)))return;var r=[],n=!0,o=!1,a=void 0;try{for(var c,u=e[Symbol.iterator]();!(n=(c=u.next()).done)&&(r.push(c.value),!t||r.length!==t);n=!0);}catch(e){o=!0,a=e}finally{try{n||null==u.return||u.return()}finally{if(o)throw a}}return r}(e,t)||function(e,t){if(!e)return;if("string"==typeof e)return i(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);"Object"===r&&e.constructor&&(r=e.constructor.name);if("Map"===r||"Set"===r)return Array.from(e);if("Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return i(e,t)}(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function i(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r=0||(o[r]=e[r]);return o}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,r)&&(o[r]=e[r])}return o}var d=o.a.createContext({}),b=function(){return o.a.useContext(d)},y=function(e){var t=e.children,r=p(e,["children"]);return o.a.createElement(d.Provider,{value:r},t)},h={debug:!1},v="react-surface: ";function m(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:h;t=l(l({},h),t);var r={mounted:function(){var r=u(g(this.el),3),n=r[0],o=r[1],a=r[2];if(!e[n])throw"".concat(v,"Missing ").concat(n," in supplied components object (").concat(Object.keys(e),")");var i=this.handleEvent.bind(this),f=this.pushEvent.bind(this),l=this.pushEventTo.bind(this);this._ReactSurface={name:n,contextProps:{handleEvent:i,pushEvent:f,pushEventTo:l}},this._ReactSurface.all=[[y,this._ReactSurface.contextProps],[e[n],o]],this._ReactSurface.comp=O(this._ReactSurface.all),"h"===a?c.a.hydrate(this._ReactSurface.comp,this.el.lastChild):c.a.render(this._ReactSurface.comp,this.el.lastChild),t.debug&&x("mounted "+j([n,o,a]))},updated:function(){var e=u(g(this.el),3),r=e[0],n=e[1],o=e[2];r!==this._ReactSurface.name&&S("Previous component name differs from updated component name"),this._ReactSurface.name=r,this._ReactSurface.all[1][1]=n,c.a.hydrate(O(this._ReactSurface.all),this.el.lastChild),t.debug&&x("updated "+j([r,n,o]))},destroyed:function(){c.a.unmountComponentAtNode(this.el.lastChild),t.debug&&x("Destroyed ".concat(this._ReactSurface.name))}};return{_RS:r}}function O(e){var t=u(e,2),r=t[0],n=t[1];return o.a.createElement(r[0],r[1],o.a.createElement(n[0],n[1]))}function g(e){var t=e.attributes["rs-c"].value,r=e.attributes["rs-p"].value,n=e.attributes["rs-m"].value;return[t,JSON.parse(atob(r)),n]}function j(e){var t=u(e,3),r=t[0],n=t[1],o=t[2];return"\nname: ".concat(r,"\nprops: ").concat(JSON.stringify(n),"\nmethod: ").concat(o)}function S(e){for(var t,r=arguments.length,n=new Array(r>1?r-1:0),o=1;o1?r-1:0),o=1;o

{name}

-------------------------------------------------------------------------------- /test/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-surface-test", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/code-frame": { 8 | "version": "7.12.11", 9 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", 10 | "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", 11 | "dev": true, 12 | "requires": { 13 | "@babel/highlight": "^7.10.4" 14 | } 15 | }, 16 | "@babel/compat-data": { 17 | "version": "7.12.7", 18 | "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.12.7.tgz", 19 | "integrity": "sha512-YaxPMGs/XIWtYqrdEOZOCPsVWfEoriXopnsz3/i7apYPXQ3698UFhS6dVT1KN5qOsWmVgw/FOrmQgpRaZayGsw==", 20 | "dev": true 21 | }, 22 | "@babel/core": { 23 | "version": "7.12.10", 24 | "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.10.tgz", 25 | "integrity": "sha512-eTAlQKq65zHfkHZV0sIVODCPGVgoo1HdBlbSLi9CqOzuZanMv2ihzY+4paiKr1mH+XmYESMAmJ/dpZ68eN6d8w==", 26 | "dev": true, 27 | "requires": { 28 | "@babel/code-frame": "^7.10.4", 29 | "@babel/generator": "^7.12.10", 30 | "@babel/helper-module-transforms": "^7.12.1", 31 | "@babel/helpers": "^7.12.5", 32 | "@babel/parser": "^7.12.10", 33 | "@babel/template": "^7.12.7", 34 | "@babel/traverse": "^7.12.10", 35 | "@babel/types": "^7.12.10", 36 | "convert-source-map": "^1.7.0", 37 | "debug": "^4.1.0", 38 | "gensync": "^1.0.0-beta.1", 39 | "json5": "^2.1.2", 40 | "lodash": "^4.17.19", 41 | "semver": "^5.4.1", 42 | "source-map": "^0.5.0" 43 | } 44 | }, 45 | "@babel/generator": { 46 | "version": "7.12.11", 47 | "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz", 48 | "integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==", 49 | "dev": true, 50 | "requires": { 51 | "@babel/types": "^7.12.11", 52 | "jsesc": "^2.5.1", 53 | "source-map": "^0.5.0" 54 | } 55 | }, 56 | "@babel/helper-annotate-as-pure": { 57 | "version": "7.12.10", 58 | "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz", 59 | "integrity": "sha512-XplmVbC1n+KY6jL8/fgLVXXUauDIB+lD5+GsQEh6F6GBF1dq1qy4DP4yXWzDKcoqXB3X58t61e85Fitoww4JVQ==", 60 | "dev": true, 61 | "requires": { 62 | "@babel/types": "^7.12.10" 63 | } 64 | }, 65 | "@babel/helper-builder-binary-assignment-operator-visitor": { 66 | "version": "7.10.4", 67 | "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz", 68 | "integrity": "sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg==", 69 | "dev": true, 70 | "requires": { 71 | "@babel/helper-explode-assignable-expression": "^7.10.4", 72 | "@babel/types": "^7.10.4" 73 | } 74 | }, 75 | "@babel/helper-compilation-targets": { 76 | "version": "7.12.5", 77 | "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.5.tgz", 78 | "integrity": "sha512-+qH6NrscMolUlzOYngSBMIOQpKUGPPsc61Bu5W10mg84LxZ7cmvnBHzARKbDoFxVvqqAbj6Tg6N7bSrWSPXMyw==", 79 | "dev": true, 80 | "requires": { 81 | "@babel/compat-data": "^7.12.5", 82 | "@babel/helper-validator-option": "^7.12.1", 83 | "browserslist": "^4.14.5", 84 | "semver": "^5.5.0" 85 | } 86 | }, 87 | "@babel/helper-create-class-features-plugin": { 88 | "version": "7.12.1", 89 | "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.1.tgz", 90 | "integrity": "sha512-hkL++rWeta/OVOBTRJc9a5Azh5mt5WgZUGAKMD8JM141YsE08K//bp1unBBieO6rUKkIPyUE0USQ30jAy3Sk1w==", 91 | "dev": true, 92 | "requires": { 93 | "@babel/helper-function-name": "^7.10.4", 94 | "@babel/helper-member-expression-to-functions": "^7.12.1", 95 | "@babel/helper-optimise-call-expression": "^7.10.4", 96 | "@babel/helper-replace-supers": "^7.12.1", 97 | "@babel/helper-split-export-declaration": "^7.10.4" 98 | } 99 | }, 100 | "@babel/helper-create-regexp-features-plugin": { 101 | "version": "7.12.7", 102 | "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.7.tgz", 103 | "integrity": "sha512-idnutvQPdpbduutvi3JVfEgcVIHooQnhvhx0Nk9isOINOIGYkZea1Pk2JlJRiUnMefrlvr0vkByATBY/mB4vjQ==", 104 | "dev": true, 105 | "requires": { 106 | "@babel/helper-annotate-as-pure": "^7.10.4", 107 | "regexpu-core": "^4.7.1" 108 | } 109 | }, 110 | "@babel/helper-define-map": { 111 | "version": "7.10.5", 112 | "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz", 113 | "integrity": "sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ==", 114 | "dev": true, 115 | "requires": { 116 | "@babel/helper-function-name": "^7.10.4", 117 | "@babel/types": "^7.10.5", 118 | "lodash": "^4.17.19" 119 | } 120 | }, 121 | "@babel/helper-explode-assignable-expression": { 122 | "version": "7.12.1", 123 | "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.12.1.tgz", 124 | "integrity": "sha512-dmUwH8XmlrUpVqgtZ737tK88v07l840z9j3OEhCLwKTkjlvKpfqXVIZ0wpK3aeOxspwGrf/5AP5qLx4rO3w5rA==", 125 | "dev": true, 126 | "requires": { 127 | "@babel/types": "^7.12.1" 128 | } 129 | }, 130 | "@babel/helper-function-name": { 131 | "version": "7.12.11", 132 | "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz", 133 | "integrity": "sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA==", 134 | "dev": true, 135 | "requires": { 136 | "@babel/helper-get-function-arity": "^7.12.10", 137 | "@babel/template": "^7.12.7", 138 | "@babel/types": "^7.12.11" 139 | } 140 | }, 141 | "@babel/helper-get-function-arity": { 142 | "version": "7.12.10", 143 | "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz", 144 | "integrity": "sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==", 145 | "dev": true, 146 | "requires": { 147 | "@babel/types": "^7.12.10" 148 | } 149 | }, 150 | "@babel/helper-hoist-variables": { 151 | "version": "7.10.4", 152 | "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz", 153 | "integrity": "sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA==", 154 | "dev": true, 155 | "requires": { 156 | "@babel/types": "^7.10.4" 157 | } 158 | }, 159 | "@babel/helper-member-expression-to-functions": { 160 | "version": "7.12.7", 161 | "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz", 162 | "integrity": "sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==", 163 | "dev": true, 164 | "requires": { 165 | "@babel/types": "^7.12.7" 166 | } 167 | }, 168 | "@babel/helper-module-imports": { 169 | "version": "7.12.5", 170 | "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz", 171 | "integrity": "sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA==", 172 | "dev": true, 173 | "requires": { 174 | "@babel/types": "^7.12.5" 175 | } 176 | }, 177 | "@babel/helper-module-transforms": { 178 | "version": "7.12.1", 179 | "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz", 180 | "integrity": "sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w==", 181 | "dev": true, 182 | "requires": { 183 | "@babel/helper-module-imports": "^7.12.1", 184 | "@babel/helper-replace-supers": "^7.12.1", 185 | "@babel/helper-simple-access": "^7.12.1", 186 | "@babel/helper-split-export-declaration": "^7.11.0", 187 | "@babel/helper-validator-identifier": "^7.10.4", 188 | "@babel/template": "^7.10.4", 189 | "@babel/traverse": "^7.12.1", 190 | "@babel/types": "^7.12.1", 191 | "lodash": "^4.17.19" 192 | } 193 | }, 194 | "@babel/helper-optimise-call-expression": { 195 | "version": "7.12.10", 196 | "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz", 197 | "integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==", 198 | "dev": true, 199 | "requires": { 200 | "@babel/types": "^7.12.10" 201 | } 202 | }, 203 | "@babel/helper-plugin-utils": { 204 | "version": "7.10.4", 205 | "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", 206 | "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", 207 | "dev": true 208 | }, 209 | "@babel/helper-remap-async-to-generator": { 210 | "version": "7.12.1", 211 | "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz", 212 | "integrity": "sha512-9d0KQCRM8clMPcDwo8SevNs+/9a8yWVVmaE80FGJcEP8N1qToREmWEGnBn8BUlJhYRFz6fqxeRL1sl5Ogsed7A==", 213 | "dev": true, 214 | "requires": { 215 | "@babel/helper-annotate-as-pure": "^7.10.4", 216 | "@babel/helper-wrap-function": "^7.10.4", 217 | "@babel/types": "^7.12.1" 218 | } 219 | }, 220 | "@babel/helper-replace-supers": { 221 | "version": "7.12.11", 222 | "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz", 223 | "integrity": "sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==", 224 | "dev": true, 225 | "requires": { 226 | "@babel/helper-member-expression-to-functions": "^7.12.7", 227 | "@babel/helper-optimise-call-expression": "^7.12.10", 228 | "@babel/traverse": "^7.12.10", 229 | "@babel/types": "^7.12.11" 230 | } 231 | }, 232 | "@babel/helper-simple-access": { 233 | "version": "7.12.1", 234 | "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz", 235 | "integrity": "sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA==", 236 | "dev": true, 237 | "requires": { 238 | "@babel/types": "^7.12.1" 239 | } 240 | }, 241 | "@babel/helper-skip-transparent-expression-wrappers": { 242 | "version": "7.12.1", 243 | "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz", 244 | "integrity": "sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA==", 245 | "dev": true, 246 | "requires": { 247 | "@babel/types": "^7.12.1" 248 | } 249 | }, 250 | "@babel/helper-split-export-declaration": { 251 | "version": "7.12.11", 252 | "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz", 253 | "integrity": "sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g==", 254 | "dev": true, 255 | "requires": { 256 | "@babel/types": "^7.12.11" 257 | } 258 | }, 259 | "@babel/helper-validator-identifier": { 260 | "version": "7.12.11", 261 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", 262 | "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", 263 | "dev": true 264 | }, 265 | "@babel/helper-validator-option": { 266 | "version": "7.12.11", 267 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.11.tgz", 268 | "integrity": "sha512-TBFCyj939mFSdeX7U7DDj32WtzYY7fDcalgq8v3fBZMNOJQNn7nOYzMaUCiPxPYfCup69mtIpqlKgMZLvQ8Xhw==", 269 | "dev": true 270 | }, 271 | "@babel/helper-wrap-function": { 272 | "version": "7.12.3", 273 | "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.12.3.tgz", 274 | "integrity": "sha512-Cvb8IuJDln3rs6tzjW3Y8UeelAOdnpB8xtQ4sme2MSZ9wOxrbThporC0y/EtE16VAtoyEfLM404Xr1e0OOp+ow==", 275 | "dev": true, 276 | "requires": { 277 | "@babel/helper-function-name": "^7.10.4", 278 | "@babel/template": "^7.10.4", 279 | "@babel/traverse": "^7.10.4", 280 | "@babel/types": "^7.10.4" 281 | } 282 | }, 283 | "@babel/helpers": { 284 | "version": "7.12.5", 285 | "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.5.tgz", 286 | "integrity": "sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA==", 287 | "dev": true, 288 | "requires": { 289 | "@babel/template": "^7.10.4", 290 | "@babel/traverse": "^7.12.5", 291 | "@babel/types": "^7.12.5" 292 | } 293 | }, 294 | "@babel/highlight": { 295 | "version": "7.10.4", 296 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", 297 | "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", 298 | "dev": true, 299 | "requires": { 300 | "@babel/helper-validator-identifier": "^7.10.4", 301 | "chalk": "^2.0.0", 302 | "js-tokens": "^4.0.0" 303 | } 304 | }, 305 | "@babel/parser": { 306 | "version": "7.12.11", 307 | "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz", 308 | "integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==", 309 | "dev": true 310 | }, 311 | "@babel/plugin-proposal-async-generator-functions": { 312 | "version": "7.12.12", 313 | "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.12.tgz", 314 | "integrity": "sha512-nrz9y0a4xmUrRq51bYkWJIO5SBZyG2ys2qinHsN0zHDHVsUaModrkpyWWWXfGqYQmOL3x9sQIcTNN/pBGpo09A==", 315 | "dev": true, 316 | "requires": { 317 | "@babel/helper-plugin-utils": "^7.10.4", 318 | "@babel/helper-remap-async-to-generator": "^7.12.1", 319 | "@babel/plugin-syntax-async-generators": "^7.8.0" 320 | } 321 | }, 322 | "@babel/plugin-proposal-class-properties": { 323 | "version": "7.12.1", 324 | "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.12.1.tgz", 325 | "integrity": "sha512-cKp3dlQsFsEs5CWKnN7BnSHOd0EOW8EKpEjkoz1pO2E5KzIDNV9Ros1b0CnmbVgAGXJubOYVBOGCT1OmJwOI7w==", 326 | "dev": true, 327 | "requires": { 328 | "@babel/helper-create-class-features-plugin": "^7.12.1", 329 | "@babel/helper-plugin-utils": "^7.10.4" 330 | } 331 | }, 332 | "@babel/plugin-proposal-dynamic-import": { 333 | "version": "7.12.1", 334 | "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.1.tgz", 335 | "integrity": "sha512-a4rhUSZFuq5W8/OO8H7BL5zspjnc1FLd9hlOxIK/f7qG4a0qsqk8uvF/ywgBA8/OmjsapjpvaEOYItfGG1qIvQ==", 336 | "dev": true, 337 | "requires": { 338 | "@babel/helper-plugin-utils": "^7.10.4", 339 | "@babel/plugin-syntax-dynamic-import": "^7.8.0" 340 | } 341 | }, 342 | "@babel/plugin-proposal-export-namespace-from": { 343 | "version": "7.12.1", 344 | "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.12.1.tgz", 345 | "integrity": "sha512-6CThGf0irEkzujYS5LQcjBx8j/4aQGiVv7J9+2f7pGfxqyKh3WnmVJYW3hdrQjyksErMGBPQrCnHfOtna+WLbw==", 346 | "dev": true, 347 | "requires": { 348 | "@babel/helper-plugin-utils": "^7.10.4", 349 | "@babel/plugin-syntax-export-namespace-from": "^7.8.3" 350 | } 351 | }, 352 | "@babel/plugin-proposal-json-strings": { 353 | "version": "7.12.1", 354 | "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.12.1.tgz", 355 | "integrity": "sha512-GoLDUi6U9ZLzlSda2Df++VSqDJg3CG+dR0+iWsv6XRw1rEq+zwt4DirM9yrxW6XWaTpmai1cWJLMfM8qQJf+yw==", 356 | "dev": true, 357 | "requires": { 358 | "@babel/helper-plugin-utils": "^7.10.4", 359 | "@babel/plugin-syntax-json-strings": "^7.8.0" 360 | } 361 | }, 362 | "@babel/plugin-proposal-logical-assignment-operators": { 363 | "version": "7.12.1", 364 | "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.12.1.tgz", 365 | "integrity": "sha512-k8ZmVv0JU+4gcUGeCDZOGd0lCIamU/sMtIiX3UWnUc5yzgq6YUGyEolNYD+MLYKfSzgECPcqetVcJP9Afe/aCA==", 366 | "dev": true, 367 | "requires": { 368 | "@babel/helper-plugin-utils": "^7.10.4", 369 | "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" 370 | } 371 | }, 372 | "@babel/plugin-proposal-nullish-coalescing-operator": { 373 | "version": "7.12.1", 374 | "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.12.1.tgz", 375 | "integrity": "sha512-nZY0ESiaQDI1y96+jk6VxMOaL4LPo/QDHBqL+SF3/vl6dHkTwHlOI8L4ZwuRBHgakRBw5zsVylel7QPbbGuYgg==", 376 | "dev": true, 377 | "requires": { 378 | "@babel/helper-plugin-utils": "^7.10.4", 379 | "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" 380 | } 381 | }, 382 | "@babel/plugin-proposal-numeric-separator": { 383 | "version": "7.12.7", 384 | "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.7.tgz", 385 | "integrity": "sha512-8c+uy0qmnRTeukiGsjLGy6uVs/TFjJchGXUeBqlG4VWYOdJWkhhVPdQ3uHwbmalfJwv2JsV0qffXP4asRfL2SQ==", 386 | "dev": true, 387 | "requires": { 388 | "@babel/helper-plugin-utils": "^7.10.4", 389 | "@babel/plugin-syntax-numeric-separator": "^7.10.4" 390 | } 391 | }, 392 | "@babel/plugin-proposal-object-rest-spread": { 393 | "version": "7.12.1", 394 | "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz", 395 | "integrity": "sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA==", 396 | "dev": true, 397 | "requires": { 398 | "@babel/helper-plugin-utils": "^7.10.4", 399 | "@babel/plugin-syntax-object-rest-spread": "^7.8.0", 400 | "@babel/plugin-transform-parameters": "^7.12.1" 401 | } 402 | }, 403 | "@babel/plugin-proposal-optional-catch-binding": { 404 | "version": "7.12.1", 405 | "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.12.1.tgz", 406 | "integrity": "sha512-hFvIjgprh9mMw5v42sJWLI1lzU5L2sznP805zeT6rySVRA0Y18StRhDqhSxlap0oVgItRsB6WSROp4YnJTJz0g==", 407 | "dev": true, 408 | "requires": { 409 | "@babel/helper-plugin-utils": "^7.10.4", 410 | "@babel/plugin-syntax-optional-catch-binding": "^7.8.0" 411 | } 412 | }, 413 | "@babel/plugin-proposal-optional-chaining": { 414 | "version": "7.12.7", 415 | "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.7.tgz", 416 | "integrity": "sha512-4ovylXZ0PWmwoOvhU2vhnzVNnm88/Sm9nx7V8BPgMvAzn5zDou3/Awy0EjglyubVHasJj+XCEkr/r1X3P5elCA==", 417 | "dev": true, 418 | "requires": { 419 | "@babel/helper-plugin-utils": "^7.10.4", 420 | "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1", 421 | "@babel/plugin-syntax-optional-chaining": "^7.8.0" 422 | } 423 | }, 424 | "@babel/plugin-proposal-private-methods": { 425 | "version": "7.12.1", 426 | "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.12.1.tgz", 427 | "integrity": "sha512-mwZ1phvH7/NHK6Kf8LP7MYDogGV+DKB1mryFOEwx5EBNQrosvIczzZFTUmWaeujd5xT6G1ELYWUz3CutMhjE1w==", 428 | "dev": true, 429 | "requires": { 430 | "@babel/helper-create-class-features-plugin": "^7.12.1", 431 | "@babel/helper-plugin-utils": "^7.10.4" 432 | } 433 | }, 434 | "@babel/plugin-proposal-unicode-property-regex": { 435 | "version": "7.12.1", 436 | "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.12.1.tgz", 437 | "integrity": "sha512-MYq+l+PvHuw/rKUz1at/vb6nCnQ2gmJBNaM62z0OgH7B2W1D9pvkpYtlti9bGtizNIU1K3zm4bZF9F91efVY0w==", 438 | "dev": true, 439 | "requires": { 440 | "@babel/helper-create-regexp-features-plugin": "^7.12.1", 441 | "@babel/helper-plugin-utils": "^7.10.4" 442 | } 443 | }, 444 | "@babel/plugin-syntax-async-generators": { 445 | "version": "7.8.4", 446 | "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", 447 | "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", 448 | "dev": true, 449 | "requires": { 450 | "@babel/helper-plugin-utils": "^7.8.0" 451 | } 452 | }, 453 | "@babel/plugin-syntax-class-properties": { 454 | "version": "7.12.1", 455 | "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz", 456 | "integrity": "sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA==", 457 | "dev": true, 458 | "requires": { 459 | "@babel/helper-plugin-utils": "^7.10.4" 460 | } 461 | }, 462 | "@babel/plugin-syntax-dynamic-import": { 463 | "version": "7.8.3", 464 | "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", 465 | "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", 466 | "dev": true, 467 | "requires": { 468 | "@babel/helper-plugin-utils": "^7.8.0" 469 | } 470 | }, 471 | "@babel/plugin-syntax-export-namespace-from": { 472 | "version": "7.8.3", 473 | "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", 474 | "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", 475 | "dev": true, 476 | "requires": { 477 | "@babel/helper-plugin-utils": "^7.8.3" 478 | } 479 | }, 480 | "@babel/plugin-syntax-json-strings": { 481 | "version": "7.8.3", 482 | "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", 483 | "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", 484 | "dev": true, 485 | "requires": { 486 | "@babel/helper-plugin-utils": "^7.8.0" 487 | } 488 | }, 489 | "@babel/plugin-syntax-jsx": { 490 | "version": "7.12.1", 491 | "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz", 492 | "integrity": "sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==", 493 | "dev": true, 494 | "requires": { 495 | "@babel/helper-plugin-utils": "^7.10.4" 496 | } 497 | }, 498 | "@babel/plugin-syntax-logical-assignment-operators": { 499 | "version": "7.10.4", 500 | "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", 501 | "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", 502 | "dev": true, 503 | "requires": { 504 | "@babel/helper-plugin-utils": "^7.10.4" 505 | } 506 | }, 507 | "@babel/plugin-syntax-nullish-coalescing-operator": { 508 | "version": "7.8.3", 509 | "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", 510 | "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", 511 | "dev": true, 512 | "requires": { 513 | "@babel/helper-plugin-utils": "^7.8.0" 514 | } 515 | }, 516 | "@babel/plugin-syntax-numeric-separator": { 517 | "version": "7.10.4", 518 | "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", 519 | "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", 520 | "dev": true, 521 | "requires": { 522 | "@babel/helper-plugin-utils": "^7.10.4" 523 | } 524 | }, 525 | "@babel/plugin-syntax-object-rest-spread": { 526 | "version": "7.8.3", 527 | "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", 528 | "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", 529 | "dev": true, 530 | "requires": { 531 | "@babel/helper-plugin-utils": "^7.8.0" 532 | } 533 | }, 534 | "@babel/plugin-syntax-optional-catch-binding": { 535 | "version": "7.8.3", 536 | "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", 537 | "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", 538 | "dev": true, 539 | "requires": { 540 | "@babel/helper-plugin-utils": "^7.8.0" 541 | } 542 | }, 543 | "@babel/plugin-syntax-optional-chaining": { 544 | "version": "7.8.3", 545 | "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", 546 | "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", 547 | "dev": true, 548 | "requires": { 549 | "@babel/helper-plugin-utils": "^7.8.0" 550 | } 551 | }, 552 | "@babel/plugin-syntax-top-level-await": { 553 | "version": "7.12.1", 554 | "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz", 555 | "integrity": "sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A==", 556 | "dev": true, 557 | "requires": { 558 | "@babel/helper-plugin-utils": "^7.10.4" 559 | } 560 | }, 561 | "@babel/plugin-transform-arrow-functions": { 562 | "version": "7.12.1", 563 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.12.1.tgz", 564 | "integrity": "sha512-5QB50qyN44fzzz4/qxDPQMBCTHgxg3n0xRBLJUmBlLoU/sFvxVWGZF/ZUfMVDQuJUKXaBhbupxIzIfZ6Fwk/0A==", 565 | "dev": true, 566 | "requires": { 567 | "@babel/helper-plugin-utils": "^7.10.4" 568 | } 569 | }, 570 | "@babel/plugin-transform-async-to-generator": { 571 | "version": "7.12.1", 572 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.12.1.tgz", 573 | "integrity": "sha512-SDtqoEcarK1DFlRJ1hHRY5HvJUj5kX4qmtpMAm2QnhOlyuMC4TMdCRgW6WXpv93rZeYNeLP22y8Aq2dbcDRM1A==", 574 | "dev": true, 575 | "requires": { 576 | "@babel/helper-module-imports": "^7.12.1", 577 | "@babel/helper-plugin-utils": "^7.10.4", 578 | "@babel/helper-remap-async-to-generator": "^7.12.1" 579 | } 580 | }, 581 | "@babel/plugin-transform-block-scoped-functions": { 582 | "version": "7.12.1", 583 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.1.tgz", 584 | "integrity": "sha512-5OpxfuYnSgPalRpo8EWGPzIYf0lHBWORCkj5M0oLBwHdlux9Ri36QqGW3/LR13RSVOAoUUMzoPI/jpE4ABcHoA==", 585 | "dev": true, 586 | "requires": { 587 | "@babel/helper-plugin-utils": "^7.10.4" 588 | } 589 | }, 590 | "@babel/plugin-transform-block-scoping": { 591 | "version": "7.12.12", 592 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.12.tgz", 593 | "integrity": "sha512-VOEPQ/ExOVqbukuP7BYJtI5ZxxsmegTwzZ04j1aF0dkSypGo9XpDHuOrABsJu+ie+penpSJheDJ11x1BEZNiyQ==", 594 | "dev": true, 595 | "requires": { 596 | "@babel/helper-plugin-utils": "^7.10.4" 597 | } 598 | }, 599 | "@babel/plugin-transform-classes": { 600 | "version": "7.12.1", 601 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.12.1.tgz", 602 | "integrity": "sha512-/74xkA7bVdzQTBeSUhLLJgYIcxw/dpEpCdRDiHgPJ3Mv6uC11UhjpOhl72CgqbBCmt1qtssCyB2xnJm1+PFjog==", 603 | "dev": true, 604 | "requires": { 605 | "@babel/helper-annotate-as-pure": "^7.10.4", 606 | "@babel/helper-define-map": "^7.10.4", 607 | "@babel/helper-function-name": "^7.10.4", 608 | "@babel/helper-optimise-call-expression": "^7.10.4", 609 | "@babel/helper-plugin-utils": "^7.10.4", 610 | "@babel/helper-replace-supers": "^7.12.1", 611 | "@babel/helper-split-export-declaration": "^7.10.4", 612 | "globals": "^11.1.0" 613 | } 614 | }, 615 | "@babel/plugin-transform-computed-properties": { 616 | "version": "7.12.1", 617 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.12.1.tgz", 618 | "integrity": "sha512-vVUOYpPWB7BkgUWPo4C44mUQHpTZXakEqFjbv8rQMg7TC6S6ZhGZ3otQcRH6u7+adSlE5i0sp63eMC/XGffrzg==", 619 | "dev": true, 620 | "requires": { 621 | "@babel/helper-plugin-utils": "^7.10.4" 622 | } 623 | }, 624 | "@babel/plugin-transform-destructuring": { 625 | "version": "7.12.1", 626 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.12.1.tgz", 627 | "integrity": "sha512-fRMYFKuzi/rSiYb2uRLiUENJOKq4Gnl+6qOv5f8z0TZXg3llUwUhsNNwrwaT/6dUhJTzNpBr+CUvEWBtfNY1cw==", 628 | "dev": true, 629 | "requires": { 630 | "@babel/helper-plugin-utils": "^7.10.4" 631 | } 632 | }, 633 | "@babel/plugin-transform-dotall-regex": { 634 | "version": "7.12.1", 635 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.12.1.tgz", 636 | "integrity": "sha512-B2pXeRKoLszfEW7J4Hg9LoFaWEbr/kzo3teWHmtFCszjRNa/b40f9mfeqZsIDLLt/FjwQ6pz/Gdlwy85xNckBA==", 637 | "dev": true, 638 | "requires": { 639 | "@babel/helper-create-regexp-features-plugin": "^7.12.1", 640 | "@babel/helper-plugin-utils": "^7.10.4" 641 | } 642 | }, 643 | "@babel/plugin-transform-duplicate-keys": { 644 | "version": "7.12.1", 645 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.12.1.tgz", 646 | "integrity": "sha512-iRght0T0HztAb/CazveUpUQrZY+aGKKaWXMJ4uf9YJtqxSUe09j3wteztCUDRHs+SRAL7yMuFqUsLoAKKzgXjw==", 647 | "dev": true, 648 | "requires": { 649 | "@babel/helper-plugin-utils": "^7.10.4" 650 | } 651 | }, 652 | "@babel/plugin-transform-exponentiation-operator": { 653 | "version": "7.12.1", 654 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.12.1.tgz", 655 | "integrity": "sha512-7tqwy2bv48q+c1EHbXK0Zx3KXd2RVQp6OC7PbwFNt/dPTAV3Lu5sWtWuAj8owr5wqtWnqHfl2/mJlUmqkChKug==", 656 | "dev": true, 657 | "requires": { 658 | "@babel/helper-builder-binary-assignment-operator-visitor": "^7.10.4", 659 | "@babel/helper-plugin-utils": "^7.10.4" 660 | } 661 | }, 662 | "@babel/plugin-transform-for-of": { 663 | "version": "7.12.1", 664 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.12.1.tgz", 665 | "integrity": "sha512-Zaeq10naAsuHo7heQvyV0ptj4dlZJwZgNAtBYBnu5nNKJoW62m0zKcIEyVECrUKErkUkg6ajMy4ZfnVZciSBhg==", 666 | "dev": true, 667 | "requires": { 668 | "@babel/helper-plugin-utils": "^7.10.4" 669 | } 670 | }, 671 | "@babel/plugin-transform-function-name": { 672 | "version": "7.12.1", 673 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.1.tgz", 674 | "integrity": "sha512-JF3UgJUILoFrFMEnOJLJkRHSk6LUSXLmEFsA23aR2O5CSLUxbeUX1IZ1YQ7Sn0aXb601Ncwjx73a+FVqgcljVw==", 675 | "dev": true, 676 | "requires": { 677 | "@babel/helper-function-name": "^7.10.4", 678 | "@babel/helper-plugin-utils": "^7.10.4" 679 | } 680 | }, 681 | "@babel/plugin-transform-literals": { 682 | "version": "7.12.1", 683 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.1.tgz", 684 | "integrity": "sha512-+PxVGA+2Ag6uGgL0A5f+9rklOnnMccwEBzwYFL3EUaKuiyVnUipyXncFcfjSkbimLrODoqki1U9XxZzTvfN7IQ==", 685 | "dev": true, 686 | "requires": { 687 | "@babel/helper-plugin-utils": "^7.10.4" 688 | } 689 | }, 690 | "@babel/plugin-transform-member-expression-literals": { 691 | "version": "7.12.1", 692 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.12.1.tgz", 693 | "integrity": "sha512-1sxePl6z9ad0gFMB9KqmYofk34flq62aqMt9NqliS/7hPEpURUCMbyHXrMPlo282iY7nAvUB1aQd5mg79UD9Jg==", 694 | "dev": true, 695 | "requires": { 696 | "@babel/helper-plugin-utils": "^7.10.4" 697 | } 698 | }, 699 | "@babel/plugin-transform-modules-amd": { 700 | "version": "7.12.1", 701 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.12.1.tgz", 702 | "integrity": "sha512-tDW8hMkzad5oDtzsB70HIQQRBiTKrhfgwC/KkJeGsaNFTdWhKNt/BiE8c5yj19XiGyrxpbkOfH87qkNg1YGlOQ==", 703 | "dev": true, 704 | "requires": { 705 | "@babel/helper-module-transforms": "^7.12.1", 706 | "@babel/helper-plugin-utils": "^7.10.4", 707 | "babel-plugin-dynamic-import-node": "^2.3.3" 708 | } 709 | }, 710 | "@babel/plugin-transform-modules-commonjs": { 711 | "version": "7.12.1", 712 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.12.1.tgz", 713 | "integrity": "sha512-dY789wq6l0uLY8py9c1B48V8mVL5gZh/+PQ5ZPrylPYsnAvnEMjqsUXkuoDVPeVK+0VyGar+D08107LzDQ6pag==", 714 | "dev": true, 715 | "requires": { 716 | "@babel/helper-module-transforms": "^7.12.1", 717 | "@babel/helper-plugin-utils": "^7.10.4", 718 | "@babel/helper-simple-access": "^7.12.1", 719 | "babel-plugin-dynamic-import-node": "^2.3.3" 720 | } 721 | }, 722 | "@babel/plugin-transform-modules-systemjs": { 723 | "version": "7.12.1", 724 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.12.1.tgz", 725 | "integrity": "sha512-Hn7cVvOavVh8yvW6fLwveFqSnd7rbQN3zJvoPNyNaQSvgfKmDBO9U1YL9+PCXGRlZD9tNdWTy5ACKqMuzyn32Q==", 726 | "dev": true, 727 | "requires": { 728 | "@babel/helper-hoist-variables": "^7.10.4", 729 | "@babel/helper-module-transforms": "^7.12.1", 730 | "@babel/helper-plugin-utils": "^7.10.4", 731 | "@babel/helper-validator-identifier": "^7.10.4", 732 | "babel-plugin-dynamic-import-node": "^2.3.3" 733 | } 734 | }, 735 | "@babel/plugin-transform-modules-umd": { 736 | "version": "7.12.1", 737 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.12.1.tgz", 738 | "integrity": "sha512-aEIubCS0KHKM0zUos5fIoQm+AZUMt1ZvMpqz0/H5qAQ7vWylr9+PLYurT+Ic7ID/bKLd4q8hDovaG3Zch2uz5Q==", 739 | "dev": true, 740 | "requires": { 741 | "@babel/helper-module-transforms": "^7.12.1", 742 | "@babel/helper-plugin-utils": "^7.10.4" 743 | } 744 | }, 745 | "@babel/plugin-transform-named-capturing-groups-regex": { 746 | "version": "7.12.1", 747 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.12.1.tgz", 748 | "integrity": "sha512-tB43uQ62RHcoDp9v2Nsf+dSM8sbNodbEicbQNA53zHz8pWUhsgHSJCGpt7daXxRydjb0KnfmB+ChXOv3oADp1Q==", 749 | "dev": true, 750 | "requires": { 751 | "@babel/helper-create-regexp-features-plugin": "^7.12.1" 752 | } 753 | }, 754 | "@babel/plugin-transform-new-target": { 755 | "version": "7.12.1", 756 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.12.1.tgz", 757 | "integrity": "sha512-+eW/VLcUL5L9IvJH7rT1sT0CzkdUTvPrXC2PXTn/7z7tXLBuKvezYbGdxD5WMRoyvyaujOq2fWoKl869heKjhw==", 758 | "dev": true, 759 | "requires": { 760 | "@babel/helper-plugin-utils": "^7.10.4" 761 | } 762 | }, 763 | "@babel/plugin-transform-object-super": { 764 | "version": "7.12.1", 765 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.1.tgz", 766 | "integrity": "sha512-AvypiGJH9hsquNUn+RXVcBdeE3KHPZexWRdimhuV59cSoOt5kFBmqlByorAeUlGG2CJWd0U+4ZtNKga/TB0cAw==", 767 | "dev": true, 768 | "requires": { 769 | "@babel/helper-plugin-utils": "^7.10.4", 770 | "@babel/helper-replace-supers": "^7.12.1" 771 | } 772 | }, 773 | "@babel/plugin-transform-parameters": { 774 | "version": "7.12.1", 775 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.12.1.tgz", 776 | "integrity": "sha512-xq9C5EQhdPK23ZeCdMxl8bbRnAgHFrw5EOC3KJUsSylZqdkCaFEXxGSBuTSObOpiiHHNyb82es8M1QYgfQGfNg==", 777 | "dev": true, 778 | "requires": { 779 | "@babel/helper-plugin-utils": "^7.10.4" 780 | } 781 | }, 782 | "@babel/plugin-transform-property-literals": { 783 | "version": "7.12.1", 784 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.12.1.tgz", 785 | "integrity": "sha512-6MTCR/mZ1MQS+AwZLplX4cEySjCpnIF26ToWo942nqn8hXSm7McaHQNeGx/pt7suI1TWOWMfa/NgBhiqSnX0cQ==", 786 | "dev": true, 787 | "requires": { 788 | "@babel/helper-plugin-utils": "^7.10.4" 789 | } 790 | }, 791 | "@babel/plugin-transform-react-display-name": { 792 | "version": "7.12.1", 793 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.12.1.tgz", 794 | "integrity": "sha512-cAzB+UzBIrekfYxyLlFqf/OagTvHLcVBb5vpouzkYkBclRPraiygVnafvAoipErZLI8ANv8Ecn6E/m5qPXD26w==", 795 | "dev": true, 796 | "requires": { 797 | "@babel/helper-plugin-utils": "^7.10.4" 798 | } 799 | }, 800 | "@babel/plugin-transform-react-jsx": { 801 | "version": "7.12.12", 802 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.12.tgz", 803 | "integrity": "sha512-JDWGuzGNWscYcq8oJVCtSE61a5+XAOos+V0HrxnDieUus4UMnBEosDnY1VJqU5iZ4pA04QY7l0+JvHL1hZEfsw==", 804 | "dev": true, 805 | "requires": { 806 | "@babel/helper-annotate-as-pure": "^7.12.10", 807 | "@babel/helper-module-imports": "^7.12.5", 808 | "@babel/helper-plugin-utils": "^7.10.4", 809 | "@babel/plugin-syntax-jsx": "^7.12.1", 810 | "@babel/types": "^7.12.12" 811 | } 812 | }, 813 | "@babel/plugin-transform-react-jsx-development": { 814 | "version": "7.12.12", 815 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.12.tgz", 816 | "integrity": "sha512-i1AxnKxHeMxUaWVXQOSIco4tvVvvCxMSfeBMnMM06mpaJt3g+MpxYQQrDfojUQldP1xxraPSJYSMEljoWM/dCg==", 817 | "dev": true, 818 | "requires": { 819 | "@babel/plugin-transform-react-jsx": "^7.12.12" 820 | } 821 | }, 822 | "@babel/plugin-transform-react-pure-annotations": { 823 | "version": "7.12.1", 824 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.12.1.tgz", 825 | "integrity": "sha512-RqeaHiwZtphSIUZ5I85PEH19LOSzxfuEazoY7/pWASCAIBuATQzpSVD+eT6MebeeZT2F4eSL0u4vw6n4Nm0Mjg==", 826 | "dev": true, 827 | "requires": { 828 | "@babel/helper-annotate-as-pure": "^7.10.4", 829 | "@babel/helper-plugin-utils": "^7.10.4" 830 | } 831 | }, 832 | "@babel/plugin-transform-regenerator": { 833 | "version": "7.12.1", 834 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.1.tgz", 835 | "integrity": "sha512-gYrHqs5itw6i4PflFX3OdBPMQdPbF4bj2REIUxlMRUFk0/ZOAIpDFuViuxPjUL7YC8UPnf+XG7/utJvqXdPKng==", 836 | "dev": true, 837 | "requires": { 838 | "regenerator-transform": "^0.14.2" 839 | } 840 | }, 841 | "@babel/plugin-transform-reserved-words": { 842 | "version": "7.12.1", 843 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.12.1.tgz", 844 | "integrity": "sha512-pOnUfhyPKvZpVyBHhSBoX8vfA09b7r00Pmm1sH+29ae2hMTKVmSp4Ztsr8KBKjLjx17H0eJqaRC3bR2iThM54A==", 845 | "dev": true, 846 | "requires": { 847 | "@babel/helper-plugin-utils": "^7.10.4" 848 | } 849 | }, 850 | "@babel/plugin-transform-shorthand-properties": { 851 | "version": "7.12.1", 852 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.1.tgz", 853 | "integrity": "sha512-GFZS3c/MhX1OusqB1MZ1ct2xRzX5ppQh2JU1h2Pnfk88HtFTM+TWQqJNfwkmxtPQtb/s1tk87oENfXJlx7rSDw==", 854 | "dev": true, 855 | "requires": { 856 | "@babel/helper-plugin-utils": "^7.10.4" 857 | } 858 | }, 859 | "@babel/plugin-transform-spread": { 860 | "version": "7.12.1", 861 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.12.1.tgz", 862 | "integrity": "sha512-vuLp8CP0BE18zVYjsEBZ5xoCecMK6LBMMxYzJnh01rxQRvhNhH1csMMmBfNo5tGpGO+NhdSNW2mzIvBu3K1fng==", 863 | "dev": true, 864 | "requires": { 865 | "@babel/helper-plugin-utils": "^7.10.4", 866 | "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1" 867 | } 868 | }, 869 | "@babel/plugin-transform-sticky-regex": { 870 | "version": "7.12.7", 871 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.7.tgz", 872 | "integrity": "sha512-VEiqZL5N/QvDbdjfYQBhruN0HYjSPjC4XkeqW4ny/jNtH9gcbgaqBIXYEZCNnESMAGs0/K/R7oFGMhOyu/eIxg==", 873 | "dev": true, 874 | "requires": { 875 | "@babel/helper-plugin-utils": "^7.10.4" 876 | } 877 | }, 878 | "@babel/plugin-transform-template-literals": { 879 | "version": "7.12.1", 880 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.12.1.tgz", 881 | "integrity": "sha512-b4Zx3KHi+taXB1dVRBhVJtEPi9h1THCeKmae2qP0YdUHIFhVjtpqqNfxeVAa1xeHVhAy4SbHxEwx5cltAu5apw==", 882 | "dev": true, 883 | "requires": { 884 | "@babel/helper-plugin-utils": "^7.10.4" 885 | } 886 | }, 887 | "@babel/plugin-transform-typeof-symbol": { 888 | "version": "7.12.10", 889 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.10.tgz", 890 | "integrity": "sha512-JQ6H8Rnsogh//ijxspCjc21YPd3VLVoYtAwv3zQmqAt8YGYUtdo5usNhdl4b9/Vir2kPFZl6n1h0PfUz4hJhaA==", 891 | "dev": true, 892 | "requires": { 893 | "@babel/helper-plugin-utils": "^7.10.4" 894 | } 895 | }, 896 | "@babel/plugin-transform-unicode-escapes": { 897 | "version": "7.12.1", 898 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.1.tgz", 899 | "integrity": "sha512-I8gNHJLIc7GdApm7wkVnStWssPNbSRMPtgHdmH3sRM1zopz09UWPS4x5V4n1yz/MIWTVnJ9sp6IkuXdWM4w+2Q==", 900 | "dev": true, 901 | "requires": { 902 | "@babel/helper-plugin-utils": "^7.10.4" 903 | } 904 | }, 905 | "@babel/plugin-transform-unicode-regex": { 906 | "version": "7.12.1", 907 | "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.12.1.tgz", 908 | "integrity": "sha512-SqH4ClNngh/zGwHZOOQMTD+e8FGWexILV+ePMyiDJttAWRh5dhDL8rcl5lSgU3Huiq6Zn6pWTMvdPAb21Dwdyg==", 909 | "dev": true, 910 | "requires": { 911 | "@babel/helper-create-regexp-features-plugin": "^7.12.1", 912 | "@babel/helper-plugin-utils": "^7.10.4" 913 | } 914 | }, 915 | "@babel/preset-env": { 916 | "version": "7.12.11", 917 | "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.12.11.tgz", 918 | "integrity": "sha512-j8Tb+KKIXKYlDBQyIOy4BLxzv1NUOwlHfZ74rvW+Z0Gp4/cI2IMDPBWAgWceGcE7aep9oL/0K9mlzlMGxA8yNw==", 919 | "dev": true, 920 | "requires": { 921 | "@babel/compat-data": "^7.12.7", 922 | "@babel/helper-compilation-targets": "^7.12.5", 923 | "@babel/helper-module-imports": "^7.12.5", 924 | "@babel/helper-plugin-utils": "^7.10.4", 925 | "@babel/helper-validator-option": "^7.12.11", 926 | "@babel/plugin-proposal-async-generator-functions": "^7.12.1", 927 | "@babel/plugin-proposal-class-properties": "^7.12.1", 928 | "@babel/plugin-proposal-dynamic-import": "^7.12.1", 929 | "@babel/plugin-proposal-export-namespace-from": "^7.12.1", 930 | "@babel/plugin-proposal-json-strings": "^7.12.1", 931 | "@babel/plugin-proposal-logical-assignment-operators": "^7.12.1", 932 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", 933 | "@babel/plugin-proposal-numeric-separator": "^7.12.7", 934 | "@babel/plugin-proposal-object-rest-spread": "^7.12.1", 935 | "@babel/plugin-proposal-optional-catch-binding": "^7.12.1", 936 | "@babel/plugin-proposal-optional-chaining": "^7.12.7", 937 | "@babel/plugin-proposal-private-methods": "^7.12.1", 938 | "@babel/plugin-proposal-unicode-property-regex": "^7.12.1", 939 | "@babel/plugin-syntax-async-generators": "^7.8.0", 940 | "@babel/plugin-syntax-class-properties": "^7.12.1", 941 | "@babel/plugin-syntax-dynamic-import": "^7.8.0", 942 | "@babel/plugin-syntax-export-namespace-from": "^7.8.3", 943 | "@babel/plugin-syntax-json-strings": "^7.8.0", 944 | "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", 945 | "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0", 946 | "@babel/plugin-syntax-numeric-separator": "^7.10.4", 947 | "@babel/plugin-syntax-object-rest-spread": "^7.8.0", 948 | "@babel/plugin-syntax-optional-catch-binding": "^7.8.0", 949 | "@babel/plugin-syntax-optional-chaining": "^7.8.0", 950 | "@babel/plugin-syntax-top-level-await": "^7.12.1", 951 | "@babel/plugin-transform-arrow-functions": "^7.12.1", 952 | "@babel/plugin-transform-async-to-generator": "^7.12.1", 953 | "@babel/plugin-transform-block-scoped-functions": "^7.12.1", 954 | "@babel/plugin-transform-block-scoping": "^7.12.11", 955 | "@babel/plugin-transform-classes": "^7.12.1", 956 | "@babel/plugin-transform-computed-properties": "^7.12.1", 957 | "@babel/plugin-transform-destructuring": "^7.12.1", 958 | "@babel/plugin-transform-dotall-regex": "^7.12.1", 959 | "@babel/plugin-transform-duplicate-keys": "^7.12.1", 960 | "@babel/plugin-transform-exponentiation-operator": "^7.12.1", 961 | "@babel/plugin-transform-for-of": "^7.12.1", 962 | "@babel/plugin-transform-function-name": "^7.12.1", 963 | "@babel/plugin-transform-literals": "^7.12.1", 964 | "@babel/plugin-transform-member-expression-literals": "^7.12.1", 965 | "@babel/plugin-transform-modules-amd": "^7.12.1", 966 | "@babel/plugin-transform-modules-commonjs": "^7.12.1", 967 | "@babel/plugin-transform-modules-systemjs": "^7.12.1", 968 | "@babel/plugin-transform-modules-umd": "^7.12.1", 969 | "@babel/plugin-transform-named-capturing-groups-regex": "^7.12.1", 970 | "@babel/plugin-transform-new-target": "^7.12.1", 971 | "@babel/plugin-transform-object-super": "^7.12.1", 972 | "@babel/plugin-transform-parameters": "^7.12.1", 973 | "@babel/plugin-transform-property-literals": "^7.12.1", 974 | "@babel/plugin-transform-regenerator": "^7.12.1", 975 | "@babel/plugin-transform-reserved-words": "^7.12.1", 976 | "@babel/plugin-transform-shorthand-properties": "^7.12.1", 977 | "@babel/plugin-transform-spread": "^7.12.1", 978 | "@babel/plugin-transform-sticky-regex": "^7.12.7", 979 | "@babel/plugin-transform-template-literals": "^7.12.1", 980 | "@babel/plugin-transform-typeof-symbol": "^7.12.10", 981 | "@babel/plugin-transform-unicode-escapes": "^7.12.1", 982 | "@babel/plugin-transform-unicode-regex": "^7.12.1", 983 | "@babel/preset-modules": "^0.1.3", 984 | "@babel/types": "^7.12.11", 985 | "core-js-compat": "^3.8.0", 986 | "semver": "^5.5.0" 987 | } 988 | }, 989 | "@babel/preset-modules": { 990 | "version": "0.1.4", 991 | "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.4.tgz", 992 | "integrity": "sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg==", 993 | "dev": true, 994 | "requires": { 995 | "@babel/helper-plugin-utils": "^7.0.0", 996 | "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", 997 | "@babel/plugin-transform-dotall-regex": "^7.4.4", 998 | "@babel/types": "^7.4.4", 999 | "esutils": "^2.0.2" 1000 | } 1001 | }, 1002 | "@babel/preset-react": { 1003 | "version": "7.12.10", 1004 | "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.12.10.tgz", 1005 | "integrity": "sha512-vtQNjaHRl4DUpp+t+g4wvTHsLQuye+n0H/wsXIZRn69oz/fvNC7gQ4IK73zGJBaxvHoxElDvnYCthMcT7uzFoQ==", 1006 | "dev": true, 1007 | "requires": { 1008 | "@babel/helper-plugin-utils": "^7.10.4", 1009 | "@babel/plugin-transform-react-display-name": "^7.12.1", 1010 | "@babel/plugin-transform-react-jsx": "^7.12.10", 1011 | "@babel/plugin-transform-react-jsx-development": "^7.12.7", 1012 | "@babel/plugin-transform-react-pure-annotations": "^7.12.1" 1013 | } 1014 | }, 1015 | "@babel/register": { 1016 | "version": "7.12.10", 1017 | "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.12.10.tgz", 1018 | "integrity": "sha512-EvX/BvMMJRAA3jZgILWgbsrHwBQvllC5T8B29McyME8DvkdOxk4ujESfrMvME8IHSDvWXrmMXxPvA/lx2gqPLQ==", 1019 | "dev": true, 1020 | "requires": { 1021 | "find-cache-dir": "^2.0.0", 1022 | "lodash": "^4.17.19", 1023 | "make-dir": "^2.1.0", 1024 | "pirates": "^4.0.0", 1025 | "source-map-support": "^0.5.16" 1026 | }, 1027 | "dependencies": { 1028 | "find-cache-dir": { 1029 | "version": "2.1.0", 1030 | "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", 1031 | "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", 1032 | "dev": true, 1033 | "requires": { 1034 | "commondir": "^1.0.1", 1035 | "make-dir": "^2.0.0", 1036 | "pkg-dir": "^3.0.0" 1037 | } 1038 | }, 1039 | "find-up": { 1040 | "version": "3.0.0", 1041 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", 1042 | "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", 1043 | "dev": true, 1044 | "requires": { 1045 | "locate-path": "^3.0.0" 1046 | } 1047 | }, 1048 | "locate-path": { 1049 | "version": "3.0.0", 1050 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", 1051 | "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", 1052 | "dev": true, 1053 | "requires": { 1054 | "p-locate": "^3.0.0", 1055 | "path-exists": "^3.0.0" 1056 | } 1057 | }, 1058 | "make-dir": { 1059 | "version": "2.1.0", 1060 | "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", 1061 | "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", 1062 | "dev": true, 1063 | "requires": { 1064 | "pify": "^4.0.1", 1065 | "semver": "^5.6.0" 1066 | } 1067 | }, 1068 | "p-locate": { 1069 | "version": "3.0.0", 1070 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", 1071 | "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", 1072 | "dev": true, 1073 | "requires": { 1074 | "p-limit": "^2.0.0" 1075 | } 1076 | }, 1077 | "path-exists": { 1078 | "version": "3.0.0", 1079 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", 1080 | "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", 1081 | "dev": true 1082 | }, 1083 | "pkg-dir": { 1084 | "version": "3.0.0", 1085 | "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", 1086 | "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", 1087 | "dev": true, 1088 | "requires": { 1089 | "find-up": "^3.0.0" 1090 | } 1091 | } 1092 | } 1093 | }, 1094 | "@babel/runtime": { 1095 | "version": "7.12.5", 1096 | "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", 1097 | "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", 1098 | "dev": true, 1099 | "requires": { 1100 | "regenerator-runtime": "^0.13.4" 1101 | } 1102 | }, 1103 | "@babel/template": { 1104 | "version": "7.12.7", 1105 | "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", 1106 | "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==", 1107 | "dev": true, 1108 | "requires": { 1109 | "@babel/code-frame": "^7.10.4", 1110 | "@babel/parser": "^7.12.7", 1111 | "@babel/types": "^7.12.7" 1112 | } 1113 | }, 1114 | "@babel/traverse": { 1115 | "version": "7.12.12", 1116 | "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.12.tgz", 1117 | "integrity": "sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w==", 1118 | "dev": true, 1119 | "requires": { 1120 | "@babel/code-frame": "^7.12.11", 1121 | "@babel/generator": "^7.12.11", 1122 | "@babel/helper-function-name": "^7.12.11", 1123 | "@babel/helper-split-export-declaration": "^7.12.11", 1124 | "@babel/parser": "^7.12.11", 1125 | "@babel/types": "^7.12.12", 1126 | "debug": "^4.1.0", 1127 | "globals": "^11.1.0", 1128 | "lodash": "^4.17.19" 1129 | } 1130 | }, 1131 | "@babel/types": { 1132 | "version": "7.12.12", 1133 | "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", 1134 | "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", 1135 | "dev": true, 1136 | "requires": { 1137 | "@babel/helper-validator-identifier": "^7.12.11", 1138 | "lodash": "^4.17.19", 1139 | "to-fast-properties": "^2.0.0" 1140 | } 1141 | }, 1142 | "@types/json-schema": { 1143 | "version": "7.0.6", 1144 | "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", 1145 | "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==", 1146 | "dev": true 1147 | }, 1148 | "ajv": { 1149 | "version": "6.12.6", 1150 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 1151 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 1152 | "dev": true, 1153 | "requires": { 1154 | "fast-deep-equal": "^3.1.1", 1155 | "fast-json-stable-stringify": "^2.0.0", 1156 | "json-schema-traverse": "^0.4.1", 1157 | "uri-js": "^4.2.2" 1158 | } 1159 | }, 1160 | "ajv-keywords": { 1161 | "version": "3.5.2", 1162 | "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", 1163 | "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", 1164 | "dev": true 1165 | }, 1166 | "ansi-styles": { 1167 | "version": "3.2.1", 1168 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 1169 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 1170 | "dev": true, 1171 | "requires": { 1172 | "color-convert": "^1.9.0" 1173 | } 1174 | }, 1175 | "babel-loader": { 1176 | "version": "8.2.2", 1177 | "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.2.tgz", 1178 | "integrity": "sha512-JvTd0/D889PQBtUXJ2PXaKU/pjZDMtHA9V2ecm+eNRmmBCMR09a+fmpGTNwnJtFmFl5Ei7Vy47LjBb+L0wQ99g==", 1179 | "dev": true, 1180 | "requires": { 1181 | "find-cache-dir": "^3.3.1", 1182 | "loader-utils": "^1.4.0", 1183 | "make-dir": "^3.1.0", 1184 | "schema-utils": "^2.6.5" 1185 | } 1186 | }, 1187 | "babel-plugin-dynamic-import-node": { 1188 | "version": "2.3.3", 1189 | "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", 1190 | "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", 1191 | "dev": true, 1192 | "requires": { 1193 | "object.assign": "^4.1.0" 1194 | } 1195 | }, 1196 | "big.js": { 1197 | "version": "5.2.2", 1198 | "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", 1199 | "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", 1200 | "dev": true 1201 | }, 1202 | "browserslist": { 1203 | "version": "4.16.1", 1204 | "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.1.tgz", 1205 | "integrity": "sha512-UXhDrwqsNcpTYJBTZsbGATDxZbiVDsx6UjpmRUmtnP10pr8wAYr5LgFoEFw9ixriQH2mv/NX2SfGzE/o8GndLA==", 1206 | "dev": true, 1207 | "requires": { 1208 | "caniuse-lite": "^1.0.30001173", 1209 | "colorette": "^1.2.1", 1210 | "electron-to-chromium": "^1.3.634", 1211 | "escalade": "^3.1.1", 1212 | "node-releases": "^1.1.69" 1213 | } 1214 | }, 1215 | "buffer-from": { 1216 | "version": "1.1.1", 1217 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 1218 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 1219 | "dev": true 1220 | }, 1221 | "call-bind": { 1222 | "version": "1.0.1", 1223 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.1.tgz", 1224 | "integrity": "sha512-tvAvUwNcRikl3RVF20X9lsYmmepsovzTWeJiXjO0PkJp15uy/6xKFZOQtuiSULwYW+6ToZBprphCgWXC2dSgcQ==", 1225 | "dev": true, 1226 | "requires": { 1227 | "function-bind": "^1.1.1", 1228 | "get-intrinsic": "^1.0.2" 1229 | } 1230 | }, 1231 | "caniuse-lite": { 1232 | "version": "1.0.30001173", 1233 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001173.tgz", 1234 | "integrity": "sha512-R3aqmjrICdGCTAnSXtNyvWYMK3YtV5jwudbq0T7nN9k4kmE4CBuwPqyJ+KBzepSTh0huivV2gLbSMEzTTmfeYw==", 1235 | "dev": true 1236 | }, 1237 | "chalk": { 1238 | "version": "2.4.2", 1239 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 1240 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 1241 | "dev": true, 1242 | "requires": { 1243 | "ansi-styles": "^3.2.1", 1244 | "escape-string-regexp": "^1.0.5", 1245 | "supports-color": "^5.3.0" 1246 | } 1247 | }, 1248 | "color-convert": { 1249 | "version": "1.9.3", 1250 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 1251 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 1252 | "dev": true, 1253 | "requires": { 1254 | "color-name": "1.1.3" 1255 | } 1256 | }, 1257 | "color-name": { 1258 | "version": "1.1.3", 1259 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 1260 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 1261 | "dev": true 1262 | }, 1263 | "colorette": { 1264 | "version": "1.2.1", 1265 | "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", 1266 | "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==", 1267 | "dev": true 1268 | }, 1269 | "commondir": { 1270 | "version": "1.0.1", 1271 | "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", 1272 | "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", 1273 | "dev": true 1274 | }, 1275 | "convert-source-map": { 1276 | "version": "1.7.0", 1277 | "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", 1278 | "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", 1279 | "dev": true, 1280 | "requires": { 1281 | "safe-buffer": "~5.1.1" 1282 | } 1283 | }, 1284 | "core-js-compat": { 1285 | "version": "3.8.2", 1286 | "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.8.2.tgz", 1287 | "integrity": "sha512-LO8uL9lOIyRRrQmZxHZFl1RV+ZbcsAkFWTktn5SmH40WgLtSNYN4m4W2v9ONT147PxBY/XrRhrWq8TlvObyUjQ==", 1288 | "dev": true, 1289 | "requires": { 1290 | "browserslist": "^4.16.0", 1291 | "semver": "7.0.0" 1292 | }, 1293 | "dependencies": { 1294 | "semver": { 1295 | "version": "7.0.0", 1296 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", 1297 | "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", 1298 | "dev": true 1299 | } 1300 | } 1301 | }, 1302 | "debug": { 1303 | "version": "4.3.1", 1304 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", 1305 | "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", 1306 | "dev": true, 1307 | "requires": { 1308 | "ms": "2.1.2" 1309 | } 1310 | }, 1311 | "define-properties": { 1312 | "version": "1.1.3", 1313 | "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", 1314 | "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", 1315 | "dev": true, 1316 | "requires": { 1317 | "object-keys": "^1.0.12" 1318 | } 1319 | }, 1320 | "electron-to-chromium": { 1321 | "version": "1.3.635", 1322 | "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.635.tgz", 1323 | "integrity": "sha512-RRriZOLs9CpW6KTLmgBqyUdnY0QNqqWs0HOtuQGGEMizOTNNn1P7sGRBxARnUeLejOsgwjDyRqT3E/CSst02ZQ==", 1324 | "dev": true 1325 | }, 1326 | "elixir-node-ssr": { 1327 | "version": "file:../deps/node_ssr" 1328 | }, 1329 | "emojis-list": { 1330 | "version": "3.0.0", 1331 | "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", 1332 | "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", 1333 | "dev": true 1334 | }, 1335 | "escalade": { 1336 | "version": "3.1.1", 1337 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 1338 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", 1339 | "dev": true 1340 | }, 1341 | "escape-string-regexp": { 1342 | "version": "1.0.5", 1343 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 1344 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 1345 | "dev": true 1346 | }, 1347 | "esutils": { 1348 | "version": "2.0.3", 1349 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 1350 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 1351 | "dev": true 1352 | }, 1353 | "fast-deep-equal": { 1354 | "version": "3.1.3", 1355 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 1356 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 1357 | "dev": true 1358 | }, 1359 | "fast-json-stable-stringify": { 1360 | "version": "2.1.0", 1361 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 1362 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 1363 | "dev": true 1364 | }, 1365 | "find-cache-dir": { 1366 | "version": "3.3.1", 1367 | "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", 1368 | "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", 1369 | "dev": true, 1370 | "requires": { 1371 | "commondir": "^1.0.1", 1372 | "make-dir": "^3.0.2", 1373 | "pkg-dir": "^4.1.0" 1374 | } 1375 | }, 1376 | "find-up": { 1377 | "version": "4.1.0", 1378 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", 1379 | "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", 1380 | "dev": true, 1381 | "requires": { 1382 | "locate-path": "^5.0.0", 1383 | "path-exists": "^4.0.0" 1384 | } 1385 | }, 1386 | "function-bind": { 1387 | "version": "1.1.1", 1388 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 1389 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 1390 | "dev": true 1391 | }, 1392 | "gensync": { 1393 | "version": "1.0.0-beta.2", 1394 | "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", 1395 | "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", 1396 | "dev": true 1397 | }, 1398 | "get-intrinsic": { 1399 | "version": "1.0.2", 1400 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.2.tgz", 1401 | "integrity": "sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg==", 1402 | "dev": true, 1403 | "requires": { 1404 | "function-bind": "^1.1.1", 1405 | "has": "^1.0.3", 1406 | "has-symbols": "^1.0.1" 1407 | } 1408 | }, 1409 | "globals": { 1410 | "version": "11.12.0", 1411 | "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", 1412 | "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", 1413 | "dev": true 1414 | }, 1415 | "has": { 1416 | "version": "1.0.3", 1417 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 1418 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 1419 | "dev": true, 1420 | "requires": { 1421 | "function-bind": "^1.1.1" 1422 | } 1423 | }, 1424 | "has-flag": { 1425 | "version": "3.0.0", 1426 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 1427 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 1428 | "dev": true 1429 | }, 1430 | "has-symbols": { 1431 | "version": "1.0.1", 1432 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", 1433 | "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", 1434 | "dev": true 1435 | }, 1436 | "js-tokens": { 1437 | "version": "4.0.0", 1438 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 1439 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 1440 | }, 1441 | "jsesc": { 1442 | "version": "2.5.2", 1443 | "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", 1444 | "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", 1445 | "dev": true 1446 | }, 1447 | "json-schema-traverse": { 1448 | "version": "0.4.1", 1449 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 1450 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 1451 | "dev": true 1452 | }, 1453 | "json5": { 1454 | "version": "2.1.3", 1455 | "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", 1456 | "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", 1457 | "dev": true, 1458 | "requires": { 1459 | "minimist": "^1.2.5" 1460 | } 1461 | }, 1462 | "loader-utils": { 1463 | "version": "1.4.0", 1464 | "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", 1465 | "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", 1466 | "dev": true, 1467 | "requires": { 1468 | "big.js": "^5.2.2", 1469 | "emojis-list": "^3.0.0", 1470 | "json5": "^1.0.1" 1471 | }, 1472 | "dependencies": { 1473 | "json5": { 1474 | "version": "1.0.1", 1475 | "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", 1476 | "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", 1477 | "dev": true, 1478 | "requires": { 1479 | "minimist": "^1.2.0" 1480 | } 1481 | } 1482 | } 1483 | }, 1484 | "locate-path": { 1485 | "version": "5.0.0", 1486 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", 1487 | "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", 1488 | "dev": true, 1489 | "requires": { 1490 | "p-locate": "^4.1.0" 1491 | } 1492 | }, 1493 | "lodash": { 1494 | "version": "4.17.20", 1495 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", 1496 | "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", 1497 | "dev": true 1498 | }, 1499 | "loose-envify": { 1500 | "version": "1.4.0", 1501 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 1502 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 1503 | "requires": { 1504 | "js-tokens": "^3.0.0 || ^4.0.0" 1505 | } 1506 | }, 1507 | "make-dir": { 1508 | "version": "3.1.0", 1509 | "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", 1510 | "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", 1511 | "dev": true, 1512 | "requires": { 1513 | "semver": "^6.0.0" 1514 | }, 1515 | "dependencies": { 1516 | "semver": { 1517 | "version": "6.3.0", 1518 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", 1519 | "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", 1520 | "dev": true 1521 | } 1522 | } 1523 | }, 1524 | "minimist": { 1525 | "version": "1.2.5", 1526 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 1527 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", 1528 | "dev": true 1529 | }, 1530 | "ms": { 1531 | "version": "2.1.2", 1532 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 1533 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 1534 | "dev": true 1535 | }, 1536 | "node-modules-regexp": { 1537 | "version": "1.0.0", 1538 | "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", 1539 | "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", 1540 | "dev": true 1541 | }, 1542 | "node-releases": { 1543 | "version": "1.1.69", 1544 | "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.69.tgz", 1545 | "integrity": "sha512-DGIjo79VDEyAnRlfSqYTsy+yoHd2IOjJiKUozD2MV2D85Vso6Bug56mb9tT/fY5Urt0iqk01H7x+llAruDR2zA==", 1546 | "dev": true 1547 | }, 1548 | "object-assign": { 1549 | "version": "4.1.1", 1550 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 1551 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 1552 | }, 1553 | "object-keys": { 1554 | "version": "1.1.1", 1555 | "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", 1556 | "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", 1557 | "dev": true 1558 | }, 1559 | "object.assign": { 1560 | "version": "4.1.2", 1561 | "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", 1562 | "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", 1563 | "dev": true, 1564 | "requires": { 1565 | "call-bind": "^1.0.0", 1566 | "define-properties": "^1.1.3", 1567 | "has-symbols": "^1.0.1", 1568 | "object-keys": "^1.1.1" 1569 | } 1570 | }, 1571 | "p-limit": { 1572 | "version": "2.3.0", 1573 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", 1574 | "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", 1575 | "dev": true, 1576 | "requires": { 1577 | "p-try": "^2.0.0" 1578 | } 1579 | }, 1580 | "p-locate": { 1581 | "version": "4.1.0", 1582 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", 1583 | "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", 1584 | "dev": true, 1585 | "requires": { 1586 | "p-limit": "^2.2.0" 1587 | } 1588 | }, 1589 | "p-try": { 1590 | "version": "2.2.0", 1591 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", 1592 | "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", 1593 | "dev": true 1594 | }, 1595 | "path-exists": { 1596 | "version": "4.0.0", 1597 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 1598 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 1599 | "dev": true 1600 | }, 1601 | "pify": { 1602 | "version": "4.0.1", 1603 | "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", 1604 | "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", 1605 | "dev": true 1606 | }, 1607 | "pirates": { 1608 | "version": "4.0.1", 1609 | "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", 1610 | "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", 1611 | "dev": true, 1612 | "requires": { 1613 | "node-modules-regexp": "^1.0.0" 1614 | } 1615 | }, 1616 | "pkg-dir": { 1617 | "version": "4.2.0", 1618 | "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", 1619 | "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", 1620 | "dev": true, 1621 | "requires": { 1622 | "find-up": "^4.0.0" 1623 | } 1624 | }, 1625 | "punycode": { 1626 | "version": "2.1.1", 1627 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 1628 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 1629 | "dev": true 1630 | }, 1631 | "react": { 1632 | "version": "17.0.1", 1633 | "resolved": "https://registry.npmjs.org/react/-/react-17.0.1.tgz", 1634 | "integrity": "sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w==", 1635 | "requires": { 1636 | "loose-envify": "^1.1.0", 1637 | "object-assign": "^4.1.1" 1638 | } 1639 | }, 1640 | "react-dom": { 1641 | "version": "17.0.1", 1642 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz", 1643 | "integrity": "sha512-6eV150oJZ9U2t9svnsspTMrWNyHc6chX0KzDeAOXftRa8bNeOKTTfCJ7KorIwenkHd2xqVTBTCZd79yk/lx/Ug==", 1644 | "requires": { 1645 | "loose-envify": "^1.1.0", 1646 | "object-assign": "^4.1.1", 1647 | "scheduler": "^0.20.1" 1648 | } 1649 | }, 1650 | "react-surface": { 1651 | "version": "file:.." 1652 | }, 1653 | "regenerate": { 1654 | "version": "1.4.2", 1655 | "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", 1656 | "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", 1657 | "dev": true 1658 | }, 1659 | "regenerate-unicode-properties": { 1660 | "version": "8.2.0", 1661 | "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz", 1662 | "integrity": "sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==", 1663 | "dev": true, 1664 | "requires": { 1665 | "regenerate": "^1.4.0" 1666 | } 1667 | }, 1668 | "regenerator-runtime": { 1669 | "version": "0.13.7", 1670 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", 1671 | "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", 1672 | "dev": true 1673 | }, 1674 | "regenerator-transform": { 1675 | "version": "0.14.5", 1676 | "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", 1677 | "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", 1678 | "dev": true, 1679 | "requires": { 1680 | "@babel/runtime": "^7.8.4" 1681 | } 1682 | }, 1683 | "regexpu-core": { 1684 | "version": "4.7.1", 1685 | "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz", 1686 | "integrity": "sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==", 1687 | "dev": true, 1688 | "requires": { 1689 | "regenerate": "^1.4.0", 1690 | "regenerate-unicode-properties": "^8.2.0", 1691 | "regjsgen": "^0.5.1", 1692 | "regjsparser": "^0.6.4", 1693 | "unicode-match-property-ecmascript": "^1.0.4", 1694 | "unicode-match-property-value-ecmascript": "^1.2.0" 1695 | } 1696 | }, 1697 | "regjsgen": { 1698 | "version": "0.5.2", 1699 | "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", 1700 | "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==", 1701 | "dev": true 1702 | }, 1703 | "regjsparser": { 1704 | "version": "0.6.4", 1705 | "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.4.tgz", 1706 | "integrity": "sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==", 1707 | "dev": true, 1708 | "requires": { 1709 | "jsesc": "~0.5.0" 1710 | }, 1711 | "dependencies": { 1712 | "jsesc": { 1713 | "version": "0.5.0", 1714 | "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", 1715 | "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", 1716 | "dev": true 1717 | } 1718 | } 1719 | }, 1720 | "safe-buffer": { 1721 | "version": "5.1.2", 1722 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 1723 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", 1724 | "dev": true 1725 | }, 1726 | "scheduler": { 1727 | "version": "0.20.1", 1728 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.1.tgz", 1729 | "integrity": "sha512-LKTe+2xNJBNxu/QhHvDR14wUXHRQbVY5ZOYpOGWRzhydZUqrLb2JBvLPY7cAqFmqrWuDED0Mjk7013SZiOz6Bw==", 1730 | "requires": { 1731 | "loose-envify": "^1.1.0", 1732 | "object-assign": "^4.1.1" 1733 | } 1734 | }, 1735 | "schema-utils": { 1736 | "version": "2.7.1", 1737 | "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", 1738 | "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", 1739 | "dev": true, 1740 | "requires": { 1741 | "@types/json-schema": "^7.0.5", 1742 | "ajv": "^6.12.4", 1743 | "ajv-keywords": "^3.5.2" 1744 | } 1745 | }, 1746 | "semver": { 1747 | "version": "5.7.1", 1748 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 1749 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", 1750 | "dev": true 1751 | }, 1752 | "source-map": { 1753 | "version": "0.5.7", 1754 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", 1755 | "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", 1756 | "dev": true 1757 | }, 1758 | "source-map-support": { 1759 | "version": "0.5.19", 1760 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 1761 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 1762 | "dev": true, 1763 | "requires": { 1764 | "buffer-from": "^1.0.0", 1765 | "source-map": "^0.6.0" 1766 | }, 1767 | "dependencies": { 1768 | "source-map": { 1769 | "version": "0.6.1", 1770 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 1771 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 1772 | "dev": true 1773 | } 1774 | } 1775 | }, 1776 | "supports-color": { 1777 | "version": "5.5.0", 1778 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 1779 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 1780 | "dev": true, 1781 | "requires": { 1782 | "has-flag": "^3.0.0" 1783 | } 1784 | }, 1785 | "to-fast-properties": { 1786 | "version": "2.0.0", 1787 | "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", 1788 | "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", 1789 | "dev": true 1790 | }, 1791 | "unicode-canonical-property-names-ecmascript": { 1792 | "version": "1.0.4", 1793 | "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", 1794 | "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", 1795 | "dev": true 1796 | }, 1797 | "unicode-match-property-ecmascript": { 1798 | "version": "1.0.4", 1799 | "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", 1800 | "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", 1801 | "dev": true, 1802 | "requires": { 1803 | "unicode-canonical-property-names-ecmascript": "^1.0.4", 1804 | "unicode-property-aliases-ecmascript": "^1.0.4" 1805 | } 1806 | }, 1807 | "unicode-match-property-value-ecmascript": { 1808 | "version": "1.2.0", 1809 | "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz", 1810 | "integrity": "sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==", 1811 | "dev": true 1812 | }, 1813 | "unicode-property-aliases-ecmascript": { 1814 | "version": "1.1.0", 1815 | "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz", 1816 | "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==", 1817 | "dev": true 1818 | }, 1819 | "uri-js": { 1820 | "version": "4.4.0", 1821 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", 1822 | "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", 1823 | "dev": true, 1824 | "requires": { 1825 | "punycode": "^2.1.0" 1826 | } 1827 | } 1828 | } 1829 | } 1830 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-surface-test", 3 | "version": "0.1.0", 4 | "description": "Tests", 5 | "license": "MIT", 6 | "repository": {}, 7 | "scripts": {}, 8 | "dependencies": { 9 | "react": ">=17", 10 | "react-dom": ">=17", 11 | "react-surface": "file:../", 12 | "elixir-node-ssr": "file:../deps/node_ssr" 13 | }, 14 | "devDependencies": { 15 | "@babel/core": "^7.9.0", 16 | "@babel/register": "^7.12.10", 17 | "@babel/preset-env": "^7.9.5", 18 | "@babel/preset-react": "^7.12.10", 19 | "babel-loader": "^8.1.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/react_surface_test.exs: -------------------------------------------------------------------------------- 1 | 2 | 3 | defmodule ReactSurfaceTest do 4 | use ReactSurface.ConnCase, async: false 5 | # doctest ReactSurface 6 | alias ReactSurface.React 7 | 8 | defmodule Hello do 9 | @moduledoc """ 10 | Test component, corresponding react component `components/Hello.js` 11 | """ 12 | use ReactSurface.SSR, default_props: %{name: "HELLO"} 13 | end 14 | 15 | defmodule SSRView do 16 | use Surface.LiveView 17 | 18 | def render(assigns) do 19 | ~H""" 20 | 21 | """ 22 | end 23 | end 24 | 25 | defmodule View do 26 | use Surface.LiveView 27 | 28 | def render(assigns) do 29 | ~H""" 30 | 31 | """ 32 | end 33 | end 34 | 35 | test "rendering a container", %{conn: conn} do 36 | encoded_props = Jason.encode!(%{test: "props"}) |> Base.encode64(padding: false) 37 | 38 | {:ok, _view, html} = live_isolated(conn, View) 39 | assert html =~ "id=\"rF7FF9E8B\" phx-update=\"ignore\"" 40 | assert html =~ "rs-p=\"#{encoded_props}\"" 41 | assert html =~ "rs-m=\"r\"" 42 | assert html =~ "phx-hook=\"_RS\"" 43 | end 44 | 45 | test "rendering a ssr container", %{conn: conn} do 46 | encoded_props = Jason.encode!(%{name: "NEW NAME"}) |> Base.encode64(padding: false) 47 | 48 | {:ok, _view, html} = live_isolated(conn, SSRView) 49 | assert html =~ "rs-p=\"#{encoded_props}\"" 50 | assert html =~ "phx-hook=\"_RS\"" 51 | assert html =~ "rs-m=\"h\"" 52 | assert html =~ "

HELLO

" 53 | end 54 | 55 | end 56 | -------------------------------------------------------------------------------- /test/ssr.js: -------------------------------------------------------------------------------- 1 | require("@babel/register")({ cwd: __dirname }); 2 | // starts local http service to perform Node SSR 3 | const startService = require("elixir-node-ssr"); 4 | // a render function that takes a component name + props and returns a json response 5 | const { render } = require("react-surface/priv/react-ssr"); 6 | 7 | const opts = { 8 | debug: false, 9 | }; 10 | 11 | // starts listening on a random tcp port for render requests 12 | startService(render, opts); -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule ReactSurface.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | It also imports other functionality to make it easier 6 | to test components. 7 | """ 8 | 9 | use ExUnit.CaseTemplate 10 | 11 | using do 12 | quote do 13 | # Import conveniences for testing 14 | use ReactSurface.LiveViewTest 15 | 16 | # The default endpoint for testing 17 | @endpoint Endpoint 18 | end 19 | end 20 | 21 | setup _tags do 22 | {:ok, conn: Phoenix.ConnTest.build_conn()} 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | defmodule Router do 4 | use Phoenix.Router 5 | end 6 | 7 | defmodule Endpoint do 8 | use Phoenix.Endpoint, otp_app: :react_surface 9 | plug(Router) 10 | end 11 | 12 | Application.put_env(:react_surface, Endpoint, 13 | secret_key_base: "J4lTFt000ENUVhu3dbIB2P2vRVl2nDBH6FLefnPUImL8mHYNX8Kln/N9J0HH19Mq", 14 | live_view: [ 15 | signing_salt: "LfCCMxfkGME8S8P8XU3Z6/7+ZlD9611u" 16 | ] 17 | ) 18 | 19 | # Application.ensure_all_started(:node_ssr) 20 | 21 | Endpoint.start_link() 22 | --------------------------------------------------------------------------------