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 CHANGED
@@ -1 +1,56 @@
1
1
  # devjar
2
+ > live code runtime for your react project in browser
3
+
4
+
5
+ ![image](https://repository-images.githubusercontent.com/483779830/28347c03-774a-4766-b113-54041fad1e72)
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
- 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, 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
- function updateImportMap(imports) {
16
- imports['react'] = getModulePath('react')
17
- imports['react-dom'] = getModulePath('react-dom')
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
- 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)
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
- 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
+ }
28
47
 
48
+ function createRenderer(createModule_, getModulePath) {
49
+ let reactRoot
29
50
 
30
- function createInlinedModule(code) {
31
- return `data:text/javascript;utf-8,${encodeURIComponent(code)}`
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
- 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({ 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
- updateImportMap(imports)
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.1.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./lib/index.mjs",
7
- "./react": "./lib/react.mjs"
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": "^0.0.4",
28
+ "codice": "latest",
28
29
  "devjar": "link:./",
29
30
  "lodash-es": "^4.17.21",
30
- "next": "^12.1.7-canary.0",
31
+ "next": "canary",
31
32
  "react": "^18.0.0",
32
33
  "react-dom": "^18.0.0",
33
- "sugar-high": "^0.4.0"
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
- }