Skip to content

Commit c8dc854

Browse files
committed
feat: 🎸 Implement cleanup of data-order and data-position attributes
1 parent 1aae25e commit c8dc854

File tree

5 files changed

+217
-97
lines changed

5 files changed

+217
-97
lines changed

.changeset/kind-nails-fetch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperse/html-webpack-plugin-loader": patch
3+
---
4+
5+
Implement cleanup of data-order and data-position attributes

src/utils/sortDocument.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ export const sortDocument = (document: DefaultTreeAdapterTypes.Document) => {
6767

6868
return !!(hasId && hasOrder && hasPosition);
6969
});
70+
71+
// Clean up data-order and data-position attributes from head
72+
cleanupSortingAttributes(head);
7073
}
7174

7275
if (body) {
@@ -84,6 +87,9 @@ export const sortDocument = (document: DefaultTreeAdapterTypes.Document) => {
8487

8588
return !!(hasId && hasOrder && hasPosition);
8689
});
90+
91+
// Clean up data-order and data-position attributes from body
92+
cleanupSortingAttributes(body);
8793
}
8894

8995
// 4. resort all nodes by data-order and data-position
@@ -161,3 +167,28 @@ const sortNodesByPosition = (
161167
element.childNodes.push(node);
162168
});
163169
};
170+
171+
/**
172+
* Remove data-order and data-position attributes from all child nodes recursively
173+
* @param element - The element to clean up attributes from
174+
*/
175+
const cleanupSortingAttributes = (element: DefaultTreeAdapterTypes.Element) => {
176+
// Clean up attributes from the current element
177+
if (element.attrs) {
178+
element.attrs = element.attrs.filter(
179+
(attr) => attr.name !== 'data-order' && attr.name !== 'data-position'
180+
);
181+
}
182+
183+
// Recursively clean up attributes from all child nodes
184+
element.childNodes.forEach((node) => {
185+
if (
186+
node.nodeName &&
187+
node.nodeName !== '#text' &&
188+
node.nodeName !== '#comment'
189+
) {
190+
const childElement = node as DefaultTreeAdapterTypes.Element;
191+
cleanupSortingAttributes(childElement);
192+
}
193+
});
194+
};

tests/parseTemplate.test.ts

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe('parseTemplate', () => {
2020
expect(result).toContain('<title>Test Title 2</title>');
2121
});
2222

