devjar 0.8.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,13 +1,13 @@
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/55bf67ee-fcc6-4a12-ad0c-5221a5f78c26)
4
+ ![image](https://repository-images.githubusercontent.com/483779830/318964f4-18ae-4dd4-9940-0fe51ccc5abc)
5
5
 
6
6
  ## Introduction
7
7
 
8
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.
9
9
 
10
- **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.
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.
11
11
 
12
12
  ## Install
13
13
 
@@ -24,8 +24,9 @@ pnpm add devjar
24
24
  **Props**
25
25
 
26
26
  * `files`: An object that specifies the files you want to include in your development environment.
27
- * `getModuleUrl`: A function that maps module names to CDN URLs.
27
+ * `resolveModule`: A function that maps module specifiers to browser-loadable module URLs.
28
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.
29
30
 
30
31
  **Example**
31
32
 
@@ -42,8 +43,8 @@ function App() {
42
43
  return (
43
44
  <DevJar
44
45
  files={files}
45
- getModuleUrl={(m) => {
46
- return `${CDN_HOST}/${m}`
46
+ resolveModule={(specifier) => {
47
+ return `${CDN_HOST}/${specifier}`
47
48
  }}
48
49
  />
49
50
  )
@@ -57,7 +58,8 @@ A hook that provides lower-level control over the live code execution environmen
57
58
  **Parameters**
58
59
 
59
60
  * `options`
60
- * `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.
61
63
 
62
64
  **Returns**
63
65
 
@@ -75,8 +77,8 @@ function Playground() {
75
77
  const { ref, error, load } = useLiveCode({
76
78
  // The CDN url of each imported module path in your code
77
79
  // e.g. `import React from 'react'` will load react from skypack.dev/react
78
- getModulePath(modPath) {
79
- return `https://cdn.skypack.dev/${modPath}`
80
+ resolveModule(specifier) {
81
+ return `https://cdn.skypack.dev/${specifier}`
80
82
  }
81
83
  })
82
84
 
@@ -106,77 +108,6 @@ function Playground() {
106
108
  }
107
109
  ```
108
110
 
109
- ## GLSL Shader Runtime
110
-
111
- ### `useGL(options)`
112
-
113
- A hook that renders GLSL fragment shaders using WebGL. Perfect for creating interactive shader playgrounds and visualizations.
114
-
115
- **Parameters**
116
-
117
- * `options`
118
- * `fragment`: The GLSL fragment shader source code as a string.
119
- * `canvasRef`: A React ref to an HTML canvas element where the shader will be rendered.
120
- * `onError`: Optional callback function that receives error messages (prefixed with `devjar:gl`).
121
-
122
- **Available Uniforms**
123
-
124
- The hook automatically provides these uniforms to your fragment shader:
125
-
126
- * `u_time`: `float` - Elapsed time in seconds since the renderer started
127
- * `u_resolution`: `vec2` - Canvas dimensions (width, height)
128
- * `u_mouse`: `vec2` - Normalized mouse position (0.0 to 1.0)
129
-
130
- **Example**
131
-
132
- ```jsx
133
- import { useGL } from 'devjar'
134
- import { useRef, useState } from 'react'
135
-
136
- function ShaderPlayground() {
137
- const canvasRef = useRef(null)
138
- const [shaderCode, setShaderCode] = useState(`
139
- precision mediump float;
140
-
141
- uniform vec2 u_resolution;
142
- uniform float u_time;
143
- uniform vec2 u_mouse;
144
-
145
- void main() {
146
- vec2 st = gl_FragCoord.xy / u_resolution.xy;
147
- vec3 color = vec3(st.x, st.y, abs(sin(u_time)));
148
- gl_FragColor = vec4(color, 1.0);
149
- }
150
- `)
151
- const [error, setError] = useState(null)
152
-
153
- useGL({
154
- fragment: shaderCode,
155
- canvasRef,
156
- onError: setError
157
- })
158
-
159
- return (
160
- <div>
161
- <textarea
162
- value={shaderCode}
163
- onChange={(e) => setShaderCode(e.target.value)}
164
- style={{ width: '100%', height: '200px' }}
165
- />
166
- <canvas ref={canvasRef} style={{ width: '100%', height: '300px' }} />
167
- {error && <pre style={{ color: 'red' }}>{error}</pre>}
168
- </div>
169
- )
170
- }
171
- ```
172
-
173
- **Error Handling**
174
-
175
- All errors are prefixed with `devjar:gl` for easy identification:
176
- - `devjar:gl Shader compilation error: ...`
177
- - `devjar:gl Program linking error: ...`
178
- - `devjar:gl WebGL is not supported in your browser`
179
-
180
111
  ## License
181
112
 
182
- The MIT License (MIT).
113
+ MIT
package/dist/index.d.ts CHANGED
@@ -1,29 +1,20 @@
1
1
  import * as react from 'react';
2
- import * as react_jsx_runtime from 'react/jsx-runtime';
3
- export { UseGlslRendererOptions, useGL } from './gl.tsx';
4
2
 
5
- declare global {
6
- interface Window {
7
- esmsInitOptions: {
8
- shimMode: boolean;
9
- mapOverrides: boolean;
10
- };
11
- }
12
- function importShim(url: string): Promise<any>;
13
- }
14
-
15
- declare function useLiveCode({ getModuleUrl }: {
16
- getModuleUrl?: (name: string) => string;
3
+ declare function useLiveCode({ resolveModule, tailwindSrc, }: {
4
+ resolveModule?: (specifier: string) => string;
5
+ tailwindSrc?: string | false;
17
6
  }): {
18
7
  ref: react.RefObject<any>;
19
8
  error: undefined;
20
- load: (files: any) => Promise<void>;
9
+ load: (files: Record<string, string>) => Promise<void>;
21
10
  };
22
11
 
23
- declare function DevJar({ files, getModuleUrl, onError, ...props }: {
12
+ declare function DevJar({ files, resolveModule, onError, tailwindSrc, ref: forwardedRef, ...props }: {
24
13
  files: Record<string, string>;
25
- getModuleUrl?: (name: string) => string;
14
+ resolveModule?: (specifier: string) => string;
26
15
  onError?: (...data: any[]) => void;
27
- } & React.IframeHTMLAttributes<HTMLIFrameElement>): react_jsx_runtime.JSX.Element;
16
+ tailwindSrc?: string | false;
17
+ ref?: React.Ref<HTMLIFrameElement>;
18
+ } & React.IframeHTMLAttributes<HTMLIFrameElement>): react.JSX.Element;
28
19
 
29
20
  export { DevJar, useLiveCode };
package/dist/index.js CHANGED
@@ -1,83 +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
- export { useGL } from './gl.js';
6
4
 
7
- // declare esmsInitOptions on global window
8
- async function createModule(files, { getModuleUrl }) {
9
- let currentImportMap;
10
- let shim;
11
- async function setupImportMap() {
12
- if (shim) return shim;
13
- window.esmsInitOptions = {
14
- shimMode: true,
15
- mapOverrides: true
16
- };
17
- shim = import(/* webpackIgnore: true */ getModuleUrl('es-module-shims'));
18
- 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)}`;
19
13
  }
20
- function updateImportMap(imports) {
21
- imports['react'] = getModuleUrl('react');
22
- imports['react-dom'] = getModuleUrl('react-dom');
23
- imports['react-dom/client'] = getModuleUrl('react-dom/client');
14
+ function registerImportMap(imports) {
15
+ var _runtime;
24
16
  const script = document.createElement('script');
25
- script.type = 'importmap-shim';
26
- script.innerHTML = JSON.stringify({
17
+ script.type = 'importmap';
18
+ script.textContent = JSON.stringify({
27
19
  imports
28
20
  });
29
- document.body.appendChild(script);
30
- if (currentImportMap) {
31
- currentImportMap.parentNode.removeChild(currentImportMap);
32
- }
33
- currentImportMap = script;
21
+ document.head.appendChild(script);
22
+ ((_runtime = runtime).importMaps || (_runtime.importMaps = [])).push(script);
34
23
  }
35
- function createInlinedModule(code, type) {
36
- if (type === 'css') return `data:text/css;utf-8,${encodeURIComponent(code)}`;
24
+ function createInlinedModule(code) {
37
25
  return `data:text/javascript;utf-8,${encodeURIComponent(code)}`;
38
26
  }
39
- await setupImportMap();
40
- const imports = Object.fromEntries(Object.entries(files).map(([fileName, code])=>[
41
- fileName,
42
- createInlinedModule(code, fileName.endsWith('.css') ? 'css' : 'js')
43
- ]));
44
- updateImportMap(imports);
45
- 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
+ };
46
110
  }
47
111
 
48
112
  let esModuleLexerInit;
49
113
  const isRelative = (s)=>s.startsWith('./');
50
114
  const removeExtension = (str)=>str.replace(/\.[^/.]+$/, '');
51
- function transformCode(_code, getModuleUrl, externals) {
52
- const code = transform(_code, {
53
- transforms: [
54
- 'jsx',
55
- 'typescript'
56
- ]
57
- }).code;
58
- 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)}__`;
59
119
  }
60
- 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) {
61
143
  let code = '';
62
144
  let lastIndex = 0;
63
145
  let hasReactImports = false;
64
146
  const [imports] = parse(source);
65
147
  const cssImports = [];
66
- let cssImportIndex = 0;
148
+ const dependencies = [];
67
149
  // start, end, statementStart, statementEnd, assertion, name
68
150
  imports.forEach(({ s, e, ss, se, a, n })=>{
69
- code += source.slice(lastIndex, ss) // content from last import to beginning of this line
70
- ;
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;
71
154
  // handle imports
72
- if (n.endsWith('.css')) {
155
+ if (localModuleKey && localModuleKey.endsWith('.css')) {
73
156
  // Map './styles.css' -> '@styles.css', and collect it
74
- const cssPath = `${'@' + n.slice(2)}`;
75
- cssImports.push(cssPath);
157
+ cssImports.push(localModuleKey);
76
158
  } else {
77
159
  code += source.substring(ss, s);
78
- code += isRelative(n) ? '@' + n.slice(2) : externals.has(n) ? n : getModuleUrl(n);
160
+ code += localModuleKey ? createLocalImportPlaceholder(localModuleKey) : resolveModule(n);
79
161
  code += source.substring(e, se);
80
162
  }
163
+ if (localModuleKey) dependencies.push(localModuleKey);
81
164
  lastIndex = se;
82
165
  if (n === 'react') {
83
166
  const statement = source.slice(ss, se);
@@ -85,64 +168,104 @@ function replaceImports(source, getModuleUrl, externals) {
85
168
  hasReactImports = true;
86
169
  }
87
170
  }
88
- cssImports.forEach((cssPath)=>{
89
- code += `\nimport sheet${cssImportIndex} from "${cssPath}" assert { type: "css" };\n`;
90
- cssImportIndex++;
91
- });
92
171
  });
93
172
  if (cssImports.length) {
94
- code += `const __customStyleSheets = [`;
95
- for(let i = 0; i < cssImports.length; i++){
96
- code += `sheet${i}`;
97
- if (i < cssImports.length - 1) {
98
- code += `, `;
99
- }
100
- }
101
- code += `];\n`;
102
- 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
+ });
103
185
  }
104
186
  code += source.substring(lastIndex);
105
187
  if (!hasReactImports) {
106
- code = `import React from 'react';\n${code}`;
188
+ code = `import React from ${JSON.stringify(resolveModule('react'))};\n${code}`;
107
189
  }
108
- 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
+ };
109
195
  }
110
196
  // createRenderer is going to be stringified and executed in the iframe
111
- function createRenderer(createModule_, getModuleUrl) {
197
+ function createRenderer(createModule_, resolveModule) {
112
198
  let reactRoot;
113
- async function render(files) {
114
- const mod = await createModule_(files, {
115
- 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
116
208
  });
117
- const ReactMod = await self.importShim('react');
118
- 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'));
119
211
  const _jsx = ReactMod.createElement;
120
212
  const root = document.getElementById('__reactRoot');
121
- class ErrorBoundary extends ReactMod.Component {
122
- componentDidCatch(error) {
123
- this.setState({
124
- error
125
- });
126
- }
127
- render() {
128
- if (this.state.error) {
129
- 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
+ };
130
219
  }
131
- return this.props.children;
132
- }
133
- constructor(props){
134
- super(props);
135
- this.state = {
136
- error: null
137
- };
138
- }
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
+ };
139
245
  }
140
246
  if (!reactRoot) {
141
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
+ }
142
268
  }
143
- const Component = mod.default;
144
- const element = _jsx(ErrorBoundary, null, _jsx(Component));
145
- reactRoot.render(element);
146
269
  }
147
270
  return render;
148
271
  }
@@ -152,20 +275,13 @@ function createMainScript({ uid }) {
152
275
  const _createModule = ${createModule.toString()};
153
276
  const _createRenderer = ${createRenderer.toString()};
154
277
 
155
- const getModuleUrl = (m) => window.parent.__devjar__[globalThis.uid].getModuleUrl(m)
278
+ const resolveModule = (specifier) => window.parent.__devjar__[globalThis.uid].resolveModule(specifier)
156
279
 
157
280
  globalThis.uid = ${JSON.stringify(uid)};
158
- globalThis.__render__ = _createRenderer(_createModule, getModuleUrl);
281
+ globalThis.__render__ = _createRenderer(_createModule, resolveModule);
159
282
  `;
160
283
  return code;
161
284
  }
162
- function createEsShimOptionsScript() {
163
- return `\
164
- window.esmsInitOptions = {
165
- polyfillEnable: ['css-modules', 'json-modules'],
166
- onerror: console.error,
167
- }`;
168
- }
169
285
  function useScript() {
170
286
  return useRef(typeof window !== 'undefined' ? document.createElement('script') : null);
171
287
  }
@@ -180,28 +296,45 @@ function createScript(scriptRef, { content, src, type } = {}) {
180
296
  }
181
297
  return script;
182
298
  }
