@wix/origami-lint 1.34.19 → 1.34.21

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.
@@ -8,7 +8,10 @@ export function readStats(file, run) {
8
8
  const validKind = (kind) => kind === 'page' || kind === 'component';
9
9
  const count = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0;
10
10
  if (!Array.isArray(stats.rules) || !Array.isArray(stats.models)
11
- || !stats.rules.every(s => s && ruleRegistry.some(rule => rule.name === s.rule) && validPage(s.page) && validKind(s.kind) && Number.isSafeInteger(s.violations) && s.violations >= 0 && count(s.duration_ms))
11
+ || !stats.rules.every(s => s && ruleRegistry.some(rule => rule.name === s.rule && s.checks && typeof s.checks === 'object' && !Array.isArray(s.checks)
12
+ && Object.keys(s.checks).length === rule.checks.length && rule.checks.every(check => Object.hasOwn(s.checks, check))
13
+ && Object.values(s.checks).every(value => Number.isSafeInteger(value) && value >= 0)
14
+ && Object.values(s.checks).reduce((total, value) => total + value, 0) === s.violations) && validPage(s.page) && validKind(s.kind) && Number.isSafeInteger(s.violations) && s.violations >= 0 && count(s.duration_ms))
12
15
  || !stats.models.every(s => s && validPage(s.page) && validKind(s.kind) && count(s.duration_ms) && count(s.components) && count(s.css_files) && (s.error_name === undefined || s.error_name === 'css_syntax_error' || s.error_name === 'unknown_error')))
13
16
  return;
14
17
  for (const model of stats.models) {
@@ -18,9 +21,13 @@ export function readStats(file, run) {
18
21
  });
19
22
  }
20
23
  for (const statsRow of stats.rules) {
21
- run.recordRow('lint_rule', statsRow.rule, { item_type: statsRow.kind, item_id: statsRow.page, step_details: { violations: statsRow.violations } }, {
24
+ const row = run.recordRow('lint_rule', statsRow.rule, { item_type: statsRow.kind, item_id: statsRow.page, step_details: { violations: statsRow.violations } }, {
22
25
  success: statsRow.violations === 0, duration_ms: statsRow.duration_ms,
23
26
  });
27
+ for (const [check, violations] of Object.entries(statsRow.checks)) {
28
+ if (violations > 0)
29
+ run.recordRow('lint_rule', statsRow.rule, { item_type: statsRow.kind, item_id: statsRow.page, step_details: { check, violations } }, { success: false, duration_ms: 0 }, row);
30
+ }
24
31
  }
25
32
  }
26
33
  catch { /* Missing, truncated or malformed stats never affect lint. */ }
package/dist/bi/stats.js CHANGED
@@ -18,7 +18,7 @@ export function pageId(filename, text, program) {
18
18
  return page;
19
19
  }
20
20
  /** Called once per rule and file by the adapter. Passing rules retain their zero count. */
21
- export function startRuleStats(rule, filename, text, program) {
21
+ export function startRuleStats(rule, checks, filename, text, program) {
22
22
  const destination = process.env.ORIGAMI_LINT_STATS;
23
23
  if (!destination)
24
24
  return;
@@ -33,7 +33,8 @@ export function startRuleStats(rule, filename, text, program) {
33
33
  });
34
34
  }
35
35
  const kind = filename.startsWith(`${GLOBAL_COMPONENTS_FOLDER}/`) ? 'component' : 'page';
36
- const stats = { rule, kind, page: pageId(filename, text, program), violations: 0, duration_ms: 0 };
36
+ const page = pageId(filename, text, program);
37
+ const stats = { rule, checks: Object.fromEntries(checks.map(check => [check, 0])), kind, page, violations: 0, duration_ms: 0 };
37
38
  rules.push(stats);
38
39
  return stats;
39
40
  }