23-
it('should corrent upsert body script by order', () => {
23+
it('should correctly upsert body script by order and remove sorting attributes', () => {
2424
const parser = parseTemplate('<html><body></body></html>', {
2525
bodyScripts: [
2626
{ id: 'script1', src: 'script1.js', position: 'beginning', order: 1 },
@@ -35,12 +35,15 @@ describe('parseTemplate', () => {
3535
{ id: 'script2', src: 'script2.js', position: 'beginning', order: 2 },
3636
])
3737
.serialize();
38-
expect(result).toContain(
39-
'<script id="script1" src="script1.js" data-order="1" data-position="beginning"></script>'
40-
);
41-
expect(result).toContain(
42-
'<script id="script2" src="script2.js" data-order="2" data-position="beginning"></script>'
43-
);
38+
39+
// Check that scripts are present but sorting attributes are removed
40+
expect(result).toContain('<script id="script1" src="script1.js"></script>');
41+
expect(result).toContain('<script id="script2" src="script2.js"></script>');
42+
43+
// Check that sorting attributes are not present in final output
44+
expect(result).not.toContain('data-order="1"');
45+
expect(result).not.toContain('data-order="2"');
46+
expect(result).not.toContain('data-position="beginning"');
4447
});
4548

4649
it('should update favicon when provided', () => {
@@ -68,7 +71,7 @@ describe('parseTemplate', () => {
6871
);
6972
});
7073

71-
it('should update head styles when provided', () => {
74+
it('should update head styles when provided and remove sorting attributes', () => {
7275
const styles: StyleItem[] = [
7376
{
7477
href: 'style.css',
@@ -80,12 +83,19 @@ describe('parseTemplate', () => {
8083
const parser = parseTemplate('<html><head></head></html>', {
8184
headStyles: styles,
8285
});
83-
expect(parser.serialize()).toContain(
84-
'<link rel="stylesheet" href="style.css" id="style1" data-order="1" data-position="beginning">'
86+
const result = parser.serialize();
87+
88+
// Check that style is present but sorting attributes are removed
89+
expect(result).toContain(
90+
'<link rel="stylesheet" href="style.css" id="style1">'
8591
);
92+
93+
// Check that sorting attributes are not present in final output
94+
expect(result).not.toContain('data-order="1"');
95+
expect(result).not.toContain('data-position="beginning"');
8696
});
8797

88-
it('should update inline styles when provided', () => {
98+
it('should update inline styles when provided and remove sorting attributes', () => {
8999
const inlineStyles: StyleInlineItem[] = [
90100
{
91101
content: 'body {}',
@@ -97,12 +107,17 @@ describe('parseTemplate', () => {
97107
const parser = parseTemplate('<html><head></head></html>', {
98108
headInlineStyles: inlineStyles,
99109
});
100-
expect(parser.serialize()).toContain(
101-
'<style id="style1" data-order="1" data-position="beginning">body {}</style>'
102-
);
110+
const result = parser.serialize();
111+
112+
// Check that style is present but sorting attributes are removed
113+
expect(result).toContain('<style id="style1">body {}</style>');
114+
115+
// Check that sorting attributes are not present in final output
116+
expect(result).not.toContain('data-order="1"');
117+
expect(result).not.toContain('data-position="beginning"');
103118
});
104119

105-
it('should update head scripts when provided', () => {
120+
it('should update head scripts when provided and remove sorting attributes', () => {
106121
const scripts: ScriptItem[] = [
107122
{
108123
src: 'script.js',
@@ -114,12 +129,17 @@ describe('parseTemplate', () => {
114129
const parser = parseTemplate('<html><head></head></html>', {
115130
headScripts: scripts,
116131
});
117-
expect(parser.serialize()).toContain(
118-
'<script id="script1" src="script.js" data-order="1" data-position="beginning"></script>'
119-
);
132+
const result = parser.serialize();
133+
134+
// Check that script is present but sorting attributes are removed
135+
expect(result).toContain('<script id="script1" src="script.js"></script>');
136+
137+
// Check that sorting attributes are not present in final output
138+
expect(result).not.toContain('data-order="1"');
139+
expect(result).not.toContain('data-position="beginning"');
120140
});
121141

122-
it('should update head inline scripts when provided', () => {
142+
it('should update head inline scripts when provided and remove sorting attributes', () => {
123143
const inlineScripts: ScriptInlineItem[] = [
124144
{
125145
content: 'console.log()',
@@ -131,12 +151,17 @@ describe('parseTemplate', () => {
131151
const parser = parseTemplate('<html><head></head></html>', {
132152
headInlineScripts: inlineScripts,
133153
});
134-
expect(parser.serialize()).toContain(
135-
'<script id="script1" data-order="1" data-position="beginning">console.log()</script>'
136-
);
154+
const result = parser.serialize();
155+
156+
// Check that script is present but sorting attributes are removed
157+
expect(result).toContain('<script id="script1">console.log()</script>');
158+
159+
// Check that sorting attributes are not present in final output
160+
expect(result).not.toContain('data-order="1"');
161+
expect(result).not.toContain('data-position="beginning"');
137162
});
138163

139-
it('should update body scripts when provided', () => {
164+
it('should update body scripts when provided and remove sorting attributes', () => {
140165
const bodyScripts: ScriptItem[] = [
141166
{
142167
src: 'script.js',
@@ -148,8 +173,13 @@ describe('parseTemplate', () => {
148173
const parser = parseTemplate('<html><head></head><body></body></html>', {
149174
bodyScripts: bodyScripts,
150175
});
151-
expect(parser.serialize()).toContain(
152-
'<script id="script1" src="script.js" data-order="1" data-position="beginning"></script>'
153-
);
176+
const result = parser.serialize();
177+
178+
// Check that script is present but sorting attributes are removed
179+
expect(result).toContain('<script id="script1" src="script.js"></script>');
180+
181+
// Check that sorting attributes are not present in final output
182+
expect(result).not.toContain('data-order="1"');
183+
expect(result).not.toContain('data-position="beginning"');
154184
});
155185
});

tests/parseTemplateOrder.test.ts

Lines changed: 41 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ describe('parseTemplate with correct DOM order', () => {
88
expect(parser).toBeInstanceOf(TemplateParser);
99
});
1010

11-
it('should maintain correct DOM order for all elements in head', () => {
11+
it('should maintain correct DOM order for all elements in head and remove sorting attributes', () => {
1212
const htmlSource = '<html><head></head><body></body></html>';
1313
const options: TemplateOptions = {
1414
title: 'Test Page Title',
@@ -72,60 +72,50 @@ describe('parseTemplate with correct DOM order', () => {
7272
// Extract head content for order validation
7373
const headMatch = result.match(/<head>([\s\S]*?)<\/head>/);
7474
expect(headMatch).toBeTruthy();
75+
7576
const headContent = headMatch![1];
7677

77-
// Create a simplified representation of the head content for order checking
78-
const headElements: string[] = headContent
79-
.split(/(?=<title>|<link|<meta|<style|<script)/)
80-
.filter((line) => line.trim())
81-
.map((line) => {
82-
if (line.includes('<title>')) return 'title';
83-
if (line.includes('rel="icon"')) return 'link[rel="icon"]';
84-
if (line.includes('name="description"'))
85-
return 'meta[name="description"]';
86-
if (line.includes('name="viewport"')) return 'meta[name="viewport"]';
87-
if (line.includes('id="critical-css"'))
88-
return 'style[id="critical-css"]';
89-
if (line.includes('id="main-css"'))
90-
return 'link[rel="stylesheet"][id="main-css"]';
91-
if (line.includes('id="main-js"')) return 'script[id="main-js"]';
92-
if (line.includes('id="vendor-css"'))
93-
return 'link[rel="stylesheet"][id="vendor-css"]';
94-
if (line.includes('id="vendor-js"')) return 'script[id="vendor-js"]';
95-
if (line.includes('id="inline-js"')) return 'script[id="inline-js"]';
96-
return 'other';
97-
})
98-
.filter((element) => element !== 'other');
78+
// Check that all expected elements are present
79+
expect(headContent).toContain('<title>Test Page Title</title>');
80+
expect(headContent).toContain(
81+
'<link rel="icon" href="/favicon.ico" sizes="32x32">'
82+
);
83+
expect(headContent).toContain(
84+
'<meta name="description" content="Test description">'
85+
);
86+
expect(headContent).toContain(
87+
'<meta name="viewport" content="width=device-width, initial-scale=1.0">'
88+
);
89+
expect(headContent).toContain(
90+
'<link rel="stylesheet" href="/styles/main.css" id="main-css">'
91+
);
92+
expect(headContent).toContain(
93+
'<link rel="stylesheet" href="/styles/vendor.css" id="vendor-css">'
94+
);
95+
expect(headContent).toContain(
96+
'<style id="critical-css">body { margin: 0; }</style>'
97+
);
98+
expect(headContent).toContain(
99+
'<script id="main-js" src="/scripts/main.js"></script>'
100+
);
101+
expect(headContent).toContain(
102+
'<script id="vendor-js" src="/scripts/vendor.js"></script>'
103+
);
104+
expect(headContent).toContain(
105+
'<script id="inline-js">console.log("Hello");</script>'
106+
);
99107

100-
// Verify that all expected elements are present
101-
expect(headElements).toContain('title');
102-
expect(headElements).toContain('link[rel="icon"]');
103-
expect(headElements).toContain('meta[name="description"]');
104-
expect(headElements).toContain('meta[name="viewport"]');
105-
expect(headElements).toContain('style[id="critical-css"]');
106-
expect(headElements).toContain('link[rel="stylesheet"][id="main-css"]');
107-
expect(headElements).toContain('script[id="main-js"]');
108-
expect(headElements).toContain('link[rel="stylesheet"][id="vendor-css"]');
109-
expect(headElements).toContain('script[id="vendor-js"]');
110-
expect(headElements).toContain('script[id="inline-js"]');
108+
// Check that sorting attributes are NOT present in final output
109+
expect(headContent).not.toContain('data-order');
110+
expect(headContent).not.toContain('data-position');
111111

112-
// Define the expected order based on the implementation
113-
const expectedOrder = [
114-
'script[id="main-js"]', // order=1
115-
// Beginning elements (sorted by order)
116-
'style[id="critical-css"]', // order=0
117-
'link[rel="stylesheet"][id="main-css"]', // order=1
118-
'title',
119-
'meta[name="description"]',
120-
'meta[name="viewport"]',
121-
'link[rel="icon"]',
122-
// End elements (sorted by order)
123-
'link[rel="stylesheet"][id="vendor-css"]', // order=2
124-
'script[id="vendor-js"]', // order=2
125-
'script[id="inline-js"]', // order=3
126-
];
112+
// Verify order: critical-css should come before main-css, and main-js should come before vendor-js
113+
const criticalCssIndex = headContent.indexOf('id="critical-css"');
114+
const mainCssIndex = headContent.indexOf('id="main-css"');
115+
const mainJsIndex = headContent.indexOf('id="main-js"');
116+
const vendorJsIndex = headContent.indexOf('id="vendor-js"');
127117

128-
// Verify the exact order
129-
expect(headElements).toEqual(expectedOrder);
118+
expect(criticalCssIndex).toBeLessThan(mainCssIndex);
119+
expect(mainJsIndex).toBeLessThan(vendorJsIndex);
130120
});
131121
});

0 commit comments

Comments
 (0)