@emulsify/core 4.3.2 → 4.5.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/.storybook/main-static-assets.js +5 -8
- package/.storybook/main-vite.js +11 -3
- package/README.md +14 -6
- package/config/a11y-wcag22.js +11 -0
- package/config/vite/entries.js +7 -2
- package/config/vite/environment.js +4 -0
- package/config/vite/plugins/assets/asset-url-rebase.js +241 -0
- package/config/vite/plugins/assets/copy-src-assets.js +82 -12
- package/config/vite/plugins/assets/copy-twig-files.js +85 -16
- package/config/vite/plugins/assets/css-asset-rebase.js +306 -0
- package/config/vite/plugins/assets/css-asset-relativizer.js +301 -21
- package/config/vite/plugins/assets/development-source-maps.js +273 -0
- package/config/vite/plugins/assets/mirror-components.js +98 -82
- package/config/vite/plugins/assets/output-freshness.js +235 -0
- package/config/vite/plugins/assets/source-file-index.js +13 -13
- package/config/vite/plugins/assets/stable-watch-output.js +165 -0
- package/config/vite/plugins/assets/storybook-output.js +27 -0
- package/config/vite/plugins/index.js +95 -9
- package/config/vite/plugins/reporter/asset-resolver.js +34 -6
- package/config/vite/plugins/reporter/build-errors.js +7 -3
- package/config/vite/plugins/reporter/diagnostics.js +140 -10
- package/config/vite/plugins/reporter/index.js +380 -75
- package/config/vite/plugins/reporter/render.js +297 -44
- package/config/vite/plugins/reporter/sass-logger.js +30 -0
- package/config/vite/plugins/reporter/source-roots.js +101 -21
- package/config/vite/plugins/reporter/strict-mode.js +99 -0
- package/config/vite/plugins/reporter/vite-logger.js +220 -8
- package/config/vite/plugins/reporter/watch-mode.js +6 -2
- package/config/vite/plugins/twig/twig-module.js +35 -258
- package/config/vite/plugins/twig/virtual-twig-asset-sources.js +48 -49
- package/config/vite/project-config.js +121 -21
- package/config/vite/project-structure.js +6 -0
- package/config/vite/utils/asset-roots.js +205 -0
- package/config/vite/utils/css-urls.js +350 -0
- package/config/vite/utils/fs-safe.js +38 -1
- package/config/vite/utils/source-directory-skips.js +13 -0
- package/config/vite/utils/source-maps.js +88 -0
- package/config/vite/utils/twig-component-resolver.js +316 -0
- package/config/vite/vite.config.js +106 -42
- package/package.json +54 -40
- package/scripts/a11y.js +88 -9
- package/scripts/audit/checks/css-asset-references.js +256 -24
- package/scripts/audit/checks/twig-references.js +16 -5
- package/scripts/audit/fix.js +836 -0
- package/scripts/audit/index.js +10 -2
- package/scripts/audit/lib/css.js +41 -35
- package/scripts/audit/lib/story-ast.js +392 -0
- package/scripts/audit/lib/story-render-paths.js +600 -0
- package/scripts/audit/lib/story-selection.js +190 -0
- package/scripts/audit/lib/twig.js +372 -80
- package/scripts/audit/report.js +83 -5
- package/scripts/audit-twig-stories.js +73 -3
- package/scripts/audit.js +87 -2
- package/src/storybook/twig/source-function.js +14 -10
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file CSS asset reference audit check.
|
|
3
|
+
*
|
|
4
|
+
* Emulsify accepts root-absolute `url('/assets/...')` and namespaced
|
|
5
|
+
* `url('@assets/...')` references. Two other forms are common and both used to
|
|
6
|
+
* ship broken: a relative URL authored against the emitted CSS location, and
|
|
7
|
+
* the bare `assets/...` form. The build repairs both when the target is
|
|
8
|
+
* unambiguous (config/vite/plugins/assets/css-asset-rebase.js), so what this
|
|
9
|
+
* check reports is (a) references nothing can resolve, and (b) references the
|
|
10
|
+
* build has to repair, which are worth writing canonically in source.
|
|
3
11
|
*/
|
|
4
12
|
|
|
5
13
|
import { dirname, resolve } from 'node:path';
|
|
6
|
-
import {
|
|
14
|
+
import { assetTailFor } from '../../../config/vite/plugins/assets/asset-url-rebase.js';
|
|
15
|
+
import { resolveAssetTail } from '../../../config/vite/utils/asset-roots.js';
|
|
16
|
+
import { isNonFilesystemCssUrl } from '../../../config/vite/utils/css-urls.js';
|
|
17
|
+
import { firstExistingFile } from '../../../config/vite/utils/fs-safe.js';
|
|
18
|
+
import { createAuditFixTargetChecker } from '../fix.js';
|
|
7
19
|
import { displayPath, makeFinding } from '../lib/findings.js';
|
|
8
20
|
import {
|
|
9
21
|
cachedReadFile,
|
|
@@ -13,14 +25,188 @@ import {
|
|
|
13
25
|
} from '../lib/files.js';
|
|
14
26
|
import { auditAssetRoots } from '../lib/twig.js';
|
|
15
27
|
import {
|
|
28
|
+
classifyCssAssetUrl,
|
|
16
29
|
cssUrlPath,
|
|
17
30
|
findCssUrlReferences,
|
|
18
|
-
|
|
31
|
+
isCssAssetAlias,
|
|
19
32
|
styleRuntimeDirectories,
|
|
20
33
|
} from '../lib/css.js';
|
|
21
34
|
|
|
35
|
+
const ASSET_DOCS =
|
|
36
|
+
'https://github.com/emulsify-ds/emulsify-core/blob/4.x/docs/asset-references.md#sass-and-css';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build the fix payload an autofix can apply to the authored stylesheet.
|
|
40
|
+
*
|
|
41
|
+
* Interpolated URLs are deliberately unfixable: the edit belongs on the
|
|
42
|
+
* variable declaration, and same-file variable scanning cannot see who else
|
|
43
|
+
* depends on it.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} filePath - Absolute stylesheet path.
|
|
46
|
+
* @param {{raw: string, start: number, end: number}} ref - URL reference.
|
|
47
|
+
* @param {string} replacement - Canonical URL.
|
|
48
|
+
* @param {boolean} fixWritable - Whether audit policy permits a rewrite.
|
|
49
|
+
* @returns {object|undefined} Fix payload, when safe to apply.
|
|
50
|
+
*/
|
|
51
|
+
function makeUrlFix(filePath, ref, replacement, fixWritable) {
|
|
52
|
+
if (!fixWritable || ref.raw.includes('#{') || ref.raw === replacement) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
filePath,
|
|
58
|
+
start: ref.start,
|
|
59
|
+
end: ref.end,
|
|
60
|
+
original: ref.raw,
|
|
61
|
+
replacement,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Build the finding for a CSS asset URL nothing can resolve.
|
|
67
|
+
*
|
|
68
|
+
* @param {object} params - Reference context.
|
|
69
|
+
* @param {string} params.filePath - Absolute stylesheet path.
|
|
70
|
+
* @param {string} [params.projectDir] - Absolute project root.
|
|
71
|
+
* @param {object} params.ref - URL reference.
|
|
72
|
+
* @param {object} params.resolution - Asset tail resolution.
|
|
73
|
+
* @returns {object} Finding.
|
|
74
|
+
*/
|
|
75
|
+
function unresolvedFinding({ filePath, projectDir = '', ref, resolution }) {
|
|
76
|
+
const ambiguous = resolution.status === 'ambiguous';
|
|
77
|
+
|
|
78
|
+
return makeFinding({
|
|
79
|
+
id: 'unresolved-css-asset-reference',
|
|
80
|
+
severity: 'warn',
|
|
81
|
+
filePath,
|
|
82
|
+
line: ref.line,
|
|
83
|
+
message: ambiguous
|
|
84
|
+
? `CSS asset URL "${ref.raw}" matches more than one project asset root.`
|
|
85
|
+
: `CSS asset URL "${ref.raw}" could not be resolved from the source file or any project asset root.`,
|
|
86
|
+
details: ambiguous
|
|
87
|
+
? [
|
|
88
|
+
`Candidates: ${resolution.candidates
|
|
89
|
+
.map((candidate) => displayPath(projectDir, candidate))
|
|
90
|
+
.join(', ')}.`,
|
|
91
|
+
'Remove the duplicate, or narrow assets.roots in project.emulsify.json so one file answers to the URL.',
|
|
92
|
+
]
|
|
93
|
+
: [
|
|
94
|
+
'Reference project assets with url(/assets/...) or url(@assets/...), and keep the file under assets/ or a root declared in project.emulsify.json assets.roots.',
|
|
95
|
+
'Otherwise check the filename for a typo.',
|
|
96
|
+
],
|
|
97
|
+
docs: ASSET_DOCS,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Report the Core-only stylesheet alias when its resolver is disabled.
|
|
103
|
+
*
|
|
104
|
+
* @param {object} params - Reference context.
|
|
105
|
+
* @param {string} params.filePath - Absolute stylesheet path.
|
|
106
|
+
* @param {object} params.ref - URL reference.
|
|
107
|
+
* @returns {object} Finding.
|
|
108
|
+
*/
|
|
109
|
+
function disabledAliasFinding({ filePath, ref }) {
|
|
110
|
+
return makeFinding({
|
|
111
|
+
id: 'unresolved-css-asset-reference',
|
|
112
|
+
severity: 'warn',
|
|
113
|
+
filePath,
|
|
114
|
+
line: ref.line,
|
|
115
|
+
message: `CSS asset URL "${ref.raw}" uses the @assets stylesheet alias, but assets.rebase is disabled.`,
|
|
116
|
+
details: [
|
|
117
|
+
'Enable assets.rebase to use @assets/..., or write a URL that Vite can resolve without the Emulsify asset pipeline.',
|
|
118
|
+
],
|
|
119
|
+
docs: ASSET_DOCS,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Audit a URL that names the published `assets/` prefix.
|
|
125
|
+
*
|
|
126
|
+
* @param {object} params - Reference context.
|
|
127
|
+
* @param {string} params.assetPath - URL path without query or hash.
|
|
128
|
+
* @param {string[]} params.assetRoots - Absolute project asset roots.
|
|
129
|
+
* @param {string} params.filePath - Absolute stylesheet path.
|
|
130
|
+
* @param {Function} params.canFix - Lazily check whether policy permits a rewrite.
|
|
131
|
+
* @param {string} params.projectDir - Absolute project root.
|
|
132
|
+
* @param {object} params.ref - URL reference.
|
|
133
|
+
* @returns {object[]} Findings.
|
|
134
|
+
*/
|
|
135
|
+
function auditAssetRootReference({
|
|
136
|
+
assetPath,
|
|
137
|
+
assetRoots,
|
|
138
|
+
canFix,
|
|
139
|
+
filePath,
|
|
140
|
+
projectDir,
|
|
141
|
+
ref,
|
|
142
|
+
}) {
|
|
143
|
+
const tail = assetTailFor(assetPath);
|
|
144
|
+
const resolution = resolveAssetTail(tail, assetRoots);
|
|
145
|
+
|
|
146
|
+
if (resolution.status !== 'resolved') {
|
|
147
|
+
return [unresolvedFinding({ filePath, projectDir, ref, resolution })];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const resolvedSuffix = ref.value.slice(cssUrlPath(ref.value).length);
|
|
151
|
+
const resolvedCanonical = `/assets/${tail}${resolvedSuffix}`;
|
|
152
|
+
const resolvedAlias = `@assets/${tail}${resolvedSuffix}`;
|
|
153
|
+
|
|
154
|
+
// The scanner expands simple same-file Sass variables in `ref.value` while
|
|
155
|
+
// retaining the authored interpolation in `ref.raw`. Accept an expansion
|
|
156
|
+
// that lands exactly on either first-class form before deciding that an
|
|
157
|
+
// interpolated reference needs manual review.
|
|
158
|
+
if (ref.value === resolvedCanonical || ref.value === resolvedAlias) return [];
|
|
159
|
+
|
|
160
|
+
const interpolated = ref.raw.includes('#{');
|
|
161
|
+
// A `?v=2` or `#id` suffix is part of the authored URL, not of the asset
|
|
162
|
+
// path, so it survives the rewrite untouched. Interpolation also begins
|
|
163
|
+
// with `#`, but it is authored Sass rather than a URL fragment; deriving a
|
|
164
|
+
// replacement from it would append the complete raw value as a suffix.
|
|
165
|
+
const canonical = interpolated
|
|
166
|
+
? undefined
|
|
167
|
+
: `/assets/${tail}${ref.raw.slice(cssUrlPath(ref.raw).length)}`;
|
|
168
|
+
const alias = interpolated
|
|
169
|
+
? undefined
|
|
170
|
+
: `@assets/${tail}${ref.raw.slice(cssUrlPath(ref.raw).length)}`;
|
|
171
|
+
|
|
172
|
+
// Both public spellings are accepted authoring forms.
|
|
173
|
+
if (ref.raw === canonical || ref.raw === alias) return [];
|
|
174
|
+
|
|
175
|
+
const fix = canonical
|
|
176
|
+
? makeUrlFix(filePath, ref, canonical, canFix())
|
|
177
|
+
: undefined;
|
|
178
|
+
const details = [
|
|
179
|
+
`Resolved asset: ${displayPath(projectDir, resolution.file)}.`,
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
if (canonical) {
|
|
183
|
+
details.push(`Rewrite it as url(${canonical}).`);
|
|
184
|
+
} else {
|
|
185
|
+
details.push(
|
|
186
|
+
'This URL contains Sass interpolation, so review its variable declaration instead of rewriting the reference automatically.',
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (fix) {
|
|
191
|
+
details.push('Run `emulsify-audit --fix` to apply this automatically.');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return [
|
|
195
|
+
makeFinding({
|
|
196
|
+
id: 'css-runtime-asset-reference',
|
|
197
|
+
severity: 'info',
|
|
198
|
+
filePath,
|
|
199
|
+
line: ref.line,
|
|
200
|
+
message: `CSS asset URL "${ref.raw}" is not the canonical asset form, so the build has to repair it.`,
|
|
201
|
+
details,
|
|
202
|
+
docs: ASSET_DOCS,
|
|
203
|
+
fix,
|
|
204
|
+
}),
|
|
205
|
+
];
|
|
206
|
+
}
|
|
207
|
+
|
|
22
208
|
/**
|
|
23
|
-
* Audit local CSS/Sass asset URLs
|
|
209
|
+
* Audit local CSS/Sass asset URLs against the project's asset roots.
|
|
24
210
|
*
|
|
25
211
|
* @param {object} context - Audit context.
|
|
26
212
|
* @returns {object[]} Findings.
|
|
@@ -28,8 +214,16 @@ import {
|
|
|
28
214
|
export function auditCssAssetReferences(context) {
|
|
29
215
|
const { env, projectDir, styleFiles } = context;
|
|
30
216
|
const findings = [];
|
|
31
|
-
const
|
|
32
|
-
const
|
|
217
|
+
const assetRoots = auditAssetRoots(env).filter(safeIsDirectory);
|
|
218
|
+
const assetAliasEnabled = env.projectStructure?.assetRebase !== false;
|
|
219
|
+
const sourceRoots = Array.isArray(context.sourceRoots)
|
|
220
|
+
? context.sourceRoots
|
|
221
|
+
: env.projectStructure?.sourceRoots;
|
|
222
|
+
const styleSourceRoots = sourceRoots || [];
|
|
223
|
+
const isFixTargetWritable = createAuditFixTargetChecker({
|
|
224
|
+
projectDir,
|
|
225
|
+
sourceRoots,
|
|
226
|
+
});
|
|
33
227
|
|
|
34
228
|
for (const filePath of styleFiles) {
|
|
35
229
|
if (
|
|
@@ -39,43 +233,81 @@ export function auditCssAssetReferences(context) {
|
|
|
39
233
|
continue;
|
|
40
234
|
}
|
|
41
235
|
|
|
236
|
+
let fixWritable;
|
|
237
|
+
const canFix = () => {
|
|
238
|
+
fixWritable ??= isFixTargetWritable(filePath);
|
|
239
|
+
return fixWritable;
|
|
240
|
+
};
|
|
42
241
|
const source = cachedReadFile(filePath);
|
|
43
242
|
const runtimeDirs = styleRuntimeDirectories(filePath, env, projectDir);
|
|
44
243
|
|
|
45
244
|
for (const ref of findCssUrlReferences(source)) {
|
|
46
|
-
if (isNonFilesystemCssUrl(ref.value)) continue;
|
|
245
|
+
if (isNonFilesystemCssUrl(ref.value, ref.quote)) continue;
|
|
47
246
|
|
|
48
247
|
const assetPath = cssUrlPath(ref.value);
|
|
49
248
|
if (!assetPath) continue;
|
|
50
249
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
250
|
+
if (isCssAssetAlias(assetPath) && !assetAliasEnabled) {
|
|
251
|
+
findings.push(disabledAliasFinding({ filePath, ref }));
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// CSS resolves non-absolute URLs from the source stylesheet first. The
|
|
256
|
+
// build plugin only sees literals Vite already failed to resolve, but the
|
|
257
|
+
// audit scans authored source and must preserve that precedence itself.
|
|
258
|
+
// Do not probe `/assets/...` against the filesystem root: it is the
|
|
259
|
+
// canonical project-asset form, not a source-relative path.
|
|
260
|
+
const sourceAsset =
|
|
261
|
+
assetPath.startsWith('/') || isCssAssetAlias(assetPath)
|
|
262
|
+
? undefined
|
|
263
|
+
: firstExistingFile([resolve(dirname(filePath), assetPath)]);
|
|
264
|
+
const classification = classifyCssAssetUrl(ref.value);
|
|
265
|
+
|
|
266
|
+
// Some other absolute URL: the platform serves it, and there is no
|
|
267
|
+
// project file to check it against.
|
|
268
|
+
if (classification === 'runtime') continue;
|
|
269
|
+
|
|
270
|
+
if (classification === 'asset-root') {
|
|
271
|
+
// Rewriting a working local reference could select a different file
|
|
272
|
+
// with the same tail under a project asset root. It needs no repair and
|
|
273
|
+
// is deliberately ineligible for --fix.
|
|
274
|
+
if (sourceAsset) continue;
|
|
275
|
+
|
|
276
|
+
findings.push(
|
|
277
|
+
...auditAssetRootReference({
|
|
278
|
+
assetPath,
|
|
279
|
+
assetRoots,
|
|
280
|
+
canFix,
|
|
281
|
+
filePath,
|
|
282
|
+
projectDir,
|
|
283
|
+
ref,
|
|
284
|
+
}),
|
|
285
|
+
);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const runtimeAsset = firstExistingFile(
|
|
55
290
|
runtimeDirs.map((directory) => resolve(directory, assetPath)),
|
|
56
291
|
);
|
|
57
292
|
const resolvedAsset = sourceAsset || runtimeAsset;
|
|
58
293
|
|
|
59
294
|
if (!resolvedAsset) {
|
|
60
295
|
findings.push(
|
|
61
|
-
|
|
62
|
-
id: 'unresolved-css-asset-reference',
|
|
63
|
-
severity: 'warn',
|
|
296
|
+
unresolvedFinding({
|
|
64
297
|
filePath,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
'Check for a typo, move the asset into a source-root-relative location Vite can resolve, or rewrite the URL to a stable Drupal/theme public path.',
|
|
69
|
-
],
|
|
70
|
-
docs: 'https://github.com/emulsify-ds/emulsify-core/blob/4.x/docs/migration-4x.md#css-asset-urls',
|
|
298
|
+
projectDir,
|
|
299
|
+
ref,
|
|
300
|
+
resolution: { status: 'missing' },
|
|
71
301
|
}),
|
|
72
302
|
);
|
|
73
303
|
continue;
|
|
74
304
|
}
|
|
75
305
|
|
|
306
|
+
// A relative URL that only resolves once the CSS is emitted is exactly
|
|
307
|
+
// the shape that breaks when the output shape changes.
|
|
76
308
|
if (
|
|
77
|
-
|
|
78
|
-
(!sourceAsset || runtimeAsset
|
|
309
|
+
assetRoots.some((root) => isSameOrInside(resolvedAsset, root)) &&
|
|
310
|
+
(!sourceAsset || runtimeAsset)
|
|
79
311
|
) {
|
|
80
312
|
findings.push(
|
|
81
313
|
makeFinding({
|
|
@@ -83,12 +315,12 @@ export function auditCssAssetReferences(context) {
|
|
|
83
315
|
severity: 'info',
|
|
84
316
|
filePath,
|
|
85
317
|
line: ref.line,
|
|
86
|
-
message: `CSS asset URL "${ref.raw}"
|
|
318
|
+
message: `CSS asset URL "${ref.raw}" reaches project assets by a path that only resolves once the CSS is emitted.`,
|
|
87
319
|
details: [
|
|
88
320
|
`Resolved asset: ${displayPath(projectDir, resolvedAsset)}.`,
|
|
89
|
-
'
|
|
321
|
+
'Write it as url(/assets/...) so the same source works in Storybook and in every emitted CSS location.',
|
|
90
322
|
],
|
|
91
|
-
docs:
|
|
323
|
+
docs: ASSET_DOCS,
|
|
92
324
|
}),
|
|
93
325
|
);
|
|
94
326
|
}
|
|
@@ -6,7 +6,7 @@ import { resolve } from 'node:path';
|
|
|
6
6
|
import { makeFinding } from '../lib/findings.js';
|
|
7
7
|
import { cachedReadFile } from '../lib/files.js';
|
|
8
8
|
import {
|
|
9
|
-
|
|
9
|
+
findTwigReferenceCalls,
|
|
10
10
|
findTwigNamespaceReferences,
|
|
11
11
|
resolvesTwigReference,
|
|
12
12
|
} from '../lib/twig.js';
|
|
@@ -23,6 +23,7 @@ export function auditTwigReferences(context) {
|
|
|
23
23
|
const knownNamespaces = new Set([...Object.keys(namespaceRoots), 'assets']);
|
|
24
24
|
const findings = [];
|
|
25
25
|
const seen = new Set();
|
|
26
|
+
const componentGroupRootsCache = new Map();
|
|
26
27
|
|
|
27
28
|
for (const twigFile of twigFiles) {
|
|
28
29
|
const source = cachedReadFile(twigFile);
|
|
@@ -46,15 +47,25 @@ export function auditTwigReferences(context) {
|
|
|
46
47
|
);
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
for (const
|
|
50
|
-
|
|
50
|
+
for (const call of findTwigReferenceCalls(source)) {
|
|
51
|
+
// Optional or uncertain calls cannot establish a required missing target.
|
|
52
|
+
if (call.ignoreMissing !== false || call.hasDynamicCandidates) continue;
|
|
53
|
+
const resolved = call.candidates.some(
|
|
54
|
+
({ value }) =>
|
|
55
|
+
value !== '' &&
|
|
56
|
+
resolvesTwigReference(value, twigFile, env, componentGroupRootsCache),
|
|
57
|
+
);
|
|
58
|
+
if (!resolved) {
|
|
59
|
+
const description = call.isFallbackArray
|
|
60
|
+
? `fallback candidates ${JSON.stringify(call.candidates.map(({ value }) => value))}`
|
|
61
|
+
: `reference "${call.candidates[0].value}"`;
|
|
51
62
|
findings.push(
|
|
52
63
|
makeFinding({
|
|
53
64
|
id: 'unresolved-twig-reference',
|
|
54
65
|
severity: 'warn',
|
|
55
66
|
filePath: twigFile,
|
|
56
|
-
line:
|
|
57
|
-
message: `${
|
|
67
|
+
line: call.line,
|
|
68
|
+
message: `${call.type}() ${description} could not be resolved from the normalized Twig roots.`,
|
|
58
69
|
docs: 'https://github.com/emulsify-ds/emulsify-core/blob/4.x/docs/storybook.md#include',
|
|
59
70
|
}),
|
|
60
71
|
);
|