devjar 0.7.0 → 0.9.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
@@ -1,34 +1,34 @@
1
1
  # devjar
2
- > live code runtime for your react project in browser
2
+ > react live preview in browser
3
3
 
4
+ ![image](https://repository-images.githubusercontent.com/483779830/318964f4-18ae-4dd4-9940-0fe51ccc5abc)
4
5
 
5
- ![image](https://repository-images.githubusercontent.com/483779830/28347c03-774a-4766-b113-54041fad1e72)
6
+ ## Introduction
6
7
 
7
- ### Introduction
8
+ 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.
8
9
 
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
+ **Notice:** devjar requires React 19 and only works for browser runtime at the moment. It will always render the default export component in `index.js` as the app entry.
10
11
 
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
-
13
- ### Install
12
+ ## Install
14
13
 
15
14
  ```sh
16
15
  pnpm add devjar
17
16
  ```
18
17
 
18
+ ## React Code Runtime
19
19
 
20
- ### Usage
21
-
22
- #### `<DevJar>`
20
+ ### `<DevJar>`
23
21
 
24
22
  `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
23
 
26
24
  **Props**
27
25
 
28
26
  * `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.
27
+ * `resolveModule`: A function that maps module specifiers to browser-loadable module URLs.
30
28
  * `onError`: Callback function of error event from the iframe sandbox. By default `console.log`.
29
+ * `tailwindSrc`: Optional Tailwind browser script URL. Pass `false` to disable Tailwind injection.
31
30
 
31
+ **Example**
32
32
 
33
33
  ```jsx
34
34
  import { DevJar } from 'devjar'
@@ -43,20 +43,23 @@ function App() {
43
43
  return (
44
44
  <DevJar
45
45
  files={files}
46
- getModuleUrl={(m) => {
47
- return `${CDN_HOST}/${m}`
46
+ resolveModule={(specifier) => {
47
+ return `${CDN_HOST}/${specifier}`
48
48
  }}
49
49
  />
50
50
  )
51
51
  }
52
52
  ```
53
53
 
54
- #### `useLiveCode(options)`
54
+ ### `useLiveCode(options)`
55
+
56
+ A hook that provides lower-level control over the live code execution environment.
55
57
 
56
58
  **Parameters**
57
59
 
58
60
  * `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.
61
+ * `resolveModule(specifier)`: A function that receives a module specifier and returns the browser-loadable URL. For example, import React from 'react' will load React from skypack.dev/react.
62
+ * `tailwindSrc`: Optional Tailwind browser script URL. Pass `false` to disable Tailwind injection.
60
63
 
61
64
  **Returns**
62
65
 
@@ -65,6 +68,8 @@ function App() {
65
68
  * `error`: An error message in case the live coding encounters an issue.
66
69
  * `load(codeFiles)`: void: Loads code files and executes them as live code.
67
70
 
71
+ **Example**
72
+
68
73
  ```jsx
69
74
  import { useLiveCode } from 'devjar'
70
75
 
@@ -72,8 +77,8 @@ function Playground() {
72
77
  const { ref, error, load } = useLiveCode({
73
78
  // The CDN url of each imported module path in your code
74
79
  // e.g. `import React from 'react'` will load react from skypack.dev/react
75
- getModulePath(modPath) {
76
- return `https://cdn.skypack.dev/${modPath}`
80
+ resolveModule(specifier) {
81
+ return `https://cdn.skypack.dev/${specifier}`
77
82
  }
78
83
  })
79
84
 
@@ -103,7 +108,6 @@ function Playground() {
103
108
  }
104
109
  ```
105
110
 
106
- ### License
107
-
108
- The MIT License (MIT).
111
+ ## License
109
112
 
113
+ MIT
package/dist/index.d.ts CHANGED
@@ -1,28 +1,20 @@
1
1
  import * as react from 'react';
2
- import * as react_jsx_runtime from 'react/jsx-runtime';
3
2
 
4
- declare global {
5
- interface Window {
6
- esmsInitOptions: {
7
- shimMode: boolean;
8
- mapOverrides: boolean;
9
- };
10
- }
11
- function importShim(url: string): Promise<any>;
12
- }
13
-
14
- declare function useLiveCode({ getModuleUrl }: {
15
- getModuleUrl?: (name: string) => string;
3
+ declare function useLiveCode({ resolveModule, tailwindSrc, }: {
4
+ resolveModule?: (specifier: string) => string;
5
+ tailwindSrc?: string | false;
16
6
  }): {
17
7
  ref: react.RefObject<any>;
18
8
  error: undefined;
19
- load: (files: any) => Promise<void>;
9
+ load: (files: Record<string, string>) => Promise<void>;
20
10
  };
21
11
 
22
- declare function DevJar({ files, getModuleUrl, onError, ...props }: {
12
+ declare function DevJar({ files, resolveModule, onError, tailwindSrc, ref: forwardedRef, ...props }: {
23
13
  files: Record<string, string>;
24
- getModuleUrl?: (name: string) => string;
14
+ resolveModule?: (specifier: string) => string;
25
15
  onError?: (...data: any[]) => void;
26
- } & React.IframeHTMLAttributes<HTMLIFrameElement>): react_jsx_runtime.JSX.Element;
16
+ tailwindSrc?: string | false;
17
+ ref?: React.Ref<HTMLIFrameElement>;
18
+ } & React.IframeHTMLAttributes<HTMLIFrameElement>): react.JSX.Element;
27
19
 
28
20
  export { DevJar, useLiveCode };
package/dist/index.js CHANGED
@@ -1,82 +1,166 @@
1
- import { useRef, useState, useId, useEffect, useCallback } from 'react';
2
- import { transform } from 'sucrase';
1
+ import { useRef, useState, useId, useEffect, useCallback, useImperativeHandle } from 'react';
3
2
  import { init, parse } from 'es-module-lexer';
4
3
  import { jsx } from 'react/jsx-runtime';
5
4
 
6
- // declare esmsInitOptions on global window
7
- async function createModule(files, { getModuleUrl }) {
8
- let currentImportMap;
9
- let shim;
10
- async function setupImportMap() {
11
- if (shim) return shim;
12
- window.esmsInitOptions = {
13
- shimMode: true,
14
- mapOverrides: true
15
- };
16
- shim = import(/* webpackIgnore: true */ getModuleUrl('es-module-shims'));
17
- await shim;
5
+ async function createModule(files, { resolveModule, dependencies = {}, runtime = {} }) {
6
+ var _runtime, _runtime1;
7
+ const localImportPrefix = '__DEVJAR_LOCAL_IMPORT__';
8
+ function createLocalImportPlaceholder(moduleKey) {
9
+ return `${localImportPrefix}${encodeURIComponent(moduleKey)}__`;
10
+ }
11
+ function createScopedSpecifier(moduleKey) {
12
+ return `devjar-internal/${runtime.revision}/${encodeURIComponent(moduleKey)}`;
18
13
  }
19
- function updateImportMap(imports) {
20
- imports['react'] = getModuleUrl('react');
21
- imports['react-dom'] = getModuleUrl('react-dom');
22
- imports['react-dom/client'] = getModuleUrl('react-dom/client');
14
+ function registerImportMap(imports) {
15
+ var _runtime;
23
16
  const script = document.createElement('script');
24
- script.type = 'importmap-shim';
25
- script.innerHTML = JSON.stringify({
17
+ script.type = 'importmap';
18
+ script.textContent = JSON.stringify({
26
19
  imports
27
20
  });
28
- document.body.appendChild(script);
29
- if (currentImportMap) {
30
- currentImportMap.parentNode.removeChild(currentImportMap);
31
- }
32
- currentImportMap = script;
21
+ document.head.appendChild(script);
22
+ ((_runtime = runtime).importMaps || (_runtime.importMaps = [])).push(script);
33
23
  }
34
- function createInlinedModule(code, type) {
35
- if (type === 'css') return `data:text/css;utf-8,${encodeURIComponent(code)}`;
24
+ function createInlinedModule(code) {
36
25
  return `data:text/javascript;utf-8,${encodeURIComponent(code)}`;
37
26
  }
38
- await setupImportMap();
39
- const imports = Object.fromEntries(Object.entries(files).map(([fileName, code])=>[
40
- fileName,
41
- createInlinedModule(code, fileName.endsWith('.css') ? 'css' : 'js')
42
- ]));
43
- updateImportMap(imports);
44
- return self.importShim('index');
27
+ function createCssModule(code, fileName) {
28
+ return `\
29
+ const sheet = new CSSStyleSheet();
30
+ sheet.replaceSync(${JSON.stringify(code)});
31
+ export default sheet;
32
+ //# sourceURL=devjar/${fileName}?v=${runtime.revision}`;
33
+ }
34
+ function rewriteLocalImports(code, specifiers) {
35
+ let rewritten = code;
36
+ for (const [moduleKey, specifier] of Object.entries(specifiers)){
37
+ rewritten = rewritten.split(createLocalImportPlaceholder(moduleKey)).join(specifier);
38
+ }
39
+ return rewritten;
40
+ }
41
+ (_runtime = runtime).files || (_runtime.files = {});
42
+ (_runtime1 = runtime).urls || (_runtime1.urls = {});
43
+ runtime.revision = (runtime.revision || 0) + 1;
44
+ const changedModules = new Set();
45
+ for (const [fileName, code] of Object.entries(files)){
46
+ if (runtime.files[fileName] !== code) changedModules.add(fileName);
47
+ }
48
+ for (const fileName of Object.keys(runtime.files)){
49
+ if (!(fileName in files)) changedModules.add(fileName);
50
+ }
51
+ const importers = {};
52
+ for (const [importer, importedModules] of Object.entries(dependencies)){
53
+ for (const imported of importedModules){
54
+ var _importers, _imported;
55
+ ((_importers = importers)[_imported = imported] || (_importers[_imported] = [])).push(importer);
56
+ }
57
+ }
58
+ const invalidated = new Set(changedModules);
59
+ const queue = [
60
+ ...changedModules
61
+ ];
62
+ while(queue.length){
63
+ const changed = queue.shift();
64
+ for (const importer of importers[changed] || []){
65
+ if (invalidated.has(importer)) continue;
66
+ invalidated.add(importer);
67
+ queue.push(importer);
68
+ }
69
+ }
70
+ const moduleKeys = new Set(Object.keys(files));
71
+ for (const importedModules of Object.values(dependencies)){
72
+ for (const moduleKey of importedModules)moduleKeys.add(moduleKey);
73
+ }
74
+ const scopedSpecifiers = {};
75
+ for (const moduleKey of moduleKeys){
76
+ scopedSpecifiers[moduleKey] = createScopedSpecifier(moduleKey);
77
+ }
78
+ const imports = {};
79
+ for (const moduleKey of moduleKeys){
80
+ if (!(moduleKey in files)) {
81
+ imports[scopedSpecifiers[moduleKey]] = createInlinedModule(`throw new Error(${JSON.stringify('devjar: Module not found: ' + moduleKey)})`);
82
+ delete runtime.urls[moduleKey];
83
+ continue;
84
+ }
85
+ if (!runtime.urls[moduleKey] || invalidated.has(moduleKey)) {
86
+ const moduleCode = moduleKey.endsWith('.css') ? createCssModule(files[moduleKey], moduleKey) : rewriteLocalImports(`${files[moduleKey]}\n//# sourceURL=devjar/${moduleKey}?v=${runtime.revision}`, scopedSpecifiers);
87
+ runtime.urls[moduleKey] = createInlinedModule(moduleCode);
88
+ }
89
+ imports[scopedSpecifiers[moduleKey]] = runtime.urls[moduleKey];
90
+ }
91
+ registerImportMap(imports);
92
+ if (!runtime.refreshRuntime) {
93
+ const refreshModule = await import(/* webpackIgnore: true */ /* @vite-ignore */ /* turbopackIgnore: true */ resolveModule('react-refresh/runtime'));
94
+ runtime.refreshRuntime = refreshModule.default || refreshModule;
95
+ runtime.refreshRuntime.injectIntoGlobalHook(self);
96
+ globalThis.__devjarRefreshRuntime = runtime.refreshRuntime;
97
+ }
98
+ const entrySpecifier = scopedSpecifiers['index'];
99
+ if (!entrySpecifier) {
100
+ throw new Error('devjar: Module not found: index');
101
+ }
102
+ const module = await import(/* webpackIgnore: true */ /* @vite-ignore */ /* turbopackIgnore: true */ entrySpecifier);
103
+ runtime.files = {
104
+ ...files
105
+ };
106
+ return {
107
+ module,
108
+ changed: changedModules.size > 0
109
+ };
45
110
  }
46
111
 
47
112
  let esModuleLexerInit;
48
113
  const isRelative = (s)=>s.startsWith('./');
49
114
  const removeExtension = (str)=>str.replace(/\.[^/.]+$/, '');
50
- function transformCode(_code, getModuleUrl, externals) {
51
- const code = transform(_code, {
52
- transforms: [
53
- 'jsx',
54
- 'typescript'
55
- ]
56
- }).code;
57
- return replaceImports(code, getModuleUrl, externals);
115
+ const localImportPrefix = '__DEVJAR_LOCAL_IMPORT__';
116
+ const defaultTailwindSrc = 'https://unpkg.com/@tailwindcss/browser@4';
117
+ function createLocalImportPlaceholder(moduleKey) {
118
+ return `${localImportPrefix}${encodeURIComponent(moduleKey)}__`;
58
119
  }
59
- function replaceImports(source, getModuleUrl, externals) {
120
+ function createTransformWorker() {
121
+ return new Worker(new URL('./transform-worker.js', import.meta.url), {
122
+ type: 'module',
123
+ name: 'devjar-transform'
124
+ });
125
+ }
126
+ function getModuleKey(filename) {
127
+ const key = isRelative(filename) ? '@' + filename.slice(2) : filename;
128
+ return filename.endsWith('.css') ? key : removeExtension(key);
129
+ }
130
+ function resolveRelativeModule(importer, imported) {
131
+ const importerPath = importer.startsWith('./') ? importer.slice(2) : importer;
132
+ const parts = importerPath.split('/');
133
+ parts.pop();
134
+ for (const part of imported.split('/')){
135
+ if (!part || part === '.') continue;
136
+ if (part === '..') parts.pop();
137
+ else parts.push(part);
138
+ }
139
+ const path = parts.join('/');
140
+ return imported.endsWith('.css') ? '@' + path : removeExtension('@' + path);
141
+ }
142
+ function replaceImports(source, filename, moduleKey, resolveModule, localModules) {
60
143
  let code = '';
61
144
  let lastIndex = 0;
62
145
  let hasReactImports = false;
63
146
  const [imports] = parse(source);
64
147
  const cssImports = [];
65
- let cssImportIndex = 0;
148
+ const dependencies = [];
66
149
  // start, end, statementStart, statementEnd, assertion, name
67
150
  imports.forEach(({ s, e, ss, se, a, n })=>{
68
- code += source.slice(lastIndex, ss) // content from last import to beginning of this line
69
- ;
151
+ if (!n) return;
152
+ code += source.slice(lastIndex, ss); // content from last import to beginning of this line
153
+ const localModuleKey = isRelative(n) ? resolveRelativeModule(filename, n) : localModules.has(n) ? n : undefined;
70
154
  // handle imports
71
- if (n.endsWith('.css')) {
155
+ if (localModuleKey && localModuleKey.endsWith('.css')) {
72
156
  // Map './styles.css' -> '@styles.css', and collect it
73
- const cssPath = `${'@' + n.slice(2)}`;
74
- cssImports.push(cssPath);
157
+ cssImports.push(localModuleKey);
75
158
  } else {
76
159
  code += source.substring(ss, s);
77
- code += isRelative(n) ? '@' + n.slice(2) : externals.has(n) ? n : getModuleUrl(n);
160
+ code += localModuleKey ? createLocalImportPlaceholder(localModuleKey) : resolveModule(n);
78
161
  code += source.substring(e, se);
79
162
  }
163
+ if (localModuleKey) dependencies.push(localModuleKey);
80
164
  lastIndex = se;
81
165
  if (n === 'react') {
82
166
  const statement = source.slice(ss, se);
@@ -84,64 +168,104 @@ function replaceImports(source, getModuleUrl, externals) {
84
168
  hasReactImports = true;
85
169
  }
86
170
  }
87
- cssImports.forEach((cssPath)=>{
88
- code += `\nimport sheet${cssImportIndex} from "${cssPath}" assert { type: "css" };\n`;
89
- cssImportIndex++;
90
- });
91
171
  });
92
172
  if (cssImports.length) {
93
- code += `const __customStyleSheets = [`;
94
- for(let i = 0; i < cssImports.length; i++){
95
- code += `sheet${i}`;
96
- if (i < cssImports.length - 1) {
97
- code += `, `;
98
- }
99
- }
100
- code += `];\n`;
101
- code += `document.adoptedStyleSheets = [...document.adoptedStyleSheets, ...__customStyleSheets];\n`;
173
+ cssImports.forEach((cssPath, index)=>{
174
+ code += `\nimport __devjarSheet${index} from "${createLocalImportPlaceholder(cssPath)}";\n`;
175
+ });
176
+ code += `globalThis.__devjarStyleSheets ||= new Map();\n`;
177
+ cssImports.forEach((cssPath, index)=>{
178
+ code += `{ const previous = globalThis.__devjarStyleSheets.get(${JSON.stringify(cssPath)});\n`;
179
+ code += `const sheets = [...document.adoptedStyleSheets];\n`;
180
+ code += `const sheetIndex = sheets.indexOf(previous);\n`;
181
+ code += `if (sheetIndex < 0) sheets.push(__devjarSheet${index}); else sheets[sheetIndex] = __devjarSheet${index};\n`;
182
+ code += `document.adoptedStyleSheets = sheets;\n`;
183
+ code += `globalThis.__devjarStyleSheets.set(${JSON.stringify(cssPath)}, __devjarSheet${index}); }\n`;
184
+ });
102
185
  }
103
186
  code += source.substring(lastIndex);
104
187
  if (!hasReactImports) {
105
- code = `import React from 'react';\n${code}`;
188
+ code = `import React from ${JSON.stringify(resolveModule('react'))};\n${code}`;
106
189
  }
107
- return code;
190
+ code = `const $RefreshReg$ = (type, id) => globalThis.__devjarRefreshRuntime.register(type, ${JSON.stringify(moduleKey + ' ')} + id);\n` + `const $RefreshSig$ = globalThis.__devjarRefreshRuntime.createSignatureFunctionForTransform;\n` + code;
191
+ return {
192
+ code,
193
+ dependencies
194
+ };
108
195
  }
109
196
  // createRenderer is going to be stringified and executed in the iframe
110
- function createRenderer(createModule_, getModuleUrl) {
197
+ function createRenderer(createModule_, resolveModule) {
111
198
  let reactRoot;
112
- async function render(files) {
113
- const mod = await createModule_(files, {
114
- getModuleUrl
199
+ let ErrorBoundary;
200
+ let errorBoundary;
201
+ let revision = 0;
202
+ const moduleRuntime = {};
203
+ async function render(files, dependencies) {
204
+ const result = await createModule_(files, {
205
+ resolveModule,
206
+ dependencies,
207
+ runtime: moduleRuntime
115
208
  });
116
- const ReactMod = await self.importShim('react');
117
- const ReactDOMMod = await self.importShim('react-dom/client');
209
+ const ReactMod = await import(/* webpackIgnore: true */ /* @vite-ignore */ /* turbopackIgnore: true */ resolveModule('react'));
210
+ const ReactDOMMod = await import(/* webpackIgnore: true */ /* @vite-ignore */ /* turbopackIgnore: true */ resolveModule('react-dom/client'));
118
211
  const _jsx = ReactMod.createElement;
119
212
  const root = document.getElementById('__reactRoot');
120
- class ErrorBoundary extends ReactMod.Component {
121
- componentDidCatch(error) {
122
- this.setState({
123
- error
124
- });
125
- }
126
- render() {
127
- if (this.state.error) {
128
- return _jsx('div', null, this.state.error?.message);
213
+ if (!ErrorBoundary) {
214
+ ErrorBoundary = class extends ReactMod.Component {
215
+ static getDerivedStateFromError(error) {
216
+ return {
217
+ error
218
+ };
129
219
  }
130
- return this.props.children;
131
- }
132
- constructor(props){
133
- super(props);
134
- this.state = {
135
- error: null
136
- };
137
- }
220
+ reset() {
221
+ if (this.state.error) this.setState({
222
+ error: null
223
+ });
224
+ }
225
+ componentDidUpdate(previousProps) {
226
+ if (previousProps.revision !== this.props.revision && this.state.error) {
227
+ this.setState({
228
+ error: null
229
+ });
230
+ }
231
+ }
232
+ render() {
233
+ if (this.state.error) {
234
+ return _jsx('div', null, this.state.error?.message);
235
+ }
236
+ return this.props.children;
237
+ }
238
+ constructor(props){
239
+ super(props);
240
+ this.state = {
241
+ error: null
242
+ };
243
+ }
244
+ };
138
245
  }
139
246
  if (!reactRoot) {
140
247
  reactRoot = ReactDOMMod.createRoot(root);
248
+ revision++;
249
+ reactRoot.render(_jsx(ErrorBoundary, {
250
+ revision,
251
+ ref: (value)=>errorBoundary = value
252
+ }, _jsx(result.module.default)));
253
+ moduleRuntime.hasRendered = true;
254
+ return;
255
+ }
256
+ if (result.changed) {
257
+ errorBoundary?.reset();
258
+ const refreshRuntime = moduleRuntime.refreshRuntime;
259
+ const refreshUpdate = refreshRuntime.performReactRefresh();
260
+ const mountedRootCount = typeof refreshRuntime._getMountedRootCount === 'function' ? refreshRuntime._getMountedRootCount() : 0;
261
+ if (!refreshUpdate || mountedRootCount === 0) {
262
+ revision++;
263
+ reactRoot.render(_jsx(ErrorBoundary, {
264
+ revision,
265
+ ref: (value)=>errorBoundary = value
266
+ }, _jsx(result.module.default)));
267
+ }
141
268
  }
142
- const Component = mod.default;
143
- const element = _jsx(ErrorBoundary, null, _jsx(Component));
144
- reactRoot.render(element);
145
269
  }
146
270
  return render;
147
271
  }
@@ -151,20 +275,13 @@ function createMainScript({ uid }) {
151
275
  const _createModule = ${createModule.toString()};
152
276
  const _createRenderer = ${createRenderer.toString()};
153
277
 
154
- const getModuleUrl = (m) => window.parent.__devjar__[globalThis.uid].getModuleUrl(m)
278
+ const resolveModule = (specifier) => window.parent.__devjar__[globalThis.uid].resolveModule(specifier)
155
279
 
156
280
  globalThis.uid = ${JSON.stringify(uid)};
157
- globalThis.__render__ = _createRenderer(_createModule, getModuleUrl);
281
+ globalThis.__render__ = _createRenderer(_createModule, resolveModule);
158
282
  `;
159
283
  return code;
160
284
  }
161
- function createEsShimOptionsScript() {
162
- return `\
163
- window.esmsInitOptions = {
164
- polyfillEnable: ['css-modules', 'json-modules'],
165
- onerror: console.error,
166
- }`;
167
- }
168
285
  function useScript() {
169
286
  return useRef(typeof window !== 'undefined' ? document.createElement('script') : null);
170
287
  }
@@ -179,28 +296,45 @@ function createScript(scriptRef, { content, src, type } = {}) {
179
296
  }
180
297
  return script;
181
298
  }
182
- function useLiveCode({ getModuleUrl }) {
299
+ function useLiveCode({ resolveModule, tailwindSrc = defaultTailwindSrc }) {
183
300
  const iframeRef = useRef(null);
184
301
  const [error, setError] = useState();
185
302
  const rerender = useState({})[1];
186
303
  const appScriptRef = useScript();
187
- const esShimOptionsScriptRef = useScript();
188
304
  const tailwindcssScriptRef = useScript();
305
+ const transformWorkerRef = useRef(undefined);
306
+ const transformCacheRef = useRef(new Map());
307
+ const transformRequestsRef = useRef(new Map());
308
+ const transformRequestIdRef = useRef(0);
309
+ const loadIdRef = useRef(0);
189
310
  const uid = useId();
190
- // Let getModuleUrl executed on parent window side since it might involve
311
+ // Let resolveModule execute on parent window side since it might involve
191
312
  // variables that iframe cannot access.
192
313
  useEffect(()=>{
193
314
  if (!globalThis.__devjar__) {
194
315
  globalThis.__devjar__ = {};
195
316
  }
196
317
  globalThis.__devjar__[uid] = {
197
- getModuleUrl
318
+ resolveModule
198
319
  };
199
320
  return ()=>{
200
321
  if (globalThis.__devjar__) {
201
322
  delete globalThis.__devjar__[uid];
202
323
  }
203
324
  };
325
+ }, [
326
+ resolveModule,
327
+ uid
328
+ ]);
329
+ useEffect(()=>{
330
+ return ()=>{
331
+ transformWorkerRef.current?.terminate();
332
+ transformWorkerRef.current = undefined;
333
+ for (const { reject } of transformRequestsRef.current.values()){
334
+ reject(new Error('devjar: transform worker was terminated'));
335
+ }
336
+ transformRequestsRef.current.clear();
337
+ };
204
338
  }, []);
205
339
  useEffect(()=>{
206
340
  const iframe = iframeRef.current;
@@ -212,72 +346,126 @@ function useLiveCode({ getModuleUrl }) {
212
346
  const appScriptContent = createMainScript({
213
347
  uid
214
348
  });
215
- const scriptOptionsContent = createEsShimOptionsScript();
216
- const esmShimOptionsScript = createScript(esShimOptionsScriptRef, {
217
- content: scriptOptionsContent
218
- });
219
349
  const appScript = createScript(appScriptRef, {
220
- content: appScriptContent,
221
- type: 'module'
222
- });
223
- const tailwindScript = createScript(tailwindcssScriptRef, {
224
- src: 'https://unpkg.com/@tailwindcss/browser@4'
350
+ content: appScriptContent
225
351
  });
352
+ const tailwindScript = tailwindSrc ? createScript(tailwindcssScriptRef, {
353
+ src: tailwindSrc
354
+ }) : null;
226
355
  body.appendChild(div);
227
- body.appendChild(esmShimOptionsScript);
228
356
  body.appendChild(appScript);
229
- body.appendChild(tailwindScript);
357
+ if (tailwindScript) body.appendChild(tailwindScript);
230
358
  return ()=>{
231
359
  if (!iframe || !iframe.contentDocument) return;
232
360
  body.removeChild(div);
233
- body.removeChild(esmShimOptionsScript);
234
361
  body.removeChild(appScript);
235
- body.removeChild(tailwindScript);
362
+ if (tailwindScript) body.removeChild(tailwindScript);
236
363
  };
237
364
  }, []);
365
+ const transformFiles = useCallback((files)=>{
366
+ if (!resolveModule) {
367
+ return Promise.reject(new Error('devjar: resolveModule is required for the browser transformer'));
368
+ }
369
+ if (!transformWorkerRef.current) {
370
+ const worker = createTransformWorker();
371
+ worker.onmessage = ({ data })=>{
372
+ const request = transformRequestsRef.current.get(data.id);
373
+ if (!request) return;
374
+ transformRequestsRef.current.delete(data.id);
375
+ if (data.error) {
376
+ const error = new Error(data.error.message);
377
+ if (data.error.stack) error.stack = data.error.stack;
378
+ request.reject(error);
379
+ } else {
380
+ request.resolve(data.transformed);
381
+ }
382
+ };
383
+ worker.onerror = (event)=>{
384
+ const error = new Error(event.message || 'devjar: transform worker failed');
385
+ for (const { reject } of transformRequestsRef.current.values())reject(error);
386
+ transformRequestsRef.current.clear();
387
+ };
388
+ transformWorkerRef.current = worker;
389
+ }
390
+ const id = ++transformRequestIdRef.current;
391
+ const worker = transformWorkerRef.current;
392
+ return new Promise((resolve, reject)=>{
393
+ transformRequestsRef.current.set(id, {
394
+ resolve,
395
+ reject
396
+ });
397
+ worker.postMessage({
398
+ id,
399
+ files,
400
+ moduleUrl: resolveModule('oxc-transform')
401
+ });
402
+ });
403
+ }, [
404
+ resolveModule
405
+ ]);
238
406
  const load = useCallback(async (files)=>{
407
+ const loadId = ++loadIdRef.current;
239
408
  if (!esModuleLexerInit) {
240
409
  await init;
241
410
  esModuleLexerInit = true;
242
411
  }
243
412
  if (files) {
244
- // { 'react', 'react-dom' }
245
- const overrideExternals = new Set(Object.keys(files).filter((name)=>!isRelative(name) && name !== 'index.js'));
246
- // Always share react as externals
247
- overrideExternals.add('react');
248
- overrideExternals.add('react-dom');
413
+ const localModules = new Set(Object.keys(files).map(getModuleKey));
249
414
  try {
415
+ const filesToTransform = Object.fromEntries(Object.entries(files).filter(([filename, source])=>{
416
+ return !filename.endsWith('.css') && transformCacheRef.current.get(filename)?.source !== source;
417
+ }));
418
+ const newTransforms = Object.keys(filesToTransform).length ? await transformFiles(filesToTransform) : {};
419
+ if (loadId !== loadIdRef.current) return;
420
+ for (const [filename, code] of Object.entries(newTransforms)){
421
+ transformCacheRef.current.set(filename, {
422
+ source: files[filename],
423
+ code: code
424
+ });
425
+ }
426
+ for (const filename of transformCacheRef.current.keys()){
427
+ if (!(filename in files)) transformCacheRef.current.delete(filename);
428
+ }
250
429
  /**
251
430
  * transformedFiles
252
431
  * {
253
432
  * 'index.js': '...',
254
433
  * '@mod1': '...',
255
434
  * '@mod2': '...',
256
- */ const transformedFiles = Object.keys(files).reduce((res, filename)=>{
435
+ */ const dependencies = {};
436
+ const transformedFiles = Object.keys(files).reduce((res, filename)=>{
257
437
  // 1. Remove ./
258
438
  // 2. For non css files, remove extension
259
439
  // e.g. './styles.css' -> '@styles.css'
260
440
  // e.g. './foo.js' -> '@foo'
261
- const moduleKey = isRelative(filename) ? '@' + filename.slice(2) : filename;
441
+ const moduleKey = getModuleKey(filename);
262
442
  if (filename.endsWith('.css')) {
263
443
  res[moduleKey] = files[filename];
444
+ dependencies[moduleKey] = [];
264
445
  } else {
265
446
  // JS or TS files
266
- const normalizedModuleKey = removeExtension(moduleKey);
267
- res[normalizedModuleKey] = transformCode(files[filename], getModuleUrl, overrideExternals);
447
+ const transformed = replaceImports(transformCacheRef.current.get(filename).code, filename, moduleKey, resolveModule, localModules);
448
+ res[moduleKey] = transformed.code;
449
+ dependencies[moduleKey] = transformed.dependencies;
268
450
  }
269
451
  return res;
270
452
  }, {});
271
453
  const iframe = iframeRef.current;
272
454
  const script = appScriptRef.current;
273
455
  if (iframe) {
456
+ const renderFiles = async ()=>{
457
+ await iframe.contentWindow.__render__(transformedFiles, dependencies);
458
+ if (loadId === loadIdRef.current) {
459
+ iframe.dispatchEvent(new CustomEvent('devjar:render'));
460
+ }
461
+ };
274
462
  const render = iframe.contentWindow.__render__;
275
463
  if (render) {
276
- render(transformedFiles);
464
+ await renderFiles();
277
465
  } else {
278
466
  // if render is not loaded yet, wait until it's loaded
279
467
  script.onload = ()=>{
280
- iframe.contentWindow.__render__(transformedFiles).catch((err)=>{
468
+ renderFiles().catch((err)=>{
281
469
  setError(err);
282
470
  });
283
471
  };
@@ -290,7 +478,10 @@ function useLiveCode({ getModuleUrl }) {
290
478
  }
291
479
  }
292
480
  rerender({});
293
- }, []);
481
+ }, [
482
+ resolveModule,
483
+ transformFiles
484
+ ]);
294
485
  return {
295
486
  ref: iframeRef,
296
487
  error,
@@ -299,11 +490,15 @@ function useLiveCode({ getModuleUrl }) {
299
490
  }
300
491
 
301
492
  const defaultOnError = typeof window !== 'undefined' ? console.error : ()=>{};
302
- function DevJar({ files, getModuleUrl, onError = defaultOnError, ...props }) {
493
+ function DevJar({ files, resolveModule, onError = defaultOnError, tailwindSrc, ref: forwardedRef, ...props }) {
303
494
  const onErrorRef = useRef(onError);
304
495
  const { ref, error, load } = useLiveCode({
305
- getModuleUrl
496
+ resolveModule,
497
+ tailwindSrc
306
498
  });
499
+ useImperativeHandle(forwardedRef, ()=>ref.current, [
500
+ ref
501
+ ]);
307
502
  useEffect(()=>{
308
503
  onErrorRef.current(error);
309
504
  }, [
@@ -0,0 +1,54 @@
1
+ // src/transform-worker.ts
2
+ var oxc;
3
+ var importModule = new Function("specifier", "return import(specifier)");
4
+ function getLang(filename) {
5
+ if (/\.[cm]?tsx?$/.test(filename))
6
+ return "tsx";
7
+ return "jsx";
8
+ }
9
+ function getTransformErrorMessage(errors) {
10
+ if (!errors?.length)
11
+ return "";
12
+ const error = errors.find((error2) => error2.severity === "Error");
13
+ if (!error)
14
+ return "";
15
+ return error.codeframe || error.message || "devjar: transform failed";
16
+ }
17
+ self.onmessage = async ({ data }) => {
18
+ const { id, moduleUrl, files } = data;
19
+ try {
20
+ if (!oxc) {
21
+ oxc = await importModule(moduleUrl);
22
+ }
23
+ const transformed = {};
24
+ for (const [filename, source] of Object.entries(files)) {
25
+ const output = oxc.transformSync(filename, source, {
26
+ lang: getLang(filename),
27
+ sourceType: "module",
28
+ target: "es2022",
29
+ decorator: {
30
+ legacy: true
31
+ },
32
+ jsx: {
33
+ runtime: "automatic",
34
+ development: true,
35
+ refresh: true
36
+ },
37
+ sourcemap: false
38
+ });
39
+ const errorMessage = getTransformErrorMessage(output.errors);
40
+ if (errorMessage)
41
+ throw new Error(errorMessage);
42
+ transformed[filename] = output.code;
43
+ }
44
+ self.postMessage({ id, transformed });
45
+ } catch (error) {
46
+ self.postMessage({
47
+ id,
48
+ error: {
49
+ message: error?.message || String(error),
50
+ stack: error?.stack
51
+ }
52
+ });
53
+ }
54
+ };
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "devjar",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "exports": {
6
- ".": "./dist/index.js",
7
- "./package.json": "./package.json"
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "default": "./dist/index.js"
9
+ }
8
10
  },
9
11
  "license": "MIT",
10
12
  "files": [
@@ -12,31 +14,37 @@
12
14
  ],
13
15
  "types": "./dist/index.d.ts",
14
16
  "scripts": {
15
- "build": "bunchee",
17
+ "build": "pnpm run build:lib && pnpm run build:worker",
18
+ "build:lib": "bunchee",
19
+ "build:worker": "bun build src/transform-worker.ts --outfile dist/transform-worker.js --target browser --format esm",
16
20
  "prepublishOnly": "pnpm run build",
17
21
  "build:site": "pnpm run build && next build ./site",
18
22
  "start": "next start ./site",
19
- "dev": "next dev ./site"
23
+ "dev": "pnpm run build && (pnpm run dev:lib & pnpm run dev:worker & pnpm run dev:site & wait)",
24
+ "dev:lib": "bunchee --watch",
25
+ "dev:worker": "bun build src/transform-worker.ts --outfile dist/transform-worker.js --target browser --format esm --watch",
26
+ "dev:site": "next dev ./site"
20
27
  },
21
28
  "peerDependencies": {
22
- "react": "^18.2.0 || ^19.0.0"
29
+ "react": "^19.0.0"
23
30
  },
24
31
  "dependencies": {
25
32
  "es-module-lexer": "1.6.0",
26
- "es-module-shims": "2.0.3",
27
- "sucrase": "3.35.0"
33
+ "oxc-transform": "^0.137.0",
34
+ "react-refresh": "^0.18.0"
28
35
  },
29
36
  "devDependencies": {
30
37
  "@types/node": "^22.10.7",
31
38
  "@types/react": "^19.0.7",
32
39
  "@types/react-dom": "^19.0.3",
33
- "bunchee": "^6.3.2",
34
- "codice": "1.0.0",
35
- "devjar": "link:./",
36
- "next": "^15.1.5",
37
- "react": "^19.0.0",
38
- "react-dom": "^19.0.0",
40
+ "bunchee": "^6.11.0",
41
+ "codice": "^1.6.0",
42
+ "dedent": "^1.7.0",
43
+ "devjar": "link:",
44
+ "next": "16.3.0-canary.69",
45
+ "react": "^19.2.1",
46
+ "react-dom": "^19.2.1",
39
47
  "typescript": "^5.7.3"
40
48
  },
41
- "packageManager": "pnpm@9.15.4"
49
+ "packageManager": "pnpm@11.9.0"
42
50
  }