devjar 0.2.2 → 0.3.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/README.md CHANGED
@@ -4,15 +4,68 @@
4
4
 
5
5
  ![image](https://repository-images.githubusercontent.com/483779830/28347c03-774a-4766-b113-54041fad1e72)
6
6
 
7
+ ### Introduction
8
+
9
+ devjar is a library that enables you to live test and share your code snippets and examples with others. devjar will generate a live code editor where you can run your code snippets and view the results in real-time based on the provided code content of your React app.
10
+
11
+ Notice: devjar only works for browser runtime at the moment. It will always render the default export component in `index.js` as the app entry.
12
+
7
13
  ### Install
8
14
 
9
15
  ```sh
10
16
  yarn add devjar
11
17
  ```
12
18
 
19
+
13
20
  ### Usage
14
21
 
15
- ```js
22
+ #### `<DevJar>`
23
+
24
+ `DevJar` is a react component that allows you to develop and test your code directly in the browser, using a CDN to load your dependencies.
25
+
26
+ **Props**
27
+
28
+ * `files`: An object that specifies the files you want to include in your development environment.
29
+ * `getModuleUrl`: A function that maps module names to CDN URLs.
30
+ * `onError`: Callback function of error event from the iframe sandbox. By default `console.log`.
31
+
32
+
33
+ ```jsx
34
+ import { DevJar } from 'devjar'
35
+
36
+ const CDN_HOST = 'https://esm.sh'
37
+
38
+ const files = {
39
+ 'index.js': `export default function App() { return 'hello world' }`
40
+ }
41
+
42
+ function App() {
43
+ return (
44
+ <DevJar
45
+ files={files}
46
+ getModuleUrl={(m) => {
47
+ return `${CDN_HOST}/${m}`
48
+ }}
49
+ />
50
+ )
51
+ }
52
+ ```
53
+
54
+ #### `useLiveCode(options)`
55
+
56
+ **Parameters**
57
+
58
+ * `options`
59
+ * `getModulePath(module)`: A function that receives the module name and returns the CDN url of each imported module path. For example, import React from 'react' will load React from skypack.dev/react.
60
+
61
+ **Returns**
62
+
63
+ * `state`
64
+ * `ref`: A reference to the iframe element where the live coding will be executed.
65
+ * `error`: An error message in case the live coding encounters an issue.
66
+ * `load(codeFiles)`: void: Loads code files and executes them as live code.
67
+
68
+ ```jsx
16
69
  import { useLiveCode } from 'devjar'
17
70
 
18
71
  function Playground() {
package/lib/core.mjs CHANGED
@@ -1,46 +1,202 @@
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
- }
1
+ import { useEffect, useCallback, useState, useId, useRef } from 'react'
2
+ import { createModule } from './module.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, getModuleUrl, externals) {
11
+ const code = transform(_code, {
12
+ transforms: ['jsx', 'typescript'],
13
+ }).code
14
+
15
+ return replaceImports(code, getModuleUrl, externals)
16
+ }
17
+
18
+ function replaceImports(_code, getModuleUrl, 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 : getModuleUrl(n)
29
+ code += _code.substring(e, se)
30
+ lastIndex = se
14
31
 
15
- function updateImportMap(imports) {
16
- imports['react'] = getModulePath('react')
17
- imports['react-dom'] = getModulePath('react-dom')
18
32
 
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)
33
+ if (n === 'react') {
34
+ const statement = _code.slice(ss, se)
35
+ if (statement.includes('React')) {
36
+ hasReactImports = true
37
+ }
25
38
  }
26
- currentImportMap = script
39
+ })
40
+ code += _code.substring(lastIndex)
41
+
42
+ if (!hasReactImports) {
43
+ code = `import React from 'react';\n${code}`
27
44
  }