183
- function useLiveCode({ getModuleUrl }) {
299
+ function useLiveCode({ resolveModule, tailwindSrc = defaultTailwindSrc }) {
184
300
  const iframeRef = useRef(null);
185
301
  const [error, setError] = useState();
186
302
  const rerender = useState({})[1];
187
303
  const appScriptRef = useScript();
188
- const esShimOptionsScriptRef = useScript();
189
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);
190
310
  const uid = useId();
191
- // Let getModuleUrl executed on parent window side since it might involve
311
+ // Let resolveModule execute on parent window side since it might involve
192
312
  // variables that iframe cannot access.
193
313
  useEffect(()=>{
194
314
  if (!globalThis.__devjar__) {
195
315
  globalThis.__devjar__ = {};
196
316
  }
197
317
  globalThis.__devjar__[uid] = {
198
- getModuleUrl
318
+ resolveModule
199
319
  };
200
320
  return ()=>{
201
321
  if (globalThis.__devjar__) {
202
322
  delete globalThis.__devjar__[uid];
203
323
  }
204
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
+ };
205
338
  }, []);
206
339
  useEffect(()=>{
207
340
  const iframe = iframeRef.current;
@@ -213,72 +346,126 @@ function useLiveCode({ getModuleUrl }) {
213
346
  const appScriptContent = createMainScript({
214
347
  uid
215
348
  });
216
- const scriptOptionsContent = createEsShimOptionsScript();
217
- const esmShimOptionsScript = createScript(esShimOptionsScriptRef, {
218
- content: scriptOptionsContent
219
- });
220
349
  const appScript = createScript(appScriptRef, {
221
- content: appScriptContent,
222
- type: 'module'
223
- });
224
- const tailwindScript = createScript(tailwindcssScriptRef, {
225
- src: 'https://unpkg.com/@tailwindcss/browser@4'
350
+ content: appScriptContent
226
351
  });
352
+ const tailwindScript = tailwindSrc ? createScript(tailwindcssScriptRef, {
353
+ src: tailwindSrc
354
+ }) : null;
227
355
  body.appendChild(div);
228
- body.appendChild(esmShimOptionsScript);
229
356
  body.appendChild(appScript);
230
- body.appendChild(tailwindScript);
357
+ if (tailwindScript) body.appendChild(tailwindScript);
231
358
  return ()=>{
232
359
  if (!iframe || !iframe.contentDocument) return;
233
360
  body.removeChild(div);
234
- body.removeChild(esmShimOptionsScript);
235
361
  body.removeChild(appScript);
236
- body.removeChild(tailwindScript);
362
+ if (tailwindScript) body.removeChild(tailwindScript);
237
363
  };
238
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
+ ]);
239
406
  const load = useCallback(async (files)=>{
407
+ const loadId = ++loadIdRef.current;
240
408
  if (!esModuleLexerInit) {
241
409
  await init;
242
410
  esModuleLexerInit = true;
243
411
  }
244
412
  if (files) {
245
- // { 'react', 'react-dom' }
246
- const overrideExternals = new Set(Object.keys(files).filter((name)=>!isRelative(name) && name !== 'index.js'));
247
- // Always share react as externals
248
- overrideExternals.add('react');
249
- overrideExternals.add('react-dom');
413
+ const localModules = new Set(Object.keys(files).map(getModuleKey));
250
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
+ }
251
429
  /**
252
430
  * transformedFiles
253
431
  * {
254
432
  * 'index.js': '...',
255
433
  * '@mod1': '...',
256
434
  * '@mod2': '...',
257
- */ const transformedFiles = Object.keys(files).reduce((res, filename)=>{
435
+ */ const dependencies = {};
436
+ const transformedFiles = Object.keys(files).reduce((res, filename)=>{
258
437
  // 1. Remove ./
259
438
  // 2. For non css files, remove extension
260
439
  // e.g. './styles.css' -> '@styles.css'
261
440
  // e.g. './foo.js' -> '@foo'
262
- const moduleKey = isRelative(filename) ? '@' + filename.slice(2) : filename;
441
+ const moduleKey = getModuleKey(filename);
263
442
  if (filename.endsWith('.css')) {
264
443
  res[moduleKey] = files[filename];
444
+ dependencies[moduleKey] = [];
265
445
  } else {
266
446
  // JS or TS files
267
- const normalizedModuleKey = removeExtension(moduleKey);
268
- 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;
269
450
  }
270
451
  return res;
271
452
  }, {});
272
453
  const iframe = iframeRef.current;
273
454
  const script = appScriptRef.current;
274
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
+ };
275
462
  const render = iframe.contentWindow.__render__;
276
463
  if (render) {
277
- render(transformedFiles);
464
+ await renderFiles();
278
465
  } else {
279
466
  // if render is not loaded yet, wait until it's loaded
280
467
  script.onload = ()=>{
281
- iframe.contentWindow.__render__(transformedFiles).catch((err)=>{
468
+ renderFiles().catch((err)=>{
282
469
  setError(err);
283
470
  });
284
471
  };
@@ -291,7 +478,10 @@ function useLiveCode({ getModuleUrl }) {
291
478
  }
292
479
  }
293
480
  rerender({});
294
- }, []);
481
+ }, [
482
+ resolveModule,
483
+ transformFiles
484
+ ]);
295
485
  return {
296
486
  ref: iframeRef,
297
487
  error,
@@ -300,11 +490,15 @@ function useLiveCode({ getModuleUrl }) {
300
490
  }
301
491
 
302
492
  const defaultOnError = typeof window !== 'undefined' ? console.error : ()=>{};
303
- function DevJar({ files, getModuleUrl, onError = defaultOnError, ...props }) {
493
+ function DevJar({ files, resolveModule, onError = defaultOnError, tailwindSrc, ref: forwardedRef, ...props }) {
304
494
  const onErrorRef = useRef(onError);
305
495
  const { ref, error, load } = useLiveCode({
306
- getModuleUrl
496
+ resolveModule,
497
+ tailwindSrc
307
498
  });
499
+ useImperativeHandle(forwardedRef, ()=>ref.current, [
500
+ ref
501
+ ]);
308
502
  useEffect(()=>{
309
503
  onErrorRef.current(error);
310
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,42 +1,50 @@
1
1
  {
2
2
  "name": "devjar",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "exports": {
6
- ".": "./dist/index.js",
7
- "./gl": "./dist/gl.js",
8
- "./package.json": "./package.json"
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "default": "./dist/index.js"
9
+ }
9
10
  },
10
11
  "license": "MIT",
11
12
  "files": [
12
13
  "dist"
13
14
  ],
14
15
  "types": "./dist/index.d.ts",
16
+ "scripts": {
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",
20
+ "prepublishOnly": "pnpm run build",
21
+ "build:site": "pnpm run build && next build ./site",
22
+ "start": "next start ./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"
27
+ },
15
28
  "peerDependencies": {
16
- "react": "^18.2.0 || ^19.0.0"
29
+ "react": "^19.0.0"
17
30
  },
18
31
  "dependencies": {
19
- "dedent": "^1.7.0",
20
32
  "es-module-lexer": "1.6.0",
21
- "es-module-shims": "2.0.3",
22
- "sucrase": "3.35.0"
33
+ "oxc-transform": "^0.137.0",
34
+ "react-refresh": "^0.18.0"
23
35
  },
24
36
  "devDependencies": {
25
37
  "@types/node": "^22.10.7",
26
38
  "@types/react": "^19.0.7",
27
39
  "@types/react-dom": "^19.0.3",
28
- "bunchee": "^6.3.3",
29
- "codice": "^1.5.4",
40
+ "bunchee": "^6.11.0",
41
+ "codice": "^1.6.0",
42
+ "dedent": "^1.7.0",
30
43
  "devjar": "link:",
31
- "next": "^16.0.3",
32
- "react": "^19.2.0",
33
- "react-dom": "^19.2.0",
44
+ "next": "16.3.0-canary.69",
45
+ "react": "^19.2.1",
46
+ "react-dom": "^19.2.1",
34
47
  "typescript": "^5.7.3"
35
48
  },
36
- "scripts": {
37
- "build": "bunchee",
38
- "build:site": "pnpm run build && next build ./site",
39
- "start": "next start ./site",
40
- "dev": "next dev ./site"
41
- }
42
- }
49
+ "packageManager": "pnpm@11.9.0"
50
+ }
package/dist/gl.js DELETED
@@ -1,214 +0,0 @@
1
- import { useRef, useEffect } from 'react';
2
-
3
- // Default vertex shader - simple fullscreen quad
4
- const DEFAULT_VERTEX_SHADER = `\
5
- attribute vec2 a_position;
6
-
7
- void main() {
8
- gl_Position = vec4(a_position, 0.0, 1.0);
9
- }
10
- `;
11
- function createShader(gl, type, source) {
12
- const shader = gl.createShader(type);
13
- if (!shader) return null;
14
- gl.shaderSource(shader, source);
15
- gl.compileShader(shader);
16
- if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
17
- const error = gl.getShaderInfoLog(shader);
18
- gl.deleteShader(shader);
19
- throw new Error(`devjar:gl Shader compilation error: ${error}`);
20
- }
21
- return shader;
22
- }
23
- function createProgram(gl, vertexShader, fragmentShader) {
24
- const program = gl.createProgram();
25
- if (!program) return null;
26
- gl.attachShader(program, vertexShader);
27
- gl.attachShader(program, fragmentShader);
28
- gl.linkProgram(program);
29
- if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
30
- const error = gl.getProgramInfoLog(program);
31
- gl.deleteProgram(program);
32
- throw new Error(`devjar:gl Program linking error: ${error}`);
33
- }
34
- return program;
35
- }
36
- function createQuad(gl) {
37
- const buffer = gl.createBuffer();
38
- if (!buffer) return null;
39
- gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
40
- // Fullscreen quad: two triangles covering the entire screen
41
- const positions = new Float32Array([
42
- -1,
43
- -1,
44
- 1,
45
- -1,
46
- -1,
47
- 1,
48
- -1,
49
- 1,
50
- 1,
51
- -1,
52
- 1,
53
- 1
54
- ]);
55
- gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
56
- return buffer;
57
- }
58
- function useGL({ fragment, canvasRef, onError }) {
59
- const animationFrameRef = useRef(undefined);
60
- const glRef = useRef(null);
61
- const programRef = useRef(null);
62
- const positionBufferRef = useRef(null);
63
- const startTimeRef = useRef(performance.now());
64
- const mouseRef = useRef({
65
- x: 0,
66
- y: 0
67
- });
68
- // Initialize WebGL
69
- useEffect(()=>{
70
- const canvas = canvasRef.current;
71
- if (!canvas) return;
72
- const gl = canvas.getContext('webgl');
73
- if (!gl) {
74
- onError?.('devjar:gl WebGL is not supported in your browser');
75
- return;
76
- }
77
- glRef.current = gl;
78
- // Create quad buffer once
79
- const positionBuffer = createQuad(gl);
80
- if (positionBuffer) {
81
- positionBufferRef.current = positionBuffer;
82
- }
83
- // Set canvas size
84
- const resizeCanvas = ()=>{
85
- const rect = canvas.getBoundingClientRect();
86
- const dpr = window.devicePixelRatio || 1;
87
- const width = rect.width;
88
- const height = rect.height;
89
- canvas.width = width * dpr;
90
- canvas.height = height * dpr;
91
- canvas.style.width = `${width}px`;
92
- canvas.style.height = `${height}px`;
93
- gl.viewport(0, 0, canvas.width, canvas.height);
94
- };
95
- resizeCanvas();
96
- window.addEventListener('resize', resizeCanvas);
97
- // Use ResizeObserver for more accurate sizing
98
- const resizeObserver = new ResizeObserver(()=>{
99
- resizeCanvas();
100
- });
101
- resizeObserver.observe(canvas);
102
- // Handle mouse movement
103
- const handleMouseMove = (e)=>{
104
- const rect = canvas.getBoundingClientRect();
105
- mouseRef.current = {
106
- x: (e.clientX - rect.left) / rect.width,
107
- y: 1.0 - (e.clientY - rect.top) / rect.height
108
- };
109
- };
110
- canvas.addEventListener('mousemove', handleMouseMove);
111
- return ()=>{
112
- window.removeEventListener('resize', resizeCanvas);
113
- canvas.removeEventListener('mousemove', handleMouseMove);
114
- resizeObserver.disconnect();
115
- if (animationFrameRef.current) {
116
- cancelAnimationFrame(animationFrameRef.current);
117
- }
118
- if (positionBufferRef.current && gl) {
119
- gl.deleteBuffer(positionBufferRef.current);
120
- }
121
- };
122
- }, [
123
- canvasRef,
124
- onError
125
- ]);
126
- // Compile and render shader
127
- useEffect(()=>{
128
- const gl = glRef.current;
129
- const canvas = canvasRef.current;
130
- if (!gl || !canvas) return;
131
- onError?.(null);
132
- try {
133
- // Create shaders
134
- const vertexShader = createShader(gl, gl.VERTEX_SHADER, DEFAULT_VERTEX_SHADER);
135
- if (!vertexShader) throw new Error('devjar:gl Failed to create vertex shader');
136
- const compiledFragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragment);
137
- if (!compiledFragmentShader) throw new Error('devjar:gl Failed to create fragment shader');
138
- // Create program
139
- const program = createProgram(gl, vertexShader, compiledFragmentShader);
140
- if (!program) throw new Error('devjar:gl Failed to create program');
141
- // Clean up old program
142
- if (programRef.current) {
143
- gl.deleteProgram(programRef.current);
144
- }
145
- programRef.current = program;
146
- // Clean up shaders (they're linked into the program)
147
- gl.deleteShader(vertexShader);
148
- gl.deleteShader(compiledFragmentShader);
149
- // Render loop
150
- const render = ()=>{
151
- if (!gl || !canvas || !programRef.current || !positionBufferRef.current) return;
152
- const program = programRef.current;
153
- const positionBuffer = positionBufferRef.current;
154
- // Clear
155
- gl.clearColor(0, 0, 0, 1);
156
- gl.clear(gl.COLOR_BUFFER_BIT);
157
- // Use program
158
- gl.useProgram(program);
159
- // Set up position attribute
160
- const positionLocation = gl.getAttribLocation(program, 'a_position');
161
- if (positionLocation >= 0) {
162
- gl.enableVertexAttribArray(positionLocation);
163
- gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
164
- gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
165
- }
166
- // Set uniforms
167
- const time = (performance.now() - startTimeRef.current) / 1000;
168
- const resolutionLocation = gl.getUniformLocation(program, 'u_resolution');
169
- const timeLocation = gl.getUniformLocation(program, 'u_time');
170
- const mouseLocation = gl.getUniformLocation(program, 'u_mouse');
171
- if (resolutionLocation) {
172
- gl.uniform2f(resolutionLocation, canvas.width, canvas.height);
173
- }
174
- if (timeLocation) {
175
- gl.uniform1f(timeLocation, time);
176
- }
177
- if (mouseLocation) {
178
- gl.uniform2f(mouseLocation, mouseRef.current.x, mouseRef.current.y);
179
- }
180
- // Draw
181
- gl.drawArrays(gl.TRIANGLES, 0, 6);
182
- animationFrameRef.current = requestAnimationFrame(render);
183
- };
184
- // Start render loop
185
- if (animationFrameRef.current) {
186
- cancelAnimationFrame(animationFrameRef.current);
187
- }
188
- render();
189
- // Cleanup function
190
- return ()=>{
191
- if (animationFrameRef.current) {
192
- cancelAnimationFrame(animationFrameRef.current);
193
- }
194
- if (programRef.current && gl) {
195
- gl.deleteProgram(programRef.current);
196
- programRef.current = null;
197
- }
198
- };
199
- } catch (err) {
200
- const errorMessage = err instanceof Error ? err.message : 'Unknown error';
201
- const prefixedMessage = errorMessage.startsWith('devjar:gl ') ? errorMessage : `devjar:gl ${errorMessage}`;
202
- onError?.(prefixedMessage);
203
- if (animationFrameRef.current) {
204
- cancelAnimationFrame(animationFrameRef.current);
205
- }
206
- }
207
- }, [
208
- fragment,
209
- canvasRef,
210
- onError
211
- ]);
212
- }
213
-
214
- export { useGL };