Skip to content
Permalink
Browse files

feat: Isolate iframe code from parent window (#57)

fixes #45
  • Loading branch information...
markdalgleish committed Apr 8, 2019
1 parent 9ccae36 commit 03e41a3dfe715caef01b948548ece40c8dc50468
@@ -0,0 +1,3 @@
cypress/plugins/
cypress/fixtures/
dist
@@ -4,7 +4,8 @@ export const typeCode = code =>
.click()
.focused()
.clear({ force: true })
.type(code, { force: true, delay: 100 });
.type(code, { force: true, delay: 100 })
.wait(1000);

export const assertFrameContains = async text => {
const iframe = await cy.get('iframe').first();
@@ -10,7 +10,7 @@ module.exports = {
components: 'braid-design-system',
themes: 'braid-design-system/lib/themes',
frameComponent: './playroom/FrameComponent.js',
widths: [320, 375, 768, 1024],
widths: [320, 1024],
exampleCode: `
<ChecklistCard>
<Checkbox id="1" label="This is a checkbox" message={false} onChange={() => {}}>
@@ -18,18 +18,21 @@ module.exports = async (playroomConfig, options) => {

const staticTypes = await getStaticTypes(playroomConfig);

const devServerEntries = options.production
? []
: [
`webpack-dev-server/client?http://localhost:${playroomConfig.port}`,
'webpack/hot/dev-server'
];

const ourConfig = {
mode: options.production ? 'production' : 'development',
entry: [
...(options.production
? []
: [
`webpack-dev-server/client?http://localhost:${playroomConfig.port}`,
'webpack/hot/dev-server'
]),
require.resolve('../src/index.js')
],
entry: {
index: [...devServerEntries, require.resolve('../src/index.js')],
frame: [...devServerEntries, require.resolve('../src/frame.js')]
},
output: {
filename: '[name].[hash].js',
path: path.resolve(playroomConfig.cwd, playroomConfig.outputPath),
publicPath: ''
},
@@ -102,6 +105,14 @@ module.exports = async (playroomConfig, options) => {
}
]
},
optimization: {
splitChunks: {
chunks: 'all'
},
runtimeChunk: {
name: 'runtime'
}
},
plugins: [
new webpack.DefinePlugin({
__PLAYROOM_GLOBAL__CONFIG__: JSON.stringify(playroomConfig),
@@ -111,7 +122,15 @@ module.exports = async (playroomConfig, options) => {
title: playroomConfig.title
? `Playroom | ${playroomConfig.title}`
: 'Playroom',
chunksSortMode: 'none'
chunksSortMode: 'none',
chunks: ['index'],
filename: 'index.html'
}),
new HtmlWebpackPlugin({
title: 'Playroom Frame',
chunksSortMode: 'none',
chunks: ['frame'],
filename: 'frame.html'
}),
...(options.production
? []
@@ -57,7 +57,6 @@
"@babel/core": "^7.1.0",
"@babel/preset-env": "^7.2.0",
"@babel/preset-react": "^7.0.0",
"acorn-jsx": "^4.1.1",
"babel-loader": "^8.0.2",
"babel-plugin-transform-class-properties": "^6.24.1",
"base64-url": "^2.2.0",
@@ -71,7 +70,7 @@
"fast-glob": "^2.2.4",
"find-up": "^3.0.0",
"friendly-errors-webpack-plugin": "^1.7.0",
"html-webpack-plugin": "^3.2.0",
"html-webpack-plugin": "^4.0.0-beta.5",
"less": "^3.8.1",
"less-loader": "^4.1.0",
"localforage": "^1.7.2",
@@ -87,7 +86,6 @@
"react-codemirror": "^1.0.0",
"react-docgen-typescript": "^1.12.2",
"react-dom": "^16.5.2",
"react-frame-component": "^4.0.1",
"read-pkg-up": "^5.0.0",
"scope-eval": "^1.0.0",
"style-loader": "^0.23.0",
@@ -4,33 +4,36 @@ import styles from './CatchErrors.less';

export default class CatchErrors extends Component {
static propTypes = {
code: PropTypes.string.isRequired,
children: PropTypes.node.isRequired
};

state = {
error: null,
invalidCode: null,
info: null
};

componentDidCatch(error, info) {
this.setState({ error, info });
const { code } = this.props;
this.setState({ invalidCode: code, error, info });
}

render() {
const { error, info } = this.state;
const { children } = this.props;
const { invalidCode, error, info } = this.state;
const { code, children } = this.props;

if (!error) {
if (code !== invalidCode) {
return children;
}

// Ensure the stack only contains user-provided components
const componentStack = info.componentStack
.split('\n')
.filter(line => /RenderJsx/.test(line))
.filter(line => /RenderCode/.test(line))
.map(line => line.replace(/ \(created by .*/g, ''));

// Ignore the RenderJsx container component
// Ignore the RenderCode container component
const lines = componentStack.slice(0, componentStack.length - 1);

return (

This file was deleted.

This file was deleted.

@@ -0,0 +1,73 @@
import React, { Component } from 'react';
import queryString from 'query-string';
import CatchErrors from './CatchErrors/CatchErrors';
import RenderCode from './RenderCode/RenderCode';

const themesImport = require('./themes');
const componentsImport = require('./components');
const frameComponentImport = require('./frameComponent');

const getQueryParams = () => {
try {
const hash = window.location.hash.replace(/^#/, '');
return queryString.parse(hash);
} catch (err) {
return {};
}
};

export default class Frame extends Component {
constructor(props) {
super(props);

const { themeName, code = '' } = getQueryParams();

this.state = {
themeName,
themes: themesImport,
components: componentsImport,
FrameComponent: frameComponentImport,
code
};
}

componentDidMount() {
window.addEventListener('hashchange', () => {
const { themeName, code } = getQueryParams();

if (themeName && code) {
this.setState({ themeName, code });
}
});

if (module.hot) {
module.hot.accept('./themes', () => {
this.setState({ themes: require('./themes') });
});

module.hot.accept('./components', () => {
this.setState({ components: require('./components') });
});

module.hot.accept('./frameComponent', () => {
this.setState({ frameComponent: require('./frameComponent') });
});
}
}

render() {
const { FrameComponent, themes, themeName, components, code } = this.state;

const resolvedThemeName =
themeName === '__PLAYROOM__NO_THEME__' ? null : themeName;
const resolvedTheme = resolvedThemeName ? themes[resolvedThemeName] : null;

return (
<CatchErrors code={code}>
<FrameComponent themeName={resolvedThemeName} theme={resolvedTheme}>
<RenderCode code={code} scope={components} />
</FrameComponent>
</CatchErrors>
);
}
}

0 comments on commit 03e41a3

Please sign in to comment.
You can’t perform that action at this time.