devjar 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,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,161 @@
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)}__`;
19
10
  }
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');
24
- const script = document.createElement('script');
25
- script.type = 'importmap-shim';
26
- script.innerHTML = JSON.stringify({
27
- imports
28
- });
29
- document.body.appendChild(script);
30
- if (currentImportMap) {
31
- currentImportMap.parentNode.removeChild(currentImportMap);
11
+ function createInlinedModule(code) {
12
+ return `data:text/javascript;utf-8,${encodeURIComponent(code)}`;
13
+ }
14
+ function createCssModule(code) {
15
+ return `\
16
+ const sheet = new CSSStyleSheet();
17
+ sheet.replaceSync(${JSON.stringify(code)});
18
+ export default sheet;`;
19
+ }
20
+ function rewriteLocalImports(code, specifiers) {
21
+ let rewritten = code;
22
+ for (const [moduleKey, specifier] of Object.entries(specifiers)){
23
+ rewritten = rewritten.split(createLocalImportPlaceholder(moduleKey)).join(specifier);
32
24
  }
33
- currentImportMap = script;
25
+ return rewritten;
34
26
  }
35
- function createInlinedModule(code, type) {
36
- if (type === 'css') return `data:text/css;utf-8,${encodeURIComponent(code)}`;
37
- return `data:text/javascript;utf-8,${encodeURIComponent(code)}`;
27
+ (_runtime = runtime).files || (_runtime.files = {});
28
+ (_runtime1 = runtime).urls || (_runtime1.urls = {});
29
+ runtime.revision = (runtime.revision || 0) + 1;
30
+ const changedModules = new Set();
31
+ for (const [fileName, code] of Object.entries(files)){
32
+ if (runtime.files[fileName] !== code) changedModules.add(fileName);
33
+ }
34
+ for (const fileName of Object.keys(runtime.files)){
35
+ if (!(fileName in files)) changedModules.add(fileName);
36
+ }
37
+ const importers = {};
38
+ for (const [importer, importedModules] of Object.entries(dependencies)){
39
+ for (const imported of importedModules){
40
+ var _importers, _imported;
41
+ ((_importers = importers)[_imported = imported] || (_importers[_imported] = [])).push(importer);
42
+ }
43
+ }
44
+ const invalidated = new Set(changedModules);
45
+ const queue = [
46
+ ...changedModules
47
+ ];
48
+ while(queue.length){
49
+ const changed = queue.shift();
50
+ for (const importer of importers[changed] || []){
51
+ if (invalidated.has(importer)) continue;
52
+ invalidated.add(importer);
53
+ queue.push(importer);
54
+ }
55
+ }
56
+ const moduleKeys = new Set(Object.keys(files));
57
+ for (const importedModules of Object.values(dependencies)){
58
+ for (const moduleKey of importedModules)moduleKeys.add(moduleKey);
59
+ }
60
+ const moduleSources = {};
61
+ for (const moduleKey of moduleKeys){
62
+ if (!(moduleKey in files)) {
63
+ moduleSources[moduleKey] = `throw new Error(${JSON.stringify('devjar: Module not found: ' + moduleKey)})`;
64
+ delete runtime.urls[moduleKey];
65
+ continue;
66
+ }
67
+ moduleSources[moduleKey] = moduleKey.endsWith('.css') ? createCssModule(files[moduleKey]) : files[moduleKey];
68
+ }
69
+ const buildingUrls = new Set();
70
+ const buildModuleUrl = (moduleKey)=>{
71
+ if (runtime.urls[moduleKey] && !invalidated.has(moduleKey)) return runtime.urls[moduleKey];
72
+ if (buildingUrls.has(moduleKey)) {
73
+ throw new Error('devjar: Circular local imports are not supported: ' + moduleKey);
74
+ }
75
+ buildingUrls.add(moduleKey);
76
+ const specifiers = {};
77
+ for (const imported of dependencies[moduleKey] || []){
78
+ specifiers[imported] = buildModuleUrl(imported);
79
+ }
80
+ const moduleCode = moduleKey.endsWith('.css') ? moduleSources[moduleKey] : rewriteLocalImports(moduleSources[moduleKey], specifiers);
81
+ runtime.urls[moduleKey] = createInlinedModule(moduleCode);
82
+ buildingUrls.delete(moduleKey);
83
+ return runtime.urls[moduleKey];
84
+ };
85
+ for (const moduleKey of moduleKeys){
86
+ buildModuleUrl(moduleKey);
87
+ }
88
+ if (!runtime.refreshRuntime) {
89
+ const refreshModule = await import(/* webpackIgnore: true */ /* @vite-ignore */ /* turbopackIgnore: true */ resolveModule('react-refresh/runtime'));
90
+ runtime.refreshRuntime = refreshModule.default || refreshModule;
91
+ runtime.refreshRuntime.injectIntoGlobalHook(self);
92
+ globalThis.__devjarRefreshRuntime = runtime.refreshRuntime;
93
+ }
94
+ if (!runtime.urls['index']) {
95
+ throw new Error('devjar: Module not found: index');
38
96
  }
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');
97
+ const module = await import(/* webpackIgnore: true */ /* @vite-ignore */ /* turbopackIgnore: true */ runtime.urls['index']);
98
+ runtime.files = {
99
+ ...files
100
+ };
101
+ return {
102
+ module,
103
+ changed: changedModules.size > 0
104
+ };
46
105
  }
47
106
 
48
107
  let esModuleLexerInit;
49
108
  const isRelative = (s)=>s.startsWith('./');
50
109
  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);
110
+ const localImportPrefix = '__DEVJAR_LOCAL_IMPORT__';
111
+ const defaultTailwindSrc = 'https://unpkg.com/@tailwindcss/browser@4';
112
+ function createLocalImportPlaceholder(moduleKey) {
113
+ return `${localImportPrefix}${encodeURIComponent(moduleKey)}__`;
114
+ }
115
+ function createTransformWorker() {
116
+ return new Worker(new URL('./transform-worker.js', import.meta.url), {
117
+ type: 'module',
118
+ name: 'devjar-transform'
119
+ });
120
+ }
121
+ function getModuleKey(filename) {
122
+ const key = isRelative(filename) ? '@' + filename.slice(2) : filename;
123
+ return filename.endsWith('.css') ? key : removeExtension(key);
59
124
  }
60
- function replaceImports(source, getModuleUrl, externals) {
125
+ function resolveRelativeModule(importer, imported) {
126
+ const importerPath = importer.startsWith('./') ? importer.slice(2) : importer;
127
+ const parts = importerPath.split('/');
128
+ parts.pop();
129
+ for (const part of imported.split('/')){
130
+ if (!part || part === '.') continue;
131
+ if (part === '..') parts.pop();
132
+ else parts.push(part);
133
+ }
134
+ const path = parts.join('/');
135
+ return imported.endsWith('.css') ? '@' + path : removeExtension('@' + path);
136
+ }
137
+ function replaceImports(source, filename, moduleKey, resolveModule, localModules) {
61
138
  let code = '';
62
139
  let lastIndex = 0;
63
140
  let hasReactImports = false;
64
141
  const [imports] = parse(source);
65
142
  const cssImports = [];
66
- let cssImportIndex = 0;
143
+ const dependencies = [];
67
144
  // start, end, statementStart, statementEnd, assertion, name
68
145
  imports.forEach(({ s, e, ss, se, a, n })=>{
69
- code += source.slice(lastIndex, ss) // content from last import to beginning of this line
70
- ;
146
+ if (!n) return;
147
+ code += source.slice(lastIndex, ss); // content from last import to beginning of this line
148
+ const localModuleKey = isRelative(n) ? resolveRelativeModule(filename, n) : localModules.has(n) ? n : undefined;
71
149
  // handle imports
72
- if (n.endsWith('.css')) {
150
+ if (localModuleKey && localModuleKey.endsWith('.css')) {
73
151
  // Map './styles.css' -> '@styles.css', and collect it
74
- const cssPath = `${'@' + n.slice(2)}`;
75
- cssImports.push(cssPath);
152
+ cssImports.push(localModuleKey);
76
153
  } else {
77
154
  code += source.substring(ss, s);
78
- code += isRelative(n) ? '@' + n.slice(2) : externals.has(n) ? n : getModuleUrl(n);
155
+ code += localModuleKey ? createLocalImportPlaceholder(localModuleKey) : resolveModule(n);
79
156
  code += source.substring(e, se);
80
157
  }
158
+ if (localModuleKey) dependencies.push(localModuleKey);
81
159
  lastIndex = se;
82
160
  if (n === 'react') {
83
161
  const statement = source.slice(ss, se);
@@ -85,64 +163,104 @@ function replaceImports(source, getModuleUrl, externals) {
85
163
  hasReactImports = true;
86
164
  }
87
165
  }
88
- cssImports.forEach((cssPath)=>{
89
- code += `\nimport sheet${cssImportIndex} from "${cssPath}" assert { type: "css" };\n`;
90
- cssImportIndex++;
91
- });
92
166
  });
93
167
  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`;
