Skip to content

Commit 020749d

Browse files
committed
feat: add preview iframe
1 parent dd09227 commit 020749d

File tree

8 files changed

+91
-40
lines changed

8 files changed

+91
-40
lines changed

src/core/files.ts

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import importMap from './templates/import-map.json?raw';
33
import AppCss from './templates/App.css?raw';
44
import App from './templates/App.tsx?raw';
55
import main from './templates/main.tsx?raw';
6+
import reactLogo from './templates/react.svg?raw';
67
import { fileName2Language } from './util';
78

89
// app 文件名
@@ -28,30 +29,10 @@ export const defaultFiles: MultipleFiles = {
2829
language: 'css',
2930
value: AppCss,
3031
},
31-
'App1.css': {
32-
name: 'App.css',
33-
language: 'css',
34-
value: AppCss,
35-
},
36-
'App2.css': {
37-
name: 'App.css',
38-
language: 'css',
39-
value: AppCss,
40-
},
41-
'App3.css': {
42-
name: 'App.css',
43-
language: 'css',
44-
value: AppCss,
45-
},
46-
'App4.css': {
47-
name: 'App.css',
48-
language: 'css',
49-
value: AppCss,
50-
},
51-
'App5.css': {
52-
name: 'App.css',
53-
language: 'css',
54-
value: AppCss,
32+
'react.svg': {
33+
name: 'react.svg',
34+
language: 'svg',
35+
value: reactLogo,
5536
},
5637
[IMPORT_MAP_FILE_NAME]: {
5738
name: IMPORT_MAP_FILE_NAME,

src/core/templates/App.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,7 @@ button:focus-visible {
6363
background-color: #f9f9f9;
6464
}
6565
}
66+
67+
.card {
68+
padding: 2em;
69+
}

src/core/templates/App.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@ function App() {
55
const [count, setCount] = useState(0);
66

77
return (
8-
<>
9-
<h1>Hello World</h1>
8+
<div className="App">
9+
<h1>Rspack + React + TypeScript</h1>
1010
<div className="card">
1111
<button type="button" onClick={() => setCount((count) => count + 1)}>
1212
count is {count}
1313
</button>
14+
<p>
15+
Edit <code>App.tsx</code> and save to test HMR
16+
</p>
1417
</div>
15-
</>
18+
</div>
1619
);
1720
}
1821

src/core/templates/main.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import React from 'react';
21
import ReactDOM from 'react-dom/client';
2+
import App from './App.tsx';
3+
import './App.css';
34

4-
import App from './App';
5-
6-
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
5+
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />);

src/layout/RootPreview/compiler.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,30 @@ export const compile = (files: MultipleFiles) => {
1515

1616
export const babelTransform = (filename: string, code: string, files: MultipleFiles) => {
1717
let result = '';
18+
const _code = beforBabelTransform(filename, code);
1819
try {
19-
result = transform(code, {
20+
result = transform(_code, {
2021
presets: ['react', 'typescript'],
2122
filename,
2223
plugins: [customResolver(files)],
23-
retainLines: true,
24+
retainLines: true, // 保留行号, 方便调试
2425
}).code!;
2526
} catch (e) {
2627
console.error('compiler Error', e);
2728
}
2829
return result;
2930
};
3031

32+
// 兼容 jsx runtime 版本的 react, 自动注入 import React from 'react'
33+
function beforBabelTransform(filename: string, code: string) {
34+
let _code = code;
35+
const regexReact = /import\s+React/;
36+
if (filename.endsWith('.tsx') || (filename.endsWith('.jsx') && !regexReact.test(code))) {
37+
_code = `import React from 'react';\n${code}`;
38+
}
39+
return _code;
40+
}
41+
3142
function customResolver(files: MultipleFiles): PluginObj {
3243
return {
3344
visitor: {
@@ -46,11 +57,15 @@ function customResolver(files: MultipleFiles): PluginObj {
4657
path.node.source.value = css2Js(file);
4758
} else if (file.name.endsWith('.json')) {
4859
path.node.source.value = json2Js(file);
60+
} else if (file.name.endsWith('.svg')) {
61+
path.node.source.value = file.value;
4962
} else {
5063
// jsx/tsx 代码是 react+ts,需要经 babel 编译才能展示
51-
path.node.source.value = URL.createObjectURL(
52-
new Blob([babelTransform(file.name, file.value, files)], { type: 'text/javascript' }),
53-
);
64+
// 再次调用 babelTransform, 进行深度递归
65+
const blob = new Blob([babelTransform(file.name, file.value, files)], {
66+
type: 'text/javascript',
67+
});
68+
path.node.source.value = URL.createObjectURL(blob);
5469
}
5570
}
5671
},

src/layout/RootPreview/iframe.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>React Playgroung Preview</title>
7+
</head>
8+
<body>
9+
<script type="importmap"></script>
10+
<script type="module" id="appSrc"></script>
11+
<div id="root"></div>
12+
</body>
13+
</html>

src/layout/RootPreview/index.tsx

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,51 @@
11
import { useContext, useEffect, useState } from 'react';
22
import { PlaygroundContext } from '@/core/context';
33
import { compile } from './compiler';
4+
import iframeSource from './iframe.html?raw';
5+
import { IMPORT_MAP_FILE_NAME } from '@/core/files';
6+
7+
function genIframeUrl(importMap: string, compilerCode: string) {
8+
const htmlStr = iframeSource
9+
.replace(
10+
`<script type="importmap"></script>`,
11+
`<script type="importmap">
12+
${importMap}
13+
</script>`,
14+
)
15+
.replace(
16+
`<script type="module" id="appSrc"></script>`,
17+
`<script type="module" id="appSrc">${compilerCode}</script>`,
18+
);
19+
20+
return URL.createObjectURL(
21+
new Blob([htmlStr], {
22+
type: 'text/html',
23+
}),
24+
);
25+
}
426

527
export default function RootPreview() {
628
const { files = {} } = useContext(PlaygroundContext);
29+
const importMapContent = files[IMPORT_MAP_FILE_NAME].value;
30+
731
const [compiledCode, setCompiledCode] = useState('');
32+
const [iframeUrl, setIframeUrl] = useState(() => genIframeUrl(importMapContent, compiledCode));
833

934
useEffect(() => {
1035
const res = compile(files);
1136
setCompiledCode(res);
1237
}, [files]);
1338

14-
return <div className="h-full overflow-auto whitespace-pre-line break-all">{compiledCode}</div>;
39+
useEffect(() => {
40+
const newFrameUrl = genIframeUrl(importMapContent, compiledCode);
41+
setIframeUrl(newFrameUrl);
42+
}, [importMapContent, compiledCode]);
43+
44+
console.log(iframeUrl);
45+
46+
return (
47+
<div className="h-full">
48+
<iframe title="preview" src={iframeUrl} className="m-0 h-full w-full border-none p-0" />
49+
</div>
50+
);
1551
}

src/main.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import './index.css';
44

55
const App = lazy(() => import('./App.tsx'));
66

7-
const laztPlacholder = (
8-
<div className="flex h-screen items-center justify-center">App Loading...</div>
7+
const lazyPlacholder = (
8+
<div className="flex h-screen items-center justify-center font-bold">App Loading...</div>
99
);
1010

1111
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
12-
<Suspense fallback={laztPlacholder}>
12+
<Suspense fallback={lazyPlacholder}>
1313
<App />
1414
</Suspense>,
1515
);

0 commit comments

Comments
 (0)