Skip to content

Commit 18e7fb8

Browse files
committed
refactor(core): move report template out of core js output
Previously, core and report were tightly coupled at build time. apps/report depends on @midscene/core because the report UI reuses core types, dump structures, and helper logic. At the same time, the built core output needed to inline the report HTML template. This meant report had to build after core, but the final usable core output still depended on report building first and writing HTML back into core dist. The old flow scanned built core JS files, found magic strings such as REPLACE_ME_WITH_REPORT_HTML, and replaced them with the full report HTML. That flow was fragile and could repeatedly inject the template. Since report HTML is large and apps/report may bundle an already-injected core dist, the next report build could include the previous template inside the new template. Writing that back into core again could cause recursive HTML growth. This change separates responsibilities. apps/report now owns report template generation and syncs apps/report/dist/index.html into core as a static asset: packages/core/dist/report-template/index.html @midscene/core no longer inlines the report template into its JS output and no longer relies on magic string replacement. At runtime, core reads the template from its own package asset when generating reports. For bundled @midscene/core targets, static assets may not be included in the final bundle. Those targets can inject the template through: globalThis.__MIDSCENE_INTERNAL_REPORT_TEMPLATE_CONTENT__ Core reads this global first, before falling back to the local template file. Internal bundle targets use a build plugin that replaces @midscene/core entries with wrapper modules. The wrapper injects the report template first and then re-exports the original core entry. This puts template injection into the bundler module graph instead of rewriting final JS output.
1 parent dd61ae4 commit 18e7fb8

31 files changed

Lines changed: 431 additions & 446 deletions

File tree

CONTRIBUTING.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,13 +152,13 @@ cd apps/playground && pnpm run dev
152152
cd apps/chrome-extension && pnpm run dev
153153
```
154154

155-
### `REPLACE_ME_WITH_REPORT_HTML` error in the report file
155+
### Missing report template errors
156156

157157
`apps/report` is not standalone at runtime. Its built `index.html` template is
158-
injected back into `packages/core/dist` during build. If report UI changes do
159-
not show up, or you see `REPLACE_ME_WITH_REPORT_HTML` in the report file, the
160-
template injection is usually stale. Rebuild the entire workspace without Nx
161-
cache to fix it:
158+
synced into `packages/core/dist/report-template/index.html` during build. If
159+
report UI changes do not show up, or the runtime says the Midscene report
160+
template is missing, rebuild the entire workspace without Nx cache to refresh
161+
the synced template:
162162

163163
```sh
164164
# Rebuild the entire project without cache

