Skip to content

Commit 001a5f8

Browse files
committed
feat: 🎸 Enhanced template parsing logic to ensure correct order
1 parent 9b90f65 commit 001a5f8

26 files changed

+1821
-801
lines changed

.yarn/releases/yarn-4.9.2.cjs renamed to .yarn/releases/yarn-4.9.4.cjs

Lines changed: 358 additions & 358 deletions
Large diffs are not rendered by default.

.yarnrc.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ enableGlobalCache: false
44

55
nodeLinker: node-modules
66

7-
yarnPath: .yarn/releases/yarn-4.9.2.cjs
7+
yarnPath: .yarn/releases/yarn-4.9.4.cjs

README.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -383,23 +383,19 @@ The loader accepts only one option:
383383
### Best Practices
384384

385385
1. **Template Organization**:
386-
387386
- Keep your HTML templates in a dedicated directory (e.g., `src/templates/`)
388387
- Use consistent naming conventions for your template files
389388

390389
2. **Environment-specific Configuration**:
391-
392390
- Use webpack's environment configuration to manage different settings for development and production
393391
- Consider using environment variables for sensitive or environment-specific values
394392

395393
3. **Performance Optimization**:
396-
397394
- Use `position: 'end'` for non-critical scripts and styles
398395
- Use `position: 'beginning'` for critical resources
399396
- Consider using `order` property to control the loading sequence
400397

401398
4. **Security**:
402-
403399
- Always use `integrity` checks for external resources when possible
404400
- Use `nonce` for inline scripts when implementing CSP
405401
- Set appropriate `crossOrigin` attributes for external resources

package.json

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,29 +55,29 @@
5555
}
5656
},
5757
"dependencies": {
58-
"parse5": "^7.3.0"
58+
"parse5": "^8.0.0"
5959
},
6060
"devDependencies": {
6161
"@changesets/changelog-github": "^0.5.1",
62-
"@changesets/cli": "^2.29.4",
62+
"@changesets/cli": "^2.29.6",
6363
"@commitlint/cli": "^19.8.1",
6464
"@commitlint/config-conventional": "^19.8.1",
65-
"@hyperse/eslint-config-hyperse": "^1.4.5",
65+
"@hyperse/eslint-config-hyperse": "^1.4.8",
6666
"@hyperse/ts-node": "^1.0.3",
67-
"@types/node": "^24.0.1",
67+
"@types/node": "^24.3.0",
6868
"commitizen": "^4.3.1",
6969
"cz-conventional-changelog": "^3.3.0",
70-
"eslint": "^9.28.0",
71-
"html-webpack-plugin": "^5.6.3",
70+
"eslint": "^9.34.0",
71+
"html-webpack-plugin": "^5.6.4",
7272
"husky": "^9.1.7",
73-
"lint-staged": "^16.1.0",
73+
"lint-staged": "^16.1.6",
7474
"npm-run-all": "^4.1.5",
7575
"tsup": "^8.5.0",
76-
"typescript": "^5.8.3",
77-
"vitest": "^3.2.3",
78-
"webpack": "^5.99.9"
76+
"typescript": "^5.9.2",
77+
"vitest": "^3.2.4",
78+
"webpack": "^5.101.3"
7979
},
80-
"packageManager": "yarn@4.9.2",
80+
"packageManager": "yarn@4.9.4",
8181
"engines": {
8282
"node": ">=20"
8383
},

