Skip to content

Commit 68c0006

Browse files
authored
fix(plugin-assets-retry): runtime code is cut off by </script> and support es5 (#2115)
1 parent 4a89515 commit 68c0006

File tree

10 files changed

+219
-3
lines changed

10 files changed

+219
-3
lines changed

e2e/cases/assets-retry/index.test.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { expect, test } from '@playwright/test';
2+
import { dev, gotoPage, proxyConsole } from '@e2e/helper';
3+
import { pluginReact } from '@rsbuild/plugin-react';
4+
import { pluginAssetsRetry } from '@rsbuild/plugin-assets-retry';
5+
import type { PluginAssetsRetryOptions } from '@rsbuild/plugin-assets-retry';
6+
import type { RequestHandler } from '@rsbuild/shared';
7+
8+
function count404Response(logs: string[]) {
9+
let count = 0;
10+
for (const log of logs) {
11+
if (log.includes('404')) {
12+
count++;
13+
}
14+
}
15+
return count;
16+
}
17+
18+
function createBlockMiddleware({
19+
urlPrefix,
20+
blockNum,
21+
}: {
22+
urlPrefix: string;
23+
blockNum: number;
24+
}): RequestHandler {
25+
let counter = 0;
26+
return (req, res, next) => {
27+
if (req.url?.startsWith(urlPrefix)) {
28+
counter++;
29+
// if blockNum is 3, 1 2 3 would be blocked, 4 would be passed
30+
if (counter % (blockNum + 1) !== 0) {
31+
res.statusCode = 404;
32+
}
33+
res.setHeader('block-async', counter);
34+
}
35+
next();
36+
};
37+
}
38+
39+
async function createRsbuildWithMiddleware(
40+
middleware: RequestHandler,
41+
options: PluginAssetsRetryOptions,
42+
) {
43+
const rsbuild = await dev({
44+
cwd: __dirname,
45+
rsbuildConfig: {
46+
plugins: [pluginReact(), pluginAssetsRetry(options)],
47+
dev: {
48+
hmr: false,
49+
liveReload: false,
50+
setupMiddlewares: [
51+
(middlewares, _server) => {
52+
middlewares.unshift(middleware);
53+
},
54+
],
55+
},
56+
},
57+
});
58+
return rsbuild;
59+
}
60+
61+
test('@rsbuild/plugin-assets-retry should work when blocking initial chunk index.js`', async ({
62+
page,
63+
}) => {
64+
process.env.DEBUG = 'rsbuild';
65+
const { logs, restore } = proxyConsole();
66+
const blockedMiddleware = createBlockMiddleware({
67+
blockNum: 3,
68+
urlPrefix: '/static/js/index.js',
69+
});
70+
const rsbuild = await createRsbuildWithMiddleware(blockedMiddleware, {});
71+
72+
await gotoPage(page, rsbuild);
73+
const compTestElement = page.locator('#comp-test');
74+
await expect(compTestElement).toHaveText('Hello CompTest');
75+
const blockedResponseCount = count404Response(logs);
76+
expect(blockedResponseCount).toBe(3);
77+
await rsbuild.close();
78+
restore();
79+
delete process.env.DEBUG;
80+
});
81+
82+
test('@rsbuild/plugin-assets-retry should work with minified runtime code when blocking initial chunk index.js`', async ({
83+
page,
84+
}) => {
85+
process.env.DEBUG = 'rsbuild';
86+
const { logs, restore } = proxyConsole();
87+
const blockedMiddleware = createBlockMiddleware({
88+
blockNum: 3,
89+
urlPrefix: '/static/js/index.js',
90+
});
91+
const rsbuild = await createRsbuildWithMiddleware(blockedMiddleware, {
92+
minify: true,
93+
});
94+
95+
await gotoPage(page, rsbuild);
96+
const compTestElement = page.locator('#comp-test');
97+
await expect(compTestElement).toHaveText('Hello CompTest');
98+
const blockedResponseCount = count404Response(logs);
99+
expect(blockedResponseCount).toBe(3);
100+
await rsbuild.close();
101+
restore();
102+
delete process.env.DEBUG;
103+
});

e2e/cases/assets-retry/src/App.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
body {
2+
margin: 0;
3+
color: #fff;
4+
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
5+
background-image: linear-gradient(to bottom, #020917, #101725);
6+
}

e2e/cases/assets-retry/src/App.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React from 'react';
2+
import './App.css';
3+
import { ErrorBoundary } from './ErrorBoundary';
4+
import CompTest from './CompTest';
5+
6+
const AsyncCompTest = React.lazy(() => import('./AsyncCompTest'));
7+
8+
const App = () => {
9+
return (
10+
<div className="content">
11+
<div style={{ border: '1px solid white', padding: 20 }}>
12+
<ErrorBoundary elementId="comp-test-error">
13+
<CompTest />
14+
</ErrorBoundary>
15+
</div>
16+
<div style={{ border: '1px solid white', padding: 20 }}>
17+
<ErrorBoundary elementId='"async-comp-test-error"'>
18+
<AsyncCompTest />
19+
</ErrorBoundary>
20+
</div>
21+
</div>
22+
);
23+
};
24+
25+
export default App;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#async-comp-test {
2+
color: white;
3+
background-color: darkblue;
4+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import React from 'react';
2+
import './AsyncCompTest.css';
3+
4+
export default function AsyncCompTest() {
5+
return <div id="async-comp-test">Hello AsyncCompTest</div>;
6+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#comp-test {
2+
color: white;
3+
background-color: darkblue;
4+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import React from 'react';
2+
import './CompTest.css';
3+
4+
export default function CompTest() {
5+
return <div id="comp-test">Hello CompTest</div>;
6+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from 'react';
2+
3+
interface Props {
4+
elementId: string;
5+
children: React.ReactNode;
6+
}
7+
8+
class ErrorBoundary extends React.Component<
9+
Props,
10+
{ hasError: boolean; error: null | Error },
11+
unknown
12+
> {
13+
constructor(props: Props) {
14+
super(props);
15+
this.state = { hasError: false, error: null };
16+
}
17+
18+
static getDerivedStateFromError(error: Error) {
19+
return { hasError: true, error };
20+
}
21+
22+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
23+
console.log(error, errorInfo);
24+
}
25+
26+
render() {
27+
if (this.state.hasError) {
28+
return (
29+
<pre id={this.props.elementId}>{this.state?.error?.toString()}</pre>
30+
);
31+
}
32+
33+
return this.props.children;
34+
}
35+
}
36+
37+
export { ErrorBoundary };

e2e/cases/assets-retry/src/index.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import React from 'react';
2+
import ReactDOM from 'react-dom/client';
3+
import App from './App';
4+
5+
const root = ReactDOM.createRoot(document.getElementById('root')!);
6+
root.render(
7+
<React.StrictMode>
8+
<App />
9+
</React.StrictMode>,
10+
);

packages/plugin-assets-retry/src/runtime/index.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// rsbuild/initial-chunk/retry
12
import type { CrossOrigin } from '@rsbuild/shared';
23
import type {
34
PluginAssetsRetryOptions,
@@ -99,10 +100,13 @@ function createElement(
99100
if (attributes.isAsync) {
100101
script.dataset.rsbuildAsync = '';
101102
}
102-
103103
return {
104104
element: script,
105-
str: `<script src="${attributes.url}" ${crossOriginAttr} ${retryTimesAttr} ${isAsyncAttr}></script>`,
105+
str:
106+
// biome-ignore lint/style/useTemplate: use "</" + "script>" instead of script tag to avoid syntax error when inlining in html
107+
`<script src="${attributes.url}" ${crossOriginAttr} ${retryTimesAttr} ${isAsyncAttr}>` +
108+
'</' +
109+
'script>',
106110
};
107111
}
108112
if (origin instanceof HTMLLinkElement) {
@@ -278,7 +282,18 @@ function resourceMonitor(
278282

279283
// @ts-expect-error init is a global function, ignore ts(6133)
280284
function init(options: PluginAssetsRetryOptions) {
281-
const config = Object.assign({}, defaultConfig, options);
285+
const config: PluginAssetsRetryOptions = {};
286+
287+
for (const key in defaultConfig) {
288+
// @ts-ignore
289+
config[key] = defaultConfig[key];
290+
}
291+
292+
for (const key in options) {
293+
// @ts-ignore
294+
config[key] = options[key];
295+
}
296+
282297
// Normalize config
283298
if (!Array.isArray(config.type) || config.type.length === 0) {
284299
config.type = defaultConfig.type;

0 commit comments

Comments
 (0)