168
+ cssImports.forEach((cssPath, index)=>{
169
+ code += `\nimport __devjarSheet${index} from "${createLocalImportPlaceholder(cssPath)}";\n`;
170
+ });
171
+ code += `globalThis.__devjarStyleSheets ||= new Map();\n`;
172
+ cssImports.forEach((cssPath, index)=>{
173
+ code += `{ const previous = globalThis.__devjarStyleSheets.get(${JSON.stringify(cssPath)});\n`;
174
+ code += `const sheets = [...document.adoptedStyleSheets];\n`;
175
+ code += `const sheetIndex = sheets.indexOf(previous);\n`;
176
+ code += `if (sheetIndex < 0) sheets.push(__devjarSheet${index}); else sheets[sheetIndex] = __devjarSheet${index};\n`;
177
+ code += `document.adoptedStyleSheets = sheets;\n`;
178
+ code += `globalThis.__devjarStyleSheets.set(${JSON.stringify(cssPath)}, __devjarSheet${index}); }\n`;
179
+ });
103
180
  }
104
181
  code += source.substring(lastIndex);
105
182
  if (!hasReactImports) {
106
- code = `import React from 'react';\n${code}`;
183
+ code = `import React from ${JSON.stringify(resolveModule('react'))};\n${code}`;
107
184
  }
