devjar 0.3.1 → 0.5.0

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
@@ -13,7 +13,7 @@ Notice: devjar only works for browser runtime at the moment. It will always rend
13
13
  ### Install
14
14
 
15
15
  ```sh
16
- yarn add devjar
16
+ pnpm add devjar
17
17
  ```
18
18
 
19
19
 
package/lib/core.js ADDED
@@ -0,0 +1,278 @@
1
+ import { useEffect, useCallback, useState, useId, useRef } from 'react'
2
+ import { createModule } from './module.js'
3
+ import { transform } from 'sucrase'
4
+ import { init, parse } from 'es-module-lexer'
5
+
6
+ let esModuleLexerInit
7
+ const isRelative = s => s.startsWith('./')
8
+
9
+ function transformCode(_code, getModuleUrl, externals) {
10
+ const code = transform(_code, {
11
+ transforms: ['jsx', 'typescript'],
12
+ }).code
13
+
14
+ return replaceImports(code, getModuleUrl, externals)
15
+ }
16
+
17
+ function replaceImports(source, getModuleUrl, externals) {
18
+ let code = ''
19
+ let lastIndex = 0
20
+ let hasReactImports = false
21
+ const [imports] = parse(source)
22
+ const cssImports = []
23
+ let cssImportIndex = 0
24
+
25
+ // start, end, statementStart, statementEnd, assertion, name
26
+ imports.forEach(({ s, e, ss, se, a, n }) => {
27
+ code += source.slice(lastIndex, ss) // content from last import to beginning of this line
28
+
29
+
30
+ // handle imports
31
+ if (n.endsWith('.css')) {
32
+ // Map './styles.css' -> '@styles.css', and collect it
33
+ const cssPath = `${'@' + n.slice(2)}`
34
+ cssImports.push(cssPath)
35
+
36
+ } else {
37
+ code += source.substring(ss, s)
38
+ code += isRelative(n)
39
+ ? ('@' + n.slice(2))
40
+ : externals.has(n) ? n : getModuleUrl(n)
41
+ code += source.substring(e, se)
42
+ }
43
+
44
+ lastIndex = se
45
+
46
+ if (n === 'react') {
47
+ const statement = source.slice(ss, se)
48
+ if (statement.includes('React')) {
49
+ hasReactImports = true
50
+ }
51
+ }
52
+
53
+ cssImports.forEach(cssPath => {
54
+ code += `\nimport sheet${cssImportIndex} from "${cssPath}" assert { type: "css" };\n`
55
+ cssImportIndex++
56
+ })
57
+ })
58
+
59
+ if (cssImports.length) {
60
+ code += `const __customStyleSheets = [`
61
+ for (let i = 0; i < cssImports.length; i++) {
62
+ code += `sheet${i}`
63
+ if (i < cssImports.length - 1) {
64
+ code += `, `
65
+ }
66
+ }
67
+ code += `];\n`
68
+ code += `document.adoptedStyleSheets = [...document.adoptedStyleSheets, ...__customStyleSheets];\n`
69
+ }
70
+
71
+ code += source.substring(lastIndex)
72
+
73
+ if (!hasReactImports) {
74
+ code = `import React from 'react';\n${code}`
75
+ }
76
+
77
+ return code
78
+ }
79
+
80
+ function createRenderer(createModule_, getModuleUrl) {
81
+ let reactRoot
82
+
83
+ async function render(files) {
84
+ const mod = await createModule_(files, { getModuleUrl })
85
+ const ReactMod = await self.importShim('react')
86
+ const ReactDOMMod = await self.importShim('react-dom')
87
+
88
+ const _jsx = ReactMod.createElement
89
+ const root = document.getElementById('__reactRoot')
90
+ class ErrorBoundary extends ReactMod.Component {
91
+ constructor(props) {
92
+ super(props)
93
+ this.state = { error: null }
94
+ }
95
+ componentDidCatch(error) {
96
+ this.setState({ error })
97
+ }
98
+ render() {
99
+ if (this.state.error) {
100
+ return _jsx('div', null, this.state.error.message)
101
+ }
102
+ return this.props.children
103
+ }
104
+ }
105
+
106
+ const isReact18 = !!ReactDOMMod.createRoot
107
+ if (isReact18 && !reactRoot) {
108
+ reactRoot = ReactDOMMod.createRoot(root)
109
+ }
110
+ const Component = mod.default
111
+ const element = _jsx(ErrorBoundary, null, _jsx(Component))
112
+ if (isReact18) {
113
+ reactRoot.render(element)
114
+ } else {
115
+ ReactDOMMod.render(element, root)
116
+ }
117
+ }
118
+
119
+ return render
120
+ }
121
+
122
+ function createMainScript({ uid }) {
123
+ const code = (`\
124
+ 'use strict';
125
+ const _createModule = ${createModule.toString()};
126
+ const _createRenderer = ${createRenderer.toString()};
127
+
128
+ const getModuleUrl = (m) => window.parent.__devjar__[globalThis.uid].getModuleUrl(m)
129
+
130
+ globalThis.uid = ${JSON.stringify(uid)};
131
+ globalThis.__render__ = _createRenderer(_createModule, getModuleUrl);
132
+ `)
133
+ return code
134
+ }
135
+
136
+ function createEsShimOptionsScript() {
137
+ return `\
138
+ window.esmsInitOptions = {
139
+ polyfillEnable: ['css-modules', 'json-modules'],
140
+ onerror: error => console.log(error),
141
+ }`
142
+ }
143
+
144
+ function useScript() {
145
+ return useRef(typeof window !== 'undefined' ? document.createElement('script') : null)
146
+ }
147
+
148
+ function createScript(scriptRef, { content, src, type } = {}) {
149
+ const script = scriptRef.current
150
+ if (type) script.type = type
151
+
152
+ if (content) {
153
+ script.src = `data:text/javascript;utf-8,${encodeURIComponent(content)}`
154
+ }
155
+ if (src) {
156
+ script.src = src
157
+ }
158
+ return script
159
+ }
160
+
161
+ function useLiveCode({ getModuleUrl }) {
162
+ const iframeRef = useRef()
163
+ const [error, setError] = useState()
164
+ const rerender = useState({})[1]
165
+ const appScriptRef = useScript()
166
+ const esShimOptionsScriptRef = useScript()
167
+ const tailwindcssScriptRef = useScript()
168
+ const uid = useId()
169
+
170
+ // Let getModuleUrl executed on parent window side since it might involve
171
+ // variables that iframe cannot access.
172
+ useEffect(() => {
173
+ if (!globalThis.__devjar__) {
174
+ globalThis.__devjar__ = {};
175
+ }
176
+ globalThis.__devjar__[uid] = {
177
+ getModuleUrl,
178
+ }
179
+
180
+ return () => {
181
+ if (globalThis.__devjar__) {
182
+ delete globalThis.__devjar__[uid]
183
+ }
184
+ }
185
+ }, [])
186
+
187
+ useEffect(() => {
188
+ const iframe = iframeRef.current
189
+ if (!iframe || !iframe.contentDocument) return
190
+
191
+ const doc = iframe.contentDocument
192
+ const body = doc.body
193
+ const div = document.createElement('div')
194
+ div.id = '__reactRoot'
195
+
196
+ const appScriptContent = createMainScript({ uid })
197
+ const scriptOptionsContent = createEsShimOptionsScript()
198
+
199
+ const esmShimOptionsScript = createScript(esShimOptionsScriptRef, { content: scriptOptionsContent })
200
+ const appScript = createScript(appScriptRef, { content: appScriptContent, type: 'module' })
201
+ const tailwindScript = createScript(tailwindcssScriptRef, { src: 'https://cdn.tailwindcss.com' })
202
+
203
+ body.appendChild(div)
204
+ body.appendChild(esmShimOptionsScript)
205
+ body.appendChild(appScript)
206
+ body.appendChild(tailwindScript)
207
+
208
+ return () => {
209
+ if (!iframe || !iframe.contentDocument) return
210
+ body.removeChild(div)
211
+ body.removeChild(esmShimOptionsScript)
212
+ body.removeChild(appScript)
213
+ body.removeChild(tailwindScript)
214
+ }
215
+ }, [])
216
+
217
+ const load = useCallback(async (files) => {
218
+ if (!esModuleLexerInit) {
219
+ await init
220
+ esModuleLexerInit = true
221
+ }
222
+
223
+ if (files) {
224
+ // { 'react', 'react-dom' }
225
+ const overrideExternals =
226
+ new Set(Object.keys(files).filter(name => !isRelative(name) && name !== 'index.js'))
227
+
228
+ // Always share react as externals
229
+ overrideExternals.add('react')
230
+ overrideExternals.add('react-dom')
231
+
232
+ try {
233
+ /**
234
+ * transformedFiles
235
+ * {
236
+ * 'index.js': '...',
237
+ * '@mod1': '...',
238
+ * '@mod2': '...',
239
+ */
240
+ const transformedFiles = Object.keys(files).reduce((res, filename) => {
241
+ const key = isRelative(filename) ? ('@' + filename.slice(2)) : filename
242
+ if (filename.endsWith('.css')) {
243
+ res[key] = files[filename]
244
+ } else {
245
+ res[key] = transformCode(files[filename], getModuleUrl, overrideExternals)
246
+ }
247
+ return res
248
+ }, {})
249
+
250
+ const iframe = iframeRef.current
251
+ const script = appScriptRef.current
252
+ if (iframe) {
253
+ const render = iframe.contentWindow.__render__
254
+ if (render) {
255
+ render(transformedFiles)
256
+ } else {
257
+ // if render is not loaded yet, wait until it's loaded
258
+ script.onload = () => {
259
+ iframe.contentWindow.__render__(transformedFiles)
260
+ }
261
+ }
262
+ }
263
+ setError()
264
+ } catch (e) {
265
+ console.error(e)
266
+ setError(e)
267
+ }
268
+ }
269
+ rerender({})
270
+ }, [])
271
+
272
+ return { ref: iframeRef, error, load }
273
+ }
274
+
275
+ export {
276
+ createModule,
277
+ useLiveCode,
278
+ }
package/lib/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { DevJar } from './render.js'
2
+ export { useLiveCode } from './core.js'
@@ -27,15 +27,17 @@ async function createModule(files, { getModuleUrl }) {
27
27
  }
