mikser-io 9.83.0 → 9.84.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 +1 -1
- package/src/engine.js +18 -3
- package/src/references.js +55 -3
package/package.json
CHANGED
package/src/engine.js
CHANGED
|
@@ -128,7 +128,15 @@ async function reportBrokenReferences(logger) {
|
|
|
128
128
|
const explain = runtime.state?.assets?.explainMissing
|
|
129
129
|
const reasons = new Map()
|
|
130
130
|
if (explain) {
|
|
131
|
-
|
|
131
|
+
// Every broken entry, not the ones that happen to sort first. The cap
|
|
132
|
+
// below is about how much gets PRINTED; asking only within it made the
|
|
133
|
+
// answer depend on the position of the entry that had one, so a site
|
|
134
|
+
// with ten unrelated broken urls reported "No derivative produced: 0"
|
|
135
|
+
// while holding the cause of the eleventh. Cheap to ask: anything not
|
|
136
|
+
// under the assets folder is rejected on the first path segment, and
|
|
137
|
+
// the catalog index behind it is built once and only if something gets
|
|
138
|
+
// past that.
|
|
139
|
+
for (const { target } of broken) {
|
|
132
140
|
// Site-relative, because that is what the assets folder is named
|
|
133
141
|
// relative to. A build that deploys out/<lang> as its own domain
|
|
134
142
|
// root resolves this target to `a/derived/web/...`, and a plain
|
|
@@ -141,7 +149,14 @@ async function reportBrokenReferences(logger) {
|
|
|
141
149
|
}
|
|
142
150
|
}
|
|
143
151
|
|
|
144
|
-
|
|
152
|
+
// A stated cause outranks a guess in what gets printed, not only in how it
|
|
153
|
+
// is worded. The cap exists so one broken preset cannot bury the rest of
|
|
154
|
+
// the build — it must not bury the answer instead.
|
|
155
|
+
const ordered = reasons.size
|
|
156
|
+
? [...broken].sort((a, b) => (reasons.get(b.target) ? 1 : 0) - (reasons.get(a.target) ? 1 : 0))
|
|
157
|
+
: broken
|
|
158
|
+
|
|
159
|
+
for (const { url, target, files, elsewhere } of ordered.slice(0, SHOWN)) {
|
|
145
160
|
const reason = reasons.get(target)
|
|
146
161
|
// Two different problems wear the same symptom. A target whose file
|
|
147
162
|
// exists elsewhere in the output is a base that is wrong, not an asset
|
|
@@ -161,7 +176,7 @@ async function reportBrokenReferences(logger) {
|
|
|
161
176
|
}
|
|
162
177
|
}
|
|
163
178
|
if (broken.length) {
|
|
164
|
-
const explained = broken.
|
|
179
|
+
const explained = broken.filter(b => reasons.get(b.target)).length
|
|
165
180
|
const misplaced = broken.filter(b => b.elsewhere?.length && !reasons.get(b.target)).length
|
|
166
181
|
logger.warn({
|
|
167
182
|
code: 'reference-broken-summary',
|
package/src/references.js
CHANGED
|
@@ -38,6 +38,9 @@ const ATTR = /(?:src|href|poster|data-bg)\s*=\s*["']([^"']*)["']/gi
|
|
|
38
38
|
const SRCSET = /(?:img|image)?srcset\s*=\s*["']([^"']*)["']/gi
|
|
39
39
|
// css url(), in a stylesheet and in an inline style attribute alike.
|
|
40
40
|
const CSS_URL = /url\(\s*(['"]?)([^'")]*)\1\s*\)/gi
|
|
41
|
+
// The same, but inside a CUSTOM PROPERTY declaration — which resolves from a
|
|
42
|
+
// different place, see below.
|
|
43
|
+
const CUSTOM_PROP_URL = /--[\w-]+\s*:\s*[^;{}]*?url\(\s*(['"]?)([^'")]*)\1\s*\)/gi
|
|
41
44
|
|
|
42
45
|
// A url this check has nothing to say about: another origin, an inline
|
|
43
46
|
// payload, a fragment or an in-page action. `//host/path` is protocol-relative
|
|
@@ -88,6 +91,10 @@ export { siteRootFor }
|
|
|
88
91
|
export function extractReferences(rawSource) {
|
|
89
92
|
const source = decodeEntities(rawSource)
|
|
90
93
|
const found = new Set()
|
|
94
|
+
// Collected first, so a url that appears in a custom property is known to
|
|
95
|
+
// be one however else it is matched — CSS_URL sees it too.
|
|
96
|
+
const custom = new Set()
|
|
97
|
+
for (const [, , url] of source.matchAll(CUSTOM_PROP_URL)) custom.add(url)
|
|
91
98
|
for (const [, url] of source.matchAll(ATTR)) found.add(url)
|
|
92
99
|
for (const [, , url] of source.matchAll(CSS_URL)) found.add(url)
|
|
93
100
|
for (const [, list] of source.matchAll(SRCSET)) {
|
|
@@ -96,7 +103,9 @@ export function extractReferences(rawSource) {
|
|
|
96
103
|
if (url) found.add(url)
|
|
97
104
|
}
|
|
98
105
|
}
|
|
99
|
-
return [...found]
|
|
106
|
+
return [...found]
|
|
107
|
+
.filter(u => !isExternal(u))
|
|
108
|
+
.map(url => ({ url, customProperty: custom.has(url) }))
|
|
100
109
|
}
|
|
101
110
|
|
|
102
111
|
// Resolve the way a browser does, which is the whole point.
|
|
@@ -133,9 +142,18 @@ export function resolveUrl(pageDir, url, { root = '' } = {}) {
|
|
|
133
142
|
// { url, target, files } — the target with the pages that named it, because
|
|
134
143
|
// "this is missing" is only actionable next to "and these link it".
|
|
135
144
|
export async function checkReferences(outputFolder, { siteRoots = [] } = {}) {
|
|
145
|
+
// Following symlinks, because that is what gets served.
|
|
146
|
+
//
|
|
147
|
+
// files() emits by symlinking the source into the output, so a stylesheet
|
|
148
|
+
// is usually a link rather than a copy — and skipping links meant no
|
|
149
|
+
// symlinked html or css was ever read. Every `url()` inside a bundle went
|
|
150
|
+
// unchecked on any site built that way, silently, while the check reported
|
|
151
|
+
// a confident total. The same-name index below has always followed them,
|
|
152
|
+
// which is how a file could be FOUND elsewhere by a scan that would not
|
|
153
|
+
// READ it.
|
|
136
154
|
const files = await globby(SCANNED, {
|
|
137
155
|
cwd: outputFolder,
|
|
138
|
-
followSymbolicLinks:
|
|
156
|
+
followSymbolicLinks: true,
|
|
139
157
|
suppressErrors: true,
|
|
140
158
|
})
|
|
141
159
|
|
|
@@ -146,6 +164,30 @@ export async function checkReferences(outputFolder, { siteRoots = [] } = {}) {
|
|
|
146
164
|
// site — one lookup each.
|
|
147
165
|
const exists = new Map()
|
|
148
166
|
|
|
167
|
+
// Where a `url()` inside a CUSTOM PROPERTY resolves from.
|
|
168
|
+
//
|
|
169
|
+
// Not the page. A custom property is substituted where it is USED, and the
|
|
170
|
+
// url resolves against the stylesheet doing the substituting — so
|
|
171
|
+
//
|
|
172
|
+
// <span style="--icon-btn-src:url("../media/icons/x.svg")">
|
|
173
|
+
//
|
|
174
|
+
// on a page three directories deep is CORRECT when the bundle that reads
|
|
175
|
+
// var(--icon-btn-src) sits at styles/. Resolving it from the page reports
|
|
176
|
+
// a base problem for markup that ships and works, and fifteen such lines
|
|
177
|
+
// are how the reader learns to skim past this check entirely.
|
|
178
|
+
//
|
|
179
|
+
// Which stylesheet substitutes it is not knowable from the bytes — any
|
|
180
|
+
// rule using the variable does — so every emitted stylesheet is a
|
|
181
|
+
// candidate base, and one that resolves is enough to stay quiet. A url
|
|
182
|
+
// that resolves from none of them is still reported: it is missing
|
|
183
|
+
// wherever it is read from.
|
|
184
|
+
const styleBases = files
|
|
185
|
+
.filter(file => file.endsWith('.css'))
|
|
186
|
+
.map((file) => {
|
|
187
|
+
const root = siteRootFor(file, siteRoots)
|
|
188
|
+
return { root, dir: path.dirname(file).slice(root.length).replace(/^\/+/, '') }
|
|
189
|
+
})
|
|
190
|
+
|
|
149
191
|
for (const file of files) {
|
|
150
192
|
let source
|
|
151
193
|
try { source = await readFile(path.join(outputFolder, file), 'utf8') }
|
|
@@ -155,13 +197,23 @@ export async function checkReferences(outputFolder, { siteRoots = [] } = {}) {
|
|
|
155
197
|
// The page's directory, relative to its own site root.
|
|
156
198
|
const pageDir = path.dirname(file).slice(root.length).replace(/^\/+/, '')
|
|
157
199
|
|
|
158
|
-
for (const url of extractReferences(source)) {
|
|
200
|
+
for (const { url, customProperty } of extractReferences(source)) {
|
|
159
201
|
const { target, overDeep, floored } = resolveUrl(pageDir, url, { root })
|
|
160
202
|
checked++
|
|
161
203
|
|
|
162
204
|
if (!exists.has(target)) {
|
|
163
205
|
exists.set(target, existsSync(path.join(outputFolder, target)))
|
|
164
206
|
}
|
|
207
|
+
|
|
208
|
+
// Resolved from a stylesheet instead, and correct there.
|
|
209
|
+
if (!exists.get(target) && customProperty && styleBases.some((base) => {
|
|
210
|
+
const from = resolveUrl(base.dir, url, { root: base.root }).target
|
|
211
|
+
if (!exists.has(from)) {
|
|
212
|
+
exists.set(from, existsSync(path.join(outputFolder, from)))
|
|
213
|
+
}
|
|
214
|
+
return exists.get(from)
|
|
215
|
+
})) continue
|
|
216
|
+
|
|
165
217
|
// Broken outranks over-deep: a url that resolves nowhere is the
|
|
166
218
|
// failure, and adding that it is also one level too deep is noise.
|
|
167
219
|
const bucket = !exists.get(target) ? broken : (overDeep ? overDeepRefs : null)
|