@wix/origami-transforms 0.1.54 → 0.1.56

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.
@@ -1,8 +1,9 @@
1
1
  export function assertKnownConstraints(rules: any): void;
2
2
  export function constrainsChildren(rule: any): boolean;
3
- export function checkRule(rule: import("./rules.js").Rule, { node, componentOf, report }: {
3
+ export function checkRule(rule: import("./rules.js").Rule, { node, componentOf, isProvider, report }: {
4
4
  node: any;
5
5
  componentOf: (node: any) => string | undefined;
6
+ isProvider: (node: any) => boolean;
6
7
  report: (check: import("./index.js").Check, message: string) => void;
7
8
  }): void;
8
9
  export function checkCreateElement(rule: import("./rules.js").Rule, { node, component, report }: {
@@ -1,4 +1,4 @@
1
- export const checks: readonly ["section_wrong_child", "section_too_few_children", "section_too_many_children", "section_non_literal_children", "section_create_element"];
1
+ export const checks: readonly ["section_wrong_child", "section_too_few_children", "section_too_many_children", "section_non_literal_children", "section_create_element", "invalid_component_catalog"];
2
2
  /** @typedef {typeof checks[number]} Check */
3
3
  /** @type {import('../../transform/types.js').JsxTransformPlugin<Check>} */
4
4
  export const layoutValidationPlugin: import("../../transform/types.js").JsxTransformPlugin<Check>;
@@ -0,0 +1 @@
1
+ export function findProviderTags(program: any, root: string | undefined): Set<string>;
@@ -1 +1,2 @@
1
1
  export function describe(node: any): string;
2
+ export function elementUnderProviders(element: any, isProvider: (element: any) => boolean): any;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/origami-transforms",
3
- "version": "0.1.54",
3
+ "version": "0.1.56",
4
4
  "description": "Babel and PostCSS checks and transforms for Wix origami page files.",
5
5
  "main": "./src/index.js",
6
6
  "files": [
@@ -24,7 +24,7 @@
24
24
  "@wix/origami-system-components": "1.10.14",
25
25
  "postcss": "^8.5.3",
26
26
  "postcss-custom-media": "^12.0.1",
27
- "@wix/origami-bi": "0.21.0"
27
+ "@wix/origami-bi": "0.22.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/node": "^22.0.0",
@@ -76,5 +76,5 @@
76
76
  },
77
77
  "type": "module",
78
78
  "types": "./dist/index.d.ts",
79
- "falconPackageHash": "aae99aa83d2e96a4134ab3ed320d8a02995bec99c275d844f2dace0d"
79
+ "falconPackageHash": "db1e35b4c8643d29244f6bad8070b058b173b7fa7761c59210f8a8c2"
80
80
  }
@@ -5,7 +5,7 @@
5
5
  // To add a kind of constraint: add a function to CHECKS, use its key in rules.js, and extend
6
6
  // expectation() so messages state it.
7
7
 
8
- import { describe } from './tree.js'
8
+ import { describe, elementUnderProviders } from './tree.js'
9
9
 
10
10
  const CHILD_CONSTRAINTS = ['allowedChildren', 'minChildren', 'maxChildren']
11
11
 
@@ -86,17 +86,19 @@ export const assertKnownConstraints = rules => {
86
86
 
87
87
  export const constrainsChildren = rule => CHILD_CONSTRAINTS.some(key => key in rule)
88
88
 
89
- /** Runs every constraint of `rule` on one element; `report` receives each full message.
89
+ /** Runs every constraint of `rule` on one element; `report` receives each full message. A child counts as the element
90
+ * under its context providers.
90
91
  * @param {import('./rules.js').Rule} rule
91
- * @param {{ node: any, componentOf: (node: any) => string | undefined, report: (check: import('./index.js').Check, message: string) => void }} input
92
+ * @param {{ node: any, componentOf: (node: any) => string | undefined, isProvider: (node: any) => boolean, report: (check: import('./index.js').Check, message: string) => void }} input
92
93
  */
93
- export const checkRule = (rule, { node, componentOf, report }) => {
94
+ export const checkRule = (rule, { node, componentOf, isProvider, report }) => {
94
95
  const prefix = `${describe(node)} ${expectation(rule)}`
95
96
  /** @param {import('./index.js').Check} check */
96
97
  const fail = check => got => report(check, `${prefix}; found ${got}.`)
97
98
 
98
99
  const elements = node.children
99
100
  .filter(child => child.type === 'JSXElement')
101
+ .map(child => elementUnderProviders(child, isProvider))
100
102
  .map(element => ({ element, component: componentOf(element) }))
101
103
  const context = { node, elements }
102
104
 
@@ -3,11 +3,13 @@
3
3
  //
4
4
  // One walk serves every rule. Rules are data (./rules.js); each names the components it looks at,
5
5
  // identified for now by JSX tag name as written (<Section>, <InternalContainer>), whatever they are
6
- // imported from, and the checks (./checks.js) run on each matching element.
6
+ // imported from, and the checks (./checks.js) run on each matching element. A context provider is the one exception: it's
7
+ // known by its import, against the site's component catalog (./providers.js), and a rule checks the element it wraps.
7
8
 
8
9
  import { basename } from 'node:path'
9
10
  import { LayoutError } from '../../transform/layoutError.js'
10
11
  import { assertKnownConstraints, checkCreateElement, checkRule, constrainsChildren } from './checks.js'
12
+ import { findProviderTags } from './providers.js'
11
13
  import { RULES } from './rules.js'
12
14
 
13
15
  const STRUCTURE_FILES = new Set(['structure.tsx', 'structure.jsx'])
@@ -26,7 +28,7 @@ const isCreateElement = callee =>
26
28
  (callee.type === 'Identifier' && callee.name === 'createElement') ||
27
29
  (callee.type === 'MemberExpression' && !callee.computed && callee.property.name === 'createElement')
28
30
 
29
- export const checks = /** @type {const} */ (['section_wrong_child', 'section_too_few_children', 'section_too_many_children', 'section_non_literal_children', 'section_create_element'])
31
+ export const checks = /** @type {const} */ (['section_wrong_child', 'section_too_few_children', 'section_too_many_children', 'section_non_literal_children', 'section_create_element', 'invalid_component_catalog'])
30
32
  /** @typedef {typeof checks[number]} Check */
31
33
 
32
34
  /** @type {import('../../transform/types.js').JsxTransformPlugin<Check>} */
@@ -37,12 +39,14 @@ export const layoutValidationPlugin = {
37
39
  order: -100,
38
40
  appliesTo: context => STRUCTURE_FILES.has(basename(context.filePath)),
39
41
 
40
- transform: program => {
42
+ transform: (program, context) => {
41
43
  // The component an element is: its tag name, as written.
42
44
  const componentOf = node => {
43
45
  const name = node.openingElement.name
44
46
  return name.type === 'JSXIdentifier' ? name.name : undefined
45
47
  }
48
+ const providerTags = findProviderTags(program, context.layout?.root)
49
+ const isProvider = node => providerTags.has(componentOf(node))
46
50
 
47
51
  /** @type {import('../../transform/types.js').Violation<Check>[]} */
48
52
  const violations = []
@@ -53,7 +57,7 @@ export const layoutValidationPlugin = {
53
57
  const component = componentOf(path.node)
54
58
  const rules = component === undefined ? undefined : RULES_BY_COMPONENT.get(component)
55
59
  if (rules === undefined) return
56
- for (const rule of rules) checkRule(rule, { node: path.node, componentOf, report })
60
+ for (const rule of rules) checkRule(rule, { node: path.node, componentOf, isProvider, report })
57
61
  },
58
62
 
59
63
  CallExpression({ node }) {
@@ -0,0 +1,57 @@
1
+ // Which tags of a structure are context providers: a tag the structure imports from an app's wrapper package, which the
2
+ // site's component catalog lists among that app's context providers.
3
+
4
+ import { existsSync, readFileSync } from 'node:fs'
5
+ import { join } from 'node:path'
6
+ import { moduleFromWrapperEntry, wrapperImport } from '../../wrapperImport.js'
7
+
8
+ const CATALOG = 'site-component-catalog.json'
9
+
10
+ /**
11
+ * The site's component catalog; empty for a site exported before it.
12
+ * @param {string | undefined} root the site app's folder
13
+ * @returns {import('../../wrapperSideEffects.js').WrapperCatalog}
14
+ */
15
+ const readCatalog = root => {
16
+ const file = root === undefined ? undefined : join(root, CATALOG)
17
+ if (file === undefined || !existsSync(file)) return {}
18
+ try {
19
+ return JSON.parse(readFileSync(file, 'utf8'))
20
+ } catch (error) {
21
+ // The export's to fix, so it carries its check, as a layout violation does.
22
+ throw Object.assign(
23
+ new Error(`${CATALOG} is not valid JSON (${error instanceof Error ? error.message : String(error)}). Export the site again.`),
24
+ { check: 'invalid_component_catalog' }
25
+ )
26
+ }
27
+ }
28
+
29
+ /** The structure's imports of app wrappers, with the app each is from. */
30
+ const wrapperImportsOf = program =>
31
+ program.node.body
32
+ .filter(statement => statement.type === 'ImportDeclaration')
33
+ .map(statement => ({ statement, module: moduleFromWrapperEntry(statement.source.value) }))
34
+ .filter(({ module }) => module !== undefined)
35
+
36
+ /**
37
+ * The tags the structure names its context providers by. A structure that imports no app wrapper doesn't read the catalog.
38
+ * @param {any} program the structure file's Babel program path
39
+ * @param {string | undefined} root the site app's folder
40
+ * @returns {Set<string>}
41
+ */
42
+ export const findProviderTags = (program, root) => {
43
+ const imports = wrapperImportsOf(program)
44
+ const catalog = imports.length === 0 ? {} : readCatalog(root)
45
+ /** @type {Set<string>} */
46
+ const tags = new Set()
47
+ for (const { statement, module } of imports) {
48
+ const providers = catalog[module]?.entries?.contextProviders ?? {}
49
+ for (const specifier of statement.specifiers) {
50
+ if (specifier.type !== 'ImportSpecifier') continue
51
+ const name = specifier.imported.type === 'Identifier' ? specifier.imported.name : specifier.imported.value
52
+ const isProvider = Object.hasOwn(providers, name) && wrapperImport({ module, symbol: name }) === statement.source.value
53
+ if (isProvider) tags.add(specifier.local.name)
54
+ }
55
+ }
56
+ return tags
57
+ }
@@ -13,3 +13,30 @@ export const describe = node => {
13
13
  const line = node.loc ? ` (line ${node.loc.start.line})` : ''
14
14
  return `<${tag}${id ? ` id="${id.value.value}"` : ''}>${line}`
15
15
  }
16
+
17
+ // Whitespace between tags and {/* comments */}, which the converter skips too (parseJsx.ts isFillerChild).
18
+ const isFiller = child =>
19
+ (child.type === 'JSXText' && child.value.trim() === '') ||
20
+ (child.type === 'JSXExpressionContainer' && child.expression.type === 'JSXEmptyExpression')
21
+
22
+ /** The element's only child element; undefined when it has none, more, or anything else in it. */
23
+ const onlyChildElement = node => {
24
+ const children = node.children.filter(child => !isFiller(child))
25
+ return children.length === 1 && children[0].type === 'JSXElement' ? children[0] : undefined
26
+ }
27
+
28
+ /**
29
+ * The element a child stands for. A context provider wraps exactly one element, which stands in its place (the
30
+ * converter's parseJsx.ts wrappedChildOf), so the element under the providers; a provider that wraps none or more is
31
+ * itself the child, so the rule reports it.
32
+ * @param {any} element
33
+ * @param {(element: any) => boolean} isProvider
34
+ */
35
+ export const elementUnderProviders = (element, isProvider) => {
36
+ while (isProvider(element)) {
37
+ const wrapped = onlyChildElement(element)
38
+ if (wrapped === undefined) break
39
+ element = wrapped
40
+ }
41
+ return element
42
+ }