devjar 0.6.0 → 0.8.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,25 +1,23 @@
1
1
  # devjar
2
2
  > live code runtime for your react project in browser
3
3
 
4
+ ![image](https://repository-images.githubusercontent.com/483779830/55bf67ee-fcc6-4a12-ad0c-5221a5f78c26)
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 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
 
@@ -29,6 +27,7 @@ pnpm add devjar
29
27
  * `getModuleUrl`: A function that maps module names to CDN URLs.
30
28
  * `onError`: Callback function of error event from the iframe sandbox. By default `console.log`.
31
29
 
30
+ **Example**
32
31
 
33
32
  ```jsx
34
33
  import { DevJar } from 'devjar'
@@ -51,7 +50,9 @@ function App() {
51
50
  }
52
51
  ```
53
52
 
54
- #### `useLiveCode(options)`
53
+ ### `useLiveCode(options)`
54
+
55
+ A hook that provides lower-level control over the live code execution environment.
55
56
 
56
57
  **Parameters**
57
58
 
@@ -65,6 +66,8 @@ function App() {
65
66
  * `error`: An error message in case the live coding encounters an issue.
66
67
  * `load(codeFiles)`: void: Loads code files and executes them as live code.
67
68
 
69
+ **Example**
70
+
68
71
  ```jsx
69
72
  import { useLiveCode } from 'devjar'
70
73
 
@@ -103,7 +106,77 @@ function Playground() {
103
106
  }
104
107
  ```
105
108
 
106
- ### License
109
+ ## GLSL Shader Runtime
107
110
 
108
- The MIT License (MIT).
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
+ })
109
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
+ ## License
181
+
182
+ The MIT License (MIT).
package/dist/gl.js ADDED
@@ -0,0 +1,214 @@
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 };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
+ export { UseGlslRendererOptions, useGL } from './gl.tsx';
3
4
 
4
5
  declare global {
5
6
  interface Window {
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import { useRef, useState, useId, useEffect, useCallback } from 'react';
2
2
  import { transform } from 'sucrase';
3
3
  import { init, parse } from 'es-module-lexer';
4
4
  import { jsx } from 'react/jsx-runtime';
5
+ export { useGL } from './gl.js';
5
6
 
6
7
  // declare esmsInitOptions on global window
7
8
  async function createModule(files, { getModuleUrl }) {
@@ -41,11 +42,12 @@ async function createModule(files, { getModuleUrl }) {
41
42
  createInlinedModule(code, fileName.endsWith('.css') ? 'css' : 'js')
42
43
  ]));
43
44
  updateImportMap(imports);
44
- return self.importShim('index.js');
45
+ return self.importShim('index');
45
46
  }
46
47
 
47
48
  let esModuleLexerInit;
48
49
  const isRelative = (s)=>s.startsWith('./');
