devjar 0.0.2 → 0.2.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
@@ -1 +1,56 @@
1
1
  # devjar
2
+ > bundless runtime for your ESM JavaScript project in browser
3
+
4
+
5
+ ![image](https://repository-images.githubusercontent.com/483779830/23b4d7c8-dd8e-48b0-a3ea-c519e8236714)
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,39 +1,179 @@
1
+ import { useEffect, useCallback, useState, useRef } from 'react'
2
+ import { createModule } from './core.mjs'
1
3
  import { transform } from 'sucrase'
4
+ import { init, parse } from 'es-module-lexer'
2
5
 
3
- const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor
6
+ let esModuleLexerInit
4
7
 
5
- async function createModuleImporter(code) {
6
- const buildModule = new AsyncFunction(undefined, `return await import('data:text/javascript;base64,${btoa(code)}')`)
7
- let mod
8
- try {
9
- mod = buildModule()
10
- } catch (e) {
11
- return {}
12
- }
13
- return mod
14
- }
8
+ const isRelative = s => s.startsWith('./')
15
9
 
16
- function transformCode(code) {
17
- return transform(code, {
10
+ function transformCode(_code, getModulePath, externals) {
11
+ const code = transform(_code, {
18
12
  transforms: ['jsx', 'typescript'],
19
13
  }).code
14
+
15
+ return replaceImports(code, getModulePath, externals)
20
16
  }
21
17
 
22
- async function compileModule(code) {
23
- const transformed = transformCode(code)
24
- return createModuleImporter(transformed)
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
25
46
  }
26
47
 
27
- async function createModule(code) {
28
- let mod = {}, error
29
- try {
30
- mod = await compileModule(code)
31
- } catch (e) {
32
- error = e
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
+ state = {
60
+ error: null,
61
+ }
62
+ componentDidCatch(error) {
63
+ this.setState({ error })
64
+ }
65
+ render() {
66
+ if (this.state.error) {
67
+ return _jsx('div', null, this.state.error.message)
68
+ }
69
+ return this.props.children
70
+ }
71
+ }
72
+
73
+ const isReact18 = !!ReactDOM_.createRoot
74
+ if (isReact18 && !reactRoot) {
75
+ reactRoot = ReactDOM_.createRoot(root)
76
+ }
77
+ const Component = mod.default
78
+ const element = _jsx(ErrorBoundary, null, _jsx(Component))
79
+ if (isReact18) {
80
+ reactRoot.render(element)
81
+ } else {
82
+ ReactDOM_.render(element, root)
83
+ }
33
84
  }
34
- return { mod, error }
85
+
86
+ return render
87
+ }
88
+
89
+ function createMainScript({ getModulePath }) {
90
+ const code = (
91
+ `'use strict';
92
+ const _createModule = ${createModule.toString()};
93
+ const _createRenderer = ${createRenderer.toString()};
94
+ const _getModulePath = ${getModulePath.toString()};
95
+
96
+ globalThis.__render__ = _createRenderer(_createModule, _getModulePath);
97
+ `)
98
+ return code
35
99
  }
36
100
 
37
- export {
38
- createModule,
101
+
102
+ export function useLiveCode({ getModulePath }) {
103
+ const iframeRef = useRef()
104
+ const [error, setError] = useState()
105
+ const rerender = useState({})[1]
106
+ const scriptRef = useRef(typeof window !== 'undefined' ? document.createElement('script') : null)
107
+
108
+ useEffect(() => {
109
+ const iframe = iframeRef.current
110
+ const doc = iframe && iframe.contentDocument
111
+
112
+ if (iframe) {
113
+ const doc = iframe.contentDocument
114
+ const div = document.createElement('div')
115
+ const script = scriptRef.current
116
+ const scriptContent = createMainScript({ getModulePath })
117
+
118
+ div.id = 'root'
119
+ script.type = 'module'
120
+ script.id = 'main'
121
+ script.src = `data:text/javascript;utf-8,${encodeURIComponent(scriptContent)}`
122
+
123
+ doc.body.appendChild(div)
124
+ doc.body.appendChild(script)
125
+ }
126
+ return () => {
127
+ if (iframe) {
128
+ doc.body.removeChild(doc.getElementById('root'))
129
+ doc.body.removeChild(doc.getElementById('main'))
130
+ }
131
+ }
132
+ }, [])
133
+
134
+ const load = useCallback(async (files) => {
135
+ if (!esModuleLexerInit) {
136
+ await init
137
+ esModuleLexerInit = true
138
+ }
139
+
140
+ if (files) {
141
+ const overrideExternals =
142
+ new Set(Object.keys(files).filter(name => !isRelative(name) && name !== 'index.js'))
143
+
144
+ // Always share react as externals
145
+ overrideExternals.add('react')
146
+ overrideExternals.add('react-dom')
147
+
148
+ try {
149
+ const transformedFiles = Object.keys(files).reduce((res, filename) => {
150
+ const key = isRelative(filename) ? ('@' + filename.slice(2)) : filename
151
+ res[key] = transformCode(files[filename], getModulePath, overrideExternals)
152
+ return res
153
+ }, {})
154
+
155
+ const iframe = iframeRef.current
156
+ const script = scriptRef.current
157
+ if (iframe) {
158
+ const render = iframe.contentWindow.__render__
159
+ if (render) {
160
+ render(transformedFiles)
161
+ } else {
162
+ // if render is not loaded yet, wait until it's loaded
163
+ script.onload = () => {
164
+ iframe.contentWindow.__render__(transformedFiles)
165
+ }
166
+ }
167
+ }
168
+ setError()
169
+ } catch (e) {
170
+ setError(e)
171
+ }
172
+ }
173
+ rerender({})
174
+ }, [])
175
+
176
+ return { ref: iframeRef, error, load }
39
177
  }
178
+
179
+ export { createModule }
package/package.json CHANGED
@@ -1,26 +1,36 @@
1
1
  {
2
2
  "name": "devjar",
3
- "version": "0.0.2",
3
+ "version": "0.2.1",
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": {
15
+ "build": "next build ./docs",
16
+ "start": "next start ./docs",
14
17
  "dev": "next dev ./docs"
15
18
  },
19
+ "peerDependencies": {
20
+ "react": "^17.0.0 || ^18.0.0"
21
+ },
16
22
  "dependencies": {
23
+ "es-module-lexer": "^0.10.5",
24
+ "es-module-shims": "^1.5.4",
17
25
  "sucrase": "3.21.0"
18
26
  },
19
27
  "devDependencies": {
28
+ "codice": "latest",
20
29
  "devjar": "link:./",
21
- "next": "^12.1.6-canary.4",
30
+ "lodash-es": "^4.17.21",
31
+ "next": "canary",
22
32
  "react": "^18.0.0",
23
33
  "react-dom": "^18.0.0",
24
- "sugar-high": "^0.3.1"
34
+ "sugar-high": "^0.4.2"
25
35
  }
26
36
  }
package/lib/react.mjs DELETED
@@ -1,29 +0,0 @@
1
- import { useCallback, useEffect, useState, useRef } from 'react'
2
- import { createModule } from './index.mjs'
3
-
4
- export function useDynamicModule(code) {
5
- const [{ mod, error }, setMod] = useState({})
6
- const [shouldRender, rerender] = useState({})
7
- const prevShouldRenderRef = useRef({})
8
-
9
- const load = () => {
10
- rerender({})
11
- }
12
-
13
- const loadMod = useCallback(() => {
14
- if (code) {
15
- createModule(code).then(_mod => {
16
- setMod(_mod)
17
- })
18
- }
19
- }, [code])
20
-
21
- useEffect(() => {
22
- if (prevShouldRenderRef.current !== shouldRender) {
23
- loadMod()
24
- }
25
- prevShouldRenderRef.current = shouldRender
26
- }, [shouldRender, loadMod])
27
-
28
- return { mod, error, load }
29
- }