28
28
 
29
29
 
30
- function createInlinedModule(code) {
30
+ function createInlinedModule(code, type) {
31
+ if (type === 'css') return `data:text/css;utf-8,${encodeURIComponent(code)}`
32
+
31
33
  return `data:text/javascript;utf-8,${encodeURIComponent(code)}`
32
34
  }
33
35
 
34
36
  await setupImportMap()
35
37
  const imports = Object.fromEntries(
36
- Object.entries(files).map(([key, code]) => [
37
- key,
38
- createInlinedModule(code),
38
+ Object.entries(files).map(([fileName, code]) => [
39
+ fileName,
40
+ createInlinedModule(code, fileName.endsWith('.css') ? 'css' : 'js'),
39
41
  ])
40
42
  )
41
43
 
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect, useRef } from 'react'
2
- import { useLiveCode } from './core.mjs'
2
+ import { useLiveCode } from './core.js'
3
3
 
4
4
  const defaultOnError = typeof window !== 'undefined' ? console.error : (() => {})
5
5
 
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "devjar",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "exports": {
6
- ".": "./lib/index.mjs",
6
+ ".": "./lib/index.js",
7
7
  "./package.json": "./package.json"
8
8
  },
9
9
  "license": "MIT",
@@ -12,25 +12,25 @@
12
12
  ],
13
13
  "types": "./lib/index.d.ts",
14
14
  "scripts": {
15
- "build": "next build ./docs",
16
- "start": "next start ./docs",
17
- "dev": "next dev ./docs"
15
+ "build:site": "next build ./site",
16
+ "start": "next start ./site",
17
+ "dev": "next dev ./site"
18
18
  },
19
19
  "peerDependencies": {
20
20
  "react": "^18.2.0"
21
21
  },
22
22
  "dependencies": {
23
- "es-module-lexer": "0.10.5",
24
- "es-module-shims": "1.5.9",
25
- "sucrase": "3.23.0"
23
+ "es-module-lexer": "1.4.1",
24
+ "es-module-shims": "1.8.2",
25
+ "sucrase": "3.35.0"
26
26
  },
27
27
  "devDependencies": {
28
- "codice": "latest",
28
+ "codice": "^0.2.0",
29
29
  "devjar": "link:./",
30
- "lodash-es": "^4.17.21",
31
- "next": "^13.1.6",
30
+ "next": "^14.1.0",
32
31
  "react": "^18.2.0",
33
32
  "react-dom": "^18.2.0",
34
- "sugar-high": "^0.4.5"
35
- }
33
+ "sugar-high": "^0.5.6"
34
+ },
35
+ "packageManager": "pnpm@7.33.5"
36
36
  }
package/lib/core.mjs DELETED
@@ -1,202 +0,0 @@
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
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_, getModuleUrl) {
49
- let reactRoot
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')
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({ 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)
97
-
98
- globalThis.uid = ${JSON.stringify(uid)};
99
- globalThis.__render__ = _createRenderer(_createModule, getModuleUrl);
100
- `)
101
- return code
102
- }
103
-
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.mjs DELETED
@@ -1,2 +0,0 @@
1
- export { DevJar } from './render.mjs'
2
- export { useLiveCode } from './core.mjs'