108
- return code;
185
+ code = `const $RefreshReg$ = (type, id) => globalThis.__devjarRefreshRuntime.register(type, ${JSON.stringify(moduleKey + ' ')} + id);\n` + `const $RefreshSig$ = globalThis.__devjarRefreshRuntime.createSignatureFunctionForTransform;\n` + code;
186
+ return {
187
+ code,
188
+ dependencies
189
+ };
109
190
  }
110
191
  // createRenderer is going to be stringified and executed in the iframe
111
- function createRenderer(createModule_, getModuleUrl) {
192
+ function createRenderer(createModule_, resolveModule) {
112
193
  let reactRoot;
113
- async function render(files) {
114
- const mod = await createModule_(files, {
115
- getModuleUrl
194
+ let ErrorBoundary;
195
+ let errorBoundary;
196
+ let revision = 0;
197
+ const moduleRuntime = {};
198
+ async function render(files, dependencies) {
199
+ const result = await createModule_(files, {
200
+ resolveModule,
201
+ dependencies,
202
+ runtime: moduleRuntime
116
203
  });
117
- const ReactMod = await self.importShim('react');
118
- const ReactDOMMod = await self.importShim('react-dom/client');
204
+ const ReactMod = await import(/* webpackIgnore: true */ /* @vite-ignore */ /* turbopackIgnore: true */ resolveModule('react'));
205
+ const ReactDOMMod = await import(/* webpackIgnore: true */ /* @vite-ignore */ /* turbopackIgnore: true */ resolveModule('react-dom/client'));
119
206
  const _jsx = ReactMod.createElement;
120
207
  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);
208
+ if (!ErrorBoundary) {
209
+ ErrorBoundary = class extends ReactMod.Component {
210
+ static getDerivedStateFromError(error) {
211
+ return {
212
+ error
213
+ };
130
214
  }
131
- return this.props.children;
132
- }
133
- constructor(props){
134
- super(props);
135
- this.state = {
136
- error: null
137
- };
138
- }
215
+ reset() {
216
+ if (this.state.error) this.setState({
217
+ error: null
218
+ });
219
+ }
220
+ componentDidUpdate(previousProps) {
221
+ if (previousProps.revision !== this.props.revision && this.state.error) {
222
+ this.setState({
223
+ error: null
224
+ });
225
+ }
226
+ }
227
+ render() {
228
+ if (this.state.error) {
229
+ return _jsx('div', null, this.state.error?.message);
230
+ }
231
+ return this.props.children;
232
+ }
233
+ constructor(props){
234
+ super(props);
235
+ this.state = {
236
+ error: null
237
+ };
238
+ }
239
+ };
139
240
  }
140
241
  if (!reactRoot) {
141
242
  reactRoot = ReactDOMMod.createRoot(root);
243
+ revision++;
244
+ reactRoot.render(_jsx(ErrorBoundary, {
245
+ revision,
246
+ ref: (value)=>errorBoundary = value
247
+ }, _jsx(result.module.default)));
248
+ moduleRuntime.hasRendered = true;
249
+ return;
250
+ }
251
+ if (result.changed) {
252
+ errorBoundary?.reset();
253
+ const refreshRuntime = moduleRuntime.refreshRuntime;
254
+ const refreshUpdate = refreshRuntime.performReactRefresh();
255
+ const mountedRootCount = typeof refreshRuntime._getMountedRootCount === 'function' ? refreshRuntime._getMountedRootCount() : 0;
256
+ if (!refreshUpdate || mountedRootCount === 0) {
257
+ revision++;
258
+ reactRoot.render(_jsx(ErrorBoundary, {
259
+ revision,
260
+ ref: (value)=>errorBoundary = value
261
+ }, _jsx(result.module.default)));
262
+ }
142
263
  }
143
- const Component = mod.default;
144
- const element = _jsx(ErrorBoundary, null, _jsx(Component));
145
- reactRoot.render(element);
146
264
  }
147
265
  return render;
148
266
  }
@@ -152,20 +270,13 @@ function createMainScript({ uid }) {
152
270
  const _createModule = ${createModule.toString()};
153
271
  const _createRenderer = ${createRenderer.toString()};
154
272
 
155
- const getModuleUrl = (m) => window.parent.__devjar__[globalThis.uid].getModuleUrl(m)
273
+ const resolveModule = (specifier) => window.parent.__devjar__[globalThis.uid].resolveModule(specifier)
156
274
 
157
275
  globalThis.uid = ${JSON.stringify(uid)};
158
- globalThis.__render__ = _createRenderer(_createModule, getModuleUrl);
276
+ globalThis.__render__ = _createRenderer(_createModule, resolveModule);
159
277
  `;
160
278
  return code;
161
279
  }
162
- function createEsShimOptionsScript() {
163
- return `\
164
- window.esmsInitOptions = {
165
- polyfillEnable: ['css-modules', 'json-modules'],
166
- onerror: console.error,
167
- }`;
168
- }
169
280
  function useScript() {
170
281
  return useRef(typeof window !== 'undefined' ? document.createElement('script') : null);
171
282
  }
@@ -180,28 +291,45 @@ function createScript(scriptRef, { content, src, type } = {}) {
180
291
  }
181
292
  return script;
182
293
  }
183
- function useLiveCode({ getModuleUrl }) {
294
+ function useLiveCode({ resolveModule, tailwindSrc = defaultTailwindSrc }) {
184
295
  const iframeRef = useRef(null);
185
296
  const [error, setError] = useState();
186
297
  const rerender = useState({})[1];
187
298
  const appScriptRef = useScript();
188
- const esShimOptionsScriptRef = useScript();
189
299
  const tailwindcssScriptRef = useScript();
300
+ const transformWorkerRef = useRef(undefined);
301
+ const transformCacheRef = useRef(new Map());
302
+ const transformRequestsRef = useRef(new Map());
303
+ const transformRequestIdRef = useRef(0);
304
+ const loadIdRef = useRef(0);
190
305
  const uid = useId();
191
- // Let getModuleUrl executed on parent window side since it might involve
306
+ // Let resolveModule execute on parent window side since it might involve
192
307
  // variables that iframe cannot access.
193
308
  useEffect(()=>{
194
309
  if (!globalThis.__devjar__) {
195
310
  globalThis.__devjar__ = {};
196
311
  }
197
312
  globalThis.__devjar__[uid] = {
198
- getModuleUrl
313
+ resolveModule
199
314
  };
200
315
  return ()=>{
201
316
  if (globalThis.__devjar__) {
202
317
  delete globalThis.__devjar__[uid];
203
318
  }
204
319
  };
320
+ }, [
321
+ resolveModule,
322
+ uid
323
+ ]);
324
+ useEffect(()=>{
325
+ return ()=>{
326
+ transformWorkerRef.current?.terminate();
327
+ transformWorkerRef.current = undefined;
328
+ for (const { reject } of transformRequestsRef.current.values()){
329
+ reject(new Error('devjar: transform worker was terminated'));
330
+ }
331
+ transformRequestsRef.current.clear();
332
+ };
205
333
  }, []);
206
334
  useEffect(()=>{
207
335
  const iframe = iframeRef.current;
@@ -213,72 +341,126 @@ function useLiveCode({ getModuleUrl }) {
213
341
  const appScriptContent = createMainScript({
214
342
  uid
215
343
  });
216
- const scriptOptionsContent = createEsShimOptionsScript();
217
- const esmShimOptionsScript = createScript(esShimOptionsScriptRef, {
218
- content: scriptOptionsContent
219
- });
220
344
  const appScript = createScript(appScriptRef, {
221
- content: appScriptContent,
222
- type: 'module'
223
- });
224
- const tailwindScript = createScript(tailwindcssScriptRef, {
225
- src: 'https://unpkg.com/@tailwindcss/browser@4'
345
+ content: appScriptContent
226
346
  });
347
+ const tailwindScript = tailwindSrc ? createScript(tailwindcssScriptRef, {
348
+ src: tailwindSrc
349
+ }) : null;
227
350
  body.appendChild(div);
228
- body.appendChild(esmShimOptionsScript);
229
351
  body.appendChild(appScript);
230
- body.appendChild(tailwindScript);
352
+ if (tailwindScript) body.appendChild(tailwindScript);
231
353
  return ()=>{
232
354
  if (!iframe || !iframe.contentDocument) return;
233
355
  body.removeChild(div);
234
- body.removeChild(esmShimOptionsScript);
235
356
  body.removeChild(appScript);
236
- body.removeChild(tailwindScript);
357
+ if (tailwindScript) body.removeChild(tailwindScript);
237
358
  };
238
359
  }, []);
360
+ const transformFiles = useCallback((files)=>{
361
+ if (!resolveModule) {
362
+ return Promise.reject(new Error('devjar: resolveModule is required for the browser transformer'));
363
+ }
364
+ if (!transformWorkerRef.current) {
365
+ const worker = createTransformWorker();
366
+ worker.onmessage = ({ data })=>{
367
+ const request = transformRequestsRef.current.get(data.id);
368
+ if (!request) return;
369
+ transformRequestsRef.current.delete(data.id);
370
+ if (data.error) {
371
+ const error = new Error(data.error.message);
372
+ if (data.error.stack) error.stack = data.error.stack;
373
+ request.reject(error);
374
+ } else {
375
+ request.resolve(data.transformed);
376
+ }
377
+ };
378
+ worker.onerror = (event)=>{
379
+ const error = new Error(event.message || 'devjar: transform worker failed');
380
+ for (const { reject } of transformRequestsRef.current.values())reject(error);
381
+ transformRequestsRef.current.clear();
382
+ };
383
+ transformWorkerRef.current = worker;
384
+ }
385
+ const id = ++transformRequestIdRef.current;
386
+ const worker = transformWorkerRef.current;
387
+ return new Promise((resolve, reject)=>{
388
+ transformRequestsRef.current.set(id, {
389
+ resolve,
390
+ reject
391
+ });
392
+ worker.postMessage({
393
+ id,
394
+ files,
395
+ moduleUrl: resolveModule('oxc-transform')
396
+ });
397
+ });
398
+ }, [
399
+ resolveModule
400
+ ]);
239
401
  const load = useCallback(async (files)=>{
402
+ const loadId = ++loadIdRef.current;
240
403
  if (!esModuleLexerInit) {
241
404
  await init;
242
405
  esModuleLexerInit = true;
243
406
  }
244
407
  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');
408
+ const localModules = new Set(Object.keys(files).map(getModuleKey));
250
409
  try {
410
+ const filesToTransform = Object.fromEntries(Object.entries(files).filter(([filename, source])=>{
411
+ return !filename.endsWith('.css') && transformCacheRef.current.get(filename)?.source !== source;
412
+ }));
413
+ const newTransforms = Object.keys(filesToTransform).length ? await transformFiles(filesToTransform) : {};
414
+ if (loadId !== loadIdRef.current) return;
415
+ for (const [filename, code] of Object.entries(newTransforms)){
416
+ transformCacheRef.current.set(filename, {
417
+ source: files[filename],
418
+ code: code
419
+ });
420
+ }
421
+ for (const filename of transformCacheRef.current.keys()){
422
+ if (!(filename in files)) transformCacheRef.current.delete(filename);
423
+ }
251
424
  /**
252
425
  * transformedFiles
253
426
  * {
254
427
  * 'index.js': '...',
255
428
  * '@mod1': '...',
256
429
  * '@mod2': '...',
257
- */ const transformedFiles = Object.keys(files).reduce((res, filename)=>{
430
+ */ const dependencies = {};
431
+ const transformedFiles = Object.keys(files).reduce((res, filename)=>{
258
432
  // 1. Remove ./
259
433
  // 2. For non css files, remove extension
260
434
  // e.g. './styles.css' -> '@styles.css'
261
435
  // e.g. './foo.js' -> '@foo'
262
- const moduleKey = isRelative(filename) ? '@' + filename.slice(2) : filename;
436
+ const moduleKey = getModuleKey(filename);
263
437
  if (filename.endsWith('.css')) {
264
438
  res[moduleKey] = files[filename];
439
+ dependencies[moduleKey] = [];
265
440
  } else {
266
441
  // JS or TS files
267
- const normalizedModuleKey = removeExtension(moduleKey);
268
- res[normalizedModuleKey] = transformCode(files[filename], getModuleUrl, overrideExternals);
442
+ const transformed = replaceImports(transformCacheRef.current.get(filename).code, filename, moduleKey, resolveModule, localModules);
443
+ res[moduleKey] = transformed.code;
444
+ dependencies[moduleKey] = transformed.dependencies;
269
445
  }
270
446
  return res;
271
447
  }, {});
272
448
  const iframe = iframeRef.current;
273
449
  const script = appScriptRef.current;
274
450
  if (iframe) {
451
+ const renderFiles = async ()=>{
452
+ await iframe.contentWindow.__render__(transformedFiles, dependencies);
453
+ if (loadId === loadIdRef.current) {
454
+ iframe.dispatchEvent(new CustomEvent('devjar:render'));
455
+ }
456
+ };
275
457
  const render = iframe.contentWindow.__render__;
276
458
  if (render) {
277
- render(transformedFiles);
459
+ await renderFiles();
278
460
  } else {
279
461
  // if render is not loaded yet, wait until it's loaded
280
462
  script.onload = ()=>{
281
- iframe.contentWindow.__render__(transformedFiles).catch((err)=>{
463
+ renderFiles().catch((err)=>{
282
464
  setError(err);
283
465
  });
284
466
  };
@@ -291,7 +473,10 @@ function useLiveCode({ getModuleUrl }) {
291
473
  }
292
474
  }
293
475
  rerender({});
294
- }, []);
476
+ }, [
477
+ resolveModule,
478
+ transformFiles
479
+ ]);
295
480
  return {
296
481
  ref: iframeRef,
297
482
  error,
@@ -300,11 +485,15 @@ function useLiveCode({ getModuleUrl }) {
300
485
  }
301
486
 
302
487
  const defaultOnError = typeof window !== 'undefined' ? console.error : ()=>{};
303
- function DevJar({ files, getModuleUrl, onError = defaultOnError, ...props }) {
488
+ function DevJar({ files, resolveModule, onError = defaultOnError, tailwindSrc, ref: forwardedRef, ...props }) {
304
489
  const onErrorRef = useRef(onError);
305
490
  const { ref, error, load } = useLiveCode({
306
- getModuleUrl
491
+ resolveModule,
492
+ tailwindSrc
307
493
  });
494
+ useImperativeHandle(forwardedRef, ()=>ref.current, [
495
+ ref
496
+ ]);
308
497
  useEffect(()=>{
309
498
  onErrorRef.current(error);
310
499
  }, [
@@ -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,53 @@
1
1
  {
2
2
  "name": "devjar",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
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",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/huozhi/devjar"
15
+ },
11
16
  "files": [
12
17
  "dist"
13
18
  ],
14
19
  "types": "./dist/index.d.ts",
20
+ "scripts": {
21
+ "build": "pnpm run build:lib && pnpm run build:worker",
22
+ "build:lib": "bunchee",
23
+ "build:worker": "bun build src/transform-worker.ts --outfile dist/transform-worker.js --target browser --format esm",
24
+ "prepublishOnly": "pnpm run build",
25
+ "build:site": "pnpm run build && next build ./site",
26
+ "start": "next start ./site",
27
+ "dev": "pnpm run build && (pnpm run dev:lib & pnpm run dev:worker & pnpm run dev:site & wait)",
28
+ "dev:lib": "bunchee --watch",
29
+ "dev:worker": "bun build src/transform-worker.ts --outfile dist/transform-worker.js --target browser --format esm --watch",
30
+ "dev:site": "next dev ./site"
31
+ },
15
32
  "peerDependencies": {
16
- "react": "^18.2.0 || ^19.0.0"
33
+ "react": "^19.0.0"
17
34
  },
18
35
  "dependencies": {
19
- "dedent": "^1.7.0",
20
- "es-module-lexer": "1.6.0",
21
- "es-module-shims": "2.0.3",
22
- "sucrase": "3.35.0"
36
+ "es-module-lexer": "1.6.0"
23
37
  },
24
38
  "devDependencies": {
25
39
  "@types/node": "^22.10.7",
26
40
  "@types/react": "^19.0.7",
27
41
  "@types/react-dom": "^19.0.3",
28
- "bunchee": "^6.3.3",
29
- "codice": "^1.5.4",
42
+ "bunchee": "^6.11.0",
43
+ "codice": "^1.6.0",
44
+ "dedent": "^1.7.0",
30
45
  "devjar": "link:",
31
- "next": "^16.0.3",
32
- "react": "^19.2.0",
33
- "react-dom": "^19.2.0",
46
+ "next": "16.3.0-canary.69",
47
+ "oxc-transform": "^0.137.0",
48
+ "react": "^19.2.1",
49
+ "react-dom": "^19.2.1",
34
50
  "typescript": "^5.7.3"
35
51
  },
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
- }
52
+ "packageManager": "pnpm@11.9.0"
53
+ }
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 };