devjar 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -81
- package/dist/index.d.ts +9 -18
- package/dist/index.js +326 -132
- package/dist/transform-worker.js +54 -0
- package/package.json +28 -20
- 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,166 @@
|
|
|
1
|
-
import { useRef, useState, useId, useEffect, useCallback } from 'react';
|
|
2
|
-
import { transform } from 'sucrase';
|
|
1
|
+
import { useRef, useState, useId, useEffect, useCallback, useImperativeHandle } from 'react';
|
|
3
2
|
import { init, parse } from 'es-module-lexer';
|
|
4
3
|
import { jsx } from 'react/jsx-runtime';
|
|
5
|
-
export { useGL } from './gl.js';
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
mapOverrides: true
|
|
16
|
-
};
|
|
17
|
-
shim = import(/* webpackIgnore: true */ getModuleUrl('es-module-shims'));
|
|
18
|
-
await shim;
|
|
5
|
+
async function createModule(files, { resolveModule, dependencies = {}, runtime = {} }) {
|
|
6
|
+
var _runtime, _runtime1;
|
|
7
|
+
const localImportPrefix = '__DEVJAR_LOCAL_IMPORT__';
|
|
8
|
+
function createLocalImportPlaceholder(moduleKey) {
|
|
9
|
+
return `${localImportPrefix}${encodeURIComponent(moduleKey)}__`;
|
|
10
|
+
}
|
|
11
|
+
function createScopedSpecifier(moduleKey) {
|
|
12
|
+
return `devjar-internal/${runtime.revision}/${encodeURIComponent(moduleKey)}`;
|
|
19
13
|
}
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
imports['react-dom'] = getModuleUrl('react-dom');
|
|
23
|
-
imports['react-dom/client'] = getModuleUrl('react-dom/client');
|
|
14
|
+
function registerImportMap(imports) {
|
|
15
|
+
var _runtime;
|
|
24
16
|
const script = document.createElement('script');
|
|
25
|
-
script.type = 'importmap
|
|
26
|
-
script.
|
|
17
|
+
script.type = 'importmap';
|
|
18
|
+
script.textContent = JSON.stringify({
|
|
27
19
|
imports
|
|
28
20
|
});
|
|
29
|
-
document.
|
|
30
|
-
|
|
31
|
-
currentImportMap.parentNode.removeChild(currentImportMap);
|
|
32
|
-
}
|
|
33
|
-
currentImportMap = script;
|
|
21
|
+
document.head.appendChild(script);
|
|
22
|
+
((_runtime = runtime).importMaps || (_runtime.importMaps = [])).push(script);
|
|
34
23
|
}
|
|
35
|
-
function createInlinedModule(code
|
|
36
|
-
if (type === 'css') return `data:text/css;utf-8,${encodeURIComponent(code)}`;
|
|
24
|
+
function createInlinedModule(code) {
|
|
37
25
|
return `data:text/javascript;utf-8,${encodeURIComponent(code)}`;
|
|
38
26
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
27
|
+
function createCssModule(code, fileName) {
|
|
28
|
+
return `\
|
|
29
|
+
const sheet = new CSSStyleSheet();
|
|
30
|
+
sheet.replaceSync(${JSON.stringify(code)});
|
|
31
|
+
export default sheet;
|
|
32
|
+
//# sourceURL=devjar/${fileName}?v=${runtime.revision}`;
|
|
33
|
+
}
|
|
34
|
+
function rewriteLocalImports(code, specifiers) {
|
|
35
|
+
let rewritten = code;
|
|
36
|
+
for (const [moduleKey, specifier] of Object.entries(specifiers)){
|
|
37
|
+
rewritten = rewritten.split(createLocalImportPlaceholder(moduleKey)).join(specifier);
|
|
38
|
+
}
|
|
39
|
+
return rewritten;
|
|
40
|
+
}
|
|
41
|
+
(_runtime = runtime).files || (_runtime.files = {});
|
|
42
|
+
(_runtime1 = runtime).urls || (_runtime1.urls = {});
|
|
43
|
+
runtime.revision = (runtime.revision || 0) + 1;
|
|
44
|
+
const changedModules = new Set();
|
|
45
|
+
for (const [fileName, code] of Object.entries(files)){
|
|
46
|
+
if (runtime.files[fileName] !== code) changedModules.add(fileName);
|
|
47
|
+
}
|
|
48
|
+
for (const fileName of Object.keys(runtime.files)){
|
|
49
|
+
if (!(fileName in files)) changedModules.add(fileName);
|
|
50
|
+
}
|
|
51
|
+
const importers = {};
|
|
52
|
+
for (const [importer, importedModules] of Object.entries(dependencies)){
|
|
53
|
+
for (const imported of importedModules){
|
|
54
|
+
var _importers, _imported;
|
|
55
|
+
((_importers = importers)[_imported = imported] || (_importers[_imported] = [])).push(importer);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const invalidated = new Set(changedModules);
|
|
59
|
+
const queue = [
|
|
60
|
+
...changedModules
|
|
61
|
+
];
|
|
62
|
+
while(queue.length){
|
|
63
|
+
const changed = queue.shift();
|
|
64
|
+
for (const importer of importers[changed] || []){
|
|
65
|
+
if (invalidated.has(importer)) continue;
|
|
66
|
+
invalidated.add(importer);
|
|
67
|
+
queue.push(importer);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const moduleKeys = new Set(Object.keys(files));
|
|
71
|
+
for (const importedModules of Object.values(dependencies)){
|
|
72
|
+
for (const moduleKey of importedModules)moduleKeys.add(moduleKey);
|
|
73
|
+
}
|
|
74
|
+
const scopedSpecifiers = {};
|
|
75
|
+
for (const moduleKey of moduleKeys){
|
|
76
|
+
scopedSpecifiers[moduleKey] = createScopedSpecifier(moduleKey);
|
|
77
|
+
}
|
|
78
|
+
const imports = {};
|
|
79
|
+
for (const moduleKey of moduleKeys){
|
|
80
|
+
if (!(moduleKey in files)) {
|
|
81
|
+
imports[scopedSpecifiers[moduleKey]] = createInlinedModule(`throw new Error(${JSON.stringify('devjar: Module not found: ' + moduleKey)})`);
|
|
82
|
+
delete runtime.urls[moduleKey];
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (!runtime.urls[moduleKey] || invalidated.has(moduleKey)) {
|
|
86
|
+
const moduleCode = moduleKey.endsWith('.css') ? createCssModule(files[moduleKey], moduleKey) : rewriteLocalImports(`${files[moduleKey]}\n//# sourceURL=devjar/${moduleKey}?v=${runtime.revision}`, scopedSpecifiers);
|
|
87
|
+
runtime.urls[moduleKey] = createInlinedModule(moduleCode);
|
|
88
|
+
}
|
|
89
|
+
imports[scopedSpecifiers[moduleKey]] = runtime.urls[moduleKey];
|
|
90
|
+
}
|
|
91
|
+
registerImportMap(imports);
|
|
92
|
+
if (!runtime.refreshRuntime) {
|
|
93
|
+
const refreshModule = await import(/* webpackIgnore: true */ /* @vite-ignore */ /* turbopackIgnore: true */ resolveModule('react-refresh/runtime'));
|
|
94
|
+
runtime.refreshRuntime = refreshModule.default || refreshModule;
|
|
95
|
+
runtime.refreshRuntime.injectIntoGlobalHook(self);
|
|
96
|
+
globalThis.__devjarRefreshRuntime = runtime.refreshRuntime;
|
|
97
|
+
}
|
|
98
|
+
const entrySpecifier = scopedSpecifiers['index'];
|
|
99
|
+
if (!entrySpecifier) {
|
|
100
|
+
throw new Error('devjar: Module not found: index');
|
|
101
|
+
}
|
|
102
|
+
const module = await import(/* webpackIgnore: true */ /* @vite-ignore */ /* turbopackIgnore: true */ entrySpecifier);
|
|
103
|
+
runtime.files = {
|
|
104
|
+
...files
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
module,
|
|
108
|
+
changed: changedModules.size > 0
|
|
109
|
+
};
|
|
46
110
|
}
|
|
47
111
|
|
|
48
112
|
let esModuleLexerInit;
|
|
49
113
|
const isRelative = (s)=>s.startsWith('./');
|
|
50
114
|
const removeExtension = (str)=>str.replace(/\.[^/.]+$/, '');
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
'typescript'
|
|
56
|
-
]
|
|
57
|
-
}).code;
|
|
58
|
-
return replaceImports(code, getModuleUrl, externals);
|
|
115
|
+
const localImportPrefix = '__DEVJAR_LOCAL_IMPORT__';
|
|
116
|
+
const defaultTailwindSrc = 'https://unpkg.com/@tailwindcss/browser@4';
|
|
117
|
+
function createLocalImportPlaceholder(moduleKey) {
|
|
118
|
+
return `${localImportPrefix}${encodeURIComponent(moduleKey)}__`;
|
|
59
119
|
}
|
|
60
|
-
function
|
|
120
|
+
function createTransformWorker() {
|
|
121
|
+
return new Worker(new URL('./transform-worker.js', import.meta.url), {
|
|
122
|
+
type: 'module',
|
|
123
|
+
name: 'devjar-transform'
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function getModuleKey(filename) {
|
|
127
|
+
const key = isRelative(filename) ? '@' + filename.slice(2) : filename;
|
|
128
|
+
return filename.endsWith('.css') ? key : removeExtension(key);
|
|
129
|
+
}
|
|
130
|
+
function resolveRelativeModule(importer, imported) {
|
|
131
|
+
const importerPath = importer.startsWith('./') ? importer.slice(2) : importer;
|
|
132
|
+
const parts = importerPath.split('/');
|
|
133
|
+
parts.pop();
|
|
134
|
+
for (const part of imported.split('/')){
|
|
135
|
+
if (!part || part === '.') continue;
|
|
136
|
+
if (part === '..') parts.pop();
|
|
137
|
+
else parts.push(part);
|
|
138
|
+
}
|
|
139
|
+
const path = parts.join('/');
|
|
140
|
+
return imported.endsWith('.css') ? '@' + path : removeExtension('@' + path);
|
|
141
|
+
}
|
|
142
|
+
function replaceImports(source, filename, moduleKey, resolveModule, localModules) {
|
|
61
143
|
let code = '';
|
|
62
144
|
let lastIndex = 0;
|
|
63
145
|
let hasReactImports = false;
|
|
64
146
|
const [imports] = parse(source);
|
|
65
147
|
const cssImports = [];
|
|
66
|
-
|
|
148
|
+
const dependencies = [];
|
|
67
149
|
// start, end, statementStart, statementEnd, assertion, name
|
|
68
150
|
imports.forEach(({ s, e, ss, se, a, n })=>{
|
|
69
|
-
|
|
70
|
-
;
|
|
151
|
+
if (!n) return;
|
|
152
|
+
code += source.slice(lastIndex, ss); // content from last import to beginning of this line
|
|
153
|
+
const localModuleKey = isRelative(n) ? resolveRelativeModule(filename, n) : localModules.has(n) ? n : undefined;
|
|
71
154
|
// handle imports
|
|
72
|
-
if (
|
|
155
|
+
if (localModuleKey && localModuleKey.endsWith('.css')) {
|
|
73
156
|
// Map './styles.css' -> '@styles.css', and collect it
|
|
74
|
-
|
|
75
|
-
cssImports.push(cssPath);
|
|
157
|
+
cssImports.push(localModuleKey);
|
|
76
158
|
} else {
|
|
77
159
|
code += source.substring(ss, s);
|
|
78
|
-
code +=
|
|
160
|
+
code += localModuleKey ? createLocalImportPlaceholder(localModuleKey) : resolveModule(n);
|
|
79
161
|
code += source.substring(e, se);
|
|
80
162
|
}
|
|
163
|
+
if (localModuleKey) dependencies.push(localModuleKey);
|
|
81
164
|
lastIndex = se;
|
|
82
165
|
if (n === 'react') {
|
|
83
166
|
const statement = source.slice(ss, se);
|
|
@@ -85,64 +168,104 @@ function replaceImports(source, getModuleUrl, externals) {
|
|
|
85
168
|
hasReactImports = true;
|
|
86
169
|
}
|
|
87
170
|
}
|
|
88
|
-
cssImports.forEach((cssPath)=>{
|
|
89
|
-
code += `\nimport sheet${cssImportIndex} from "${cssPath}" assert { type: "css" };\n`;
|
|
90
|
-
cssImportIndex++;
|
|
91
|
-
});
|
|
92
171
|
});
|
|
93
172
|
if (cssImports.length) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
173
|
+
cssImports.forEach((cssPath, index)=>{
|
|
174
|
+
code += `\nimport __devjarSheet${index} from "${createLocalImportPlaceholder(cssPath)}";\n`;
|
|
175
|
+
});
|
|
176
|
+
code += `globalThis.__devjarStyleSheets ||= new Map();\n`;
|
|
177
|
+
cssImports.forEach((cssPath, index)=>{
|
|
178
|
+
code += `{ const previous = globalThis.__devjarStyleSheets.get(${JSON.stringify(cssPath)});\n`;
|
|
179
|
+
code += `const sheets = [...document.adoptedStyleSheets];\n`;
|
|
180
|
+
code += `const sheetIndex = sheets.indexOf(previous);\n`;
|
|
181
|
+
code += `if (sheetIndex < 0) sheets.push(__devjarSheet${index}); else sheets[sheetIndex] = __devjarSheet${index};\n`;
|
|
182
|
+
code += `document.adoptedStyleSheets = sheets;\n`;
|
|
183
|
+
code += `globalThis.__devjarStyleSheets.set(${JSON.stringify(cssPath)}, __devjarSheet${index}); }\n`;
|
|
184
|
+
});
|
|
103
185
|
}
|
|
104
186
|
code += source.substring(lastIndex);
|
|
105
187
|
if (!hasReactImports) {
|
|
106
|
-
code = `import React from 'react';\n${code}`;
|
|
188
|
+
code = `import React from ${JSON.stringify(resolveModule('react'))};\n${code}`;
|
|
107
189
|
}
|
|
108
|
-
|
|
190
|
+
code = `const $RefreshReg$ = (type, id) => globalThis.__devjarRefreshRuntime.register(type, ${JSON.stringify(moduleKey + ' ')} + id);\n` + `const $RefreshSig$ = globalThis.__devjarRefreshRuntime.createSignatureFunctionForTransform;\n` + code;
|
|
191
|
+
return {
|
|
192
|
+
code,
|
|
193
|
+
dependencies
|
|
194
|
+
};
|
|
109
195
|
}
|
|
110
196
|
// createRenderer is going to be stringified and executed in the iframe
|
|
111
|
-
function createRenderer(createModule_,
|
|
197
|
+
function createRenderer(createModule_, resolveModule) {
|
|
112
198
|
let reactRoot;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
199
|
+
let ErrorBoundary;
|
|
200
|
+
let errorBoundary;
|
|
201
|
+
let revision = 0;
|
|
202
|
+
const moduleRuntime = {};
|
|
203
|
+
async function render(files, dependencies) {
|
|
204
|
+
const result = await createModule_(files, {
|
|
205
|
+
resolveModule,
|
|
206
|
+
dependencies,
|
|
207
|
+
runtime: moduleRuntime
|
|
116
208
|
});
|
|
117
|
-
const ReactMod = await
|
|
118
|
-
const ReactDOMMod = await
|
|
209
|
+
const ReactMod = await import(/* webpackIgnore: true */ /* @vite-ignore */ /* turbopackIgnore: true */ resolveModule('react'));
|
|
210
|
+
const ReactDOMMod = await import(/* webpackIgnore: true */ /* @vite-ignore */ /* turbopackIgnore: true */ resolveModule('react-dom/client'));
|
|
119
211
|
const _jsx = ReactMod.createElement;
|
|
120
212
|
const root = document.getElementById('__reactRoot');
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
render() {
|
|
128
|
-
if (this.state.error) {
|
|
129
|
-
return _jsx('div', null, this.state.error?.message);
|
|
213
|
+
if (!ErrorBoundary) {
|
|
214
|
+
ErrorBoundary = class extends ReactMod.Component {
|
|
215
|
+
static getDerivedStateFromError(error) {
|
|
216
|
+
return {
|
|
217
|
+
error
|
|
218
|
+
};
|
|
130
219
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
220
|
+
reset() {
|
|
221
|
+
if (this.state.error) this.setState({
|
|
222
|
+
error: null
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
componentDidUpdate(previousProps) {
|
|
226
|
+
if (previousProps.revision !== this.props.revision && this.state.error) {
|
|
227
|
+
this.setState({
|
|
228
|
+
error: null
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
render() {
|
|
233
|
+
if (this.state.error) {
|
|
234
|
+
return _jsx('div', null, this.state.error?.message);
|
|
235
|
+
}
|
|
236
|
+
return this.props.children;
|
|
237
|
+
}
|
|
238
|
+
constructor(props){
|
|
239
|
+
super(props);
|
|
240
|
+
this.state = {
|
|
241
|
+
error: null
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
};
|
|
139
245
|
}
|
|
140
246
|
if (!reactRoot) {
|
|
141
247
|
reactRoot = ReactDOMMod.createRoot(root);
|
|
248
|
+
revision++;
|
|
249
|
+
reactRoot.render(_jsx(ErrorBoundary, {
|
|
250
|
+
revision,
|
|
251
|
+
ref: (value)=>errorBoundary = value
|
|
252
|
+
}, _jsx(result.module.default)));
|
|
253
|
+
moduleRuntime.hasRendered = true;
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (result.changed) {
|
|
257
|
+
errorBoundary?.reset();
|
|
258
|
+
const refreshRuntime = moduleRuntime.refreshRuntime;
|
|
259
|
+
const refreshUpdate = refreshRuntime.performReactRefresh();
|
|
260
|
+
const mountedRootCount = typeof refreshRuntime._getMountedRootCount === 'function' ? refreshRuntime._getMountedRootCount() : 0;
|
|
261
|
+
if (!refreshUpdate || mountedRootCount === 0) {
|
|
262
|
+
revision++;
|
|
263
|
+
reactRoot.render(_jsx(ErrorBoundary, {
|
|
264
|
+
revision,
|
|
265
|
+
ref: (value)=>errorBoundary = value
|
|
266
|
+
}, _jsx(result.module.default)));
|
|
267
|
+
}
|
|
142
268
|
}
|
|
143
|
-
const Component = mod.default;
|
|
144
|
-
const element = _jsx(ErrorBoundary, null, _jsx(Component));
|
|
145
|
-
reactRoot.render(element);
|
|
146
269
|
}
|
|
147
270
|
return render;
|
|
148
271
|
}
|
|
@@ -152,20 +275,13 @@ function createMainScript({ uid }) {
|
|
|
152
275
|
const _createModule = ${createModule.toString()};
|
|
153
276
|
const _createRenderer = ${createRenderer.toString()};
|
|
154
277
|
|
|
155
|
-
const
|
|
278
|
+
const resolveModule = (specifier) => window.parent.__devjar__[globalThis.uid].resolveModule(specifier)
|
|
156
279
|
|
|
157
280
|
globalThis.uid = ${JSON.stringify(uid)};
|
|
158
|
-
globalThis.__render__ = _createRenderer(_createModule,
|
|
281
|
+
globalThis.__render__ = _createRenderer(_createModule, resolveModule);
|
|
159
282
|
`;
|
|
160
283
|
return code;
|
|
161
284
|
}
|
|
162
|
-
function createEsShimOptionsScript() {
|
|
163
|
-
return `\
|
|
164
|
-
window.esmsInitOptions = {
|
|
165
|
-
polyfillEnable: ['css-modules', 'json-modules'],
|
|
166
|
-
onerror: console.error,
|
|
167
|
-
}`;
|
|
168
|
-
}
|
|
169
285
|
function useScript() {
|
|
170
286
|
return useRef(typeof window !== 'undefined' ? document.createElement('script') : null);
|
|
171
287
|
}
|
|
@@ -180,28 +296,45 @@ function createScript(scriptRef, { content, src, type } = {}) {
|
|
|
180
296
|
}
|
|
181
297
|
return script;
|
|
182
298
|
}
|
|
183
|
-
function useLiveCode({
|
|
299
|
+
function useLiveCode({ resolveModule, tailwindSrc = defaultTailwindSrc }) {
|
|
184
300
|
const iframeRef = useRef(null);
|
|
185
301
|
const [error, setError] = useState();
|
|
186
302
|
const rerender = useState({})[1];
|
|
187
303
|
const appScriptRef = useScript();
|
|
188
|
-
const esShimOptionsScriptRef = useScript();
|
|
189
304
|
const tailwindcssScriptRef = useScript();
|
|
305
|
+
const transformWorkerRef = useRef(undefined);
|
|
306
|
+
const transformCacheRef = useRef(new Map());
|
|
307
|
+
const transformRequestsRef = useRef(new Map());
|
|
308
|
+
const transformRequestIdRef = useRef(0);
|
|
309
|
+
const loadIdRef = useRef(0);
|
|
190
310
|
const uid = useId();
|
|
191
|
-
// Let
|
|
311
|
+
// Let resolveModule execute on parent window side since it might involve
|
|
192
312
|
// variables that iframe cannot access.
|
|
193
313
|
useEffect(()=>{
|
|
194
314
|
if (!globalThis.__devjar__) {
|
|
195
315
|
globalThis.__devjar__ = {};
|
|
196
316
|
}
|
|
197
317
|
globalThis.__devjar__[uid] = {
|
|
198
|
-
|
|
318
|
+
resolveModule
|
|
199
319
|
};
|
|
200
320
|
return ()=>{
|
|
201
321
|
if (globalThis.__devjar__) {
|
|
202
322
|
delete globalThis.__devjar__[uid];
|
|
203
323
|
}
|
|
204
324
|
};
|
|
325
|
+
}, [
|
|
326
|
+
resolveModule,
|
|
327
|
+
uid
|
|
328
|
+
]);
|
|
329
|
+
useEffect(()=>{
|
|
330
|
+
return ()=>{
|
|
331
|
+
transformWorkerRef.current?.terminate();
|
|
332
|
+
transformWorkerRef.current = undefined;
|
|
333
|
+
for (const { reject } of transformRequestsRef.current.values()){
|
|
334
|
+
reject(new Error('devjar: transform worker was terminated'));
|
|
335
|
+
}
|
|
336
|
+
transformRequestsRef.current.clear();
|
|
337
|
+
};
|
|
205
338
|
}, []);
|
|
206
339
|
useEffect(()=>{
|
|
207
340
|
const iframe = iframeRef.current;
|
|
@@ -213,72 +346,126 @@ function useLiveCode({ getModuleUrl }) {
|
|
|
213
346
|
const appScriptContent = createMainScript({
|
|
214
347
|
uid
|
|
215
348
|
});
|
|
216
|
-
const scriptOptionsContent = createEsShimOptionsScript();
|
|
217
|
-
const esmShimOptionsScript = createScript(esShimOptionsScriptRef, {
|
|
218
|
-
content: scriptOptionsContent
|
|
219
|
-
});
|
|
220
349
|
const appScript = createScript(appScriptRef, {
|
|
221
|
-
content: appScriptContent
|
|
222
|
-
type: 'module'
|
|
223
|
-
});
|
|
224
|
-
const tailwindScript = createScript(tailwindcssScriptRef, {
|
|
225
|
-
src: 'https://unpkg.com/@tailwindcss/browser@4'
|
|
350
|
+
content: appScriptContent
|
|
226
351
|
});
|
|
352
|
+
const tailwindScript = tailwindSrc ? createScript(tailwindcssScriptRef, {
|
|
353
|
+
src: tailwindSrc
|
|
354
|
+
}) : null;
|
|
227
355
|
body.appendChild(div);
|
|
228
|
-
body.appendChild(esmShimOptionsScript);
|
|
229
356
|
body.appendChild(appScript);
|
|
230
|
-
body.appendChild(tailwindScript);
|
|
357
|
+
if (tailwindScript) body.appendChild(tailwindScript);
|
|
231
358
|
return ()=>{
|
|
232
359
|
if (!iframe || !iframe.contentDocument) return;
|
|
233
360
|
body.removeChild(div);
|
|
234
|
-
body.removeChild(esmShimOptionsScript);
|
|
235
361
|
body.removeChild(appScript);
|
|
236
|
-
body.removeChild(tailwindScript);
|
|
362
|
+
if (tailwindScript) body.removeChild(tailwindScript);
|
|
237
363
|
};
|
|
238
364
|
}, []);
|
|
365
|
+
const transformFiles = useCallback((files)=>{
|
|
366
|
+
if (!resolveModule) {
|
|
367
|
+
return Promise.reject(new Error('devjar: resolveModule is required for the browser transformer'));
|
|
368
|
+
}
|
|
369
|
+
if (!transformWorkerRef.current) {
|
|
370
|
+
const worker = createTransformWorker();
|
|
371
|
+
worker.onmessage = ({ data })=>{
|
|
372
|
+
const request = transformRequestsRef.current.get(data.id);
|
|
373
|
+
if (!request) return;
|
|
374
|
+
transformRequestsRef.current.delete(data.id);
|
|
375
|
+
if (data.error) {
|
|
376
|
+
const error = new Error(data.error.message);
|
|
377
|
+
if (data.error.stack) error.stack = data.error.stack;
|
|
378
|
+
request.reject(error);
|
|
379
|
+
} else {
|
|
380
|
+
request.resolve(data.transformed);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
worker.onerror = (event)=>{
|
|
384
|
+
const error = new Error(event.message || 'devjar: transform worker failed');
|
|
385
|
+
for (const { reject } of transformRequestsRef.current.values())reject(error);
|
|
386
|
+
transformRequestsRef.current.clear();
|
|
387
|
+
};
|
|
388
|
+
transformWorkerRef.current = worker;
|
|
389
|
+
}
|
|
390
|
+
const id = ++transformRequestIdRef.current;
|
|
391
|
+
const worker = transformWorkerRef.current;
|
|
392
|
+
return new Promise((resolve, reject)=>{
|
|
393
|
+
transformRequestsRef.current.set(id, {
|
|
394
|
+
resolve,
|
|
395
|
+
reject
|
|
396
|
+
});
|
|
397
|
+
worker.postMessage({
|
|
398
|
+
id,
|
|
399
|
+
files,
|
|
400
|
+
moduleUrl: resolveModule('oxc-transform')
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
}, [
|
|
404
|
+
resolveModule
|
|
405
|
+
]);
|
|
239
406
|
const load = useCallback(async (files)=>{
|
|
407
|
+
const loadId = ++loadIdRef.current;
|
|
240
408
|
if (!esModuleLexerInit) {
|
|
241
409
|
await init;
|
|
242
410
|
esModuleLexerInit = true;
|
|
243
411
|
}
|
|
244
412
|
if (files) {
|
|
245
|
-
|
|
246
|
-
const overrideExternals = new Set(Object.keys(files).filter((name)=>!isRelative(name) && name !== 'index.js'));
|
|
247
|
-
// Always share react as externals
|
|
248
|
-
overrideExternals.add('react');
|
|
249
|
-
overrideExternals.add('react-dom');
|
|
413
|
+
const localModules = new Set(Object.keys(files).map(getModuleKey));
|
|
250
414
|
try {
|
|
415
|
+
const filesToTransform = Object.fromEntries(Object.entries(files).filter(([filename, source])=>{
|
|
416
|
+
return !filename.endsWith('.css') && transformCacheRef.current.get(filename)?.source !== source;
|
|
417
|
+
}));
|
|
418
|
+
const newTransforms = Object.keys(filesToTransform).length ? await transformFiles(filesToTransform) : {};
|
|
419
|
+
if (loadId !== loadIdRef.current) return;
|
|
420
|
+
for (const [filename, code] of Object.entries(newTransforms)){
|
|
421
|
+
transformCacheRef.current.set(filename, {
|
|
422
|
+
source: files[filename],
|
|
423
|
+
code: code
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
for (const filename of transformCacheRef.current.keys()){
|
|
427
|
+
if (!(filename in files)) transformCacheRef.current.delete(filename);
|
|
428
|
+
}
|
|
251
429
|
/**
|
|
252
430
|
* transformedFiles
|
|
253
431
|
* {
|
|
254
432
|
* 'index.js': '...',
|
|
255
433
|
* '@mod1': '...',
|
|
256
434
|
* '@mod2': '...',
|
|
257
|
-
*/ const
|
|
435
|
+
*/ const dependencies = {};
|
|
436
|
+
const transformedFiles = Object.keys(files).reduce((res, filename)=>{
|
|
258
437
|
// 1. Remove ./
|
|
259
438
|
// 2. For non css files, remove extension
|
|
260
439
|
// e.g. './styles.css' -> '@styles.css'
|
|
261
440
|
// e.g. './foo.js' -> '@foo'
|
|
262
|
-
const moduleKey =
|
|
441
|
+
const moduleKey = getModuleKey(filename);
|
|
263
442
|
if (filename.endsWith('.css')) {
|
|
264
443
|
res[moduleKey] = files[filename];
|
|
444
|
+
dependencies[moduleKey] = [];
|
|
265
445
|
} else {
|
|
266
446
|
// JS or TS files
|
|
267
|
-
const
|
|
268
|
-
res[
|
|
447
|
+
const transformed = replaceImports(transformCacheRef.current.get(filename).code, filename, moduleKey, resolveModule, localModules);
|
|
448
|
+
res[moduleKey] = transformed.code;
|
|
449
|
+
dependencies[moduleKey] = transformed.dependencies;
|
|
269
450
|
}
|
|
270
451
|
return res;
|
|
271
452
|
}, {});
|
|
272
453
|
const iframe = iframeRef.current;
|
|
273
454
|
const script = appScriptRef.current;
|
|
274
455
|
if (iframe) {
|
|
456
|
+
const renderFiles = async ()=>{
|
|
457
|
+
await iframe.contentWindow.__render__(transformedFiles, dependencies);
|
|
458
|
+
if (loadId === loadIdRef.current) {
|
|
459
|
+
iframe.dispatchEvent(new CustomEvent('devjar:render'));
|
|
460
|
+
}
|
|
461
|
+
};
|
|
275
462
|
const render = iframe.contentWindow.__render__;
|
|
276
463
|
if (render) {
|
|
277
|
-
|
|
464
|
+
await renderFiles();
|
|
278
465
|
} else {
|
|
279
466
|
// if render is not loaded yet, wait until it's loaded
|
|
280
467
|
script.onload = ()=>{
|
|
281
|
-
|
|
468
|
+
renderFiles().catch((err)=>{
|
|
282
469
|
setError(err);
|
|
283
470
|
});
|
|
284
471
|
};
|
|
@@ -291,7 +478,10 @@ function useLiveCode({ getModuleUrl }) {
|
|
|
291
478
|
}
|
|
292
479
|
}
|
|
293
480
|
rerender({});
|
|
294
|
-
}, [
|
|
481
|
+
}, [
|
|
482
|
+
resolveModule,
|
|
483
|
+
transformFiles
|
|
484
|
+
]);
|
|
295
485
|
return {
|
|
296
486
|
ref: iframeRef,
|
|
297
487
|
error,
|
|
@@ -300,11 +490,15 @@ function useLiveCode({ getModuleUrl }) {
|
|
|
300
490
|
}
|
|
301
491
|
|
|
302
492
|
const defaultOnError = typeof window !== 'undefined' ? console.error : ()=>{};
|
|
303
|
-
function DevJar({ files,
|
|
493
|
+
function DevJar({ files, resolveModule, onError = defaultOnError, tailwindSrc, ref: forwardedRef, ...props }) {
|
|
304
494
|
const onErrorRef = useRef(onError);
|
|
305
495
|
const { ref, error, load } = useLiveCode({
|
|
306
|
-
|
|
496
|
+
resolveModule,
|
|
497
|
+
tailwindSrc
|
|
307
498
|
});
|
|
499
|
+
useImperativeHandle(forwardedRef, ()=>ref.current, [
|
|
500
|
+
ref
|
|
501
|
+
]);
|
|
308
502
|
useEffect(()=>{
|
|
309
503
|
onErrorRef.current(error);
|
|
310
504
|
}, [
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// src/transform-worker.ts
|
|
2
|
+
var oxc;
|
|
3
|
+
var importModule = new Function("specifier", "return import(specifier)");
|
|
4
|
+
function getLang(filename) {
|
|
5
|
+
if (/\.[cm]?tsx?$/.test(filename))
|
|
6
|
+
return "tsx";
|
|
7
|
+
return "jsx";
|
|
8
|
+
}
|
|
9
|
+
function getTransformErrorMessage(errors) {
|
|
10
|
+
if (!errors?.length)
|
|
11
|
+
return "";
|
|
12
|
+
const error = errors.find((error2) => error2.severity === "Error");
|
|
13
|
+
if (!error)
|
|
14
|
+
return "";
|
|
15
|
+
return error.codeframe || error.message || "devjar: transform failed";
|
|
16
|
+
}
|
|
17
|
+
self.onmessage = async ({ data }) => {
|
|
18
|
+
const { id, moduleUrl, files } = data;
|
|
19
|
+
try {
|
|
20
|
+
if (!oxc) {
|
|
21
|
+
oxc = await importModule(moduleUrl);
|
|
22
|
+
}
|
|
23
|
+
const transformed = {};
|
|
24
|
+
for (const [filename, source] of Object.entries(files)) {
|
|
25
|
+
const output = oxc.transformSync(filename, source, {
|
|
26
|
+
lang: getLang(filename),
|
|
27
|
+
sourceType: "module",
|
|
28
|
+
target: "es2022",
|
|
29
|
+
decorator: {
|
|
30
|
+
legacy: true
|
|
31
|
+
},
|
|
32
|
+
jsx: {
|
|
33
|
+
runtime: "automatic",
|
|
34
|
+
development: true,
|
|
35
|
+
refresh: true
|
|
36
|
+
},
|
|
37
|
+
sourcemap: false
|
|
38
|
+
});
|
|
39
|
+
const errorMessage = getTransformErrorMessage(output.errors);
|
|
40
|
+
if (errorMessage)
|
|
41
|
+
throw new Error(errorMessage);
|
|
42
|
+
transformed[filename] = output.code;
|
|
43
|
+
}
|
|
44
|
+
self.postMessage({ id, transformed });
|
|
45
|
+
} catch (error) {
|
|
46
|
+
self.postMessage({
|
|
47
|
+
id,
|
|
48
|
+
error: {
|
|
49
|
+
message: error?.message || String(error),
|
|
50
|
+
stack: error?.stack
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
};
|
package/package.json
CHANGED
|
@@ -1,42 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devjar",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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",
|
|
11
12
|
"files": [
|
|
12
13
|
"dist"
|
|
13
14
|
],
|
|
14
15
|
"types": "./dist/index.d.ts",
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "pnpm run build:lib && pnpm run build:worker",
|
|
18
|
+
"build:lib": "bunchee",
|
|
19
|
+
"build:worker": "bun build src/transform-worker.ts --outfile dist/transform-worker.js --target browser --format esm",
|
|
20
|
+
"prepublishOnly": "pnpm run build",
|
|
21
|
+
"build:site": "pnpm run build && next build ./site",
|
|
22
|
+
"start": "next start ./site",
|
|
23
|
+
"dev": "pnpm run build && (pnpm run dev:lib & pnpm run dev:worker & pnpm run dev:site & wait)",
|
|
24
|
+
"dev:lib": "bunchee --watch",
|
|
25
|
+
"dev:worker": "bun build src/transform-worker.ts --outfile dist/transform-worker.js --target browser --format esm --watch",
|
|
26
|
+
"dev:site": "next dev ./site"
|
|
27
|
+
},
|
|
15
28
|
"peerDependencies": {
|
|
16
|
-
"react": "^
|
|
29
|
+
"react": "^19.0.0"
|
|
17
30
|
},
|
|
18
31
|
"dependencies": {
|
|
19
|
-
"dedent": "^1.7.0",
|
|
20
32
|
"es-module-lexer": "1.6.0",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
33
|
+
"oxc-transform": "^0.137.0",
|
|
34
|
+
"react-refresh": "^0.18.0"
|
|
23
35
|
},
|
|
24
36
|
"devDependencies": {
|
|
25
37
|
"@types/node": "^22.10.7",
|
|
26
38
|
"@types/react": "^19.0.7",
|
|
27
39
|
"@types/react-dom": "^19.0.3",
|
|
28
|
-
"bunchee": "^6.
|
|
29
|
-
"codice": "^1.
|
|
40
|
+
"bunchee": "^6.11.0",
|
|
41
|
+
"codice": "^1.6.0",
|
|
42
|
+
"dedent": "^1.7.0",
|
|
30
43
|
"devjar": "link:",
|
|
31
|
-
"next": "
|
|
32
|
-
"react": "^19.2.
|
|
33
|
-
"react-dom": "^19.2.
|
|
44
|
+
"next": "16.3.0-canary.69",
|
|
45
|
+
"react": "^19.2.1",
|
|
46
|
+
"react-dom": "^19.2.1",
|
|
34
47
|
"typescript": "^5.7.3"
|
|
35
48
|
},
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
"build:site": "pnpm run build && next build ./site",
|
|
39
|
-
"start": "next start ./site",
|
|
40
|
-
"dev": "next dev ./site"
|
|
41
|
-
}
|
|
42
|
-
}
|
|
49
|
+
"packageManager": "pnpm@11.9.0"
|
|
50
|
+
}
|
package/dist/gl.js
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
import { useRef, useEffect } from 'react';
|
|
2
|
-
|
|
3
|
-
// Default vertex shader - simple fullscreen quad
|
|
4
|
-
const DEFAULT_VERTEX_SHADER = `\
|
|
5
|
-
attribute vec2 a_position;
|
|
6
|
-
|
|
7
|
-
void main() {
|
|
8
|
-
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
9
|
-
}
|
|
10
|
-
`;
|
|
11
|
-
function createShader(gl, type, source) {
|
|
12
|
-
const shader = gl.createShader(type);
|
|
13
|
-
if (!shader) return null;
|
|
14
|
-
gl.shaderSource(shader, source);
|
|
15
|
-
gl.compileShader(shader);
|
|
16
|
-
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
17
|
-
const error = gl.getShaderInfoLog(shader);
|
|
18
|
-
gl.deleteShader(shader);
|
|
19
|
-
throw new Error(`devjar:gl Shader compilation error: ${error}`);
|
|
20
|
-
}
|
|
21
|
-
return shader;
|
|
22
|
-
}
|
|
23
|
-
function createProgram(gl, vertexShader, fragmentShader) {
|
|
24
|
-
const program = gl.createProgram();
|
|
25
|
-
if (!program) return null;
|
|
26
|
-
gl.attachShader(program, vertexShader);
|
|
27
|
-
gl.attachShader(program, fragmentShader);
|
|
28
|
-
gl.linkProgram(program);
|
|
29
|
-
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
30
|
-
const error = gl.getProgramInfoLog(program);
|
|
31
|
-
gl.deleteProgram(program);
|
|
32
|
-
throw new Error(`devjar:gl Program linking error: ${error}`);
|
|
33
|
-
}
|
|
34
|
-
return program;
|
|
35
|
-
}
|
|
36
|
-
function createQuad(gl) {
|
|
37
|
-
const buffer = gl.createBuffer();
|
|
38
|
-
if (!buffer) return null;
|
|
39
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
40
|
-
// Fullscreen quad: two triangles covering the entire screen
|
|
41
|
-
const positions = new Float32Array([
|
|
42
|
-
-1,
|
|
43
|
-
-1,
|
|
44
|
-
1,
|
|
45
|
-
-1,
|
|
46
|
-
-1,
|
|
47
|
-
1,
|
|
48
|
-
-1,
|
|
49
|
-
1,
|
|
50
|
-
1,
|
|
51
|
-
-1,
|
|
52
|
-
1,
|
|
53
|
-
1
|
|
54
|
-
]);
|
|
55
|
-
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
|
|
56
|
-
return buffer;
|
|
57
|
-
}
|
|
58
|
-
function useGL({ fragment, canvasRef, onError }) {
|
|
59
|
-
const animationFrameRef = useRef(undefined);
|
|
60
|
-
const glRef = useRef(null);
|
|
61
|
-
const programRef = useRef(null);
|
|
62
|
-
const positionBufferRef = useRef(null);
|
|
63
|
-
const startTimeRef = useRef(performance.now());
|
|
64
|
-
const mouseRef = useRef({
|
|
65
|
-
x: 0,
|
|
66
|
-
y: 0
|
|
67
|
-
});
|
|
68
|
-
// Initialize WebGL
|
|
69
|
-
useEffect(()=>{
|
|
70
|
-
const canvas = canvasRef.current;
|
|
71
|
-
if (!canvas) return;
|
|
72
|
-
const gl = canvas.getContext('webgl');
|
|
73
|
-
if (!gl) {
|
|
74
|
-
onError?.('devjar:gl WebGL is not supported in your browser');
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
glRef.current = gl;
|
|
78
|
-
// Create quad buffer once
|
|
79
|
-
const positionBuffer = createQuad(gl);
|
|
80
|
-
if (positionBuffer) {
|
|
81
|
-
positionBufferRef.current = positionBuffer;
|
|
82
|
-
}
|
|
83
|
-
// Set canvas size
|
|
84
|
-
const resizeCanvas = ()=>{
|
|
85
|
-
const rect = canvas.getBoundingClientRect();
|
|
86
|
-
const dpr = window.devicePixelRatio || 1;
|
|
87
|
-
const width = rect.width;
|
|
88
|
-
const height = rect.height;
|
|
89
|
-
canvas.width = width * dpr;
|
|
90
|
-
canvas.height = height * dpr;
|
|
91
|
-
canvas.style.width = `${width}px`;
|
|
92
|
-
canvas.style.height = `${height}px`;
|
|
93
|
-
gl.viewport(0, 0, canvas.width, canvas.height);
|
|
94
|
-
};
|
|
95
|
-
resizeCanvas();
|
|
96
|
-
window.addEventListener('resize', resizeCanvas);
|
|
97
|
-
// Use ResizeObserver for more accurate sizing
|
|
98
|
-
const resizeObserver = new ResizeObserver(()=>{
|
|
99
|
-
resizeCanvas();
|
|
100
|
-
});
|
|
101
|
-
resizeObserver.observe(canvas);
|
|
102
|
-
// Handle mouse movement
|
|
103
|
-
const handleMouseMove = (e)=>{
|
|
104
|
-
const rect = canvas.getBoundingClientRect();
|
|
105
|
-
mouseRef.current = {
|
|
106
|
-
x: (e.clientX - rect.left) / rect.width,
|
|
107
|
-
y: 1.0 - (e.clientY - rect.top) / rect.height
|
|
108
|
-
};
|
|
109
|
-
};
|
|
110
|
-
canvas.addEventListener('mousemove', handleMouseMove);
|
|
111
|
-
return ()=>{
|
|
112
|
-
window.removeEventListener('resize', resizeCanvas);
|
|
113
|
-
canvas.removeEventListener('mousemove', handleMouseMove);
|
|
114
|
-
resizeObserver.disconnect();
|
|
115
|
-
if (animationFrameRef.current) {
|
|
116
|
-
cancelAnimationFrame(animationFrameRef.current);
|
|
117
|
-
}
|
|
118
|
-
if (positionBufferRef.current && gl) {
|
|
119
|
-
gl.deleteBuffer(positionBufferRef.current);
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
}, [
|
|
123
|
-
canvasRef,
|
|
124
|
-
onError
|
|
125
|
-
]);
|
|
126
|
-
// Compile and render shader
|
|
127
|
-
useEffect(()=>{
|
|
128
|
-
const gl = glRef.current;
|
|
129
|
-
const canvas = canvasRef.current;
|
|
130
|
-
if (!gl || !canvas) return;
|
|
131
|
-
onError?.(null);
|
|
132
|
-
try {
|
|
133
|
-
// Create shaders
|
|
134
|
-
const vertexShader = createShader(gl, gl.VERTEX_SHADER, DEFAULT_VERTEX_SHADER);
|
|
135
|
-
if (!vertexShader) throw new Error('devjar:gl Failed to create vertex shader');
|
|
136
|
-
const compiledFragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragment);
|
|
137
|
-
if (!compiledFragmentShader) throw new Error('devjar:gl Failed to create fragment shader');
|
|
138
|
-
// Create program
|
|
139
|
-
const program = createProgram(gl, vertexShader, compiledFragmentShader);
|
|
140
|
-
if (!program) throw new Error('devjar:gl Failed to create program');
|
|
141
|
-
// Clean up old program
|
|
142
|
-
if (programRef.current) {
|
|
143
|
-
gl.deleteProgram(programRef.current);
|
|
144
|
-
}
|
|
145
|
-
programRef.current = program;
|
|
146
|
-
// Clean up shaders (they're linked into the program)
|
|
147
|
-
gl.deleteShader(vertexShader);
|
|
148
|
-
gl.deleteShader(compiledFragmentShader);
|
|
149
|
-
// Render loop
|
|
150
|
-
const render = ()=>{
|
|
151
|
-
if (!gl || !canvas || !programRef.current || !positionBufferRef.current) return;
|
|
152
|
-
const program = programRef.current;
|
|
153
|
-
const positionBuffer = positionBufferRef.current;
|
|
154
|
-
// Clear
|
|
155
|
-
gl.clearColor(0, 0, 0, 1);
|
|
156
|
-
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
157
|
-
// Use program
|
|
158
|
-
gl.useProgram(program);
|
|
159
|
-
// Set up position attribute
|
|
160
|
-
const positionLocation = gl.getAttribLocation(program, 'a_position');
|
|
161
|
-
if (positionLocation >= 0) {
|
|
162
|
-
gl.enableVertexAttribArray(positionLocation);
|
|
163
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
|
164
|
-
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
|
|
165
|
-
}
|
|
166
|
-
// Set uniforms
|
|
167
|
-
const time = (performance.now() - startTimeRef.current) / 1000;
|
|
168
|
-
const resolutionLocation = gl.getUniformLocation(program, 'u_resolution');
|
|
169
|
-
const timeLocation = gl.getUniformLocation(program, 'u_time');
|
|
170
|
-
const mouseLocation = gl.getUniformLocation(program, 'u_mouse');
|
|
171
|
-
if (resolutionLocation) {
|
|
172
|
-
gl.uniform2f(resolutionLocation, canvas.width, canvas.height);
|
|
173
|
-
}
|
|
174
|
-
if (timeLocation) {
|
|
175
|
-
gl.uniform1f(timeLocation, time);
|
|
176
|
-
}
|
|
177
|
-
if (mouseLocation) {
|
|
178
|
-
gl.uniform2f(mouseLocation, mouseRef.current.x, mouseRef.current.y);
|
|
179
|
-
}
|
|
180
|
-
// Draw
|
|
181
|
-
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
182
|
-
animationFrameRef.current = requestAnimationFrame(render);
|
|
183
|
-
};
|
|
184
|
-
// Start render loop
|
|
185
|
-
if (animationFrameRef.current) {
|
|
186
|
-
cancelAnimationFrame(animationFrameRef.current);
|
|
187
|
-
}
|
|
188
|
-
render();
|
|
189
|
-
// Cleanup function
|
|
190
|
-
return ()=>{
|
|
191
|
-
if (animationFrameRef.current) {
|
|
192
|
-
cancelAnimationFrame(animationFrameRef.current);
|
|
193
|
-
}
|
|
194
|
-
if (programRef.current && gl) {
|
|
195
|
-
gl.deleteProgram(programRef.current);
|
|
196
|
-
programRef.current = null;
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
|
-
} catch (err) {
|
|
200
|
-
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
201
|
-
const prefixedMessage = errorMessage.startsWith('devjar:gl ') ? errorMessage : `devjar:gl ${errorMessage}`;
|
|
202
|
-
onError?.(prefixedMessage);
|
|
203
|
-
if (animationFrameRef.current) {
|
|
204
|
-
cancelAnimationFrame(animationFrameRef.current);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}, [
|
|
208
|
-
fragment,
|
|
209
|
-
canvasRef,
|
|
210
|
-
onError
|
|
211
|
-
]);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export { useGL };
|