@wix/origami-lint 1.34.28 → 1.34.30
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/dist/rules/index.js +3 -1
- package/dist/rules/site-app/allowed-imports.js +1 -3
- package/dist/rules/site-app/known-tags.js +100 -0
- package/dist/rules/site-app/theme-font-ids.js +7 -49
- package/dist/rules/site-app/theme-font-known.js +30 -0
- package/dist/rules-api/index.js +2 -2
- package/dist/rules-api/rules-api.js +3 -1
- package/dist/shared/constants.js +6 -0
- package/dist/shared/css.js +54 -0
- package/package.json +2 -2
package/dist/rules/index.js
CHANGED
|
@@ -7,13 +7,15 @@ import cssLayoutProperties from "./site-app/css-layout-properties.js";
|
|
|
7
7
|
import cssSelectors from "./site-app/css-selectors.js";
|
|
8
8
|
import dataValues from "./site-app/data-values.js";
|
|
9
9
|
import globalStyles from "./site-app/global-styles.js";
|
|
10
|
+
import knownTags from "./site-app/known-tags.js";
|
|
10
11
|
import noAbsoluteSection from "./site-app/no-absolute-section.js";
|
|
11
12
|
import pageStructure from "./site-app/page-structure.js";
|
|
12
13
|
import slotAttributes from "./site-app/slot-attributes.js";
|
|
13
14
|
import slotPlacement from "./site-app/slot-placement.js";
|
|
14
15
|
import themeFontIds from "./site-app/theme-font-ids.js";
|
|
16
|
+
import themeFontKnown from "./site-app/theme-font-known.js";
|
|
15
17
|
import themeRoot from "./site-app/theme-root.js";
|
|
16
18
|
import noFreeCode from "./site-app/no-free-code.js";
|
|
17
19
|
// Execution registries combine already-checked rules; only this boundary erases their individual key unions.
|
|
18
|
-
export const ruleRegistry = [allowedImports, noFreeCode, pageStructure, noAbsoluteSection, themeFontIds, slotAttributes, themeRoot, breakpoints, cssFileNesting, globalStyles, applyAndPresets, bindings, dataValues, cssLayoutProperties, cssSelectors, slotPlacement];
|
|
20
|
+
export const ruleRegistry = [allowedImports, noFreeCode, pageStructure, noAbsoluteSection, themeFontIds, themeFontKnown, slotAttributes, themeRoot, breakpoints, cssFileNesting, globalStyles, applyAndPresets, bindings, dataValues, cssLayoutProperties, cssSelectors, slotPlacement, knownTags];
|
|
19
21
|
export { defineRule } from "./site-app/define-rule.js";
|
|
@@ -3,12 +3,10 @@
|
|
|
3
3
|
// refuses a tag from them, parseJsx.ts:471, identity.ts:80; a call is free code, and fn() / ctx() refuse them as a source).
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { CSS_FILES, JSX_FILES } from "../../page-model/index.js";
|
|
6
|
-
import { BUILDERS, RUNTIME_ONLY_BUILDERS, RUNTIME_PACKAGE } from "../../rules-api/index.js";
|
|
6
|
+
import { BUILDERS, INTERACTIONS_PACKAGES, 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
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'];
|
|
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. */
|
|
11
|
-
const INTERACTIONS_PACKAGES = ['interactReact', 'interact', 'motionPresets'];
|
|
12
10
|
const SOURCE_HINT = `Only packages, files in ${path.posix.basename(GLOBAL_FOLDER)}/, and ${CSS_FILES.map((file) => `'./${file}'`).join(' or ')} can be.`;
|
|
13
11
|
export default defineRule({
|
|
14
12
|
name: 'allowed-imports',
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// How a structure file writes its tags, as the converter reads them (tagOf and validTagOrIssue, parseJsx.ts, converter 0.88):
|
|
2
|
+
// - a tag is <Name> or <NS.Name>: no <svg:rect>, no <UI.Forms.Input> (tracker CONV-parseJsx.ts:401);
|
|
3
|
+
// - a lowercase tag is an HTML element, which the import refuses: <div> (CONV-parseJsx.ts:379). page-structure already reports
|
|
4
|
+
// one at the top of the file or directly in a section, so this rule doesn't. <NS.Name> needs `import * as NS` (379);
|
|
5
|
+
// - no component comes from a source that holds none: the runtime (except <Remove>), a menu, `document/…`, @wix/sdk (CONV-parseJsx.ts:351).
|
|
6
|
+
// The DM's own `document` holds the classic components (<ClassicSection>). allowed-imports reports @wix/interact and
|
|
7
|
+
// @wix/motion-presets on the import line;
|
|
8
|
+
// - a tag's name is capitalized (CONV-parseJsx.ts:359). It reads the local name: the export imports a context provider as
|
|
9
|
+
// `import { cartContext as CartContext }`, and only the site's providers tell it apart from `import { image as Image }`.
|
|
10
|
+
// A capitalized tag with no import, <NS.Name> with no NS, and a namespace written as <NS> are TypeScript's (#409): it reports a name not in scope.
|
|
11
|
+
import { INTERACTIONS_PACKAGES, SECTIONS } from "../../rules-api/index.js";
|
|
12
|
+
import { defineRule } from "./define-rule.js";
|
|
13
|
+
const CHECKS = ['unsupported_tag_name', 'html_tag', 'named_import_as_namespace', 'tag_from_reserved_source', 'lowercase_tag'];
|
|
14
|
+
/** The converter's TAG_PATTERN (identity.ts). */
|
|
15
|
+
const TAG_PATTERN = /^[A-Z][A-Za-z0-9_$]*$/;
|
|
16
|
+
/** The sources the converter reserves (isReservedRegistry, identity.ts) that allowed-imports doesn't already report. */
|
|
17
|
+
const RESERVED_SOURCES = ['runtime', 'menu', 'document', 'sdk'];
|
|
18
|
+
/** The one tag the runtime has: the <Remove> wrapper. */
|
|
19
|
+
const RUNTIME_TAG = 'Remove';
|
|
20
|
+
export default defineRule({
|
|
21
|
+
name: 'known-tags',
|
|
22
|
+
checks: CHECKS,
|
|
23
|
+
editors: ['studio2'],
|
|
24
|
+
callbacks: {
|
|
25
|
+
JSXElement: (input) => {
|
|
26
|
+
if (!input.rulesApi.getComponent(input.node))
|
|
27
|
+
return;
|
|
28
|
+
if (!checkTagNameShape(input))
|
|
29
|
+
return;
|
|
30
|
+
if (!checkTagNotHtml(input))
|
|
31
|
+
return;
|
|
32
|
+
if (!checkTagNamespaceImport(input))
|
|
33
|
+
return;
|
|
34
|
+
if (!checkTagSource(input))
|
|
35
|
+
return;
|
|
36
|
+
checkTagCapitalized(input);
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
// ── the steps: each reports its problem; one that returns false ends the steps for the tag, as the converter stops at its first ──
|
|
41
|
+
/** Reports <svg:rect> and <UI.Forms.Input> (CONV-parseJsx.ts:401). */
|
|
42
|
+
function checkTagNameShape({ node, rulesApi, report }) {
|
|
43
|
+
const { name } = node.openingElement;
|
|
44
|
+
if (name.type === 'JSXIdentifier' || (name.type === 'JSXMemberExpression' && name.object.type === 'JSXIdentifier'))
|
|
45
|
+
return true;
|
|
46
|
+
report({ check: 'unsupported_tag_name', loc: name.loc, message: `<${rulesApi.getTagName(node)}> is not a supported tag name: write <Name> or <NS.Name> (one namespace import, one dot).` });
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
/** Reports a lowercase tag with no import: an HTML element (CONV-parseJsx.ts:379), unless page-structure reports it already. */
|
|
50
|
+
function checkTagNotHtml({ node, rulesApi, report }) {
|
|
51
|
+
const { name } = node.openingElement;
|
|
52
|
+
if (name.type !== 'JSXIdentifier' || !/^[a-z]/.test(name.name) || rulesApi.getImport(node))
|
|
53
|
+
return true;
|
|
54
|
+
if (!isReportedByPageStructure(node, rulesApi)) {
|
|
55
|
+
report({ check: 'html_tag', loc: name.loc, message: `<${name.name}> is an HTML tag, which the import refuses: use an imported component (import { Name } from '@origami-wrappers/<app>/<name>', then <Name>).` });
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
/** Reports <NS.Name> whose NS is imported by name, not as `import * as NS` (CONV-parseJsx.ts:379). With no import at all, it's TypeScript's; from an interactions package, allowed-imports'. */
|
|
60
|
+
function checkTagNamespaceImport({ node, rulesApi, report }) {
|
|
61
|
+
const { name } = node.openingElement;
|
|
62
|
+
const found = rulesApi.getImport(node);
|
|
63
|
+
if (name.type !== 'JSXMemberExpression' || name.object.type !== 'JSXIdentifier' || !found || found.imported === '*' || (found.kind && INTERACTIONS_PACKAGES.includes(found.kind)))
|
|
64
|
+
return true;
|
|
65
|
+
const namespace = name.object.name;
|
|
66
|
+
report({ check: 'named_import_as_namespace', loc: name.loc, message: `<${namespace}.${name.property.name}> reads \`${namespace}\` as a namespace, but \`${namespace}\` is imported by name: import it as \`import * as ${namespace} from '${found.source}'\`, or import ${name.property.name} by name and write <${name.property.name}>.` });
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
/** Reports a tag imported from a source that holds no components (CONV-parseJsx.ts:351). <Remove> from the runtime is a wrapper. */
|
|
70
|
+
function checkTagSource({ node, rulesApi, report }) {
|
|
71
|
+
const found = rulesApi.getImport(node);
|
|
72
|
+
if (!found?.kind || !RESERVED_SOURCES.includes(found.kind) || (found.kind === 'runtime' && found.imported === RUNTIME_TAG))
|
|
73
|
+
return true;
|
|
74
|
+
const tag = rulesApi.getTagName(node);
|
|
75
|
+
const menuHint = found.kind === 'menu' ? `, and use the menu as a value (items: ${tag}.items)` : '';
|
|
76
|
+
report({ check: 'tag_from_reserved_source', loc: node.openingElement.name.loc, message: `<${tag}> is imported from '${found.source}', which holds no components (menus, runtime builders, document/…, @wix/sdk): import the component from its @origami-wrappers package${menuHint}.` });
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
/** Reports a tag whose name isn't capitalized (CONV-parseJsx.ts:359): its local name, and the name the converter reads (the imported name), both.
|
|
80
|
+
* A namespace as a tag is TypeScript's; a tag from an interactions package, allowed-imports'. */
|
|
81
|
+
function checkTagCapitalized({ node, rulesApi, report }) {
|
|
82
|
+
const { name } = node.openingElement;
|
|
83
|
+
const found = rulesApi.getImport(node);
|
|
84
|
+
if (name.type !== 'JSXIdentifier' || !found || found.imported === '*' || (found.kind && INTERACTIONS_PACKAGES.includes(found.kind)))
|
|
85
|
+
return;
|
|
86
|
+
if (TAG_PATTERN.test(name.name) || TAG_PATTERN.test(found.imported))
|
|
87
|
+
return;
|
|
88
|
+
report({ check: 'lowercase_tag', loc: name.loc, message: `<${name.name}> must be capitalized: import the component under its capitalized export name (import { Name } from '${found.source}') and write <Name>; a lowercase tag is an HTML element.` });
|
|
89
|
+
}
|
|
90
|
+
// ── reading the code ──
|
|
91
|
+
/** page-structure reports an element at the top of the file, and one directly in a section: its closest holder, wrappers skipped, is none or a section. */
|
|
92
|
+
function isReportedByPageStructure(node, rulesApi) {
|
|
93
|
+
for (let holder = node.parent; holder; holder = holder.parent) {
|
|
94
|
+
if (holder.type !== 'JSXElement' || rulesApi.isWrapper(holder))
|
|
95
|
+
continue;
|
|
96
|
+
const type = rulesApi.getType(holder);
|
|
97
|
+
return type !== null && SECTIONS.includes(type) && rulesApi.getChildElements(holder).includes(node);
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
@@ -1,71 +1,29 @@
|
|
|
1
1
|
// Every theme font token (`--wst-heading-1-font: normal normal 700 --spx(88)/1em benzin`) must end in exactly one font id
|
|
2
2
|
// from the Wix font catalog in @wix/origami-fonts: no quotes, commas or fallbacks, the same case. The editor reads that
|
|
3
|
-
// family as one Wix font id
|
|
4
|
-
//
|
|
3
|
+
// family as one Wix font id. The import keeps any family as written (an unknown id, a quoted one, a list, another case),
|
|
4
|
+
// so this is a warning. A legacy font is in the catalog, and the import takes it. A `var(…)` token is left alone.
|
|
5
5
|
// theme.css isn't a file oxlint lints, so the rule runs on each linted page (a theme change lints every page) and
|
|
6
6
|
// marks the page's start; the message names the token's line in theme.css.
|
|
7
|
-
import {
|
|
8
|
-
import { reportCssProblem } from "../../rules-api/index.js";
|
|
7
|
+
import { CATALOG_FONTS, readFontShorthand, reportCssProblem } from "../../rules-api/index.js";
|
|
9
8
|
import { defineRule } from "./define-rule.js";
|
|
10
9
|
const CHECKS = ['invalid_font_id'];
|
|
11
|
-
const FONT_TOKEN = /^--wst-[\w-]+-font$/;
|
|
12
|
-
// The size in a `font` shorthand, with its optional line height: the family follows it. The number's quantifiers
|
|
13
|
-
// never overlap, so a long run of digits that is not a size fails in linear time.
|
|
14
|
-
const SIZE = /^(?:--spx\(.*\)|calc\(.*\)|(?:\d+(?:\.\d*)?|\.\d+)(?:px|em|rem|%|vw|vh|pt)?)(?:\/\S+)?$/i;
|
|
15
|
-
const FONT_IDS = new Set(fonts.filter((font) => font.permissions !== 'legacy').map((font) => font.id));
|
|
16
10
|
export default defineRule({
|
|
17
11
|
name: 'theme-font-ids',
|
|
18
12
|
checks: CHECKS,
|
|
19
13
|
editors: 'all',
|
|
14
|
+
severity: 'warn',
|
|
20
15
|
callbacks: {
|
|
21
16
|
Program: (input) => {
|
|
22
|
-
for (const token of
|
|
17
|
+
for (const token of input.rulesApi.getThemeFontTokens())
|
|
23
18
|
hasCatalogFontId(token, input);
|
|
24
19
|
},
|
|
25
20
|
},
|
|
26
21
|
});
|
|
27
22
|
// ── the steps ──
|
|
28
|
-
/** The theme's font tokens: `--wst-…-font`. */
|
|
29
|
-
function getFontTokens({ rulesApi }) {
|
|
30
|
-
return rulesApi.getThemeTokens().filter(({ name }) => FONT_TOKEN.test(name));
|
|
31
|
-
}
|
|
32
23
|
/** Reports a font token whose family isn't exactly one id from the catalog. A `var(…)` or a value without a family isn't checked. */
|
|
33
24
|
function hasCatalogFontId({ name, value, line }, { report }) {
|
|
34
|
-
const family =
|
|
35
|
-
if (family === undefined ||
|
|
25
|
+
const family = readFontShorthand(value)?.family;
|
|
26
|
+
if (family === undefined || CATALOG_FONTS.has(family))
|
|
36
27
|
return;
|
|
37
28
|
reportCssProblem(report, 'theme.css', line, 'invalid_font_id', `${name} must name exactly one Wix font id from the catalog; found ${family}. Run \`wix origami search-font --font <name>\` for the id.`);
|
|
38
29
|
}
|
|
39
|
-
// ── reading a font value ──
|
|
40
|
-
/** A value split on top-level whitespace, so `--spx(16)/1.3em` stays one part. */
|
|
41
|
-
function splitParts(value) {
|
|
42
|
-
const parts = [];
|
|
43
|
-
let depth = 0;
|
|
44
|
-
let current = '';
|
|
45
|
-
for (const char of value) {
|
|
46
|
-
if (char === '(')
|
|
47
|
-
depth++;
|
|
48
|
-
else if (char === ')')
|
|
49
|
-
depth--;
|
|
50
|
-
if (depth === 0 && /\s/.test(char)) {
|
|
51
|
-
if (current)
|
|
52
|
-
parts.push(current);
|
|
53
|
-
current = '';
|
|
54
|
-
}
|
|
55
|
-
else
|
|
56
|
-
current += char;
|
|
57
|
-
}
|
|
58
|
-
if (current)
|
|
59
|
-
parts.push(current);
|
|
60
|
-
return parts;
|
|
61
|
-
}
|
|
62
|
-
/** A font shorthand's family as written, after its last size; undefined for `var(…)` or no family. */
|
|
63
|
-
function getFontFamily(value) {
|
|
64
|
-
if (value.includes('var('))
|
|
65
|
-
return undefined;
|
|
66
|
-
const parts = splitParts(value);
|
|
67
|
-
const size = parts.findLastIndex((part) => SIZE.test(part));
|
|
68
|
-
if (size === -1 || size === parts.length - 1)
|
|
69
|
-
return undefined;
|
|
70
|
-
return parts.slice(size + 1).join(' ');
|
|
71
|
-
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// A theme font token (`--wst-heading-1-font: normal normal 700 --spx(88)/1em benzin`) names a weight its font has, in
|
|
2
|
+
// the Wix font catalog's weights (@wix/origami-fonts); tracker row KIT-fontCatalog. The import keeps the value as written,
|
|
3
|
+
// so a weight the font lacks only renders worse (the browser synthesizes it): a warning. A font that isn't a catalog id is
|
|
4
|
+
// theme-font-ids' warning, not checked again here; nor is a `var(…)`, or `bolder` / `lighter`, which depend on the parent.
|
|
5
|
+
import { CATALOG_FONTS, readFontShorthand, reportCssProblem } from "../../rules-api/index.js";
|
|
6
|
+
import { defineRule } from "./define-rule.js";
|
|
7
|
+
const CHECKS = ['missing_font_weight'];
|
|
8
|
+
export default defineRule({
|
|
9
|
+
name: 'theme-font-known',
|
|
10
|
+
checks: CHECKS,
|
|
11
|
+
editors: ['studio2'],
|
|
12
|
+
severity: 'warn',
|
|
13
|
+
callbacks: {
|
|
14
|
+
Program: checkThemeFontWeights,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
// ── the steps ──
|
|
18
|
+
/** Each theme font token's weight. */
|
|
19
|
+
function checkThemeFontWeights(input) {
|
|
20
|
+
for (const token of input.rulesApi.getThemeFontTokens())
|
|
21
|
+
checkFontTokenWeight(token, input);
|
|
22
|
+
}
|
|
23
|
+
/** Reports a font token whose weight isn't one of its catalog font's weights. */
|
|
24
|
+
function checkFontTokenWeight({ name, value, line }, { report }) {
|
|
25
|
+
const font = readFontShorthand(value);
|
|
26
|
+
const weights = font && CATALOG_FONTS.get(font.family)?.weights;
|
|
27
|
+
if (!font || font.weight === undefined || !weights || weights.includes(font.weight))
|
|
28
|
+
return;
|
|
29
|
+
reportCssProblem(report, 'theme.css', line, 'missing_font_weight', `${name} uses weight ${font.weight}, which ${font.family} does not have; the browser will synthesize it. Write one of its weights: ${weights.join(', ')} (rule KIT-fontCatalog).`);
|
|
30
|
+
}
|
package/dist/rules-api/index.js
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
export { createRulesApi, getOrCreateRulesApi } from "./rules-api.js";
|
|
3
3
|
export { createCssFileReporter, describeAtRule, reportCssProblem } from "./report.js";
|
|
4
4
|
export { bare, getAttribute, getExpression, getKey, getName, getObject, getProperties, getCallbackKind, getParamReadKind, getProperty, isBound, isOneEditAway, isRecord, isReservedAttribute, isStructure, isReadByImport, ownerOf, } from "../shared/utils.js";
|
|
5
|
-
export { BUILDERS, CALLBACK_PROP, DATA_KEY, ELEMENT_PROPS, EVENT_ATTRIBUTE, GLOBAL_SECTIONS, INSTANCE_PROPS, LAYER_ATTRIBUTES, RESERVED_ATTRIBUTES, RESERVED_ENTRY_KEYS, RUNTIME_ONLY_BUILDERS, RUNTIME_PACKAGE, SECTIONS, SHARED_COMP_ID, STRUCTURE, SYSTEM_PACKAGE, TRANSFORM_BUILDERS, TYPED_BUILDERS, WRAPPERS, } from "../shared/constants.js";
|
|
6
|
-
export { BREAKPOINT_AT_RULES, CONTENT_TARGET, getCssChildren, getDashedId, getMixinId, getSegmentClass, getSelectorSegments, isCssIdentifier, isPresetProperty, isSafeCssIdentifier, ORIGAMI_TARGETS, OVERFLOW_TARGET, PREAMBLE_LAYER, readSegment } from "../shared/css.js";
|
|
5
|
+
export { BUILDERS, CATALOG_FONTS, CALLBACK_PROP, DATA_KEY, ELEMENT_PROPS, EVENT_ATTRIBUTE, GLOBAL_SECTIONS, INSTANCE_PROPS, INTERACTIONS_PACKAGES, LAYER_ATTRIBUTES, RESERVED_ATTRIBUTES, RESERVED_ENTRY_KEYS, RUNTIME_ONLY_BUILDERS, RUNTIME_PACKAGE, SECTIONS, SHARED_COMP_ID, STRUCTURE, SYSTEM_PACKAGE, TRANSFORM_BUILDERS, TYPED_BUILDERS, WRAPPERS, } from "../shared/constants.js";
|
|
6
|
+
export { BREAKPOINT_AT_RULES, CONTENT_TARGET, getCssChildren, getDashedId, getMixinId, getSegmentClass, getSelectorSegments, isCssIdentifier, isPresetProperty, isSafeCssIdentifier, ORIGAMI_TARGETS, OVERFLOW_TARGET, PREAMBLE_LAYER, readFontShorthand, readSegment } from "../shared/css.js";
|
|
@@ -6,7 +6,7 @@ import { GLOBAL_COMPONENTS_FOLDER } from "../shared/site-files-tree-mapping.js";
|
|
|
6
6
|
import { models, pageId } from "../bi/stats.js";
|
|
7
7
|
import { readPage } from "../page-model/index.js";
|
|
8
8
|
import { getAttributeElements as getHeldElements, getAttributeValue, getChildren, getId, getImport, getImportKind, getImportedName, getPropNames, getSlot, getTagName, getType, hasDataAttributes, isTypeScript } from "../shared/utils.js";
|
|
9
|
-
import { readBreakpointName, readCustomMedia, readThemeTokens } from "../shared/css.js";
|
|
9
|
+
import { readBreakpointName, readCustomMedia, readThemeFontTokens, readThemeTokens } from "../shared/css.js";
|
|
10
10
|
import { WRAPPERS } from "../shared/constants.js";
|
|
11
11
|
import { getEnclosingAtRules, isBaseDeclaration } from "./utils.js";
|
|
12
12
|
/** Theme lines come from @apply inside style.css, so style.css's own lines usually come after them. */
|
|
@@ -35,10 +35,12 @@ export function createRulesApi(page) {
|
|
|
35
35
|
getCss,
|
|
36
36
|
getBaseCss: (component, property) => getCss(component, property).filter(isBaseDeclaration).at(-1),
|
|
37
37
|
getThemeTokens: () => readThemeTokens(page.css.files.theme),
|
|
38
|
+
getThemeFontTokens: () => readThemeFontTokens(page.css.files.theme),
|
|
38
39
|
getCustomMedia: (kind) => readCustomMedia(page.css.files[kind]),
|
|
39
40
|
getBreakpointName: readBreakpointName,
|
|
40
41
|
getEnclosingAtRules,
|
|
41
42
|
getType: (node) => getType(node, getProgram(node)),
|
|
43
|
+
isWrapper,
|
|
42
44
|
getTagName,
|
|
43
45
|
getId,
|
|
44
46
|
getChildren,
|
package/dist/shared/constants.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
// The constants more than one part reads: the groups of element types, the packages' names, and what the converter reserves on a tag (reservedAttributes.ts). A rule imports them from rules-api/index.ts; a part from here.
|
|
2
|
+
import { fonts } from '@wix/origami-fonts';
|
|
3
|
+
/** The Wix font catalog (@wix/origami-fonts) by id, legacy fonts too: the import takes a legacy font as it takes any other. */
|
|
4
|
+
export const CATALOG_FONTS = new Map(fonts.map((font) => [font.id, font]));
|
|
1
5
|
/** Any section: rules that don't care which one. */
|
|
2
6
|
export const SECTIONS = ['section', 'header', 'footer'];
|
|
3
7
|
/** A page's reference to a global component (`<HeaderSection_x sharedCompId="…" />`). */
|
|
4
8
|
export const GLOBAL_SECTIONS = ['globalHeader', 'globalFooter', 'globalSection'];
|
|
5
9
|
/** A tag that wraps one component and isn't one itself: `<Remove when={…}>`, `<Interaction interactKey="…">`, `<PinnedToScreen>` (the import drops it). */
|
|
6
10
|
export const WRAPPERS = ['remove', 'interaction', 'pinnedToScreen'];
|
|
11
|
+
/** 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. */
|
|
12
|
+
export const INTERACTIONS_PACKAGES = ['interactReact', 'interact', 'motionPresets'];
|
|
7
13
|
/** The name a structure file exports its tree under: `export const Structure = () => (…)`. */
|
|
8
14
|
export const STRUCTURE = 'Structure';
|
|
9
15
|
/** The props a global component's Structure may take: its instance identity, `({ id, className })` (#300). */
|
package/dist/shared/css.js
CHANGED
|
@@ -18,6 +18,13 @@ const BASE_CLASS = /^\.[^:[\]"]+/;
|
|
|
18
18
|
const FUNCTIONAL_PSEUDO = /^:[A-Za-z-]+\([^)]*\)/;
|
|
19
19
|
/** A class after the first: a state (`&.onSale`). */
|
|
20
20
|
const STATE_CLASS = /^\.[^.:[\]"]+/;
|
|
21
|
+
/** A theme font token: `--wst-heading-1-font`. */
|
|
22
|
+
const THEME_FONT_TOKEN = /^--wst-[\w-]+-font$/;
|
|
23
|
+
// The size in a `font` shorthand, with its optional line height: the family follows it. The number's quantifiers
|
|
24
|
+
// never overlap, so a long run of digits that is not a size fails in linear time.
|
|
25
|
+
const FONT_SIZE = /^(?:--spx\(.*\)|calc\(.*\)|(?:\d+(?:\.\d*)?|\.\d+)(?:px|em|rem|%|vw|vh|pt)?)(?:\/\S+)?$/i;
|
|
26
|
+
/** A number weight in a `font` shorthand: before the size, only the weight is a bare number. */
|
|
27
|
+
const FONT_WEIGHT = /^\d+(?:\.\d+)?$/;
|
|
21
28
|
const PSEUDO_CLASS = /^:[A-Za-z-]+/;
|
|
22
29
|
const ATTRIBUTE = /^\[[^\]]*\]/;
|
|
23
30
|
/** Every `--name: value` in a file, wherever it's declared, in order. No file → none. */
|
|
@@ -28,6 +35,8 @@ export function readThemeTokens(file) {
|
|
|
28
35
|
});
|
|
29
36
|
return tokens;
|
|
30
37
|
}
|
|
38
|
+
/** The theme's font tokens (`--wst-heading-1-font: …`), as readThemeTokens reads them. */
|
|
39
|
+
export const readThemeFontTokens = (file) => readThemeTokens(file).filter(({ name }) => THEME_FONT_TOKEN.test(name));
|
|
31
40
|
/** Every `@custom-media --name (query)` at a file's top level, in order, as the import reads them; theme.css's are the site's breakpoints. No file → none. */
|
|
32
41
|
export function readCustomMedia(file) {
|
|
33
42
|
if (file === undefined)
|
|
@@ -43,6 +52,51 @@ function parseCustomMedia(params) {
|
|
|
43
52
|
const [, name = '', query = ''] = /^(\S*)\s*([\s\S]*)$/.exec(params.trim()) ?? [];
|
|
44
53
|
return { name, query };
|
|
45
54
|
}
|
|
55
|
+
/** A `font` shorthand's weight and family; undefined for a `var(…)` or a value with no family after its size. */
|
|
56
|
+
export function readFontShorthand(value) {
|
|
57
|
+
if (value.includes('var('))
|
|
58
|
+
return undefined;
|
|
59
|
+
const parts = splitFontParts(value);
|
|
60
|
+
const size = parts.findLastIndex((part) => FONT_SIZE.test(part));
|
|
61
|
+
if (size === -1 || size === parts.length - 1)
|
|
62
|
+
return undefined;
|
|
63
|
+
return { weight: readFontWeight(parts.slice(0, size)), family: parts.slice(size + 1).join(' ') };
|
|
64
|
+
}
|
|
65
|
+
/** The weight among a shorthand's parts before its size: a number, `bold`, or 400 (`normal`, or none written). */
|
|
66
|
+
function readFontWeight(parts) {
|
|
67
|
+
for (const part of parts) {
|
|
68
|
+
if (FONT_WEIGHT.test(part))
|
|
69
|
+
return Number(part);
|
|
70
|
+
const keyword = part.toLowerCase();
|
|
71
|
+
if (keyword === 'bold')
|
|
72
|
+
return 700;
|
|
73
|
+
if (keyword === 'bolder' || keyword === 'lighter')
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
return 400;
|
|
77
|
+
}
|
|
78
|
+
/** A value split on top-level whitespace, so `--spx(16)/1.3em` stays one part. */
|
|
79
|
+
function splitFontParts(value) {
|
|
80
|
+
const parts = [];
|
|
81
|
+
let depth = 0;
|
|
82
|
+
let current = '';
|
|
83
|
+
for (const char of value) {
|
|
84
|
+
if (char === '(')
|
|
85
|
+
depth++;
|
|
86
|
+
else if (char === ')')
|
|
87
|
+
depth--;
|
|
88
|
+
if (depth === 0 && /\s/.test(char)) {
|
|
89
|
+
if (current)
|
|
90
|
+
parts.push(current);
|
|
91
|
+
current = '';
|
|
92
|
+
}
|
|
93
|
+
else
|
|
94
|
+
current += char;
|
|
95
|
+
}
|
|
96
|
+
if (current)
|
|
97
|
+
parts.push(current);
|
|
98
|
+
return parts;
|
|
99
|
+
}
|
|
46
100
|
/** A container's children, comments aside; none for a node with no block. */
|
|
47
101
|
export const getCssChildren = (node) => (node.nodes ?? []).filter((child) => child.type !== 'comment');
|
|
48
102
|
/** Whether a property picks a preset (css/utils.ts `isPresetProperty`): `--preset` (the rule's own element) or `--preset-<part path>`. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/origami-lint",
|
|
3
|
-
"version": "1.34.
|
|
3
|
+
"version": "1.34.30",
|
|
4
4
|
"description": "Offline lint for an exported Origami site: lints the pages that changed since the last passing run",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -52,5 +52,5 @@
|
|
|
52
52
|
]
|
|
53
53
|
}
|
|
54
54
|
},
|
|
55
|
-
"falconPackageHash": "
|
|
55
|
+
"falconPackageHash": "335c078a8c6afc26c556fcd6c71643e777664f3b26b63a3b7b9832a4"
|
|
56
56
|
}
|