Skip to content

Commit bed17e4

Browse files
authored
docs: fix extra HTML end tags in docs build (adobe#9477)
* add temp script to fail build when errors in build detected * lint * try patch * try another build * make validation function more generic * cleanup script
1 parent ca4dfd3 commit bed17e4

4 files changed

Lines changed: 165 additions & 0 deletions

File tree

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,16 +154,19 @@ s2-docs:
154154
DOCS_ENV=stage PUBLIC_URL=/$(BRANCH_TYPE)/$(HASH) $(MAKE) build-s2-docs
155155
cp packages/dev/docs/pages/disallow-robots.txt dist/s2-docs/react-aria/$(BRANCH_TYPE)/$(HASH)/robots.txt
156156
cp packages/dev/docs/pages/disallow-robots.txt dist/s2-docs/s2/$(BRANCH_TYPE)/$(HASH)/robots.txt
157+
yarn check:s2-docs-build dist/s2-docs
157158

158159
s2-docs-stage:
159160
DOCS_ENV=stage PUBLIC_URL=/ $(MAKE) build-s2-docs
160161
cp packages/dev/docs/pages/disallow-robots.txt dist/s2-docs/react-aria/robots.txt
161162
cp packages/dev/docs/pages/disallow-robots.txt dist/s2-docs/s2/robots.txt
163+
yarn check:s2-docs-build dist/s2-docs
162164

163165
s2-docs-production:
164166
DOCS_ENV=prod PUBLIC_URL=/ $(MAKE) build-s2-docs
165167
cp packages/dev/docs/pages/robots.txt dist/s2-docs/react-aria/robots.txt
166168
cp packages/dev/docs/pages/robots.txt dist/s2-docs/s2/robots.txt
169+
yarn check:s2-docs-build dist/s2-docs
167170
cd starters/docs && yarn install --no-immutable && yarn up react-aria-components
168171
cd starters/tailwind && yarn install --no-immutable && yarn up react-aria-components tailwindcss-react-aria-components
169172
$(MAKE) build-starters

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"build:docs": "DOCS_ENV=staging parcel build 'packages/@react-{spectrum,aria,stately}/*/docs/*.mdx' 'packages/dev/docs/pages/{react-spectrum,releases}/**/*.mdx'",
3232
"start:s2-docs": "yarn workspace @react-spectrum/s2-docs start",
3333
"build:s2-docs": "yarn workspace @react-spectrum/s2-docs build",
34+
"check:s2-docs-build": "node packages/dev/s2-docs/scripts/validateS2DocsBuild.mjs",
3435
"build:mcp": "yarn workspace @react-spectrum/mcp build && yarn workspace @react-aria/mcp build",
3536
"start:mcp": "yarn workspace @react-spectrum/s2-docs generate:md && yarn build:mcp && node packages/dev/mcp/s2/dist/index.js && node packages/dev/mcp/react-aria/dist/index.js",
3637
"test:mcp": "yarn build:s2-docs && yarn build:mcp && node packages/dev/mcp/scripts/smoke-list-pages.mjs",
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Script to validate s2-docs build output.
4+
* - Confirms the build directory exists and contains files.
5+
* - Checks for duplicate </script></body></html> occurrences in HTML files.
6+
*
7+
* Usage: node scripts/validateS2DocsBuild.mjs [directory]
8+
* Default directory: ./dist
9+
*/
10+
11+
import {dirname, join} from 'path';
12+
import fg from 'fast-glob';
13+
import {fileURLToPath} from 'url';
14+
import {readFile, stat} from 'fs/promises';
15+
16+
const __filename = fileURLToPath(import.meta.url);
17+
const __dirname = dirname(__filename);
18+
19+
const PATTERN = '</script></body></html>';
20+
21+
async function collectBuildFiles(dir) {
22+
const files = await fg(['**/*'], {
23+
cwd: dir,
24+
onlyFiles: true,
25+
dot: true,
26+
absolute: true
27+
});
28+
29+
const htmlFiles = files.filter(filePath => filePath.endsWith('.html'));
30+
return {htmlFiles, totalFiles: files.length};
31+
}
32+
33+
async function checkFile(filePath) {
34+
const content = await readFile(filePath, 'utf-8');
35+
36+
// Count occurrences of the pattern
37+
const matches = content.match(new RegExp(PATTERN.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'));
38+
const count = matches ? matches.length : 0;
39+
40+
return {
41+
filePath,
42+
count,
43+
hasDuplicate: count > 1
44+
};
45+
}
46+
47+
async function main() {
48+
const targetDir = process.argv[2] || join(__dirname, '..', 'dist');
49+
50+
console.log('\nValidating s2-docs build output...');
51+
console.log(`Directory: ${targetDir}\n`);
52+
53+
let dirStats;
54+
try {
55+
dirStats = await stat(targetDir);
56+
} catch {
57+
console.error(`Build directory does not exist: ${targetDir}`);
58+
process.exit(1);
59+
}
60+
61+
if (!dirStats.isDirectory()) {
62+
console.error(`Build path is not a directory: ${targetDir}`);
63+
process.exit(1);
64+
}
65+
66+
let htmlFiles;
67+
let totalFiles;
68+
try {
69+
({htmlFiles, totalFiles} = await collectBuildFiles(targetDir));
70+
} catch (error) {
71+
console.error(`Error reading build directory: ${error.message}`);
72+
process.exit(1);
73+
}
74+
75+
if (totalFiles === 0) {
76+
console.error('Build directory is empty. No files found.');
77+
process.exit(1);
78+
}
79+
80+
if (htmlFiles.length === 0) {
81+
console.error('No HTML files found in the build output.');
82+
process.exit(1);
83+
}
84+
85+
console.log(`Found ${htmlFiles.length} HTML files to check.\n`);
86+
87+
const results = await Promise.all(htmlFiles.map(checkFile));
88+
const duplicates = results.filter(r => r.hasDuplicate);
89+
90+
if (duplicates.length === 0) {
91+
console.log('✅ All HTML validated for duplicate \'</script></body></html>\' occurrences.');
92+
} else {
93+
console.log(`❌ Found ${duplicates.length} file(s) with duplicate '</script></body></html>' occurrences:\n`);
94+
95+
for (const {filePath, count} of duplicates) {
96+
const relativePath = filePath.replace(targetDir, '').replace(/^\//, '');
97+
console.log(` 📄 ${relativePath}`);
98+
console.log(` Pattern appears ${count} times (expected: 1)\n`);
99+
}
100+
101+
process.exit(1);
102+
}
103+
}
104+
105+
main().catch(error => {
106+
console.error('Fatal error:', error);
107+
process.exit(1);
108+
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
diff --git a/node_modules/rsc-html-stream/server.js b/node_modules/rsc-html-stream/server.js
2+
index 1234567..abcdefg 100644
3+
--- a/node_modules/rsc-html-stream/server.js
4+
+++ b/node_modules/rsc-html-stream/server.js
5+
@@ -14,22 +14,22 @@ export function injectRSCPayload(rscStream, options) {
6+
let buffered = [];
7+
let timeout = null;
8+
function flushBufferedChunks(controller) {
9+
+ // Decode all buffered chunks together so we can reliably detect a trailer that
10+
+ // might be split across chunk boundaries.
11+
+ let combined = '';
12+
for (let chunk of buffered) {
13+
- let buf = decoder.decode(chunk, {stream: true});
14+
- if (buf.endsWith(trailer)) {
15+
- buf = buf.slice(0, -trailer.length);
16+
- }
17+
- controller.enqueue(encoder.encode(buf));
18+
+ combined += decoder.decode(chunk, {stream: true});
19+
}
20+
+ combined += decoder.decode();
21+
22+
- let remaining = decoder.decode();
23+
- if (remaining.length) {
24+
- if (remaining.endsWith(trailer)) {
25+
- remaining = remaining.slice(0, -trailer.length);
26+
- }
27+
- controller.enqueue(encoder.encode(remaining));
28+
+ if (combined.endsWith(trailer)) {
29+
+ combined = combined.slice(0, -trailer.length);
30+
}
31+
32+
+ if (combined.length) {
33+
+ controller.enqueue(encoder.encode(combined));
34+
+ }
35+
+
36+
buffered.length = 0;
37+
timeout = null;
38+
}
39+
@@ -42,7 +42,13 @@ export function injectRSCPayload(rscStream, options) {
40+
}
41+
42+
timeout = setTimeout(async () => {
43+
- flushBufferedChunks(controller);
44+
+ try {
45+
+ flushBufferedChunks(controller);
46+
+ } catch (e) {
47+
+ controller.error(e);
48+
+ resolveFlightDataPromise();
49+
+ return;
50+
+ }
51+
if (!startedRSC) {
52+
startedRSC = true;
53+
writeRSCStream(rscStream, controller, nonce)

0 commit comments

Comments
 (0)