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 +12 -81
- package/dist/index.d.ts +9 -18
- package/dist/index.js +324 -135
- package/dist/transform-worker.js +54 -0
- package/package.json +32 -21
- package/dist/gl.js +0 -214
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# devjar
|
|
2
|
-
> live
|
|
2
|
+
> react live preview in browser
|
|
3
3
|
|
|
4
|
-

|
|
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
|
-
* `
|
|
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
|
-
|
|
46
|
-
return `${CDN_HOST}/${
|
|
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
|
-
* `
|
|
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
|
-
|
|
79
|
-
return `https://cdn.skypack.dev/${
|
|
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
|
-
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
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:
|
|
9
|
+
load: (files: Record<string, string>) => Promise<void>;
|
|
21
10
|
};
|
|
22
11
|
|
|
23
|
-
declare function DevJar({ files,
|
|
12
|
+
declare function DevJar({ files, resolveModule, onError, tailwindSrc, ref: forwardedRef, ...props }: {
|
|
24
13
|
files: Record<string, string>;
|
|
25
|
-
|
|
14
|
+
resolveModule?: (specifier: string) => string;
|
|
26
15
|
onError?: (...data: any[]) => void;
|
|
27
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
25
|
+
return rewritten;
|
|
34
26
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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
|
-
|
|
143
|
+
const dependencies = [];
|
|
67
144
|
// start, end, statementStart, statementEnd, assertion, name
|
|
68
145
|
imports.forEach(({ s, e, ss, se, a, n })=>{
|
|
69
|
-
|
|
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 (
|
|
150
|
+
if (localModuleKey && localModuleKey.endsWith('.css')) {
|
|
73
151
|
// Map './styles.css' -> '@styles.css', and collect it
|
|
74
|
-
|
|
75
|
-
cssImports.push(cssPath);
|
|
152
|
+
cssImports.push(localModuleKey);
|
|
76
153
|
} else {
|
|
77
154
|
code += source.substring(ss, s);
|
|
78
|
-
code +=
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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_,
|
|
192
|
+
function createRenderer(createModule_, resolveModule) {
|
|
112
193
|
let reactRoot;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
118
|
-
const ReactDOMMod = await
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
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,
|
|
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({
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
268
|
-
res[
|
|
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
|
-
|
|
459
|
+
await renderFiles();
|
|
278
460
|
} else {
|
|
279
461
|
// if render is not loaded yet, wait until it's loaded
|
|
280
462
|
script.onload = ()=>{
|
|
281
|
-
|
|
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,
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
|
-
".":
|
|
7
|
-
|
|
8
|
-
|
|
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": "^
|
|
33
|
+
"react": "^19.0.0"
|
|
17
34
|
},
|
|
18
35
|
"dependencies": {
|
|
19
|
-
"
|
|
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.
|
|
29
|
-
"codice": "^1.
|
|
42
|
+
"bunchee": "^6.11.0",
|
|
43
|
+
"codice": "^1.6.0",
|
|
44
|
+
"dedent": "^1.7.0",
|
|
30
45
|
"devjar": "link:",
|
|
31
|
-
"next": "
|
|
32
|
-
"
|
|
33
|
-
"react
|
|
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
|
-
"
|
|
37
|
-
|
|
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 };
|