@@ -9,7 +9,8 @@ export function runProjectRules(files, { rules = projectRuleRegistry, readFile =
9
9
  log(`${rules.length} project rules to run`);
10
10
  let passed = true;
11
11
  for (const rule of rules) {
12
- const row = parentRow?.run.openRow('lint_rule', rule.name, { item_type: 'site', step_details: { scope: 'project', severity: rule.severity ?? 'error' } });
12
+ const started = performance.now();
13
+ const details = { scope: 'project', severity: rule.severity ?? 'error' };
13
14
  let problems;
14
15
  try {
15
16
  problems = rule.check(createProjectModel(rule.name, files, readFile));
@@ -17,12 +18,17 @@ export function runProjectRules(files, { rules = projectRuleRegistry, readFile =
17
18
  catch (error) {
18
19
  // A rule that couldn't finish (a file it can't parse, a path out of the site) fails the run, whatever its severity.
19
20
  print(formatProblem({ name: rule.name }, { file: '.', message: `the rule couldn't finish, so this run can't pass: ${error instanceof Error ? error.message : String(error)}` }));
20
- row?.end(false, { error_name: 'rule_crashed' });
21
+ parentRow?.run.recordRow('lint_rule', rule.name, { item_type: 'site', step_details: details }, { success: false, failure: { error_name: 'rule_crashed' }, duration_ms: performance.now() - started });
21
22
  passed = false;
22
23
  continue;
23
24
  }
24
- row?.update({ step_details: { violations: problems.length } });
25
- row?.end(problems.length === 0);
25
+ const duration = performance.now() - started;
26
+ const row = parentRow?.run.recordRow('lint_rule', rule.name, { item_type: 'site', step_details: { ...details, violations: problems.length } }, { success: problems.length === 0, duration_ms: duration });
27
+ for (const check of rule.checks) {
28
+ const violations = problems.filter(problem => problem.check === check).length;
29
+ if (violations > 0 && row)
30
+ row.run.recordRow('lint_rule', rule.name, { item_type: 'site', step_details: { ...details, check, violations } }, { success: false, duration_ms: 0 }, row);
31
+ }
26
32
  for (const problem of problems) {
27
33
  print(formatProblem(rule, problem));
28
34
  if ((rule.severity ?? 'error') === 'error')
@@ -13,5 +13,6 @@ import slotAttributes from "./site-app/slot-attributes.js";
13
13
  import themeFontIds from "./site-app/theme-font-ids.js";
14
14
  import themeRoot from "./site-app/theme-root.js";
15
15
  import noFreeCode from "./site-app/no-free-code.js";
16
+ // Execution registries combine already-checked rules; only this boundary erases their individual key unions.
16
17
  export const ruleRegistry = [allowedImports, noFreeCode, pageStructure, noAbsoluteSection, themeFontIds, slotAttributes, themeRoot, breakpoints, cssFileNesting, globalStyles, applyAndPresets, bindings, dataValues, cssLayoutProperties, cssSelectors];
17
18
  export { defineRule } from "./site-app/define-rule.js";
@@ -4,6 +4,7 @@
4
4
  import { findPages } from "../../page-model/index.js";
5
5
  import { PAGES_FOLDER, POPUPS_FOLDER } from "../../shared/site-files-tree-mapping.js";
6
6
  import { defineProjectRule } from "./define-project-rule.js";
7
+ const CHECKS = ['missing_home', 'multiple_home_folders', 'nested_home_folder', 'popup_in_parentheses'];
7
8
  const EXPECTED_ONE_HOME = `Expected exactly one home page folder "pages/(<slug>)" in ${PAGES_FOLDER}`;
8
9
  export const NO_HOME = `${EXPECTED_ONE_HOME}; found 0. Name the home page's folder in parentheses, directly under ${PAGES_FOLDER}, like ${PAGES_FOLDER}/(home) (rule P2-R01; tracker OUR-035). The import stops without it.`;
9
10
  export const moreHomes = (count, folder) => `${EXPECTED_ONE_HOME}; found ${count}. Keep the parentheses on the home page's folder only: rename ${folder} to ${withoutParentheses(folder)} (rule P2-R01; tracker OUR-035).`;
@@ -12,6 +13,7 @@ export const onlyHomeInParentheses = (folder) => `Only the home page is written
12
13
  const HOME_FOLDER = /^\(.+\)$/;
13
14
  export default defineProjectRule({
14
15
  name: 'home-page-folder',
16
+ checks: CHECKS,
15
17
  editors: ['studio2'],
16
18
  check: (project) => [...checkOneHome(project), ...checkNoNestedHome(project), ...checkNoPopupInParentheses(project)],
17
19
  });
@@ -20,14 +22,14 @@ export default defineProjectRule({
20
22
  function checkOneHome(project) {
21
23
  const homes = getPages(project).filter((page) => page.folders.length === 1 && HOME_FOLDER.test(page.folders[0]));
22
24
  if (homes.length === 0)
23
- return [{ file: PAGES_FOLDER, message: NO_HOME }];
24
- return homes.slice(1).map((home) => ({ file: home.jsxFile, message: moreHomes(homes.length, home.folders[0]) }));
25
+ return [{ check: 'missing_home', file: PAGES_FOLDER, message: NO_HOME }];
26
+ return homes.slice(1).map((home) => ({ check: 'multiple_home_folders', file: home.jsxFile, message: moreHomes(homes.length, home.folders[0]) }));
25
27
  }
26
28
  /** Reports every page with a folder in parentheses below the top of pages/ on its path, as import-site refuses them. */
27
29
  function checkNoNestedHome(project) {
28
30
  return getPages(project).flatMap((page) => {
29
31
  const nested = page.folders.slice(1).find((folder) => HOME_FOLDER.test(folder));
30
- return nested ? [{ file: page.jsxFile, message: onlyHomeInParentheses(nested) }] : [];
32
+ return nested ? [{ check: 'nested_home_folder', file: page.jsxFile, message: onlyHomeInParentheses(nested) }] : [];
31
33
  });
32
34
  }
33
35
  /** Reports each folder in parentheses under popups/, once, as the converter refuses them. */
@@ -39,7 +41,7 @@ function checkNoPopupInParentheses(project) {
39
41
  if (index !== -1)
40
42
  folders.add(`${POPUPS_FOLDER}/${segments.slice(0, index + 1).join('/')}`);
41
43
  }
42
- return [...folders].sort().map((folder) => ({ file: folder, message: onlyHomeInParentheses(folder.slice(folder.lastIndexOf('/') + 1)) }));
44
+ return [...folders].sort().map((folder) => ({ check: 'popup_in_parentheses', file: folder, message: onlyHomeInParentheses(folder.slice(folder.lastIndexOf('/') + 1)) }));
43
45
  }
44
46
  /** The site's pages under pages/, sorted by path. */
45
47
  function getPages(project) {
@@ -1,3 +1,4 @@
1
1
  import homePageFolder from "./home-page-folder.js";
2
2
  export { defineProjectRule } from "./define-project-rule.js";
3
+ // Execution registries combine already-checked rules; only this boundary erases their individual key unions.
3
4
  export const projectRuleRegistry = [homePageFolder];
@@ -6,11 +6,13 @@ import { CSS_FILES, JSX_FILES } from "../../page-model/index.js";
6
6
  import { BUILDERS, RUNTIME_ONLY_BUILDERS, RUNTIME_PACKAGE } from "../../rules-api/index.js";
7
7
  import { GLOBAL_COMPONENTS, GLOBAL_COMPONENTS_FOLDER, GLOBAL_FOLDER, SITE_SRC } from "../../shared/site-files-tree-mapping.js";
8
8
  import { defineRule } from "./define-rule.js";
9
+ const CHECKS = ['disallowed_import_source', 'interaction_import', 'missing_global_import_name', 'invalid_global_import_name', 'default_builder_import', 'aliased_builder_import', 'wrong_builder_source'];
9
10
  /** The interactions packages, which the import reserves: structure.jsx imports nothing from them. Matched by source, not by name: `Interact` from @site/site-code is the site's own. */
10
11
  const INTERACTIONS_PACKAGES = ['interactReact', 'interact', 'motionPresets'];
11
12
  const SOURCE_HINT = `Only packages, files in ${path.posix.basename(GLOBAL_FOLDER)}/, and ${CSS_FILES.map((file) => `'./${file}'`).join(' or ')} can be.`;
12
13
  export default defineRule({
13
14
  name: 'allowed-imports',
15
+ checks: CHECKS,
14
16
  editors: 'all',
15
17
  callbacks: {
16
18
  ImportDeclaration: (input) => {
@@ -28,7 +30,7 @@ function hasAllowedSource({ node, rulesApi, report }) {
28
30
  const source = node.source.value;
29
31
  if (isAllowedSource(source, rulesApi.page.jsxFile))
30
32
  return true;
31
- report({ loc: node.loc, message: `\`${source}\` can't be imported. ${SOURCE_HINT}` });
33
+ report({ check: 'disallowed_import_source', loc: node.loc, message: `\`${source}\` can't be imported. ${SOURCE_HINT}` });
32
34
  return false;
33
35
  }
34
36
  /** Reports an import from @wix/interact or @wix/motion-presets; an `import type` is erased, so it passes. */
@@ -37,7 +39,7 @@ function isNotInteractionsPackage({ node, rulesApi, report }) {
37
39
  const kind = rulesApi.getImportKind(source);
38
40
  if (!kind || !INTERACTIONS_PACKAGES.includes(kind) || (node.specifiers.length > 0 && getValueSpecifiers(node).length === 0))
39
41
  return true;
40
- report({ loc: node.loc, message: `\`${source}\` can't be imported: a page's interactions go in its servicesConfigs/interactions.ts, with the element's id as \`key\`. Remove the import and any <Interaction> tag around an element (keep the element).` });
42
+ report({ check: 'interaction_import', loc: node.loc, message: `\`${source}\` can't be imported: a page's interactions go in its servicesConfigs/interactions.ts, with the element's id as \`key\`. Remove the import and any <Interaction> tag around an element (keep the element).` });
41
43
  return false;
42
44
  }
43
45
  /** An import that reaches global/components must be `import <Folder> from '…/global/components/<Folder>'`, as the converter reads it. */
@@ -48,13 +50,13 @@ function checkGlobalComponentImport({ node, rulesApi, report }) {
48
50
  if (!folder)
49
51
  return;
50
52
  if (node.specifiers.length === 0) {
51
- report({ loc: node.loc, message: `${source} is imported without a name. Import a global component under its folder's name: \`import ${folder} from '${getGlobalComponentImportPath(folder, jsxFile)}'\`, and write \`<${folder}>\`.` });
53
+ report({ check: 'missing_global_import_name', loc: node.loc, message: `${source} is imported without a name. Import a global component under its folder's name: \`import ${folder} from '${getGlobalComponentImportPath(folder, jsxFile)}'\`, and write \`<${folder}>\`.` });
52
54
  }
53
55
  for (const specifier of getValueSpecifiers(node)) {
54
56
  const name = specifier.type === 'ImportNamespaceSpecifier' ? '*' : specifier.local.name;
55
57
  if (withoutLeadingDots(source) === `${GLOBAL_COMPONENTS}/${name}`)
56
58
  continue;
57
- report({
59
+ report({ check: 'invalid_global_import_name',
58
60
  loc: specifier.loc,
59
61
  message: `\`${specifier.local.name}\` is imported from ${source}. Import a global component under its folder's name: \`import ${folder} from '${getGlobalComponentImportPath(folder, jsxFile)}'\`, and write \`<${folder}>\`.`,
60
62
  });
@@ -67,14 +69,14 @@ function checkBuilderImports({ node, rulesApi, report }) {
67
69
  const importedName = rulesApi.getImportedName(specifier);
68
70
  if (source === RUNTIME_PACKAGE) {
69
71
  if (importedName === 'default') {
70
- report({ loc: specifier.loc, message: `\`${localName}\` is a default import of ${RUNTIME_PACKAGE}. Import each builder by name: \`import { ctx } from '${RUNTIME_PACKAGE}'\`.` });
72
+ report({ check: 'default_builder_import', loc: specifier.loc, message: `\`${localName}\` is a default import of ${RUNTIME_PACKAGE}. Import each builder by name: \`import { ctx } from '${RUNTIME_PACKAGE}'\`.` });
71
73
  }
72
74
  else if (BUILDERS.includes(importedName) && localName !== importedName) {
73
- report({ loc: specifier.loc, message: `The builder \`${importedName}\` is imported as \`${localName}\`. Import it under its own name: \`import { ${importedName} } from '${RUNTIME_PACKAGE}'\`, and call it as \`${importedName}(…)\`.` });
75
+ report({ check: 'aliased_builder_import', loc: specifier.loc, message: `The builder \`${importedName}\` is imported as \`${localName}\`. Import it under its own name: \`import { ${importedName} } from '${RUNTIME_PACKAGE}'\`, and call it as \`${importedName}(…)\`.` });
74
76
  }
75
77
  }
76
78
  else if (RUNTIME_ONLY_BUILDERS.includes(localName)) {
77
- report({ loc: specifier.loc, message: `\`${localName}\` is imported from ${source}, but \`${localName}\` is a builder of ${RUNTIME_PACKAGE}. Import it from there: \`import { ${localName} } from '${RUNTIME_PACKAGE}'\`.` });
79
+ report({ check: 'wrong_builder_source', loc: specifier.loc, message: `\`${localName}\` is imported from ${source}, but \`${localName}\` is a builder of ${RUNTIME_PACKAGE}. Import it from there: \`import { ${localName} } from '${RUNTIME_PACKAGE}'\`.` });
78
80
  }
79
81
  }
80
82
  }
@@ -1,7 +1,9 @@
1
1
  import { createCssFileReporter, describeAtRule, getCssChildren, getDashedId, getMixinId, getSegmentClass, getSelectorSegments, isOneEditAway, isPresetProperty, isSafeCssIdentifier } from "../../rules-api/index.js";
2
2
  import { defineRule } from "./define-rule.js";
3
+ const CHECKS = ['apply_with_declarations', 'nested_part_preset', 'duplicate_apply', 'invalid_apply_target', 'missing_apply_target', 'invalid_preset_name', 'missing_preset_value', 'duplicate_default_style', 'duplicate_preset', 'invalid_apply_form'];
3
4
  export default defineRule({
4
5
  name: 'apply-and-presets',
6
+ checks: CHECKS,
5
7
  editors: ['studio2'],
6
8
  callbacks: {
7
9
  Program: ({ rulesApi, report }) => {
@@ -57,7 +59,7 @@ function checkElementRule(node, breakpoint, media, reportInFile) {
57
59
  const declarations = children.filter((child) => child.type === 'decl');
58
60
  const values = declarations.filter((declaration) => !isPresetProperty(declaration.prop));
59
61
  if (apply && values.length > 0) {
60
- reportInFile(values[0], `${where}: \`${describeAtRule(apply)}\` can't share a rule with style declarations (${describeProperties(values)}). Put them in the global style (\`@mixin ${apply.params.trim()}\` in theme.css), or under another breakpoint or state; only \`--preset…\` may stay beside the \`@apply\` (tracker CONV-astParser.ts:117).`);
62
+ reportInFile(values[0], 'apply_with_declarations', `${where}: \`${describeAtRule(apply)}\` can't share a rule with style declarations (${describeProperties(values)}). Put them in the global style (\`@mixin ${apply.params.trim()}\` in theme.css), or under another breakpoint or state; only \`--preset…\` may stay beside the \`@apply\` (tracker CONV-astParser.ts:117).`);
61
63
  return null;
62
64
  }
63
65
  return { node, selector: node.selector, breakpoint, media, apply, presets: declarations.filter((declaration) => isPresetProperty(declaration.prop)), values };
@@ -81,7 +83,7 @@ function checkMixinRule(node, mixin, reportInFile) {
81
83
  return;
82
84
  const declarations = children.filter((child) => child.type === 'decl');
83
85
  if (declarations.length > 0)
84
- return reportInFile(declarations[0], `${where}: \`${describeAtRule(apply)}\` can't share a rule with declarations (${describeProperties(declarations)}). Move them into \`@mixin ${apply.params.trim()}\` (tracker CONV-astParser.ts:117).`);
86
+ return reportInFile(declarations[0], 'apply_with_declarations', `${where}: \`${describeAtRule(apply)}\` can't share a rule with declarations (${describeProperties(declarations)}). Move them into \`@mixin ${apply.params.trim()}\` (tracker CONV-astParser.ts:117).`);
85
87
  if (!isApplyWritten(apply))
86
88
  reportApplyForm(apply, where, reportInFile);
87
89
  }
@@ -91,7 +93,7 @@ function checkSelectorRules(group, componentClasses, reportInFile) {
91
93
  const picks = group.flatMap((rule) => rule.presets.flatMap((declaration) => checkPresetName(declaration, where, reportInFile)));
92
94
  if (isInnerPartRule(group[0].selector, componentClasses)) {
93
95
  for (const { declaration } of picks.filter(({ path }) => path.length > 0)) {
94
- reportInFile(declaration, `${where}: \`${declaration.prop}\` can't be set on an inner part's rule, which picks only the part's own preset (\`--preset\`). Write it on the component's own rule, with the part's whole path: \`--preset-<part>-${declaration.prop.slice('--preset-'.length)}\` (tracker CONV-styleParser.ts:583).`);
96
+ reportInFile(declaration, 'nested_part_preset', `${where}: \`${declaration.prop}\` can't be set on an inner part's rule, which picks only the part's own preset (\`--preset\`). Write it on the component's own rule, with the part's whole path: \`--preset-<part>-${declaration.prop.slice('--preset-'.length)}\` (tracker CONV-styleParser.ts:583).`);
95
97
  }
96
98
  }
97
99
  for (const rules of groupByBreakpoint(group)) {
@@ -100,12 +102,12 @@ function checkSelectorRules(group, componentClasses, reportInFile) {
100
102
  const breakpointDescription = rules[0].media ? `in \`${describeAtRule(rules[0].media)}\`` : 'outside any @media';
101
103
  const applies = rules.filter((rule) => rule.apply);
102
104
  for (const rule of applies.slice(1)) {
103
- reportInFile(rule.apply, `${where}: two \`@apply\` for this selector ${breakpointDescription} (\`${describeAtRule(applies[0].apply)}\` on line ${getLine(applies[0].apply)}, \`${describeAtRule(rule.apply)}\`): keep one (tracker CONV-styleParser.ts:179).`);
105
+ reportInFile(rule.apply, 'duplicate_apply', `${where}: two \`@apply\` for this selector ${breakpointDescription} (\`${describeAtRule(applies[0].apply)}\` on line ${getLine(applies[0].apply)}, \`${describeAtRule(rule.apply)}\`): keep one (tracker CONV-styleParser.ts:179).`);
104
106
  }
105
107
  const ruleWithValues = rules.find((rule) => rule.values.length > 0);
106
108
  if (applies.length > 0 && ruleWithValues) {
107
109
  const { apply } = applies[0];
108
- reportInFile(ruleWithValues.values[0], `${where} has both \`${describeAtRule(apply)}\` (line ${getLine(apply)}) and style declarations ${breakpointDescription}: keep one, or move the declarations into the global style (\`@mixin ${apply.params.trim()}\` in theme.css) (tracker CONV-styleParser.ts:204).`);
110
+ reportInFile(ruleWithValues.values[0], 'apply_with_declarations', `${where} has both \`${describeAtRule(apply)}\` (line ${getLine(apply)}) and style declarations ${breakpointDescription}: keep one, or move the declarations into the global style (\`@mixin ${apply.params.trim()}\` in theme.css) (tracker CONV-styleParser.ts:204).`);
109
111
  }
110
112
  }
111
113
  }
@@ -121,12 +123,12 @@ function checkApplyTargets(elementRules, declaredIds, kind, reportInFile) {
121
123
  // the build strips a trailing `()`; the import doesn't, and reads `btn()` as the id (astParser.ts MIXIN_ID_PATTERN)
122
124
  const called = id.endsWith('()') ? id.slice(0, -2) : null;
123
125
  if (called !== null && declaredIds.has(called)) {
124
- reportInFile(apply, `rule \`${selector.trim()}\`: \`@apply --${id}\` reads as the global style \`${id}\`, which the site doesn't have: write \`@apply --${called}\`, without the parentheses (tracker OUR-006).`);
126
+ reportInFile(apply, 'invalid_apply_target', `rule \`${selector.trim()}\`: \`@apply --${id}\` reads as the global style \`${id}\`, which the site doesn't have: write \`@apply --${called}\`, without the parentheses (tracker OUR-006).`);
125
127
  continue;
126
128
  }
127
129
  const nearMiss = [...declaredIds].find((declared) => isOneEditAway(id, declared));
128
130
  const fix = nearMiss ? `Did you mean \`@apply --${nearMiss}\`? Otherwise apply` : 'Apply';
129
- reportInFile(apply, `rule \`${selector.trim()}\`: \`@apply --${id}\` names no global style: no \`@mixin --${id}\` in theme.css${kind === 'style' ? ' or style.css' : ''}. ${fix} one theme.css declares, or declare \`@mixin --${id}\` in theme.css (tracker OUR-006).`);
131
+ reportInFile(apply, 'missing_apply_target', `rule \`${selector.trim()}\`: \`@apply --${id}\` names no global style: no \`@mixin --${id}\` in theme.css${kind === 'style' ? ' or style.css' : ''}. ${fix} one theme.css declares, or declare \`@mixin --${id}\` in theme.css (tracker OUR-006).`);
130
132
  }
131
133
  }
132
134
  /** A `--preset-<path>` names each part as a CSS identifier (CONV-styleParser.ts:62); returns the pick, or none after reporting. */
@@ -136,7 +138,7 @@ function checkPresetName(declaration, where, reportInFile) {
136
138
  const path = decodePresetPath(declaration.prop.slice('--preset-'.length));
137
139
  if (path.every(isSafeCssIdentifier))
138
140
  return [{ declaration, path }];
139
- reportInFile(declaration, `${where}: \`${declaration.prop}\` doesn't name an inner part: write \`--preset-<part>\`, each part a CSS identifier, parts joined by \`-\` and a \`-\` inside a part's name written \`--\` (\`close-button\` → \`--preset-close--button\`) (tracker CONV-styleParser.ts:62).`);
141
+ reportInFile(declaration, 'invalid_preset_name', `${where}: \`${declaration.prop}\` doesn't name an inner part: write \`--preset-<part>\`, each part a CSS identifier, parts joined by \`-\` and a \`-\` inside a part's name written \`--\` (\`close-button\` → \`--preset-close--button\`) (tracker CONV-styleParser.ts:62).`);
140
142
  return [];
141
143
  }
142
144
  /** The last pick of each path at one breakpoint is the one the import reads: it has a value (CONV-styleParser.ts:88). */
@@ -148,7 +150,7 @@ function checkPresetValues(picks, where, reportInFile) {
148
150
  continue;
149
151
  seen.add(key);
150
152
  if (!declaration.value.trim())
151
- reportInFile(declaration, `${where}: \`${declaration.prop}\` has no value. Write a preset name, or remove the declaration to use no preset (tracker CONV-styleParser.ts:88).`);
153
+ reportInFile(declaration, 'missing_preset_value', `${where}: \`${declaration.prop}\` has no value. Write a preset name, or remove the declaration to use no preset (tracker CONV-styleParser.ts:88).`);
152
154
  }
153
155
  }
154
156
  /**
@@ -168,7 +170,7 @@ function checkOneSelectorPerElement(groups, reportInFile) {
168
170
  if (stylingRule) {
169
171
  const first = firstStylingRules.get(target.join(' '));
170
172
  if (first && first.selector !== stylingRule.selector) {
171
- reportInFile(stylingRule.node, `\`${stylingRule.selector.trim()}\` and \`${first.selector.trim()}\` (line ${getLine(first.node)}) are one element, and both set its default style: write it under one selector (tracker CONV-styleParser.ts:109).`);
173
+ reportInFile(stylingRule.node, 'duplicate_default_style', `\`${stylingRule.selector.trim()}\` and \`${first.selector.trim()}\` (line ${getLine(first.node)}) are one element, and both set its default style: write it under one selector (tracker CONV-styleParser.ts:109).`);
172
174
  }
173
175
  else if (!first)
174
176
  firstStylingRules.set(target.join(' '), stylingRule);
@@ -185,7 +187,7 @@ function checkOneSelectorPerElement(groups, reportInFile) {
185
187
  if (!first)
186
188
  firstPicks.set(key, { selector: group[0].selector, declaration });
187
189
  else
188
- reportInFile(declaration, `${describePresetOwner(key)}'s preset is picked twice: \`${first.declaration.prop}\` on \`${first.selector.trim()}\` (line ${getLine(first.declaration)}) and \`${declaration.prop}\` on \`${group[0].selector.trim()}\`. Keep one (tracker CONV-styleParser.ts:109).`);
190
+ reportInFile(declaration, 'duplicate_preset', `${describePresetOwner(key)}'s preset is picked twice: \`${first.declaration.prop}\` on \`${first.selector.trim()}\` (line ${getLine(first.declaration)}) and \`${declaration.prop}\` on \`${group[0].selector.trim()}\`. Keep one (tracker CONV-styleParser.ts:109).`);
189
191
  }
190
192
  }
191
193
  }
@@ -317,12 +319,12 @@ function groupByBreakpoint(rules) {
317
319
  // ── reporting ──
318
320
  /** Reports the second `@apply` of one rule (CONV-astParser.ts:99); returns null, as the rule isn't read further. */
319
321
  function reportTwoApplies(applies, where, why, reportInFile) {
320
- reportInFile(applies[1], `${where}: ${applies.length} \`@apply\` in one rule (${applies.map((apply) => `\`${describeAtRule(apply)}\``).join(', ')}). Keep one: ${why} (tracker CONV-astParser.ts:99).`);
322
+ reportInFile(applies[1], 'duplicate_apply', `${where}: ${applies.length} \`@apply\` in one rule (${applies.map((apply) => `\`${describeAtRule(apply)}\``).join(', ')}). Keep one: ${why} (tracker CONV-astParser.ts:99).`);
321
323
  return null;
322
324
  }
323
325
  /** Reports an `@apply` that isn't one dashed id with no block (CONV-astParser.ts:41); returns null, as the rule isn't read further. */
324
326
  function reportApplyForm(apply, where, reportInFile) {
325
- reportInFile(apply, `${where}: \`${describeAtRule(apply)}\` must name one global style by its dashed id. Write \`@apply --<global-style-id>;\`, with no block after it (tracker CONV-astParser.ts:41).`);
327
+ reportInFile(apply, 'invalid_apply_form', `${where}: \`${describeAtRule(apply)}\` must name one global style by its dashed id. Write \`@apply --<global-style-id>;\`, with no block after it (tracker CONV-astParser.ts:41).`);
326
328
  return null;
327
329
  }
328
330
  /** A pick's key → the element it picks the preset of: '.comp-x|label' → '`label` of `.comp-x`'; '.comp-x' → '`.comp-x`'. */