devjar 0.7.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 +24 -20
- package/dist/index.d.ts +9 -17
- package/dist/index.js +326 -131
- package/dist/transform-worker.js +54 -0
- package/package.json +23 -15
package/README.md
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
1
|
# devjar
|
|
2
|
-
> live
|
|
2
|
+
> react live preview in browser
|
|
3
3
|
|
|
4
|
+

|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
## Introduction
|
|
6
7
|
|
|
7
|
-
|
|
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
|
|
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.
|
|
10
11
|
|
|
11
|
-
|
|
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
|
-
###
|
|
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
|
|
|
26
24
|
**Props**
|
|
27
25
|
|
|
28
26
|
* `files`: An object that specifies the files you want to include in your development environment.
|
|
29
|
-
* `
|
|
27
|
+
* `resolveModule`: A function that maps module specifiers to browser-loadable module URLs.
|
|
30
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.
|
|
31
30
|
|
|
31
|
+
**Example**
|
|
32
32
|
|
|
33
33
|
```jsx
|
|
34
34
|
import { DevJar } from 'devjar'
|
|
@@ -43,20 +43,23 @@ function App() {
|
|
|
43
43
|
return (
|
|
44
44
|
<DevJar
|
|
45
45
|
files={files}
|
|
46
|
-
|
|
47
|
-
return `${CDN_HOST}/${
|
|
46
|
+
resolveModule={(specifier) => {
|
|
47
|
+
return `${CDN_HOST}/${specifier}`
|
|
48
48
|
}}
|
|
49
49
|
/>
|
|
50
50
|
)
|
|
51
51
|
}
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
### `useLiveCode(options)`
|
|
55
|
+
|
|
56
|
+
A hook that provides lower-level control over the live code execution environment.
|
|
55
57
|
|
|
56
58
|
**Parameters**
|
|
57
59
|
|
|
58
60
|
* `options`
|
|
59
|
-
* `
|
|
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.
|
|
60
63
|
|
|
61
64
|
**Returns**
|
|
62
65
|
|
|
@@ -65,6 +68,8 @@ function App() {
|
|
|
65
68
|
* `error`: An error message in case the live coding encounters an issue.
|
|
66
69
|
* `load(codeFiles)`: void: Loads code files and executes them as live code.
|
|
67
70
|
|
|
71
|
+
**Example**
|
|
72
|
+
|
|
68
73
|
```jsx
|
|
69
74
|
import { useLiveCode } from 'devjar'
|
|
70
75
|
|
|
@@ -72,8 +77,8 @@ function Playground() {
|
|
|
72
77
|
const { ref, error, load } = useLiveCode({
|
|
73
78
|
// The CDN url of each imported module path in your code
|
|
74
79
|
// e.g. `import React from 'react'` will load react from skypack.dev/react
|
|
75
|
-
|
|
76
|
-
return `https://cdn.skypack.dev/${
|
|
80
|
+
resolveModule(specifier) {
|
|
81
|
+
return `https://cdn.skypack.dev/${specifier}`
|
|
77
82
|
}
|
|
78
83
|
})
|
|
79
84
|
|
|
@@ -103,7 +108,6 @@ function Playground() {
|
|
|
103
108
|
}
|
|
104
109
|
```
|
|
105
110
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
The MIT License (MIT).
|
|
111
|
+
## License
|
|
109
112
|
|
|
113
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -1,28 +1,20 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
3
2
|
|
|
4
|
-
declare
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
shimMode: boolean;
|
|
8
|
-
mapOverrides: boolean;
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
|
-
function importShim(url: string): Promise<any>;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
declare function useLiveCode({ getModuleUrl }: {
|
|
15
|
-
getModuleUrl?: (name: string) => string;
|
|
3
|
+
declare function useLiveCode({ resolveModule, tailwindSrc, }: {
|
|
4
|
+
resolveModule?: (specifier: string) => string;
|
|
5
|
+
tailwindSrc?: string | false;
|
|
16
6
|
}): {
|
|
17
7
|
ref: react.RefObject<any>;
|
|
18
8
|
error: undefined;
|
|
19
|
-
load: (files:
|
|
9
|
+
load: (files: Record<string, string>) => Promise<void>;
|
|
20
10
|
};
|
|
21
11
|
|
|
22
|
-
declare function DevJar({ files,
|
|
12
|
+
declare function DevJar({ files, resolveModule, onError, tailwindSrc, ref: forwardedRef, ...props }: {
|
|
23
13
|
files: Record<string, string>;
|
|
24
|
-
|
|
14
|
+
resolveModule?: (specifier: string) => string;
|
|
25
15
|
onError?: (...data: any[]) => void;
|
|
26
|
-
|
|
16
|
+
tailwindSrc?: string | false;
|
|
17
|
+
ref?: React.Ref<HTMLIFrameElement>;
|
|
18
|
+
} & React.IframeHTMLAttributes<HTMLIFrameElement>): react.JSX.Element;
|
|
27
19
|
|
|
28
20
|
export { DevJar, useLiveCode };
|
package/dist/index.js
CHANGED
|
@@ -1,82 +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
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
mapOverrides: true
|
|
15
|
-
};
|
|
16
|
-
shim = import(/* webpackIgnore: true */ getModuleUrl('es-module-shims'));
|
|
17
|
-
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)}`;
|
|
18
13
|
}
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
imports['react-dom'] = getModuleUrl('react-dom');
|
|
22
|
-
imports['react-dom/client'] = getModuleUrl('react-dom/client');
|
|
14
|
+
function registerImportMap(imports) {
|
|
15
|
+
var _runtime;
|
|
23
16
|
const script = document.createElement('script');
|
|
24
|
-
script.type = 'importmap
|
|
25
|
-
script.
|
|
17
|
+
script.type = 'importmap';
|
|
18
|
+
script.textContent = JSON.stringify({
|
|
26
19
|
imports
|
|
27
20
|
});
|
|
28
|
-
document.
|
|
29
|
-
|
|
30
|
-
currentImportMap.parentNode.removeChild(currentImportMap);
|
|
31
|
-
}
|
|
32
|
-
currentImportMap = script;
|
|
21
|
+
document.head.appendChild(script);
|
|
22
|
+
((_runtime = runtime).importMaps || (_runtime.importMaps = [])).push(script);
|
|
33
23
|
}
|
|
34
|
-
function createInlinedModule(code
|
|
35
|
-
if (type === 'css') return `data:text/css;utf-8,${encodeURIComponent(code)}`;
|
|
24
|
+
function createInlinedModule(code) {
|
|
36
25
|
return `data:text/javascript;utf-8,${encodeURIComponent(code)}`;
|
|
37
26
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
};
|
|
45
110
|
}
|
|
46
111
|
|
|
47
112
|
let esModuleLexerInit;
|
|
48
113
|
const isRelative = (s)=>s.startsWith('./');
|
|
49
114
|
const removeExtension = (str)=>str.replace(/\.[^/.]+$/, '');
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
'typescript'
|
|
55
|
-
]
|
|
56
|
-
}).code;
|
|
57
|
-
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)}__`;
|
|
58
119
|
}
|
|
59
|
-
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) {
|
|
60
143
|
let code = '';
|
|
61
144
|
let lastIndex = 0;
|
|
62
145
|
let hasReactImports = false;
|
|
63
146
|
const [imports] = parse(source);
|
|
64
147
|
const cssImports = [];
|
|
65
|
-
|
|
148
|
+
const dependencies = [];
|
|
66
149
|
// start, end, statementStart, statementEnd, assertion, name
|
|
67
150
|
imports.forEach(({ s, e, ss, se, a, n })=>{
|
|
68
|
-
|
|
69
|
-
;
|
|
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;
|
|
70
154
|
// handle imports
|
|
71
|
-
if (
|
|
155
|
+
if (localModuleKey && localModuleKey.endsWith('.css')) {
|
|
72
156
|
// Map './styles.css' -> '@styles.css', and collect it
|
|
73
|
-
|
|
74
|
-
cssImports.push(cssPath);
|
|
157
|
+
cssImports.push(localModuleKey);
|
|
75
158
|
} else {
|
|
76
159
|
code += source.substring(ss, s);
|
|
77
|
-
code +=
|
|
160
|
+
code += localModuleKey ? createLocalImportPlaceholder(localModuleKey) : resolveModule(n);
|
|
78
161
|
code += source.substring(e, se);
|
|
79
162
|
}
|
|
163
|
+
if (localModuleKey) dependencies.push(localModuleKey);
|
|
80
164
|
lastIndex = se;
|
|
81
165
|
if (n === 'react') {
|
|
82
166
|
const statement = source.slice(ss, se);
|
|
@@ -84,64 +168,104 @@ function replaceImports(source, getModuleUrl, externals) {
|
|
|
84
168
|
hasReactImports = true;
|
|
85
169
|
}
|
|
86
170
|
}
|
|
87
|
-
cssImports.forEach((cssPath)=>{
|
|
88
|
-
code += `\nimport sheet${cssImportIndex} from "${cssPath}" assert { type: "css" };\n`;
|
|
89
|
-
cssImportIndex++;
|
|
90
|
-
});
|
|
91
171
|
});
|
|
92
172
|
if (cssImports.length) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
+
});
|
|
102
185
|
}
|
|
103
186
|
code += source.substring(lastIndex);
|
|
104
187
|
if (!hasReactImports) {
|
|
105
|
-
code = `import React from 'react';\n${code}`;
|
|
188
|
+
code = `import React from ${JSON.stringify(resolveModule('react'))};\n${code}`;
|
|
106
189
|
}
|
|
107
|
-
|
|
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
|
+
};
|
|
108
195
|
}
|
|
109
196
|
// createRenderer is going to be stringified and executed in the iframe
|
|
110
|
-
function createRenderer(createModule_,
|
|
197
|
+
function createRenderer(createModule_, resolveModule) {
|
|
111
198
|
let reactRoot;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
115
208
|
});
|
|
116
|
-
const ReactMod = await
|
|
117
|
-
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'));
|
|
118
211
|
const _jsx = ReactMod.createElement;
|
|
119
212
|
const root = document.getElementById('__reactRoot');
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
render() {
|
|
127
|
-
if (this.state.error) {
|
|
128
|
-
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
|
+
};
|
|
129
219
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
+
};
|
|
138
245
|
}
|
|
139
246
|
if (!reactRoot) {
|
|
140
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
|
+
}
|
|
141
268
|
}
|
|
142
|
-
const Component = mod.default;
|
|
143
|
-
const element = _jsx(ErrorBoundary, null, _jsx(Component));
|
|
144
|
-
reactRoot.render(element);
|
|
145
269
|
}
|
|
146
270
|
return render;
|
|
147
271
|
}
|
|
@@ -151,20 +275,13 @@ function createMainScript({ uid }) {
|
|
|
151
275
|
const _createModule = ${createModule.toString()};
|
|
152
276
|
const _createRenderer = ${createRenderer.toString()};
|
|
153
277
|
|
|
154
|
-
const
|
|
278
|
+
const resolveModule = (specifier) => window.parent.__devjar__[globalThis.uid].resolveModule(specifier)
|
|
155
279
|
|
|
156
280
|
globalThis.uid = ${JSON.stringify(uid)};
|
|
157
|
-
globalThis.__render__ = _createRenderer(_createModule,
|
|
281
|
+
globalThis.__render__ = _createRenderer(_createModule, resolveModule);
|
|
158
282
|
`;
|
|
159
283
|
return code;
|
|
160
284
|
}
|
|
161
|
-
function createEsShimOptionsScript() {
|
|
162
|
-
return `\
|
|
163
|
-
window.esmsInitOptions = {
|
|
164
|
-
polyfillEnable: ['css-modules', 'json-modules'],
|
|
165
|
-
onerror: console.error,
|
|
166
|
-
}`;
|
|
167
|
-
}
|
|
168
285
|
function useScript() {
|
|
169
286
|
return useRef(typeof window !== 'undefined' ? document.createElement('script') : null);
|
|
170
287
|
}
|
|
@@ -179,28 +296,45 @@ function createScript(scriptRef, { content, src, type } = {}) {
|
|
|
179
296
|
}
|
|
180
297
|
return script;
|
|
181
298
|
}
|
|
182
|
-
function useLiveCode({
|
|
299
|
+
function useLiveCode({ resolveModule, tailwindSrc = defaultTailwindSrc }) {
|
|
183
300
|
const iframeRef = useRef(null);
|
|
184
301
|
const [error, setError] = useState();
|
|
185
302
|
const rerender = useState({})[1];
|
|
186
303
|
const appScriptRef = useScript();
|
|
187
|
-
const esShimOptionsScriptRef = useScript();
|
|
188
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);
|
|
189
310
|
const uid = useId();
|
|
190
|
-
// Let
|
|
311
|
+
// Let resolveModule execute on parent window side since it might involve
|
|
191
312
|
// variables that iframe cannot access.
|
|
192
313
|
useEffect(()=>{
|
|
193
314
|
if (!globalThis.__devjar__) {
|
|
194
315
|
globalThis.__devjar__ = {};
|
|
195
316
|
}
|
|
196
317
|
globalThis.__devjar__[uid] = {
|
|
197
|
-
|
|
318
|
+
resolveModule
|
|
198
319
|
};
|
|
199
320
|
return ()=>{
|
|
200
321
|
if (globalThis.__devjar__) {
|
|
201
322
|
delete globalThis.__devjar__[uid];
|
|
202
323
|
}
|
|
203
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
|
+
};
|
|
204
338
|
}, []);
|
|
205
339
|
useEffect(()=>{
|
|
206
340
|
const iframe = iframeRef.current;
|
|
@@ -212,72 +346,126 @@ function useLiveCode({ getModuleUrl }) {
|
|
|
212
346
|
const appScriptContent = createMainScript({
|
|
213
347
|
uid
|
|
214
348
|
});
|
|
215
|
-
const scriptOptionsContent = createEsShimOptionsScript();
|
|
216
|
-
const esmShimOptionsScript = createScript(esShimOptionsScriptRef, {
|
|
217
|
-
content: scriptOptionsContent
|
|
218
|
-
});
|
|
219
349
|
const appScript = createScript(appScriptRef, {
|
|
220
|
-
content: appScriptContent
|
|
221
|
-
type: 'module'
|
|
222
|
-
});
|
|
223
|
-
const tailwindScript = createScript(tailwindcssScriptRef, {
|
|
224
|
-
src: 'https://unpkg.com/@tailwindcss/browser@4'
|
|
350
|
+
content: appScriptContent
|
|
225
351
|
});
|
|
352
|
+
const tailwindScript = tailwindSrc ? createScript(tailwindcssScriptRef, {
|
|
353
|
+
src: tailwindSrc
|
|
354
|
+
}) : null;
|
|
226
355
|
body.appendChild(div);
|
|
227
|
-
body.appendChild(esmShimOptionsScript);
|
|
228
356
|
body.appendChild(appScript);
|
|
229
|
-
body.appendChild(tailwindScript);
|
|
357
|
+
if (tailwindScript) body.appendChild(tailwindScript);
|
|
230
358
|
return ()=>{
|
|
231
359
|
if (!iframe || !iframe.contentDocument) return;
|
|
232
360
|
body.removeChild(div);
|
|
233
|
-
body.removeChild(esmShimOptionsScript);
|
|
234
361
|
body.removeChild(appScript);
|
|
235
|
-
body.removeChild(tailwindScript);
|
|
362
|
+
if (tailwindScript) body.removeChild(tailwindScript);
|
|
236
363
|
};
|
|
237
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
|
+
]);
|
|
238
406
|
const load = useCallback(async (files)=>{
|
|
407
|
+
const loadId = ++loadIdRef.current;
|
|
239
408
|
if (!esModuleLexerInit) {
|
|
240
409
|
await init;
|
|
241
410
|
esModuleLexerInit = true;
|
|
242
411
|
}
|
|
243
412
|
if (files) {
|
|
244
|
-
|
|
245
|
-
const overrideExternals = new Set(Object.keys(files).filter((name)=>!isRelative(name) && name !== 'index.js'));
|
|
246
|
-
// Always share react as externals
|
|
247
|
-
overrideExternals.add('react');
|
|
248
|
-
overrideExternals.add('react-dom');
|
|
413
|
+
const localModules = new Set(Object.keys(files).map(getModuleKey));
|
|
249
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
|
+
}
|
|
250
429
|
/**
|
|
251
430
|
* transformedFiles
|
|
252
431
|
* {
|
|
253
432
|
* 'index.js': '...',
|
|
254
433
|
* '@mod1': '...',
|
|
255
434
|
* '@mod2': '...',
|
|
256
|
-
*/ const
|
|
435
|
+
*/ const dependencies = {};
|
|
436
|
+
const transformedFiles = Object.keys(files).reduce((res, filename)=>{
|
|
257
437
|
// 1. Remove ./
|
|
258
438
|
// 2. For non css files, remove extension
|
|
259
439
|
// e.g. './styles.css' -> '@styles.css'
|
|
260
440
|
// e.g. './foo.js' -> '@foo'
|
|
261
|
-
const moduleKey =
|
|
441
|
+
const moduleKey = getModuleKey(filename);
|
|
262
442
|
if (filename.endsWith('.css')) {
|
|
263
443
|
res[moduleKey] = files[filename];
|
|
444
|
+
dependencies[moduleKey] = [];
|
|
264
445
|
} else {
|
|
265
446
|
// JS or TS files
|
|
266
|
-
const
|
|
267
|
-
res[
|
|
447
|
+
const transformed = replaceImports(transformCacheRef.current.get(filename).code, filename, moduleKey, resolveModule, localModules);
|
|
448
|
+
res[moduleKey] = transformed.code;
|
|
449
|
+
dependencies[moduleKey] = transformed.dependencies;
|
|
268
450
|
}
|
|
269
451
|
return res;
|
|
270
452
|
}, {});
|
|
271
453
|
const iframe = iframeRef.current;
|
|
272
454
|
const script = appScriptRef.current;
|
|
273
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
|
+
};
|
|
274
462
|
const render = iframe.contentWindow.__render__;
|
|
275
463
|
if (render) {
|
|
276
|
-
|
|
464
|
+
await renderFiles();
|
|
277
465
|
} else {
|
|
278
466
|
// if render is not loaded yet, wait until it's loaded
|
|
279
467
|
script.onload = ()=>{
|
|
280
|
-
|
|
468
|
+
renderFiles().catch((err)=>{
|
|
281
469
|
setError(err);
|
|
282
470
|
});
|
|
283
471
|
};
|
|
@@ -290,7 +478,10 @@ function useLiveCode({ getModuleUrl }) {
|
|
|
290
478
|
}
|
|
291
479
|
}
|
|
292
480
|
rerender({});
|
|
293
|
-
}, [
|
|
481
|
+
}, [
|
|
482
|
+
resolveModule,
|
|
483
|
+
transformFiles
|
|
484
|
+
]);
|
|
294
485
|
return {
|
|
295
486
|
ref: iframeRef,
|
|
296
487
|
error,
|
|
@@ -299,11 +490,15 @@ function useLiveCode({ getModuleUrl }) {
|
|
|
299
490
|
}
|
|
300
491
|
|
|
301
492
|
const defaultOnError = typeof window !== 'undefined' ? console.error : ()=>{};
|
|
302
|
-
function DevJar({ files,
|
|
493
|
+
function DevJar({ files, resolveModule, onError = defaultOnError, tailwindSrc, ref: forwardedRef, ...props }) {
|
|
303
494
|
const onErrorRef = useRef(onError);
|
|
304
495
|
const { ref, error, load } = useLiveCode({
|
|
305
|
-
|
|
496
|
+
resolveModule,
|
|
497
|
+
tailwindSrc
|
|
306
498
|
});
|
|
499
|
+
useImperativeHandle(forwardedRef, ()=>ref.current, [
|
|
500
|
+
ref
|
|
501
|
+
]);
|
|
307
502
|
useEffect(()=>{
|
|
308
503
|
onErrorRef.current(error);
|
|
309
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,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devjar",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
|
-
".":
|
|
7
|
-
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"default": "./dist/index.js"
|
|
9
|
+
}
|
|
8
10
|
},
|
|
9
11
|
"license": "MIT",
|
|
10
12
|
"files": [
|
|
@@ -12,31 +14,37 @@
|
|
|
12
14
|
],
|
|
13
15
|
"types": "./dist/index.d.ts",
|
|
14
16
|
"scripts": {
|
|
15
|
-
"build": "
|
|
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",
|
|
16
20
|
"prepublishOnly": "pnpm run build",
|
|
17
21
|
"build:site": "pnpm run build && next build ./site",
|
|
18
22
|
"start": "next start ./site",
|
|
19
|
-
"dev": "
|
|
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"
|
|
20
27
|
},
|
|
21
28
|
"peerDependencies": {
|
|
22
|
-
"react": "^
|
|
29
|
+
"react": "^19.0.0"
|
|
23
30
|
},
|
|
24
31
|
"dependencies": {
|
|
25
32
|
"es-module-lexer": "1.6.0",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
33
|
+
"oxc-transform": "^0.137.0",
|
|
34
|
+
"react-refresh": "^0.18.0"
|
|
28
35
|
},
|
|
29
36
|
"devDependencies": {
|
|
30
37
|
"@types/node": "^22.10.7",
|
|
31
38
|
"@types/react": "^19.0.7",
|
|
32
39
|
"@types/react-dom": "^19.0.3",
|
|
33
|
-
"bunchee": "^6.
|
|
34
|
-
"codice": "1.
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"react
|
|
40
|
+
"bunchee": "^6.11.0",
|
|
41
|
+
"codice": "^1.6.0",
|
|
42
|
+
"dedent": "^1.7.0",
|
|
43
|
+
"devjar": "link:",
|
|
44
|
+
"next": "16.3.0-canary.69",
|
|
45
|
+
"react": "^19.2.1",
|
|
46
|
+
"react-dom": "^19.2.1",
|
|
39
47
|
"typescript": "^5.7.3"
|
|
40
48
|
},
|
|
41
|
-
"packageManager": "pnpm@9.
|
|
49
|
+
"packageManager": "pnpm@11.9.0"
|
|
42
50
|
}
|