apps/android-playground/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"react-dom": "18.3.1"
1616
},
1717
"devDependencies": {
18+
"@midscene/report": "workspace:*",
1819
"@rsbuild/core": "^1.6.15",
1920
"@rsbuild/plugin-less": "^1.5.0",
2021
"@rsbuild/plugin-node-polyfill": "1.4.2",

apps/android-playground/rsbuild.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { pluginWorkspaceDev } from 'rsbuild-plugin-workspace-dev';
99
import { version as playgroundVersion } from '../../packages/playground/package.json';
1010
import {
1111
commonIgnoreWarnings,
12+
createCoreReportTemplateReplacementPlugin,
1213
createPlaygroundCopyPlugin,
1314
} from '../../scripts/rsbuild-utils.ts';
1415

@@ -63,6 +64,9 @@ export default defineConfig({
6364
pluginNodePolyfill(),
6465
pluginLess(),
6566
pluginSvgr(),
67+
createCoreReportTemplateReplacementPlugin({
68+
appDir: __dirname,
69+
}),
6670
createPlaygroundCopyPlugin(
6771
path.join(__dirname, 'dist'),
6872
path.join(__dirname, '../../packages/android-playground/static'),

apps/chrome-extension/rsbuild.config.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import { pluginSvgr } from '@rsbuild/plugin-svgr';
77
import { pluginTypeCheck } from '@rsbuild/plugin-type-check';
88
import { pluginWorkspaceDev } from 'rsbuild-plugin-workspace-dev';
99
import { version } from '../../packages/visualizer/package.json';
10-
import { commonIgnoreWarnings } from '../../scripts/rsbuild-utils.ts';
10+
import {
11+
commonIgnoreWarnings,
12+
createCoreReportTemplateReplacementPlugin,
13+
} from '../../scripts/rsbuild-utils.ts';
1114

1215
export default defineConfig({
1316
tools: {
@@ -119,6 +122,9 @@ export default defineConfig({
119122
pluginNodePolyfill(),
120123
pluginLess(),
121124
pluginSvgr(),
125+
createCoreReportTemplateReplacementPlugin({
126+
appDir: __dirname,
127+
}),
122128
pluginTypeCheck(),
123129
pluginWorkspaceDev({
124130
projects: {

apps/computer-playground/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"react-dom": "18.3.1"
1616
},
1717
"devDependencies": {
18+
"@midscene/report": "workspace:*",
1819
"@rsbuild/core": "^1.6.15",
1920
"@rsbuild/plugin-less": "^1.5.0",
2021
"@rsbuild/plugin-node-polyfill": "1.4.2",

apps/computer-playground/rsbuild.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { pluginWorkspaceDev } from 'rsbuild-plugin-workspace-dev';
99
import { version as playgroundVersion } from '../../packages/playground/package.json';
1010
import {
1111
commonIgnoreWarnings,
12+
createCoreReportTemplateReplacementPlugin,
1213
createPlaygroundCopyPlugin,
1314
} from '../../scripts/rsbuild-utils.ts';
1415

@@ -63,6 +64,9 @@ export default defineConfig({
6364
pluginNodePolyfill(),
6465
pluginLess(),
6566
pluginSvgr(),
67+
createCoreReportTemplateReplacementPlugin({
68+
appDir: __dirname,
69+
}),
6670
createPlaygroundCopyPlugin(
6771
path.join(__dirname, 'dist'),
6872
path.join(__dirname, '../../packages/computer-playground/static'),

apps/playground/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"react-dom": "18.3.1"
1919
},
2020
"devDependencies": {
21+
"@midscene/report": "workspace:*",
2122
"@midscene/web": "workspace:*",
2223
"@rsbuild/core": "^1.6.15",
2324
"@rsbuild/plugin-less": "^1.5.0",

apps/playground/rsbuild.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { pluginWorkspaceDev } from 'rsbuild-plugin-workspace-dev';
99
import { version as playgroundVersion } from '../../packages/playground/package.json';
1010
import {
1111
commonIgnoreWarnings,
12+
createCoreReportTemplateReplacementPlugin,
1213
createPlaygroundCopyPlugin,
1314
} from '../../scripts/rsbuild-utils.ts';
1415

@@ -23,6 +24,9 @@ export default defineConfig({
2324
pluginLess(),
2425
pluginNodePolyfill(),
2526
pluginSvgr(),
27+
createCoreReportTemplateReplacementPlugin({
28+
appDir: __dirname,
29+
}),
2630
createPlaygroundCopyPlugin(
2731
path.join(__dirname, 'dist'),
2832
path.join(__dirname, '../../packages/playground/static'),

apps/report/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@
1313
"generate-demo": "node scripts/generate-demo-report.mjs",
1414
"e2e": "node scripts/generate-demo-report.mjs && node ../../packages/cli/bin/midscene ./e2e/"
1515
},
16+
"nx": {
17+
"targets": {
18+
"build": {
19+
"outputs": [
20+
"{projectRoot}/dist",
21+
"{workspaceRoot}/packages/core/dist/report-template"
22+
]
23+
}
24+
}
25+
},
1626
"dependencies": {
1727
"@ant-design/icons": "^5.3.1",
1828
"@midscene/core": "workspace:*",

apps/report/rsbuild.config.ts

Lines changed: 20 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import assert from 'node:assert';
21
import fs from 'node:fs';
32
import path from 'node:path';
43
import { defineConfig } from '@rsbuild/core';
@@ -8,7 +7,10 @@ import { pluginReact } from '@rsbuild/plugin-react';
87
import { pluginSvgr } from '@rsbuild/plugin-svgr';
98
import { pluginTypeCheck } from '@rsbuild/plugin-type-check';
109
import { pluginWorkspaceDev } from 'rsbuild-plugin-workspace-dev';
11-
import { commonIgnoreWarnings } from '../../scripts/rsbuild-utils.ts';
10+
import {
11+
commonIgnoreWarnings,
12+
createReportTemplateSyncPlugin,
13+
} from '../../scripts/rsbuild-utils.ts';
1214

1315
// Read all JSON files from test-data directory
1416
const testDataDir = path.join(__dirname, 'test-data');
@@ -24,88 +26,17 @@ const allTestData = jsonFiles.map((file) => {
2426
};
2527
});
2628

27-
// put back the report template to the core package
28-
// this is a workaround for the circular dependency issue
29-
// ERROR: This repository uses pkg in bundler mode. It is necessary to declare @midscene/report in the dependency; otherwise, it may cause packaging order issues and thus lead to the failure of report injection
30-
const copyReportTemplate = () => ({
31-
name: 'copy-report-template',
32-
setup(api: {
33-
onAfterBuild: (arg0: ({ compiler }: { compiler: any }) => void) => void;
34-
}) {
35-
api.onAfterBuild(({ compiler }) => {
36-
const magicString = 'REPLACE_ME_WITH_REPORT_HTML';
37-
const replacedMark = '/*REPORT_HTML_REPLACED*/';
38-
const regExpForReplace = /\/\*REPORT_HTML_REPLACED\*\/.*/;
39-
40-
// read the template file
41-
const srcPath = path.join(__dirname, 'dist', 'index.html');
42-
const tplFileContent = fs
43-
.readFileSync(srcPath, 'utf-8')
44-
.replaceAll(magicString, '');
45-
assert(
46-
!tplFileContent.includes(magicString),
47-
'magic string should not be in the template file',
48-
);
49-
const finalContent = `${replacedMark}${JSON.stringify(tplFileContent)}`;
50-
51-
// find the core package
52-
const corePkgDir = path.join(__dirname, '..', '..', 'packages', 'core');
53-
const corePkgJson = JSON.parse(
54-
fs.readFileSync(path.join(corePkgDir, 'package.json'), 'utf-8'),
55-
);
56-
assert(
57-
corePkgJson.name === '@midscene/core',
58-
'core package name is not @midscene/core',
59-
);
60-
const corePkgDistDir = path.join(corePkgDir, 'dist');
61-
62-
// traverse all .js files and inject (or update) the template
63-
const jsFiles = fs.readdirSync(corePkgDistDir, { recursive: true });
64-
let replacedCount = 0;
65-
for (const file of jsFiles) {
66-
if (
67-
typeof file === 'string' &&
68-
(file.endsWith('.js') || file.endsWith('.mjs'))
69-
) {
70-
const filePath = path.join(corePkgDistDir, file.toString());
71-
const fileContent = fs.readFileSync(filePath, 'utf-8');
72-
if (fileContent.includes(replacedMark)) {
73-
assert(
74-
regExpForReplace.test(fileContent),
75-
'a replaced mark is found but cannot match',
76-
);
77-
78-
const replacedContent = fileContent.replace(
79-
regExpForReplace,
80-
() => finalContent,
81-
);
82-
fs.writeFileSync(filePath, replacedContent);
83-
replacedCount++;
84-
console.log(`Template updated in file ${filePath}`);
85-
} else if (fileContent.includes(magicString)) {
86-
const magicStringCount = (
87-
fileContent.match(new RegExp(magicString, 'g')) || []
88-
).length;
89-
assert(
90-
magicStringCount === 1,
91-
'magic string shows more than once in the file, cannot process',
92-
);
93-
const replacedContent = fileContent.replace(
94-
`'${magicString}'`,
95-
() => finalContent, // there are some $- code in the tpl, so we have to use a function as the second argument
96-
);
97-
fs.writeFileSync(filePath, replacedContent);
98-
replacedCount++;
99-
console.log(`Template injected into ${filePath}`);
100-
}
101-
}
102-
}
103-
if (replacedCount === 0) {
104-
throw new Error('No html template found in the core package');
105-
}
106-
});
107-
},
108-
});
29+
const reportTemplatePath = path.join(__dirname, 'dist', 'index.html');
30+
const coreReportTemplatePath = path.join(
31+
__dirname,
32+
'..',
33+
'..',
34+
'packages',
35+
'core',
36+
'dist',
37+
'report-template',
38+
'index.html',
39+
);
10940

11041
export default defineConfig({
11142
html: {
@@ -163,7 +94,11 @@ export default defineConfig({
16394
pluginLess(),
16495
pluginNodePolyfill(),
16596
pluginSvgr(),
166-
copyReportTemplate(),
97+
createReportTemplateSyncPlugin({
98+
srcPath: reportTemplatePath,
99+
destPath: coreReportTemplatePath,
100+
pluginName: 'sync-report-template-to-core',
101+
}),
167102
pluginTypeCheck(),
168103
pluginWorkspaceDev({
169104
projects: {

0 commit comments

Comments
 (0)