@jsenv/ast 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +46 -0
- package/src/css/apply_post_css.js +57 -0
- package/src/css/parcel_css.js +58 -0
- package/src/css/postcss_plugin_url_visitor.js +287 -0
- package/src/html/html_analysis.js +35 -0
- package/src/html/html_inlining.js +40 -0
- package/src/html/html_node.js +112 -0
- package/src/html/html_node_attributes.js +43 -0
- package/src/html/html_node_position.js +122 -0
- package/src/html/html_node_text.js +32 -0
- package/src/html/html_parse.js +89 -0
- package/src/html/html_search.js +64 -0
- package/src/html/html_src_set.js +18 -0
- package/src/js/apply_babel_plugins.js +100 -0
- package/src/js/babel_utils.js +64 -0
- package/src/js/js_parse_error.js +15 -0
- package/src/js/js_static_analysis/helpers.js +17 -0
- package/src/js/js_static_analysis/import_export.js +135 -0
- package/src/js/js_static_analysis/new_url.js +132 -0
- package/src/js/js_static_analysis/system.js +74 -0
- package/src/js/js_static_analysis/web_worker.js +32 -0
- package/src/js/js_static_analysis/web_worker_entry_point.js +143 -0
- package/src/js/parse_js_urls.js +122 -0
- package/src/js/parse_js_with_acorn.js +40 -0
- package/src/main.js +44 -0
- package/src/require.js +3 -0
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jsenv/ast",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Parse and transform HTML,CSS and JS",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/jsenv/jsenv-core",
|
|
9
|
+
"directory": "packages/ast"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public",
|
|
13
|
+
"registry": "https://registry.npmjs.org"
|
|
14
|
+
},
|
|
15
|
+
"author": {
|
|
16
|
+
"name": "dmail",
|
|
17
|
+
"email": "dmaillard06@gmail.com",
|
|
18
|
+
"url": "https://twitter.com/damienmaillard"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=16.14.0"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"import": "./src/main.js"
|
|
27
|
+
},
|
|
28
|
+
"./*": "./*"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"/src/"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "node --conditions=development ./scripts/test.mjs"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@babel/parser": "7.18.5",
|
|
38
|
+
"@parcel/css": "1.10.1",
|
|
39
|
+
"acorn": "8.7.1",
|
|
40
|
+
"acorn-import-assertions": "1.8.0",
|
|
41
|
+
"acorn-walk": "8.2.0",
|
|
42
|
+
"parse5": "7.0.0",
|
|
43
|
+
"postcss": "8.4.14",
|
|
44
|
+
"postcss-value-parser": "4.2.0"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url"
|
|
2
|
+
|
|
3
|
+
export const applyPostCss = async ({
|
|
4
|
+
sourcemaps = "comment",
|
|
5
|
+
plugins,
|
|
6
|
+
// https://github.com/postcss/postcss#options
|
|
7
|
+
options = {},
|
|
8
|
+
url,
|
|
9
|
+
map,
|
|
10
|
+
content,
|
|
11
|
+
}) => {
|
|
12
|
+
const { default: postcss } = await import("postcss")
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const cssFileUrl = urlToFileUrl(url)
|
|
16
|
+
const result = await postcss(plugins).process(content, {
|
|
17
|
+
collectUrls: true,
|
|
18
|
+
from: fileURLToPath(cssFileUrl),
|
|
19
|
+
to: fileURLToPath(cssFileUrl),
|
|
20
|
+
map: {
|
|
21
|
+
annotation: sourcemaps === "file",
|
|
22
|
+
inline: sourcemaps === "inline",
|
|
23
|
+
// https://postcss.org/api/#sourcemapoptions
|
|
24
|
+
...(map ? { prev: JSON.stringify(map) } : {}),
|
|
25
|
+
},
|
|
26
|
+
...options,
|
|
27
|
+
})
|
|
28
|
+
return {
|
|
29
|
+
postCssMessages: result.messages,
|
|
30
|
+
map: result.map.toJSON(),
|
|
31
|
+
content: result.css,
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error.name === "CssSyntaxError") {
|
|
35
|
+
console.error(String(error))
|
|
36
|
+
throw error
|
|
37
|
+
}
|
|
38
|
+
throw error
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// the goal of this function is to take an url that is likely an http url
|
|
43
|
+
// info a file:// url
|
|
44
|
+
// for instance http://example.com/dir/file.js
|
|
45
|
+
// must becomes file:///dir/file.js
|
|
46
|
+
// but in windows it must be file://C:/dir/file.js
|
|
47
|
+
const filesystemRootUrl =
|
|
48
|
+
process.platform === "win32" ? `file:///${process.cwd()[0]}:/` : "file:///"
|
|
49
|
+
const urlToFileUrl = (url) => {
|
|
50
|
+
const urlString = String(url)
|
|
51
|
+
if (urlString.startsWith("file:")) {
|
|
52
|
+
return urlString
|
|
53
|
+
}
|
|
54
|
+
const origin = new URL(url).origin
|
|
55
|
+
const afterOrigin = urlString.slice(origin.length)
|
|
56
|
+
return new URL(afterOrigin, filesystemRootUrl).href
|
|
57
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createRequire } from "node:module"
|
|
2
|
+
import { fileURLToPath } from "node:url"
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url)
|
|
5
|
+
|
|
6
|
+
export const transpileWithParcel = (urlInfo, context) => {
|
|
7
|
+
const css = require("@parcel/css")
|
|
8
|
+
const targets = runtimeCompatToTargets(context.runtimeCompat)
|
|
9
|
+
const { code, map } = css.transform({
|
|
10
|
+
filename: fileURLToPath(urlInfo.originalUrl),
|
|
11
|
+
code: Buffer.from(urlInfo.content),
|
|
12
|
+
targets,
|
|
13
|
+
minify: false,
|
|
14
|
+
})
|
|
15
|
+
return { code, map }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const minifyWithParcel = (urlInfo, context) => {
|
|
19
|
+
const css = require("@parcel/css")
|
|
20
|
+
const targets = runtimeCompatToTargets(context.runtimeCompat)
|
|
21
|
+
const { code, map } = css.transform({
|
|
22
|
+
filename: fileURLToPath(urlInfo.originalUrl),
|
|
23
|
+
code: Buffer.from(urlInfo.content),
|
|
24
|
+
targets,
|
|
25
|
+
minify: true,
|
|
26
|
+
})
|
|
27
|
+
return { code, map }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const bundleWithParcel = (urlInfo, context) => {
|
|
31
|
+
const targets = runtimeCompatToTargets(context.runtimeCompat)
|
|
32
|
+
const css = require("@parcel/css")
|
|
33
|
+
const { code, map } = css.bundle({
|
|
34
|
+
filename: fileURLToPath(urlInfo.originalUrl),
|
|
35
|
+
targets,
|
|
36
|
+
minify: false,
|
|
37
|
+
})
|
|
38
|
+
return { code, map }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const runtimeCompatToTargets = (runtimeCompat) => {
|
|
42
|
+
const targets = {}
|
|
43
|
+
;["chrome", "firefox", "ie", "opera", "safari"].forEach((runtimeName) => {
|
|
44
|
+
const version = runtimeCompat[runtimeName]
|
|
45
|
+
if (version) {
|
|
46
|
+
targets[runtimeName] = versionToBits(version)
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
return targets
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const versionToBits = (version) => {
|
|
53
|
+
const [major, minor = 0, patch = 0] = version
|
|
54
|
+
.split("-")[0]
|
|
55
|
+
.split(".")
|
|
56
|
+
.map((v) => parseInt(v, 10))
|
|
57
|
+
return (major << 16) | (minor << 8) | patch
|
|
58
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
|
|
3
|
+
https://github.com/postcss/postcss/blob/master/docs/writing-a-plugin.md
|
|
4
|
+
https://github.com/postcss/postcss/blob/master/docs/guidelines/plugin.md
|
|
5
|
+
https://github.com/postcss/postcss/blob/master/docs/guidelines/runner.md#31-dont-show-js-stack-for-csssyntaxerror
|
|
6
|
+
|
|
7
|
+
In case css sourcemap contains no%20source
|
|
8
|
+
This is because of https://github.com/postcss/postcss/blob/fd30d3df5abc0954a0ec642a3cdc644ab2aacf9c/lib/map-generator.js#L231
|
|
9
|
+
and it indicates a node has been replaced without passing source
|
|
10
|
+
hence sourcemap cannot point the original source location
|
|
11
|
+
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { pathToFileURL } from "node:url"
|
|
15
|
+
|
|
16
|
+
import { require } from "../require.js"
|
|
17
|
+
|
|
18
|
+
export const postCssPluginUrlVisitor = ({ urlVisitor = () => null }) => {
|
|
19
|
+
const parseCssValue = require("postcss-value-parser")
|
|
20
|
+
const stringifyCssNodes = parseCssValue.stringify
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
postcssPlugin: "url_visitor",
|
|
24
|
+
prepare: (result) => {
|
|
25
|
+
const { from } = result.opts
|
|
26
|
+
const fromUrl = String(pathToFileURL(from))
|
|
27
|
+
const mutations = []
|
|
28
|
+
return {
|
|
29
|
+
AtRule: {
|
|
30
|
+
import: (atImportNode, { AtRule }) => {
|
|
31
|
+
if (atImportNode.parent.type !== "root") {
|
|
32
|
+
atImportNode.warn(result, "`@import` should be top level")
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
if (atImportNode.nodes) {
|
|
36
|
+
atImportNode.warn(
|
|
37
|
+
result,
|
|
38
|
+
"`@import` was not terminated correctly",
|
|
39
|
+
)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
const parsed = parseCssValue(atImportNode.params)
|
|
43
|
+
let [urlNode] = parsed.nodes
|
|
44
|
+
if (
|
|
45
|
+
!urlNode ||
|
|
46
|
+
(urlNode.type !== "string" && urlNode.type !== "function")
|
|
47
|
+
) {
|
|
48
|
+
atImportNode.warn(
|
|
49
|
+
result,
|
|
50
|
+
`No URL in \`${atImportNode.toString()}\``,
|
|
51
|
+
)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
let url = ""
|
|
55
|
+
if (urlNode.type === "string") {
|
|
56
|
+
url = urlNode.value
|
|
57
|
+
} else if (urlNode.type === "function") {
|
|
58
|
+
// Invalid function
|
|
59
|
+
if (!/^url$/i.test(urlNode.value)) {
|
|
60
|
+
atImportNode.warn(
|
|
61
|
+
result,
|
|
62
|
+
`Invalid \`url\` function in \`${atImportNode.toString()}\``,
|
|
63
|
+
)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
const firstNode = urlNode.nodes[0]
|
|
67
|
+
if (firstNode && firstNode.type === "string") {
|
|
68
|
+
urlNode = firstNode
|
|
69
|
+
url = urlNode.value
|
|
70
|
+
} else {
|
|
71
|
+
urlNode = urlNode.nodes
|
|
72
|
+
url = stringifyCssNodes(urlNode.nodes)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
url = url.trim()
|
|
77
|
+
if (url.length === 0) {
|
|
78
|
+
atImportNode.warn(
|
|
79
|
+
result,
|
|
80
|
+
`Empty URL in \`${atImportNode.toString()}\``,
|
|
81
|
+
)
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const specifier = url
|
|
86
|
+
url = new URL(specifier, fromUrl).href
|
|
87
|
+
if (url === fromUrl) {
|
|
88
|
+
atImportNode.warn(
|
|
89
|
+
result,
|
|
90
|
+
`\`@import\` loop in \`${atImportNode.toString()}\``,
|
|
91
|
+
)
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const atRuleStart = atImportNode.source.start.offset
|
|
96
|
+
const atRuleEnd = atImportNode.source.end.offset + 1 // for the ";"
|
|
97
|
+
const atRuleRaw = atImportNode.source.input.css.slice(
|
|
98
|
+
atRuleStart,
|
|
99
|
+
atRuleEnd,
|
|
100
|
+
)
|
|
101
|
+
const specifierIndex = atRuleRaw.indexOf(atImportNode.params)
|
|
102
|
+
const specifierStart = atRuleStart + specifierIndex
|
|
103
|
+
const specifierEnd = specifierStart + atImportNode.params.length
|
|
104
|
+
const specifierLine = atImportNode.source.start.line
|
|
105
|
+
const specifierColumn =
|
|
106
|
+
atImportNode.source.start.column + specifierIndex
|
|
107
|
+
urlVisitor({
|
|
108
|
+
declarationNode: atImportNode,
|
|
109
|
+
type: "@import",
|
|
110
|
+
atRuleStart,
|
|
111
|
+
atRuleEnd,
|
|
112
|
+
specifier,
|
|
113
|
+
specifierLine,
|
|
114
|
+
specifierColumn,
|
|
115
|
+
specifierStart,
|
|
116
|
+
specifierEnd,
|
|
117
|
+
replace: (newUrlSpecifier) => {
|
|
118
|
+
if (newUrlSpecifier === urlNode.value) {
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
urlNode.value = newUrlSpecifier
|
|
122
|
+
const newParams = parsed.toString()
|
|
123
|
+
const newAtImportRule = new AtRule({
|
|
124
|
+
name: "import",
|
|
125
|
+
params: newParams,
|
|
126
|
+
source: atImportNode.source,
|
|
127
|
+
})
|
|
128
|
+
atImportNode.replaceWith(newAtImportRule)
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
Declaration: (declarationNode) => {
|
|
134
|
+
const parsed = parseCssValue(declarationNode.value)
|
|
135
|
+
const urlMutations = []
|
|
136
|
+
walkUrls(parsed, {
|
|
137
|
+
stringifyCssNodes,
|
|
138
|
+
visitor: ({ url, urlNode }) => {
|
|
139
|
+
// Empty URL
|
|
140
|
+
if (!urlNode || url.length === 0) {
|
|
141
|
+
declarationNode.warn(
|
|
142
|
+
result,
|
|
143
|
+
`Empty URL in \`${declarationNode.toString()}\``,
|
|
144
|
+
)
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
// Skip Data URI
|
|
148
|
+
if (isDataUrl(url)) {
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
const specifier = url
|
|
152
|
+
url = new URL(specifier, pathToFileURL(from))
|
|
153
|
+
|
|
154
|
+
const declarationNodeStart = declarationNode.source.start.offset
|
|
155
|
+
const afterDeclarationNode =
|
|
156
|
+
declarationNode.source.input.css.slice(declarationNodeStart)
|
|
157
|
+
const valueIndex = afterDeclarationNode.indexOf(
|
|
158
|
+
declarationNode.value,
|
|
159
|
+
)
|
|
160
|
+
const valueStart = declarationNodeStart + valueIndex
|
|
161
|
+
const specifierStart = valueStart + urlNode.sourceIndex
|
|
162
|
+
const specifierEnd =
|
|
163
|
+
specifierStart +
|
|
164
|
+
(urlNode.type === "word"
|
|
165
|
+
? urlNode.value.length
|
|
166
|
+
: urlNode.value.length + 2) // the quotes
|
|
167
|
+
// value raw
|
|
168
|
+
// declarationNode.source.input.css.slice(valueStart)
|
|
169
|
+
// specifier raw
|
|
170
|
+
// declarationNode.source.input.css.slice(specifierStart, specifierEnd)
|
|
171
|
+
const specifierLine = declarationNode.source.start.line
|
|
172
|
+
const specifierColumn =
|
|
173
|
+
declarationNode.source.start.column +
|
|
174
|
+
(specifierStart - declarationNodeStart)
|
|
175
|
+
|
|
176
|
+
urlVisitor({
|
|
177
|
+
declarationNode,
|
|
178
|
+
type: "url",
|
|
179
|
+
specifier,
|
|
180
|
+
specifierLine,
|
|
181
|
+
specifierColumn,
|
|
182
|
+
specifierStart,
|
|
183
|
+
specifierEnd,
|
|
184
|
+
replace: (newUrlSpecifier) => {
|
|
185
|
+
urlMutations.push(() => {
|
|
186
|
+
// the specifier desires to be inside double quotes
|
|
187
|
+
if (newUrlSpecifier[0] === `"`) {
|
|
188
|
+
urlNode.type = "word"
|
|
189
|
+
urlNode.value = newUrlSpecifier
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
// the specifier desires to be inside simple quotes
|
|
193
|
+
if (newUrlSpecifier[0] === `'`) {
|
|
194
|
+
urlNode.type = "word"
|
|
195
|
+
urlNode.value = newUrlSpecifier
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
// the specifier desired to be just a word
|
|
199
|
+
// for the "word" type so that newUrlSpecifier can opt-out of being between quotes
|
|
200
|
+
// useful to inject __v__ calls for css inside js
|
|
201
|
+
urlNode.type = "word"
|
|
202
|
+
urlNode.value = newUrlSpecifier
|
|
203
|
+
})
|
|
204
|
+
},
|
|
205
|
+
})
|
|
206
|
+
},
|
|
207
|
+
})
|
|
208
|
+
if (urlMutations.length) {
|
|
209
|
+
mutations.push(() => {
|
|
210
|
+
urlMutations.forEach((urlMutation) => {
|
|
211
|
+
urlMutation()
|
|
212
|
+
})
|
|
213
|
+
declarationNode.value = parsed.toString()
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
OnceExit: () => {
|
|
218
|
+
mutations.forEach((mutation) => {
|
|
219
|
+
mutation()
|
|
220
|
+
})
|
|
221
|
+
},
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
postCssPluginUrlVisitor.postcss = true
|
|
227
|
+
|
|
228
|
+
const walkUrls = (parsed, { stringifyCssNodes, visitor }) => {
|
|
229
|
+
parsed.walk((node) => {
|
|
230
|
+
// https://github.com/andyjansson/postcss-functions
|
|
231
|
+
if (isUrlFunctionNode(node)) {
|
|
232
|
+
const { nodes } = node
|
|
233
|
+
const [urlNode] = nodes
|
|
234
|
+
const url =
|
|
235
|
+
urlNode && urlNode.type === "string"
|
|
236
|
+
? urlNode.value
|
|
237
|
+
: stringifyCssNodes(nodes)
|
|
238
|
+
visitor({
|
|
239
|
+
url: url.trim(),
|
|
240
|
+
urlNode,
|
|
241
|
+
})
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (isImageSetFunctionNode(node)) {
|
|
246
|
+
Array.from(node.nodes).forEach((childNode) => {
|
|
247
|
+
if (childNode.type === "string") {
|
|
248
|
+
visitor({
|
|
249
|
+
url: childNode.value.trim(),
|
|
250
|
+
urlNode: childNode,
|
|
251
|
+
})
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (isUrlFunctionNode(node)) {
|
|
256
|
+
const { nodes } = childNode
|
|
257
|
+
const [urlNode] = nodes
|
|
258
|
+
const url =
|
|
259
|
+
urlNode && urlNode.type === "string"
|
|
260
|
+
? urlNode.value
|
|
261
|
+
: stringifyCssNodes(nodes)
|
|
262
|
+
visitor({
|
|
263
|
+
url: url.trim(),
|
|
264
|
+
urlNode,
|
|
265
|
+
})
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const isUrlFunctionNode = (node) => {
|
|
274
|
+
return node.type === "function" && /^url$/i.test(node.value)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const isImageSetFunctionNode = (node) => {
|
|
278
|
+
return (
|
|
279
|
+
node.type === "function" && /^(?:-webkit-)?image-set$/i.test(node.value)
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const isDataUrl = (url) => {
|
|
284
|
+
return /data:[^\n\r;]+?(?:;charset=[^\n\r;]+?)?;base64,([\d+/A-Za-z]+={0,2})/.test(
|
|
285
|
+
url,
|
|
286
|
+
)
|
|
287
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { getHtmlNodeAttribute } from "./html_node_attributes.js"
|
|
2
|
+
|
|
3
|
+
export const analyzeScriptNode = (scriptNode) => {
|
|
4
|
+
const type = getHtmlNodeAttribute(scriptNode, "type")
|
|
5
|
+
if (type === undefined || type === "text/javascript") {
|
|
6
|
+
return "classic"
|
|
7
|
+
}
|
|
8
|
+
if (type === "module") {
|
|
9
|
+
return "module"
|
|
10
|
+
}
|
|
11
|
+
if (type === "importmap") {
|
|
12
|
+
return "importmap"
|
|
13
|
+
}
|
|
14
|
+
return type
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const analyzeLinkNode = (linkNode) => {
|
|
18
|
+
const rel = getHtmlNodeAttribute(linkNode, "rel")
|
|
19
|
+
if (rel === "stylesheet") {
|
|
20
|
+
return {
|
|
21
|
+
isStylesheet: true,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const isRessourceHint = [
|
|
25
|
+
"preconnect",
|
|
26
|
+
"dns-prefetch",
|
|
27
|
+
"prefetch",
|
|
28
|
+
"preload",
|
|
29
|
+
"modulepreload",
|
|
30
|
+
].includes(rel)
|
|
31
|
+
return {
|
|
32
|
+
isRessourceHint,
|
|
33
|
+
rel,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getHtmlNodeAttribute,
|
|
3
|
+
setHtmlNodeAttributes,
|
|
4
|
+
} from "./html_node_attributes.js"
|
|
5
|
+
import { setHtmlNodeText } from "./html_node_text.js"
|
|
6
|
+
|
|
7
|
+
export const inlineScriptNode = (script, textContent) => {
|
|
8
|
+
const src = getHtmlNodeAttribute(script, "src")
|
|
9
|
+
setHtmlNodeAttributes(script, {
|
|
10
|
+
"generated-from-src": src,
|
|
11
|
+
"src": undefined,
|
|
12
|
+
"crossorigin": undefined,
|
|
13
|
+
"integrity": undefined,
|
|
14
|
+
})
|
|
15
|
+
setHtmlNodeText(script, textContent)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const inlineLinkStylesheetNode = (link, textContent) => {
|
|
19
|
+
const href = getHtmlNodeAttribute(link, "href")
|
|
20
|
+
setHtmlNodeAttributes(link, {
|
|
21
|
+
"generated-from-href": href,
|
|
22
|
+
"href": undefined,
|
|
23
|
+
"rel": undefined,
|
|
24
|
+
"type": undefined,
|
|
25
|
+
"as": undefined,
|
|
26
|
+
"crossorigin": undefined,
|
|
27
|
+
"integrity": undefined,
|
|
28
|
+
})
|
|
29
|
+
link.nodeName = "style"
|
|
30
|
+
link.tagName = "style"
|
|
31
|
+
setHtmlNodeText(link, textContent)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const inlineImgNode = (img, contentAsBase64) => {
|
|
35
|
+
const src = getHtmlNodeAttribute(img, "src")
|
|
36
|
+
setHtmlNodeAttributes(img, {
|
|
37
|
+
"generated-from-src": src,
|
|
38
|
+
"src": contentAsBase64,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { parseFragment } from "parse5"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getHtmlNodeAttribute,
|
|
5
|
+
setHtmlNodeAttributes,
|
|
6
|
+
} from "./html_node_attributes.js"
|
|
7
|
+
import { findHtmlNode } from "./html_search.js"
|
|
8
|
+
import { analyzeScriptNode } from "./html_analysis.js"
|
|
9
|
+
|
|
10
|
+
export const removeHtmlNode = (htmlNode) => {
|
|
11
|
+
const { childNodes } = htmlNode.parentNode
|
|
12
|
+
childNodes.splice(childNodes.indexOf(htmlNode), 1)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const createHtmlNode = ({ tagName, textContent = "", ...rest }) => {
|
|
16
|
+
const html = `<${tagName} ${stringifyAttributes(
|
|
17
|
+
rest,
|
|
18
|
+
)}>${textContent}</${tagName}>`
|
|
19
|
+
const fragment = parseFragment(html)
|
|
20
|
+
return fragment.childNodes[0]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const injectScriptNodeAsEarlyAsPossible = (htmlAst, scriptNode) => {
|
|
24
|
+
const injectedBy = getHtmlNodeAttribute(scriptNode, "injected-by")
|
|
25
|
+
if (injectedBy === undefined) {
|
|
26
|
+
setHtmlNodeAttributes(scriptNode, {
|
|
27
|
+
"injected-by": "jsenv",
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
const isModule = analyzeScriptNode(scriptNode) === "module"
|
|
31
|
+
if (isModule) {
|
|
32
|
+
const firstImportmapScript = findHtmlNode(htmlAst, (node) => {
|
|
33
|
+
if (node.nodeName !== "script") return false
|
|
34
|
+
return analyzeScriptNode(node) === "importmap"
|
|
35
|
+
})
|
|
36
|
+
if (firstImportmapScript) {
|
|
37
|
+
return insertAfter(
|
|
38
|
+
scriptNode,
|
|
39
|
+
firstImportmapScript.parentNode,
|
|
40
|
+
firstImportmapScript,
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const headNode = findChild(htmlAst, (node) => node.nodeName === "html")
|
|
45
|
+
.childNodes[0]
|
|
46
|
+
const firstHeadScript = findChild(headNode, (node) => {
|
|
47
|
+
return node.nodeName === "script"
|
|
48
|
+
})
|
|
49
|
+
return insertBefore(scriptNode, headNode, firstHeadScript)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const insertBefore = (nodeToInsert, futureParentNode, futureNextSibling) => {
|
|
53
|
+
const { childNodes = [] } = futureParentNode
|
|
54
|
+
const futureIndex = futureNextSibling
|
|
55
|
+
? childNodes.indexOf(futureNextSibling)
|
|
56
|
+
: 0
|
|
57
|
+
injectWithWhitespaces(nodeToInsert, futureParentNode, futureIndex)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const insertAfter = (nodeToInsert, futureParentNode, futurePrevSibling) => {
|
|
61
|
+
const { childNodes = [] } = futureParentNode
|
|
62
|
+
const futureIndex = futurePrevSibling
|
|
63
|
+
? childNodes.indexOf(futurePrevSibling) + 1
|
|
64
|
+
: childNodes.length
|
|
65
|
+
injectWithWhitespaces(nodeToInsert, futureParentNode, futureIndex)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const injectWithWhitespaces = (nodeToInsert, futureParentNode, futureIndex) => {
|
|
69
|
+
const { childNodes = [] } = futureParentNode
|
|
70
|
+
const previousSiblings = childNodes.slice(0, futureIndex)
|
|
71
|
+
const nextSiblings = childNodes.slice(futureIndex)
|
|
72
|
+
const futureChildNodes = []
|
|
73
|
+
const previousSibling = previousSiblings[0]
|
|
74
|
+
if (previousSibling) {
|
|
75
|
+
futureChildNodes.push(...previousSiblings)
|
|
76
|
+
}
|
|
77
|
+
if (!previousSibling || previousSibling.nodeName !== "#text") {
|
|
78
|
+
futureChildNodes.push({
|
|
79
|
+
nodeName: "#text",
|
|
80
|
+
value: "\n ",
|
|
81
|
+
parentNode: futureParentNode,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
futureChildNodes.push(nodeToInsert)
|
|
85
|
+
const nextSibling = nextSiblings[0]
|
|
86
|
+
if (!nextSibling || nextSibling.nodeName !== "#text") {
|
|
87
|
+
futureChildNodes.push({
|
|
88
|
+
nodeName: "#text",
|
|
89
|
+
value: "\n ",
|
|
90
|
+
parentNode: futureParentNode,
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
if (nextSibling) {
|
|
94
|
+
futureChildNodes.push(...nextSiblings)
|
|
95
|
+
}
|
|
96
|
+
futureParentNode.childNodes = futureChildNodes
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const findChild = ({ childNodes = [] }, predicate) => childNodes.find(predicate)
|
|
100
|
+
|
|
101
|
+
const stringifyAttributes = (object) => {
|
|
102
|
+
return Object.keys(object)
|
|
103
|
+
.map((key) => `${key}=${valueToHtmlAttributeValue(object[key])}`)
|
|
104
|
+
.join(" ")
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const valueToHtmlAttributeValue = (value) => {
|
|
108
|
+
if (typeof value === "string") {
|
|
109
|
+
return JSON.stringify(value)
|
|
110
|
+
}
|
|
111
|
+
return `"${JSON.stringify(value)}"`
|
|
112
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const getHtmlNodeAttribute = (htmlNode, attributeName) => {
|
|
2
|
+
const attribute = getHtmlAttributeByName(htmlNode, attributeName)
|
|
3
|
+
return attribute ? attribute.value || "" : undefined
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export const setHtmlNodeAttributes = (htmlNode, attributesToAssign) => {
|
|
7
|
+
if (typeof attributesToAssign !== "object") {
|
|
8
|
+
throw new TypeError(`attributesToAssign must be an object`)
|
|
9
|
+
}
|
|
10
|
+
const { attrs } = htmlNode
|
|
11
|
+
if (!attrs) return
|
|
12
|
+
Object.keys(attributesToAssign).forEach((key) => {
|
|
13
|
+
const existingAttributeIndex = attrs.findIndex(({ name }) => name === key)
|
|
14
|
+
const value = attributesToAssign[key]
|
|
15
|
+
// remove no-op
|
|
16
|
+
if (existingAttributeIndex === -1 && value === undefined) {
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
// add
|
|
20
|
+
if (existingAttributeIndex === -1 && value !== undefined) {
|
|
21
|
+
attrs.push({
|
|
22
|
+
name: key,
|
|
23
|
+
value,
|
|
24
|
+
})
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
// remove
|
|
28
|
+
if (value === undefined) {
|
|
29
|
+
attrs.splice(existingAttributeIndex, 1)
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
// update
|
|
33
|
+
attrs[existingAttributeIndex].value = value
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const getHtmlAttributeByName = (htmlNode, attributeName) => {
|
|
38
|
+
const attrs = htmlNode.attrs
|
|
39
|
+
const attribute = attrs
|
|
40
|
+
? attrs.find((attr) => attr.name === attributeName)
|
|
41
|
+
: null
|
|
42
|
+
return attribute
|
|
43
|
+
}
|