45
+ return code
46
+ }
47
+
48
+ function createRenderer(createModule_, getModuleUrl) {
49
+ let reactRoot
28
50
 
51
+ async function render(files) {
52
+ const mod = await createModule_(files, { getModuleUrl })
53
+ const React_ = await self.importShim('react')
54
+ const ReactDOM_ = await self.importShim('react-dom')
29
55
 
30
- function createInlinedModule(code) {
31
- return `data:text/javascript;utf-8,${encodeURIComponent(code)}`
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
- await setupImportMap()
35
- const imports = Object.fromEntries(
36
- Object.entries(files).map(([key, code]) => [
37
- key,
38
- createInlinedModule(code),
39
- ])
40
- )
87
+ return render
88
+ }
89
+
90
+ function createMainScript({ uid }) {
91
+ const code = (
92
+ `'use strict';
93
+ const _createModule = ${createModule.toString()};
94
+ const _createRenderer = ${createRenderer.toString()};
95
+
96
+ const getModuleUrl = (m) => window.parent.__devjar__[globalThis.uid].getModuleUrl(m)
41
97
 
42
- updateImportMap(imports)
43
- return self.importShim('index.js')
98
+ globalThis.uid = ${JSON.stringify(uid)};
99
+ globalThis.__render__ = _createRenderer(_createModule, getModuleUrl);
100
+ `)
101
+ return code
44
102
  }
45
103
 
46
- export { createModule }
104
+ function useLiveCode({ getModuleUrl }) {
105
+ const iframeRef = useRef()
106
+ const [error, setError] = useState()
107
+ const rerender = useState({})[1]
108
+ const scriptRef = useRef(typeof window !== 'undefined' ? document.createElement('script') : null)
109
+ const uid = useId()
110
+
111
+ // Let getModuleUrl executed on parent window side since it might involve
112
+ // variables that iframe cannot access.
113
+ useEffect(() => {
114
+ if (!globalThis.__devjar__) {
115
+ globalThis.__devjar__ = {};
116
+ }
117
+ globalThis.__devjar__[uid] = {
118
+ getModuleUrl,
119
+ }
120
+
121
+ return () => {
122
+ if (globalThis.__devjar__) {
123
+ delete globalThis.__devjar__[uid]
124
+ }
125
+ }
126
+ }, [])
127
+
128
+ useEffect(() => {
129
+ const iframe = iframeRef.current
130
+ const doc = iframe && iframe.contentDocument
131
+
132
+ if (iframe) {
133
+ const doc = iframe.contentDocument
134
+ const div = document.createElement('div')
135
+ const script = scriptRef.current
136
+ const scriptContent = createMainScript({ uid })
137
+
138
+ div.id = 'root'
139
+ script.type = 'module'
140
+ script.id = 'main'
141
+ script.src = `data:text/javascript;utf-8,${encodeURIComponent(scriptContent)}`
142
+
143
+ doc.body.appendChild(div)
144
+ doc.body.appendChild(script)
145
+ }
146
+ return () => {
147
+ if (iframe) {
148
+ doc.body.removeChild(doc.getElementById('root'))
149
+ doc.body.removeChild(doc.getElementById('main'))
150
+ }
151
+ }
152
+ }, [])
153
+
154
+ const load = useCallback(async (files) => {
155
+ if (!esModuleLexerInit) {
156
+ await init
157
+ esModuleLexerInit = true
158
+ }
159
+
160
+ if (files) {
161
+ const overrideExternals =
162
+ new Set(Object.keys(files).filter(name => !isRelative(name) && name !== 'index.js'))
163
+
164
+ // Always share react as externals
165
+ overrideExternals.add('react')
166
+ overrideExternals.add('react-dom')
167
+
168
+ try {
169
+ const transformedFiles = Object.keys(files).reduce((res, filename) => {
170
+ const key = isRelative(filename) ? ('@' + filename.slice(2)) : filename
171
+ res[key] = transformCode(files[filename], getModuleUrl, overrideExternals)
172
+ return res
173
+ }, {})
174
+
175
+ const iframe = iframeRef.current
176
+ const script = scriptRef.current
177
+ if (iframe) {
178
+ const render = iframe.contentWindow.__render__
179
+ if (render) {
180
+ render(transformedFiles)
181
+ } else {
182
+ // if render is not loaded yet, wait until it's loaded
183
+ script.onload = () => {
184
+ iframe.contentWindow.__render__(transformedFiles)
185
+ }
186
+ }
187
+ }
188
+ setError()
189
+ } catch (e) {
190
+ setError(e)
191
+ }
192
+ }
193
+ rerender({})
194
+ }, [])
195
+
196
+ return { ref: iframeRef, error, load }
197
+ }
198
+
199
+ export {
200
+ createModule,
201
+ useLiveCode,
202
+ }
package/lib/index.d.ts CHANGED
@@ -7,7 +7,7 @@ type LiveCodeHandles = {
7
7
  }
8
8
 
9
9
  type Options = {
10
- getModulePath(modulePath: string): string
10
+ getModuleUrl(modulePath: string): string
11
11
  }
12
12
 
13
13
  export function useLiveCode(options: Options): LiveCodeHandles
package/lib/index.mjs CHANGED
@@ -1,180 +1,2 @@
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('./')
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
- 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
- }
38
- }
39
- })
40
- code += _code.substring(lastIndex)
41
-
42
- if (!hasReactImports) {
43
- code = `import React from 'react';\n${code}`
44
- }
45
- return code
46
- }
47
-
48
- function createRenderer(createModule_, getModulePath) {
49
- let reactRoot
50
-
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
- }
85
- }
86
-
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
- }, [])
176
-
177
- return { ref: iframeRef, error, load }
178
- }
179
-
180
- export { createModule }
1
+ export { DevJar } from './render.mjs'
2
+ export { useLiveCode } from './core.mjs'
package/lib/module.mjs ADDED
@@ -0,0 +1,46 @@
1
+ async function createModule(files, { getModuleUrl }) {
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 */ getModuleUrl('es-module-shims'))
12
+ await shim
13
+ }
14
+
15
+ function updateImportMap(imports) {
16
+ imports['react'] = getModuleUrl('react')
17
+ imports['react-dom'] = getModuleUrl('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/render.mjs ADDED
@@ -0,0 +1,21 @@
1
+ import React, { useEffect, useRef } from 'react'
2
+ import { useLiveCode } from './core.mjs'
3
+
4
+ const defaultOnError = typeof window !== 'undefined' ? console.error : (() => {})
5
+
6
+ export function DevJar({ files, getModuleUrl, onError = defaultOnError, ...props }) {
7
+ const onErrorRef = useRef(onError)
8
+ const { ref, error, load } = useLiveCode({ getModuleUrl })
9
+
10
+ useEffect(() => {
11
+ onErrorRef.current(error)
12
+ }, [error])
13
+
14
+ // load code files and execute them as live code
15
+ useEffect(() => {
16
+ load(files)
17
+ }, [files])
18
+
19
+ // Attach the ref to an iframe element for runtime of code execution
20
+ return React.createElement('iframe', { ...props, ref })
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devjar",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./lib/index.mjs",
@@ -17,20 +17,20 @@
17
17
  "dev": "next dev ./docs"
18
18
  },
19
19
  "peerDependencies": {
20
- "react": "^17.0.0 || ^18.0.0"
20
+ "react": "^18.2.0"
21
21
  },
22
22
  "dependencies": {
23
- "es-module-lexer": "^0.10.5",
24
- "es-module-shims": "^1.5.4",
25
- "sucrase": "3.21.0"
23
+ "es-module-lexer": "0.10.5",
24
+ "es-module-shims": "1.5.9",
25
+ "sucrase": "3.23.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "codice": "latest",
29
29
  "devjar": "link:./",
30
30
  "lodash-es": "^4.17.21",
31
- "next": "canary",
32
- "react": "^18.0.0",
33
- "react-dom": "^18.0.0",
34
- "sugar-high": "^0.4.4"
31
+ "next": "^13.1.6",
32
+ "react": "^18.2.0",
33
+ "react-dom": "^18.2.0",
34
+ "sugar-high": "^0.4.5"
35
35
  }
36
36
  }