src/parser/TemplateParser.ts

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import type {
66
StyleItem,
77
} from '../types.js';
88
import { parseDocument } from '../utils/parseDocument.js';
9+
import { sortDocument } from '../utils/sortDocument.js';
10+
import { upsertBodySctipts } from './upsertBodySctipts.js';
911
import { upsertFavicon } from './upsertFavicon.js';
1012
import { upsertHeadInlineScripts } from './upsertHeadInlineScripts.js';
1113
import { upsertHeadInlineStyles } from './upsertHeadInlineStyles.js';
@@ -18,16 +20,18 @@ export class TemplateParser {
1820
protected readonly document: DefaultTreeAdapterTypes.Document;
1921
protected readonly head: DefaultTreeAdapterTypes.Element;
2022
protected readonly body: DefaultTreeAdapterTypes.Element;
23+
protected readonly html: DefaultTreeAdapterTypes.Element;
2124

2225
constructor(htmlSource: string) {
23-
const { document, head, body } = parseDocument(htmlSource);
26+
const { document, head, body, html } = parseDocument(htmlSource);
2427
this.document = document;
28+
this.html = html;
2529
this.head = head;
2630
this.body = body;
2731
}
2832

2933
/**
30-
* Upsert the title tag
34+
* Upsert the title tag - inserts at beginning of <head>
3135
* @param title - The title to upsert
3236
* @returns The TemplateParser instance
3337
*/
@@ -37,7 +41,7 @@ export class TemplateParser {
3741
}
3842

3943
/**
40-
* Upsert the favicon tag
44+
* Upsert the favicon tag - inserts at beginning of <head> after title tag
4145
* @param href - The favicon to upsert
4246
* @param rel - The rel attribute of the favicon tag
4347
* @param attributes - The attributes of the favicon tag
@@ -53,8 +57,8 @@ export class TemplateParser {
5357
}
5458

5559
/**
56-
* Upsert the head before html tags
57-
* @param tags - The tags to upsert
60+
* Upsert meta tags in <head> - inserts at beginning for SEO priority
61+
* @param tags - The meta tags to upsert
5862
* @returns The TemplateParser instance
5963
*/
6064
public upsertHeadMetaTags(tags: string[]): TemplateParser {
@@ -63,8 +67,8 @@ export class TemplateParser {
6367
}
6468

6569
/**
66-
* Upsert the head before styles
67-
* @param styles - The styles to upsert
70+
* Upsert external stylesheets in <head> - supports position-based insertion
71+
* @param styles - The external styles to upsert
6872
* @returns The TemplateParser instance
6973
*/
7074
public upsertHeadStyles(styles: StyleItem[]): TemplateParser {
@@ -73,8 +77,8 @@ export class TemplateParser {
7377
}
7478

7579
/**
76-
* Upsert the head inline styles
77-
* @param styles - The styles to upsert
80+
* Upsert inline styles in <head> - supports position-based insertion
81+
* @param styles - The inline styles to upsert
7882
* @returns The TemplateParser instance
7983
*/
8084
public upsertHeadInlineStyles(styles: StyleInlineItem[]): TemplateParser {
@@ -83,8 +87,8 @@ export class TemplateParser {
8387
}
8488

8589
/**
86-
* Upsert the head before scripts
87-
* @param scripts - The scripts to upsert
90+
* Upsert external scripts in <head> - supports position-based insertion
91+
* @param scripts - The external scripts to upsert
8892
* @returns The TemplateParser instance
8993
*/
9094
public upsertHeadScripts(scripts: ScriptItem[]): TemplateParser {
@@ -93,8 +97,8 @@ export class TemplateParser {
9397
}
9498

9599
/**
96-
* Upsert the inline scripts
97-
* @param scripts - The scripts to upsert
100+
* Upsert inline scripts in <head> - supports position-based insertion
101+
* @param scripts - The inline scripts to upsert
98102
* @returns The TemplateParser instance
99103
*/
100104
public upsertHeadInlineScripts(scripts: ScriptInlineItem[]): TemplateParser {
@@ -103,12 +107,12 @@ export class TemplateParser {
103107
}
104108

105109
/**
106-
* Upsert the body after scripts
110+
* Upsert scripts in <body> - supports position-based insertion, typically at end for performance
107111
* @param scripts - The scripts to upsert
108112
* @returns The TemplateParser instance
109113
*/
110114
public upsertBodyScripts(scripts: ScriptItem[]): TemplateParser {
111-
upsertScripts(this.body, scripts);
115+
upsertBodySctipts(this.body, scripts);
112116
return this;
113117
}
114118

@@ -117,7 +121,8 @@ export class TemplateParser {
117121
* @returns The serialized html string
118122
*/
119123
public serialize(): string {
120-
return serialize(this.document);
124+
const sortedDocument = sortDocument(this.document);
125+
return serialize(sortedDocument);
121126
}
122127

123128
/**

src/parser/parseTemplate.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ export const parseTemplate = (
1313
): TemplateParser => {
1414
const parser = new TemplateParser(htmlSource);
1515

16-
if (options.title) {
17-
parser.upsertTitleTag(options.title);
16+
// 1. Meta tags in head - processed first, important for SEO and viewport settings
17+
if (options.headMetaTags?.length) {
18+
parser.upsertHeadMetaTags(options.headMetaTags);
1819
}
1920

21+
// 2. Favicon in head - processed second, typically placed early in <head> before title tag
2022
if (options.favicon) {
2123
parser.upsertFaviconTag(
2224
options.favicon.href,
@@ -25,26 +27,32 @@ export const parseTemplate = (
2527
);
2628
}
2729

28-
if (options.headMetaTags?.length) {
29-
parser.upsertHeadMetaTags(options.headMetaTags);
30+
// 3. Title tag in head - processed third to ensure it appears at the beginning of <head>, after favicon tag
31+
if (options.title) {
32+
parser.upsertTitleTag(options.title);
3033
}
3134

35+
// 4. External stylesheets in head- processed fourth, loaded before inline styles after title tag
3236
if (options.headStyles?.length) {
3337
parser.upsertHeadStyles(options.headStyles);
3438
}
3539

40+
// 5. Inline styles in head- processed fifth, can override external styles after external styles tag
3641
if (options.headInlineStyles?.length) {
3742
parser.upsertHeadInlineStyles(options.headInlineStyles);
3843
}
3944

45+
// 6. External scripts in head - processed sixth, loaded before inline scripts after inline styles tag
4046
if (options.headScripts?.length) {
4147
parser.upsertHeadScripts(options.headScripts);
4248
}
4349

50+
// 7. Inline scripts in head - processed seventh, executed after external scripts after external scripts tag
4451
if (options.headInlineScripts?.length) {
4552
parser.upsertHeadInlineScripts(options.headInlineScripts);
4653
}
4754

55+
// 8. Body scripts - processed last, typically placed at end of <body> for performance after inline scripts tag
4856
if (options.bodyScripts?.length) {
4957
parser.upsertBodyScripts(options.bodyScripts);
5058
}

src/parser/upsertBodySctipts.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { type DefaultTreeAdapterTypes } from 'parse5';
2+
import type { ScriptItem } from '../types.js';
3+
import { upsertScripts } from './upsertScripts.js';
4+
5+
/**
6+
* Updates the body scripts
7+
* @param body - The body element
8+
* @param scripts - The scripts to update
9+
* @returns The updated body element
10+
*/
11+
export const upsertBodySctipts = (
12+
body: DefaultTreeAdapterTypes.Element,
13+
scripts: ScriptItem[]
14+
) => {
15+
return upsertScripts(body, scripts);
16+
};

src/parser/upsertFavicon.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { type DefaultTreeAdapterTypes, parseFragment } from 'parse5';
22

33
/**
4-
* Upsert the favicon tag
4+
* Upserts a favicon link tag into the head element. The tag is inserted after the last meta tag,
5+
* or at the beginning of head if no meta tags exist. If a link tag with the same rel attribute
6+
* already exists, it will be replaced.
57
* @param head - The head element
68
* @param favicon - The favicon to upsert
79
* @param rel - The rel attribute value
@@ -35,5 +37,25 @@ export const upsertFavicon = (
3537
`<link rel="${rel}" href="${href}" ${attributesString}>`
3638
).childNodes[0] as DefaultTreeAdapterTypes.Element;
3739

38-
head.childNodes.push(linkNode);
40+
// Find the position after the last meta tag
41+
let insertIndex = 0;
42+
let lastMetaIndex = -1;
43+
44+
// Find the last meta tag position
45+
for (let i = 0; i < head.childNodes.length; i++) {
46+
if (head.childNodes[i].nodeName === 'meta') {
47+
lastMetaIndex = i;
48+
}
49+
}
50+
51+
// If meta tags exist, insert after the last one
52+
if (lastMetaIndex >= 0) {
53+
insertIndex = lastMetaIndex + 1;
54+
} else {
55+
// If no meta tags exist, insert at the beginning
56+
insertIndex = 0;
57+
}
58+
59+
// Insert at the found position
60+
head.childNodes.splice(insertIndex, 0, linkNode);
3961
};

src/parser/upsertHeadInlineScripts.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const upsertHeadInlineScripts = (
3434
// Create script nodes
3535
const scriptTags = sortedScripts.map((script) => {
3636
const scriptNode = parseFragment(
37-
`<script id="${script.id}">${script.content}</script>`
37+
`<script id="${script.id}" data-order="${script.order}" data-position="${script.position}">${script.content}</script>`
3838
).childNodes[0] as DefaultTreeAdapterTypes.Element;
3939
return scriptNode;
4040
});

src/parser/upsertHeadInlineStyles.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const upsertHeadInlineStyles = (
3333
// Create new style nodes
3434
const styleTags = sortedStyles.map((style) => {
3535
const styleNode = parseFragment(
36-
`<style id="${style.id}">${style.content}</style>`
36+
`<style id="${style.id}" data-order="${style.order}" data-position="${style.position}">${style.content}</style>`
3737
).childNodes[0] as DefaultTreeAdapterTypes.Element;
3838

3939
return styleNode;

0 commit comments

Comments
 (0)