devjar 0.1.0 → 0.2.2
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/README.md +55 -0
- package/lib/core.mjs +46 -0
- package/lib/index.d.ts +13 -0
- package/lib/index.mjs +168 -34
- package/package.json +6 -5
- package/lib/react.mjs +0 -164
package/README.md
CHANGED
|
@@ -1 +1,56 @@
|
|
|
1
1
|
# devjar
|
|
2
|
+
> live code runtime for your react project in browser
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
### Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
yarn add devjar
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Usage
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { useLiveCode } from 'devjar'
|
|
17
|
+
|
|
18
|
+
function Playground() {
|
|
19
|
+
const { ref, error, load } = useLiveCode({
|
|
20
|
+
// The CDN url of each imported module path in your code
|
|
21
|
+
// e.g. `import React from 'react'` will load react from skypack.dev/react
|
|
22
|
+
getModulePath(modPath) {
|
|
23
|
+
return `https://cdn.skypack.dev/${modPath}`
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// logging failures
|
|
28
|
+
if (error) {
|
|
29
|
+
console.error(error)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// load code files and execute them as live code
|
|
33
|
+
function run() {
|
|
34
|
+
load({
|
|
35
|
+
// `index.js` is the entry of every project
|
|
36
|
+
'index.js': `export default function App() { return 'hello world' }`,
|
|
37
|
+
|
|
38
|
+
// other relative modules can be used in the live coding
|
|
39
|
+
'./mod': `export default function Mod() { return 'mod' }`,
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Attach the ref to an iframe element for runtime of code execution
|
|
44
|
+
return (
|
|
45
|
+
<div>
|
|
46
|
+
<button onClick={run}>run</button>
|
|
47
|
+
<iframe ref={ref} />
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### License
|
|
54
|
+
|
|
55
|
+
The MIT License (MIT).
|
|
56
|
+
|
package/lib/core.mjs
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
async function createModule(files, { getModulePath }) {
|
|
2
|
+
let currentImportMap
|
|
3
|
+
let shim
|
|
4
|
+
|
|
5
|
+
async function setupImportMap() {
|
|
6
|
+
if (shim) return shim
|
|
7
|
+
window.esmsInitOptions = {
|
|
8
|
+
shimMode: true,
|
|
9
|
+
mapOverrides: true,
|
|
10
|
+
}
|
|
11
|
+
shim = import(/* webpackIgnore: true */ getModulePath('es-module-shims'))
|
|
12
|
+
await shim
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function updateImportMap(imports) {
|
|
16
|
+
imports['react'] = getModulePath('react')
|
|
17
|
+
imports['react-dom'] = getModulePath('react-dom')
|
|
18
|
+
|
|
19
|
+
const script = document.createElement('script')
|
|
20
|
+
script.type = 'importmap-shim'
|
|
21
|
+
script.innerHTML = JSON.stringify({ imports })
|
|
22
|
+
document.body.appendChild(script)
|
|
23
|
+
if (currentImportMap) {
|
|
24
|
+
currentImportMap.parentNode.removeChild(currentImportMap)
|
|
25
|
+
}
|
|
26
|
+
currentImportMap = script
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
function createInlinedModule(code) {
|
|
31
|
+
return `data:text/javascript;utf-8,${encodeURIComponent(code)}`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await setupImportMap()
|
|
35
|
+
const imports = Object.fromEntries(
|
|
36
|
+
Object.entries(files).map(([key, code]) => [
|
|
37
|
+
key,
|
|
38
|
+
createInlinedModule(code),
|
|
39
|
+
])
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
updateImportMap(imports)
|
|
43
|
+
return self.importShim('index.js')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { createModule }
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
type LiveCodeHandles = {
|
|
4
|
+
load(files: Record<string, string>): void
|
|
5
|
+
ref: React.Ref
|
|
6
|
+
error?: unknown
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type Options = {
|
|
10
|
+
getModulePath(modulePath: string): string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useLiveCode(options: Options): LiveCodeHandles
|
package/lib/index.mjs
CHANGED
|
@@ -1,46 +1,180 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
mapOverrides: true,
|
|
10
|
-
}
|
|
11
|
-
shim = import(/* webpackIgnore: true */ getModulePath('es-module-shims'))
|
|
12
|
-
await shim
|
|
13
|
-
}
|
|
1
|
+
import { useEffect, useCallback, useState, useRef } from 'react'
|
|
2
|
+
import { createModule } from './core.mjs'
|
|
3
|
+
import { transform } from 'sucrase'
|
|
4
|
+
import { init, parse } from 'es-module-lexer'
|
|
5
|
+
|
|
6
|
+
let esModuleLexerInit
|
|
7
|
+
|
|
8
|
+
const isRelative = s => s.startsWith('./')
|
|
14
9
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
function transformCode(_code, getModulePath, externals) {
|
|
11
|
+
const code = transform(_code, {
|
|
12
|
+
transforms: ['jsx', 'typescript'],
|
|
13
|
+
}).code
|
|
14
|
+
|
|
15
|
+
return replaceImports(code, getModulePath, externals)
|
|
16
|
+
}
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
function replaceImports(_code, getModulePath, externals) {
|
|
19
|
+
let code = ''
|
|
20
|
+
let lastIndex = 0
|
|
21
|
+
let hasReactImports = false
|
|
22
|
+
const [imports] = parse(_code)
|
|
23
|
+
imports.forEach(({ s, e, ss, se, n }) => {
|
|
24
|
+
code += _code.slice(lastIndex, ss)
|
|
25
|
+
code += _code.substring(ss, s)
|
|
26
|
+
code += isRelative(n)
|
|
27
|
+
? ('@' + n.slice(2))
|
|
28
|
+
: externals.has(n) ? n : getModulePath(n)
|
|
29
|
+
code += _code.substring(e, se)
|
|
30
|
+
lastIndex = se
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if (n === 'react') {
|
|
34
|
+
const statement = _code.slice(ss, se)
|
|
35
|
+
if (statement.includes('React')) {
|
|
36
|
+
hasReactImports = true
|
|
37
|
+
}
|
|
25
38
|
}
|
|
26
|
-
|
|
39
|
+
})
|
|
40
|
+
code += _code.substring(lastIndex)
|
|
41
|
+
|
|
42
|
+
if (!hasReactImports) {
|
|
43
|
+
code = `import React from 'react';\n${code}`
|
|
27
44
|
}
|
|
45
|
+
return code
|
|
46
|
+
}
|
|
28
47
|
|
|
48
|
+
function createRenderer(createModule_, getModulePath) {
|
|
49
|
+
let reactRoot
|
|
29
50
|
|
|
30
|
-
function
|
|
31
|
-
|
|
51
|
+
async function render(files) {
|
|
52
|
+
const mod = await createModule_(files, { getModulePath })
|
|
53
|
+
const React_ = await self.importShim('react')
|
|
54
|
+
const ReactDOM_ = await self.importShim('react-dom')
|
|
55
|
+
|
|
56
|
+
const _jsx = React_.createElement
|
|
57
|
+
const root = document.getElementById('root')
|
|
58
|
+
class ErrorBoundary extends React_.Component {
|
|
59
|
+
constructor(props) {
|
|
60
|
+
super(props)
|
|
61
|
+
this.state = { error: null }
|
|
62
|
+
}
|
|
63
|
+
componentDidCatch(error) {
|
|
64
|
+
this.setState({ error })
|
|
65
|
+
}
|
|
66
|
+
render() {
|
|
67
|
+
if (this.state.error) {
|
|
68
|
+
return _jsx('div', null, this.state.error.message)
|
|
69
|
+
}
|
|
70
|
+
return this.props.children
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const isReact18 = !!ReactDOM_.createRoot
|
|
75
|
+
if (isReact18 && !reactRoot) {
|
|
76
|
+
reactRoot = ReactDOM_.createRoot(root)
|
|
77
|
+
}
|
|
78
|
+
const Component = mod.default
|
|
79
|
+
const element = _jsx(ErrorBoundary, null, _jsx(Component))
|
|
80
|
+
if (isReact18) {
|
|
81
|
+
reactRoot.render(element)
|
|
82
|
+
} else {
|
|
83
|
+
ReactDOM_.render(element, root)
|
|
84
|
+
}
|
|
32
85
|
}
|
|
33
86
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
87
|
+
return render
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function createMainScript({ getModulePath }) {
|
|
91
|
+
const code = (
|
|
92
|
+
`'use strict';
|
|
93
|
+
const _createModule = ${createModule.toString()};
|
|
94
|
+
const _createRenderer = ${createRenderer.toString()};
|
|
95
|
+
const _getModulePath = ${getModulePath.toString()};
|
|
96
|
+
|
|
97
|
+
globalThis.__render__ = _createRenderer(_createModule, _getModulePath);
|
|
98
|
+
`)
|
|
99
|
+
return code
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
export function useLiveCode({ getModulePath }) {
|
|
104
|
+
const iframeRef = useRef()
|
|
105
|
+
const [error, setError] = useState()
|
|
106
|
+
const rerender = useState({})[1]
|
|
107
|
+
const scriptRef = useRef(typeof window !== 'undefined' ? document.createElement('script') : null)
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const iframe = iframeRef.current
|
|
111
|
+
const doc = iframe && iframe.contentDocument
|
|
112
|
+
|
|
113
|
+
if (iframe) {
|
|
114
|
+
const doc = iframe.contentDocument
|
|
115
|
+
const div = document.createElement('div')
|
|
116
|
+
const script = scriptRef.current
|
|
117
|
+
const scriptContent = createMainScript({ getModulePath })
|
|
118
|
+
|
|
119
|
+
div.id = 'root'
|
|
120
|
+
script.type = 'module'
|
|
121
|
+
script.id = 'main'
|
|
122
|
+
script.src = `data:text/javascript;utf-8,${encodeURIComponent(scriptContent)}`
|
|
123
|
+
|
|
124
|
+
doc.body.appendChild(div)
|
|
125
|
+
doc.body.appendChild(script)
|
|
126
|
+
}
|
|
127
|
+
return () => {
|
|
128
|
+
if (iframe) {
|
|
129
|
+
doc.body.removeChild(doc.getElementById('root'))
|
|
130
|
+
doc.body.removeChild(doc.getElementById('main'))
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}, [])
|
|
134
|
+
|
|
135
|
+
const load = useCallback(async (files) => {
|
|
136
|
+
if (!esModuleLexerInit) {
|
|
137
|
+
await init
|
|
138
|
+
esModuleLexerInit = true
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (files) {
|
|
142
|
+
const overrideExternals =
|
|
143
|
+
new Set(Object.keys(files).filter(name => !isRelative(name) && name !== 'index.js'))
|
|
144
|
+
|
|
145
|
+
// Always share react as externals
|
|
146
|
+
overrideExternals.add('react')
|
|
147
|
+
overrideExternals.add('react-dom')
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const transformedFiles = Object.keys(files).reduce((res, filename) => {
|
|
151
|
+
const key = isRelative(filename) ? ('@' + filename.slice(2)) : filename
|
|
152
|
+
res[key] = transformCode(files[filename], getModulePath, overrideExternals)
|
|
153
|
+
return res
|
|
154
|
+
}, {})
|
|
155
|
+
|
|
156
|
+
const iframe = iframeRef.current
|
|
157
|
+
const script = scriptRef.current
|
|
158
|
+
if (iframe) {
|
|
159
|
+
const render = iframe.contentWindow.__render__
|
|
160
|
+
if (render) {
|
|
161
|
+
render(transformedFiles)
|
|
162
|
+
} else {
|
|
163
|
+
// if render is not loaded yet, wait until it's loaded
|
|
164
|
+
script.onload = () => {
|
|
165
|
+
iframe.contentWindow.__render__(transformedFiles)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
setError()
|
|
170
|
+
} catch (e) {
|
|
171
|
+
setError(e)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
rerender({})
|
|
175
|
+
}, [])
|
|
41
176
|
|
|
42
|
-
|
|
43
|
-
return self.importShim('index.js')
|
|
177
|
+
return { ref: iframeRef, error, load }
|
|
44
178
|
}
|
|
45
179
|
|
|
46
180
|
export { createModule }
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devjar",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./lib/index.mjs",
|
|
7
|
-
"./
|
|
7
|
+
"./package.json": "./package.json"
|
|
8
8
|
},
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"files": [
|
|
11
11
|
"lib"
|
|
12
12
|
],
|
|
13
|
+
"types": "./lib/index.d.ts",
|
|
13
14
|
"scripts": {
|
|
14
15
|
"build": "next build ./docs",
|
|
15
16
|
"start": "next start ./docs",
|
|
@@ -24,12 +25,12 @@
|
|
|
24
25
|
"sucrase": "3.21.0"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
|
-
"codice": "
|
|
28
|
+
"codice": "latest",
|
|
28
29
|
"devjar": "link:./",
|
|
29
30
|
"lodash-es": "^4.17.21",
|
|
30
|
-
"next": "
|
|
31
|
+
"next": "canary",
|
|
31
32
|
"react": "^18.0.0",
|
|
32
33
|
"react-dom": "^18.0.0",
|
|
33
|
-
"sugar-high": "^0.4.
|
|
34
|
+
"sugar-high": "^0.4.4"
|
|
34
35
|
}
|
|
35
36
|
}
|
package/lib/react.mjs
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import { useEffect, useCallback, useState, useRef } from 'react'
|
|
2
|
-
import { createModule } from './index.mjs'
|
|
3
|
-
import { transform } from 'sucrase'
|
|
4
|
-
import { init, parse } from 'es-module-lexer'
|
|
5
|
-
|
|
6
|
-
let esModuleLexerInit
|
|
7
|
-
|
|
8
|
-
const isRelative = s => s.startsWith('./')
|
|
9
|
-
|
|
10
|
-
function transformCode(_code, getModulePath, externals) {
|
|
11
|
-
const code = transform(_code, {
|
|
12
|
-
transforms: ['jsx', 'typescript'],
|
|
13
|
-
}).code
|
|
14
|
-
|
|
15
|
-
return replaceImports(code, getModulePath, externals)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function replaceImports(_code, getModulePath, externals) {
|
|
19
|
-
const [imports] = parse(_code)
|
|
20
|
-
let code = ''
|
|
21
|
-
let lastIndex = 0
|
|
22
|
-
imports.forEach(({ ss, s, e, se, n }) => {
|
|
23
|
-
code += _code.slice(lastIndex, ss)
|
|
24
|
-
code += _code.substring(ss, s)
|
|
25
|
-
code += isRelative(n)
|
|
26
|
-
? ('@' + n.slice(2))
|
|
27
|
-
: externals.has(n) ? n : getModulePath(n)
|
|
28
|
-
code += _code.substring(e, se)
|
|
29
|
-
lastIndex = se
|
|
30
|
-
})
|
|
31
|
-
code += _code.substring(lastIndex)
|
|
32
|
-
return code
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function createRenderer(_React0, _ReactDOM0, _createModule, _getModulePath) {
|
|
36
|
-
let reactRoot
|
|
37
|
-
|
|
38
|
-
async function render(files) {
|
|
39
|
-
const mod = await _createModule(files, { getModulePath: _getModulePath })
|
|
40
|
-
const _React = await self.importShim('react')
|
|
41
|
-
const _ReactDOM = await self.importShim('react-dom')
|
|
42
|
-
|
|
43
|
-
const _jsx = _React.createElement
|
|
44
|
-
const root = document.getElementById('root')
|
|
45
|
-
class ErrorBoundary extends _React.Component {
|
|
46
|
-
state = {
|
|
47
|
-
error: null,
|
|
48
|
-
}
|
|
49
|
-
componentDidCatch(error) {
|
|
50
|
-
this.setState({ error })
|
|
51
|
-
}
|
|
52
|
-
render() {
|
|
53
|
-
if (this.state.error) {
|
|
54
|
-
return _jsx('div', null, this.state.error.message)
|
|
55
|
-
}
|
|
56
|
-
return this.props.children
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const isReact18 = !!_ReactDOM.createRoot
|
|
61
|
-
if (isReact18 && !reactRoot) {
|
|
62
|
-
reactRoot = _ReactDOM.createRoot(root)
|
|
63
|
-
}
|
|
64
|
-
const Component = mod.default
|
|
65
|
-
const element = _jsx(ErrorBoundary, null, _jsx(Component))
|
|
66
|
-
if (isReact18) {
|
|
67
|
-
reactRoot.render(element)
|
|
68
|
-
} else {
|
|
69
|
-
_ReactDOM.render(element, root)
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return render
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function createMainScript({ getModulePath }) {
|
|
77
|
-
const code = (
|
|
78
|
-
`'use strict';
|
|
79
|
-
const createModule = ${createModule.toString()};
|
|
80
|
-
const createRenderer = ${createRenderer.toString()};
|
|
81
|
-
const getModulePath = ${getModulePath.toString()};
|
|
82
|
-
|
|
83
|
-
globalThis.__render__ = createRenderer(0, 0, createModule, getModulePath);
|
|
84
|
-
`)
|
|
85
|
-
return code
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
export function useLiveCode({ getModulePath }) {
|
|
90
|
-
const iframeRef = useRef()
|
|
91
|
-
const [error, setError] = useState()
|
|
92
|
-
const rerender = useState({})[1]
|
|
93
|
-
const scriptRef = useRef(typeof window !== 'undefined' ? document.createElement('script') : null)
|
|
94
|
-
|
|
95
|
-
useEffect(() => {
|
|
96
|
-
const iframe = iframeRef.current
|
|
97
|
-
const doc = iframe && iframe.contentDocument
|
|
98
|
-
|
|
99
|
-
if (iframe) {
|
|
100
|
-
const doc = iframe.contentDocument
|
|
101
|
-
const div = document.createElement('div')
|
|
102
|
-
const script = scriptRef.current
|
|
103
|
-
const scriptContent = createMainScript({ getModulePath })
|
|
104
|
-
|
|
105
|
-
div.id = 'root'
|
|
106
|
-
script.type = 'module'
|
|
107
|
-
script.id = 'main'
|
|
108
|
-
script.src = `data:text/javascript;utf-8,${encodeURIComponent(scriptContent)}`
|
|
109
|
-
|
|
110
|
-
doc.body.appendChild(div)
|
|
111
|
-
doc.body.appendChild(script)
|
|
112
|
-
}
|
|
113
|
-
return () => {
|
|
114
|
-
if (iframe) {
|
|
115
|
-
doc.body.removeChild(doc.getElementById('root'))
|
|
116
|
-
doc.body.removeChild(doc.getElementById('main'))
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}, [])
|
|
120
|
-
|
|
121
|
-
const load = useCallback(async (files) => {
|
|
122
|
-
if (!esModuleLexerInit) {
|
|
123
|
-
await init
|
|
124
|
-
esModuleLexerInit = true
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (files) {
|
|
128
|
-
const overrideExternals =
|
|
129
|
-
new Set(Object.keys(files).filter(name => !isRelative(name) && name !== 'index.js'))
|
|
130
|
-
|
|
131
|
-
// Always share react as externals
|
|
132
|
-
overrideExternals.add('react')
|
|
133
|
-
overrideExternals.add('react-dom')
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
const transformedFiles = Object.keys(files).reduce((res, filename) => {
|
|
137
|
-
const key = isRelative(filename) ? ('@' + filename.slice(2)) : filename
|
|
138
|
-
res[key] = transformCode(files[filename], getModulePath, overrideExternals)
|
|
139
|
-
return res
|
|
140
|
-
}, {})
|
|
141
|
-
|
|
142
|
-
const iframe = iframeRef.current
|
|
143
|
-
const script = scriptRef.current
|
|
144
|
-
if (iframe) {
|
|
145
|
-
const render = iframe.contentWindow.__render__
|
|
146
|
-
if (render) {
|
|
147
|
-
render(transformedFiles)
|
|
148
|
-
} else {
|
|
149
|
-
// if render is not loaded yet, wait until it's loaded
|
|
150
|
-
script.onload = () => {
|
|
151
|
-
iframe.contentWindow.__render__(transformedFiles)
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
setError()
|
|
156
|
-
} catch (e) {
|
|
157
|
-
setError(e)
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
rerender({})
|
|
161
|
-
}, [])
|
|
162
|
-
|
|
163
|
-
return { ref: iframeRef, error, load }
|
|
164
|
-
}
|