50
+ const removeExtension = (str)=>str.replace(/\.[^/.]+$/, '');
49
51
  function transformCode(_code, getModuleUrl, externals) {
50
52
  const code = transform(_code, {
51
53
  transforms: [
@@ -105,6 +107,7 @@ function replaceImports(source, getModuleUrl, externals) {
105
107
  }
106
108
  return code;
107
109
  }
110
+ // createRenderer is going to be stringified and executed in the iframe
108
111
  function createRenderer(createModule_, getModuleUrl) {
109
112
  let reactRoot;
110
113
  async function render(files) {
@@ -160,7 +163,7 @@ function createEsShimOptionsScript() {
160
163
  return `\
161
164
  window.esmsInitOptions = {
162
165
  polyfillEnable: ['css-modules', 'json-modules'],
163
- onerror: error => console.log(error),
166
+ onerror: console.error,
164
167
  }`;
165
168
  }
166
169
  function useScript() {
@@ -219,7 +222,7 @@ function useLiveCode({ getModuleUrl }) {
219
222
  type: 'module'
220
223
  });
221
224
  const tailwindScript = createScript(tailwindcssScriptRef, {
222
- src: 'https://cdn.tailwindcss.com'
225
+ src: 'https://unpkg.com/@tailwindcss/browser@4'
223
226
  });
224
227
  body.appendChild(div);
225
228
  body.appendChild(esmShimOptionsScript);
@@ -252,11 +255,17 @@ function useLiveCode({ getModuleUrl }) {
252
255
  * '@mod1': '...',
253
256
  * '@mod2': '...',
254
257
  */ const transformedFiles = Object.keys(files).reduce((res, filename)=>{
255
- const key = isRelative(filename) ? '@' + filename.slice(2) : filename;
258
+ // 1. Remove ./
259
+ // 2. For non css files, remove extension
260
+ // e.g. './styles.css' -> '@styles.css'
261
+ // e.g. './foo.js' -> '@foo'
262
+ const moduleKey = isRelative(filename) ? '@' + filename.slice(2) : filename;
256
263
  if (filename.endsWith('.css')) {
257
- res[key] = files[filename];
264
+ res[moduleKey] = files[filename];
258
265
  } else {
259
- res[key] = transformCode(files[filename], getModuleUrl, overrideExternals);
266
+ // JS or TS files
267
+ const normalizedModuleKey = removeExtension(moduleKey);
268
+ res[normalizedModuleKey] = transformCode(files[filename], getModuleUrl, overrideExternals);
260
269
  }
261
270
  return res;
262
271
  }, {});
@@ -269,13 +278,15 @@ function useLiveCode({ getModuleUrl }) {
269
278
  } else {
270
279
  // if render is not loaded yet, wait until it's loaded
271
280
  script.onload = ()=>{
272
- iframe.contentWindow.__render__(transformedFiles);
281
+ iframe.contentWindow.__render__(transformedFiles).catch((err)=>{
282
+ setError(err);
283
+ });
273
284
  };
274
285
  }
275
286
  }
276
287
  setError(undefined);
277
288
  } catch (e) {
278
- console.error(e);
289
+ console.warn(e);
279
290
  setError(e);
280
291
  }
281
292
  }
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "devjar",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js",
7
+ "./gl": "./dist/gl.js",
7
8
  "./package.json": "./package.json"
8
9
  },
9
10
  "license": "MIT",
@@ -11,17 +12,11 @@
11
12
  "dist"
12
13
  ],
13
14
  "types": "./dist/index.d.ts",
14
- "scripts": {
15
- "build": "bunchee",
16
- "prepublishOnly": "pnpm run build",
17
- "build:site": "pnpm run build && next build ./site",
18
- "start": "next start ./site",
19
- "dev": "next dev ./site"
20
- },
21
15
  "peerDependencies": {
22
16
  "react": "^18.2.0 || ^19.0.0"
23
17
  },
24
18
  "dependencies": {
19
+ "dedent": "^1.7.0",
25
20
  "es-module-lexer": "1.6.0",
26
21
  "es-module-shims": "2.0.3",
27
22
  "sucrase": "3.35.0"
@@ -30,14 +25,18 @@
30
25
  "@types/node": "^22.10.7",
31
26
  "@types/react": "^19.0.7",
32
27
  "@types/react-dom": "^19.0.3",
33
- "bunchee": "^6.3.2",
34
- "codice": "^0.4.4",
35
- "devjar": "link:./",
36
- "next": "^15.1.5",
37
- "react": "^19.0.0",
38
- "react-dom": "^19.0.0",
39
- "sugar-high": "^0.8.2",
28
+ "bunchee": "^6.3.3",
29
+ "codice": "^1.5.4",
30
+ "devjar": "link:",
31
+ "next": "^16.0.3",
32
+ "react": "^19.2.0",
33
+ "react-dom": "^19.2.0",
40
34
  "typescript": "^5.7.3"
41
35
  },
42
- "packageManager": "pnpm@9.15.4"
43
- }
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
+ }