playroom 0.33.0 → 0.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/images/favicon-inverted.png +0 -0
- package/images/favicon.png +0 -0
- package/lib/makeWebpackConfig.js +5 -0
- package/package.json +5 -4
- package/src/Playroom/CodeEditor/CodeEditor.css.ts +3 -0
- package/src/Playroom/CodeEditor/CodeEditor.tsx +12 -31
- package/src/Playroom/Frames/Frames.tsx +3 -4
- package/src/Playroom/Playroom.tsx +0 -1
- package/src/Playroom/RenderCode/RenderCode.js +23 -1
- package/src/Playroom/Snippets/Snippets.tsx +0 -1
- package/src/StoreContext/StoreContext.tsx +0 -1
- package/src/index.js +11 -0
- package/src/utils/compileJsx.test.ts +66 -0
- package/src/utils/compileJsx.ts +38 -10
- package/src/utils/cursor.ts +1 -1
- package/tsconfig.json +1 -1
- package/@types/babel__standalone.d.ts +0 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# playroom
|
|
2
2
|
|
|
3
|
+
## 0.34.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- e3b820b: Add favicon to Playroom site.
|
|
8
|
+
- 4fb69cb: Improve affordance of error marker detail
|
|
9
|
+
|
|
10
|
+
## 0.34.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- 1c8ae6b: Use smaller React pragmas to reduce the payload sent to iframes
|
|
15
|
+
- c4b639c: Replace `@babel/standalone` with `sucrase` for JSX compilation
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- 1c8ae6b: Highlight the correct error location when code has syntax errors
|
|
20
|
+
|
|
3
21
|
## 0.33.0
|
|
4
22
|
|
|
5
23
|
### Minor Changes
|
|
Binary file
|
|
Binary file
|
package/lib/makeWebpackConfig.js
CHANGED
|
@@ -141,6 +141,10 @@ module.exports = async (playroomConfig, options) => {
|
|
|
141
141
|
include: path.dirname(require.resolve('codemirror/package.json')),
|
|
142
142
|
use: [MiniCssExtractPlugin.loader, require.resolve('css-loader')],
|
|
143
143
|
},
|
|
144
|
+
{
|
|
145
|
+
test: /\.png$/i,
|
|
146
|
+
type: 'asset/resource',
|
|
147
|
+
},
|
|
144
148
|
],
|
|
145
149
|
},
|
|
146
150
|
optimization: {
|
|
@@ -163,6 +167,7 @@ module.exports = async (playroomConfig, options) => {
|
|
|
163
167
|
chunksSortMode: 'none',
|
|
164
168
|
chunks: ['index'],
|
|
165
169
|
filename: 'index.html',
|
|
170
|
+
favicon: 'images/favicon.png',
|
|
166
171
|
base: playroomConfig.baseUrl,
|
|
167
172
|
}),
|
|
168
173
|
new HtmlWebpackPlugin({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "playroom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.1",
|
|
4
4
|
"description": "Design with code, powered by your own component library",
|
|
5
5
|
"main": "utils/index.js",
|
|
6
6
|
"types": "utils/index.d.ts",
|
|
@@ -26,12 +26,11 @@
|
|
|
26
26
|
},
|
|
27
27
|
"homepage": "https://github.com/seek-oss/playroom#readme",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@babel/cli": "^7.19.3",
|
|
30
29
|
"@babel/core": "^7.20.5",
|
|
30
|
+
"@babel/parser": "^7.23.4",
|
|
31
31
|
"@babel/preset-env": "^7.20.2",
|
|
32
32
|
"@babel/preset-react": "^7.18.6",
|
|
33
33
|
"@babel/preset-typescript": "^7.18.6",
|
|
34
|
-
"@babel/standalone": "^7.20.6",
|
|
35
34
|
"@soda/friendly-errors-webpack-plugin": "^1.8.1",
|
|
36
35
|
"@types/base64-url": "^2.2.0",
|
|
37
36
|
"@types/codemirror": "^5.60.5",
|
|
@@ -63,6 +62,7 @@
|
|
|
63
62
|
"localforage": "^1.10.0",
|
|
64
63
|
"lodash": "^4.17.21",
|
|
65
64
|
"lz-string": "^1.4.4",
|
|
65
|
+
"memoize-one": "^6.0.0",
|
|
66
66
|
"mini-css-extract-plugin": "^2.7.2",
|
|
67
67
|
"parse-prop-types": "^0.3.0",
|
|
68
68
|
"polished": "^4.2.2",
|
|
@@ -75,8 +75,9 @@
|
|
|
75
75
|
"react-use": "^17.4.0",
|
|
76
76
|
"read-pkg-up": "^7.0.1",
|
|
77
77
|
"scope-eval": "^1.0.0",
|
|
78
|
+
"sucrase": "^3.34.0",
|
|
78
79
|
"typescript": ">=5.0.0",
|
|
79
|
-
"use-debounce": "^9.0.
|
|
80
|
+
"use-debounce": "^9.0.4",
|
|
80
81
|
"webpack": "^5.75.0",
|
|
81
82
|
"webpack-dev-server": "^4.11.1",
|
|
82
83
|
"webpack-merge": "^5.8.0"
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useRef, useContext, useEffect, useCallback } from 'react';
|
|
2
|
-
// @ts-expect-error no types
|
|
3
2
|
import { useDebouncedCallback } from 'use-debounce';
|
|
4
3
|
import type { Editor } from 'codemirror';
|
|
5
4
|
import 'codemirror/lib/codemirror.css';
|
|
@@ -10,11 +9,7 @@ import {
|
|
|
10
9
|
type CursorPosition,
|
|
11
10
|
} from '../../StoreContext/StoreContext';
|
|
12
11
|
import { formatCode as format, isMac } from '../../utils/formatting';
|
|
13
|
-
import {
|
|
14
|
-
closeFragmentTag,
|
|
15
|
-
compileJsx,
|
|
16
|
-
openFragmentTag,
|
|
17
|
-
} from '../../utils/compileJsx';
|
|
12
|
+
import { validateCode } from '../../utils/compileJsx';
|
|
18
13
|
|
|
19
14
|
import * as styles from './CodeEditor.css';
|
|
20
15
|
|
|
@@ -42,33 +37,19 @@ import {
|
|
|
42
37
|
} from './keymaps/cursors';
|
|
43
38
|
import { wrapInTag } from './keymaps/wrap';
|
|
44
39
|
|
|
45
|
-
const
|
|
46
|
-
|
|
40
|
+
const validateCodeInEditor = (editorInstance: Editor, code: string) => {
|
|
41
|
+
const maybeValid = validateCode(code);
|
|
47
42
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
const errorMessage =
|
|
52
|
-
const
|
|
53
|
-
const lineNumber =
|
|
54
|
-
matches && matches.length >= 2 && matches[1] && parseInt(matches[1], 10);
|
|
43
|
+
if (maybeValid === true) {
|
|
44
|
+
editorInstance.clearGutter('errorGutter');
|
|
45
|
+
} else {
|
|
46
|
+
const errorMessage = maybeValid.message;
|
|
47
|
+
const lineNumber = maybeValid.loc?.line;
|
|
55
48
|
|
|
56
49
|
if (lineNumber) {
|
|
57
|
-
// Remove our wrapping Fragment from error message
|
|
58
|
-
const openWrapperStartIndex = errorMessage.indexOf(openFragmentTag);
|
|
59
|
-
const closeWrapperStartIndex = errorMessage.lastIndexOf(closeFragmentTag);
|
|
60
|
-
const formattedMessage = [
|
|
61
|
-
errorMessage.slice(0, openWrapperStartIndex),
|
|
62
|
-
errorMessage.slice(
|
|
63
|
-
openWrapperStartIndex + openFragmentTag.length,
|
|
64
|
-
closeWrapperStartIndex
|
|
65
|
-
),
|
|
66
|
-
errorMessage.slice(closeWrapperStartIndex + closeFragmentTag.length),
|
|
67
|
-
].join('');
|
|
68
|
-
|
|
69
50
|
const marker = document.createElement('div');
|
|
70
51
|
marker.setAttribute('class', styles.errorMarker);
|
|
71
|
-
marker.setAttribute('title',
|
|
52
|
+
marker.setAttribute('title', errorMessage);
|
|
72
53
|
marker.innerText = String(lineNumber);
|
|
73
54
|
editorInstance.setGutterMarker(lineNumber - 1, 'errorGutter', marker);
|
|
74
55
|
}
|
|
@@ -172,7 +153,7 @@ export const CodeEditor = ({ code, onChange, previewCode, hints }: Props) => {
|
|
|
172
153
|
}
|
|
173
154
|
|
|
174
155
|
editorInstanceRef.current.setValue(code);
|
|
175
|
-
|
|
156
|
+
validateCodeInEditor(editorInstanceRef.current, code);
|
|
176
157
|
}
|
|
177
158
|
}, [code, previewCode]);
|
|
178
159
|
|
|
@@ -213,12 +194,12 @@ export const CodeEditor = ({ code, onChange, previewCode, hints }: Props) => {
|
|
|
213
194
|
<ReactCodeMirror
|
|
214
195
|
editorDidMount={(editorInstance) => {
|
|
215
196
|
editorInstanceRef.current = editorInstance;
|
|
216
|
-
|
|
197
|
+
validateCodeInEditor(editorInstance, code);
|
|
217
198
|
setCursorPosition(cursorPosition);
|
|
218
199
|
}}
|
|
219
200
|
onChange={(editorInstance, data, newCode) => {
|
|
220
201
|
if (editorInstance.hasFocus() && !previewCode) {
|
|
221
|
-
|
|
202
|
+
validateCodeInEditor(editorInstance, newCode);
|
|
222
203
|
debouncedChange(newCode);
|
|
223
204
|
}
|
|
224
205
|
}}
|
|
@@ -16,10 +16,9 @@ interface FramesProps {
|
|
|
16
16
|
widths: PlayroomProps['widths'];
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
let renderCode = '<React.Fragment></React.Fragment>';
|
|
20
|
-
|
|
21
19
|
export default function Frames({ code, themes, widths }: FramesProps) {
|
|
22
20
|
const scrollingPanelRef = useRef<HTMLDivElement | null>(null);
|
|
21
|
+
const renderCode = useRef<string>('');
|
|
23
22
|
|
|
24
23
|
const frames = flatMap(widths, (width) =>
|
|
25
24
|
themes.map((theme) => ({
|
|
@@ -30,7 +29,7 @@ export default function Frames({ code, themes, widths }: FramesProps) {
|
|
|
30
29
|
);
|
|
31
30
|
|
|
32
31
|
try {
|
|
33
|
-
renderCode = compileJsx(code);
|
|
32
|
+
renderCode.current = compileJsx(code);
|
|
34
33
|
} catch (e) {}
|
|
35
34
|
|
|
36
35
|
return (
|
|
@@ -45,7 +44,7 @@ export default function Frames({ code, themes, widths }: FramesProps) {
|
|
|
45
44
|
<Iframe
|
|
46
45
|
intersectionRootRef={scrollingPanelRef}
|
|
47
46
|
src={frameSrc(
|
|
48
|
-
{ themeName: frame.theme, code: renderCode },
|
|
47
|
+
{ themeName: frame.theme, code: renderCode.current },
|
|
49
48
|
playroomConfig
|
|
50
49
|
)}
|
|
51
50
|
className={styles.frame}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useContext, type ComponentType, Fragment } from 'react';
|
|
2
2
|
import classnames from 'classnames';
|
|
3
|
-
// @ts-expect-error no types
|
|
4
3
|
import { useDebouncedCallback } from 'use-debounce';
|
|
5
4
|
import { Resizable } from 're-resizable';
|
|
6
5
|
import Frames from './Frames/Frames';
|
|
@@ -4,10 +4,32 @@ import scopeEval from 'scope-eval';
|
|
|
4
4
|
// eslint-disable-next-line import/no-unresolved
|
|
5
5
|
import useScope from '__PLAYROOM_ALIAS__USE_SCOPE__';
|
|
6
6
|
|
|
7
|
+
import {
|
|
8
|
+
ReactCreateElementPragma,
|
|
9
|
+
ReactFragmentPragma,
|
|
10
|
+
} from '../../utils/compileJsx';
|
|
11
|
+
|
|
7
12
|
export default function RenderCode({ code, scope }) {
|
|
8
|
-
|
|
13
|
+
const userScope = {
|
|
9
14
|
...(useScope() ?? {}),
|
|
10
15
|
...scope,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
if (ReactCreateElementPragma in userScope) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`'${ReactCreateElementPragma}' is used internally by Playroom and is not allowed in scope`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
if (ReactFragmentPragma in userScope) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`'${ReactFragmentPragma}' is used internally by Playroom and is not allowed in scope`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return scopeEval(code, {
|
|
30
|
+
...userScope,
|
|
11
31
|
React,
|
|
32
|
+
[ReactCreateElementPragma]: React.createElement,
|
|
33
|
+
[ReactFragmentPragma]: React.Fragment,
|
|
12
34
|
});
|
|
13
35
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect, useMemo, useRef } from 'react';
|
|
2
2
|
import classnames from 'classnames';
|
|
3
3
|
import fuzzy from 'fuzzy';
|
|
4
|
-
// @ts-expect-error no types
|
|
5
4
|
import { useDebouncedCallback } from 'use-debounce';
|
|
6
5
|
import type { PlayroomProps } from '../Playroom';
|
|
7
6
|
import type { Snippet } from '../../../utils';
|
|
@@ -9,7 +9,6 @@ import copy from 'copy-to-clipboard';
|
|
|
9
9
|
import localforage from 'localforage';
|
|
10
10
|
import lzString from 'lz-string';
|
|
11
11
|
import dedent from 'dedent';
|
|
12
|
-
// @ts-expect-error no types
|
|
13
12
|
import { useDebouncedCallback } from 'use-debounce';
|
|
14
13
|
|
|
15
14
|
import { type Snippet, compressParams } from '../../utils';
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,8 @@ import { renderElement } from './render';
|
|
|
2
2
|
import Playroom from './Playroom/Playroom';
|
|
3
3
|
import { StoreProvider } from './StoreContext/StoreContext';
|
|
4
4
|
import playroomConfig from './config';
|
|
5
|
+
import faviconPath from '../images/favicon.png';
|
|
6
|
+
import faviconInvertedPath from '../images/favicon-inverted.png';
|
|
5
7
|
|
|
6
8
|
const polyfillIntersectionObserver = () =>
|
|
7
9
|
typeof window.IntersectionObserver !== 'undefined'
|
|
@@ -14,6 +16,15 @@ polyfillIntersectionObserver().then(() => {
|
|
|
14
16
|
const outlet = document.createElement('div');
|
|
15
17
|
document.body.appendChild(outlet);
|
|
16
18
|
|
|
19
|
+
const selectedElement = document.head.querySelector('link[rel="icon"]');
|
|
20
|
+
const favicon = window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
21
|
+
? faviconInvertedPath
|
|
22
|
+
: faviconPath;
|
|
23
|
+
|
|
24
|
+
if (selectedElement) {
|
|
25
|
+
selectedElement.setAttribute('href', favicon);
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
const renderPlayroom = ({
|
|
18
29
|
themes = require('./themes'),
|
|
19
30
|
components = require('./components'),
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import dedent from 'dedent';
|
|
2
|
+
import { type ErrorWithLocation, compileJsx, validateCode } from './compileJsx';
|
|
3
|
+
|
|
4
|
+
describe('compileJsx', () => {
|
|
5
|
+
test('valid code', () => {
|
|
6
|
+
expect(
|
|
7
|
+
compileJsx(dedent`
|
|
8
|
+
<Foo />
|
|
9
|
+
<Bar />
|
|
10
|
+
`)
|
|
11
|
+
).toMatchInlineSnapshot(`
|
|
12
|
+
"R_cE(R_F, null, R_cE(Foo, null )
|
|
13
|
+
, R_cE(Bar, null ))"
|
|
14
|
+
`);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('invalid code - no error', () => {
|
|
18
|
+
expect(
|
|
19
|
+
compileJsx(`
|
|
20
|
+
<Foo--BarBaz ::invalid />
|
|
21
|
+
`)
|
|
22
|
+
).toMatchInlineSnapshot(
|
|
23
|
+
`"R_cE(R_F, null, R_cE(Foo--BarBaz ::invalid, null ))"`
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('invalid code - with error', () => {
|
|
28
|
+
expect(() =>
|
|
29
|
+
compileJsx(`
|
|
30
|
+
<Foo />
|
|
31
|
+
<Bar />
|
|
32
|
+
<Foo--BarBaz>
|
|
33
|
+
`)
|
|
34
|
+
).toThrowErrorMatchingInlineSnapshot(`"Unterminated JSX contents (3:25)"`);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('validateCode', () => {
|
|
39
|
+
test('valid code', () => {
|
|
40
|
+
expect(
|
|
41
|
+
validateCode(`
|
|
42
|
+
<Foo />
|
|
43
|
+
<Bar />
|
|
44
|
+
`)
|
|
45
|
+
).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('invalid code', () => {
|
|
49
|
+
const error = validateCode(`- line 1
|
|
50
|
+
<Foo /> - line 2
|
|
51
|
+
<Bar /> - line 3
|
|
52
|
+
<This is not ::valid /> - line 4
|
|
53
|
+
^ column 20
|
|
54
|
+
`);
|
|
55
|
+
expect(error).toMatchInlineSnapshot(
|
|
56
|
+
`[SyntaxError: Unexpected token (4:20)]`
|
|
57
|
+
);
|
|
58
|
+
expect((error as ErrorWithLocation).loc).toMatchInlineSnapshot(`
|
|
59
|
+
Position {
|
|
60
|
+
"column": 20,
|
|
61
|
+
"index": 113,
|
|
62
|
+
"line": 4,
|
|
63
|
+
}
|
|
64
|
+
`);
|
|
65
|
+
});
|
|
66
|
+
});
|
package/src/utils/compileJsx.ts
CHANGED
|
@@ -1,18 +1,46 @@
|
|
|
1
|
-
import { transform } from '
|
|
1
|
+
import { transform } from 'sucrase';
|
|
2
|
+
import { parseExpression } from '@babel/parser';
|
|
3
|
+
import memoizeOne from 'memoize-one';
|
|
2
4
|
|
|
3
|
-
export const
|
|
4
|
-
export const
|
|
5
|
+
export const ReactFragmentPragma = 'R_F';
|
|
6
|
+
export const ReactCreateElementPragma = 'R_cE';
|
|
5
7
|
|
|
6
|
-
export const
|
|
7
|
-
|
|
8
|
-
presets: ['react'],
|
|
9
|
-
}).code;
|
|
8
|
+
export const openFragmentTag = '<>';
|
|
9
|
+
export const closeFragmentTag = '</>';
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
const wrapInFragment = (code: string) =>
|
|
12
|
+
`${openFragmentTag}${code}${closeFragmentTag}`;
|
|
13
|
+
|
|
14
|
+
export const compileJsx = memoizeOne(
|
|
15
|
+
(code: string) =>
|
|
16
|
+
transform(wrapInFragment(code.trim()), {
|
|
17
|
+
transforms: ['jsx'],
|
|
18
|
+
jsxPragma: ReactCreateElementPragma,
|
|
19
|
+
jsxFragmentPragma: ReactFragmentPragma,
|
|
20
|
+
production: true,
|
|
21
|
+
}).code
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const parseWithBabel = memoizeOne((code: string) =>
|
|
25
|
+
parseExpression(wrapInFragment(code), {
|
|
26
|
+
plugins: ['jsx'],
|
|
27
|
+
sourceType: 'script',
|
|
28
|
+
strictMode: true,
|
|
29
|
+
})
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export interface ErrorWithLocation extends Error {
|
|
33
|
+
loc?: {
|
|
34
|
+
line: number;
|
|
35
|
+
column: number;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const validateCode = (code: string): true | ErrorWithLocation => {
|
|
12
40
|
try {
|
|
13
|
-
|
|
41
|
+
parseWithBabel(code);
|
|
14
42
|
return true;
|
|
15
43
|
} catch (err) {
|
|
16
|
-
return
|
|
44
|
+
return err as ErrorWithLocation;
|
|
17
45
|
}
|
|
18
46
|
};
|
package/src/utils/cursor.ts
CHANGED
package/tsconfig.json
CHANGED