@wix/origami-lint 1.34.2 → 1.34.4

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/lint.js CHANGED
@@ -1,13 +1,17 @@
1
1
  #!/usr/bin/env node
2
- // Origami lint: lints the pages whose files changed since the last passing run. See lint/README.md.
2
+ // Origami lint: runs the project rules on the site's own files, then lints the pages whose files changed since the last
3
+ // passing run. See lint/README.md.
3
4
  import { getChangedFiles, markPassing } from "./history/index.js";
4
5
  import { runRules } from "./rules-adapter/index.js";
5
6
  import { findPagesToLint } from "./page-model/index.js";
7
+ import { runProjectRules } from "./project-rules-runner/index.js";
6
8
  import { checkStartedAtSiteRoot } from "./shared/paths.js";
7
9
  checkStartedAtSiteRoot();
8
10
  const changedFiles = getChangedFiles();
11
+ const projectPassed = runProjectRules(changedFiles.all);
9
12
  const pagesToLint = findPagesToLint(changedFiles);
10
- const passed = runRules(pagesToLint);
13
+ const pagesPassed = runRules(pagesToLint);
14
+ const passed = projectPassed && pagesPassed;
11
15
  if (passed)
12
16
  markPassing(changedFiles);
13
17
  process.exit(passed ? 0 : 1);
@@ -0,0 +1,2 @@
1
+ // The project model, beside the page model (part 2): what a project rule sees of the site.
2
+ export { createProjectModel } from "./project-model.js";
@@ -0,0 +1,15 @@
1
+ /** A path from the site root that stays inside the site: no leading /, no .. segment. */
2
+ function isSitePath(file) {
3
+ return !file.startsWith('/') && !file.split('/').includes('..');
4
+ }
5
+ /** The rule's view of the site: its files, and readFile. ruleName names the rule in readFile's error. */
6
+ export function createProjectModel(ruleName, files, readFile) {
7
+ return {
8
+ files,
9
+ readFile(file) {
10
+ if (!isSitePath(file))
11
+ throw new Error(`project rule ${ruleName} read ${file}: write a path from the site root, without ..`);
12
+ return readFile(file);
13
+ },
14
+ };
15
+ }
@@ -0,0 +1,3 @@
1
+ // The project rules runner, beside the rules adapter (part 5): runs the rules that check the site's own files and
2
+ // folders (lint/rules/project/), not one page.
3
+ export { runProjectRules } from "./run-project-rules.js";
@@ -0,0 +1,44 @@
1
+ // Runs every project rule, on every run, on the whole site; prints each problem in the line shape the page rules'
2
+ // problems print in.
3
+ import { readFileSync } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { projectRuleRegistry } from "../rules/project/index.js";
6
+ import { log } from "../shared/log.js";
7
+ import { ROOT } from "../shared/paths.js";
8
+ import { createProjectModel } from "../project-model/index.js";
9
+ /** true when no error rule reported a problem, on the site of these files. Logs how many rules run, and whether they passed. */
10
+ export function runProjectRules(files, { rules = projectRuleRegistry, readFile = readFromSite, print = console.log } = {}) {
11
+ log(`${rules.length} project rules to run`);
12
+ let passed = true;
13
+ for (const rule of rules) {
14
+ let problems;
15
+ try {
16
+ problems = rule.check(createProjectModel(rule.name, files, readFile));
17
+ }
18
+ catch (error) {
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.
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)}` }));
21
+ passed = false;
22
+ continue;
23
+ }
24
+ for (const problem of problems) {
25
+ print(formatProblem(rule, problem));
26
+ if ((rule.severity ?? 'error') === 'error')
27
+ passed = false;
28
+ }
29
+ }
30
+ log(passed ? 'project rules passed' : 'project rules failed');
31
+ return passed;
32
+ }
33
+ /** `file:line:1: error origami(name): message`, the shape oxlint prints the page rules' problems in. */
34
+ export function formatProblem({ name, severity = 'error' }, { file, line = 1, message }) {
35
+ return `${file}:${line}:1: ${severity === 'error' ? 'error' : 'warning'} origami(${name}): ${message}`;
36
+ }
37
+ function readFromSite(file) {
38
+ try {
39
+ return readFileSync(path.join(ROOT, file), 'utf8');
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ }
@@ -6,6 +6,8 @@
6
6
  // recognized by import source (rulesApi.getType), so renaming an import doesn't matter.
7
7
  // Checked in every file, pages and global components alike:
8
8
  // - Structure returns one element, with a string id (tracker CONV-runValidation.ts:9);
9
+ // - every section, wherever it sits (a page's, a global component's top one, one nested lower down), holds exactly one
10
+ // <InternalContainer> and nothing else: the site's own build refuses anything else (rule S2-SECTION-001, tracker OUR-001);
9
11
  // - a <Page> is only ever the top of its own page: one held by another element is refused at save (rule card P1-R16);
10
12
  // - the header and footer sit outside every <InternalContainer>: inside one, that container can no longer be deleted in the
11
13
  // editor (rule card P1-R03). Both the HeaderSection / FooterSection tags and a page's reference to the global ones;
@@ -38,6 +40,8 @@ export default defineRule({
38
40
  if (isLandmark(input.node, input.rulesApi))
39
41
  hasSectionInLandmark(input);
40
42
  hasOnlyElements(input);
43
+ if (isSection(input.node, input.rulesApi))
44
+ hasOneInternalContainer(input.node, input);
41
45
  hasLayerArrays(input);
42
46
  isNotNestedPage(input);
43
47
  isOutsideInternalContainers(input);
@@ -50,8 +54,7 @@ export default defineRule({
50
54
  return;
51
55
  if (!startsWithPage(input))
52
56
  return;
53
- for (const section of getSections(input))
54
- hasOneInternalContainer(section, input);
57
+ checkPageChildren(input);
55
58
  },
56
59
  },
57
60
  });
@@ -94,29 +97,25 @@ function startsWithPage({ node, rulesApi, report }) {
94
97
  report({ loc: getLoc(node, rulesApi), message: `A page must start with <Page>, found ${describe(node, rulesApi)}.` });
95
98
  return false;
96
99
  }
97
- /** <Page>'s sections. Any other component (the page's <Background>, a pinned element) is accepted, as the import accepts it; reports text and {…} (CONV-parseJsx.ts:1668). */
98
- function getSections({ node: page, rulesApi, report }) {
99
- const sections = [];
100
+ /** Reports text and {…} directly in <Page> (CONV-parseJsx.ts:1668). Any component is accepted there: a section, the page's <Background>, a pinned element, as the import accepts it. */
101
+ function checkPageChildren({ node: page, rulesApi, report }) {
100
102
  for (const child of getStructuralChildren(page, rulesApi)) {
101
- if (child.type === 'JSXElement') {
102
- const type = rulesApi.getType(child);
103
- if (type && SECTIONS.includes(type))
104
- sections.push(child);
103
+ if (child.type === 'JSXElement')
105
104
  continue;
106
- }
107
105
  report({ loc: getLoc(page, rulesApi), message: `<Page> has ${describe(child, rulesApi)} as a child. Only elements go directly inside <Page>: a section, or a component pinned to the page (tracker CONV-parseJsx.ts:1668).` });
108
106
  }
109
- return sections;
110
107
  }
111
- /** Reports a section that doesn't hold exactly one <InternalContainer>, and nothing else. A wrapper in it that holds the wrong number of elements is reported by its own step first. */
108
+ /** Reports a section, anywhere in the file, that doesn't hold exactly one <InternalContainer>, and nothing else (S2-SECTION-001, OUR-001). A wrapper in it
109
+ * that holds the wrong number of elements is reported by its own step first; text and {…} count only where hasOnlyElements leaves them to this step. */
112
110
  function hasOneInternalContainer(section, { rulesApi, report }) {
113
111
  if (rulesApi.getChildren(section).some((child) => child.type === 'JSXElement' && isWrapper(child, rulesApi) && !holdsOne(child)))
114
112
  return;
115
- const inside = getStructuralChildren(section, rulesApi);
113
+ const reportsText = isRootTreeHolder(section, rulesApi);
114
+ const inside = getStructuralChildren(section, rulesApi).filter((child) => reportsText || child.type === 'JSXElement');
116
115
  if (inside.length === 1 && inside[0].type === 'JSXElement' && rulesApi.getType(inside[0]) === 'innerElement')
117
116
  return;
118
117
  const name = rulesApi.getId(section) ?? `<${rulesApi.getTagName(section)}>`;
119
- report({ loc: getLoc(section, rulesApi), message: `Section ${name} must contain exactly one <InternalContainer>, found: ${inside.map((child) => describe(child, rulesApi)).join(', ') || 'nothing'}.` });
118
+ report({ loc: getLoc(section, rulesApi), message: `Section ${name} must contain exactly one <InternalContainer>, found: ${inside.map((child) => describe(child, rulesApi)).join(', ') || 'nothing'}. Put the section's content inside its one <InternalContainer> (rule S2-SECTION-001).` });
120
119
  }
121
120
  /** Reports a <Page> held by another element (P1-R16), unless the root steps above report that placement already (a section's children). */
122
121
  function isNotNestedPage({ node, rulesApi, report }) {
@@ -250,6 +249,11 @@ function describe(child, rulesApi) {
250
249
  return `<${rulesApi.getTagName(child)}>`;
251
250
  return child.type === 'JSXText' ? 'text' : 'a {…} expression';
252
251
  }
252
+ /** Section, HeaderSection, FooterSection (by import source); not a page's reference to a global one. */
253
+ function isSection(node, rulesApi) {
254
+ const type = rulesApi.getType(node);
255
+ return type !== null && SECTIONS.includes(type);
256
+ }
253
257
  function isLandmark(node, rulesApi) {
254
258
  const type = rulesApi.getType(node);
255
259
  return type === 'headerLandmark' || type === 'footerLandmark';
@@ -259,10 +263,8 @@ function hasSectionInLandmark(input) {
259
263
  const { node, rulesApi, report } = input;
260
264
  const expected = rulesApi.getType(node) === 'headerLandmark' ? 'header' : 'footer';
261
265
  const children = getStructuralChildren(node, rulesApi);
262
- if (children.length === 1 && children[0].type === 'JSXElement' && rulesApi.getType(children[0]) === expected) {
263
- hasOneInternalContainer(children[0], input);
266
+ if (children.length === 1 && children[0].type === 'JSXElement' && rulesApi.getType(children[0]) === expected)
264
267
  return;
265
- }
266
268
  report({ loc: getLoc(node, rulesApi), message: `<${rulesApi.getTagName(node)}> must contain exactly one ${expected === 'header' ? 'HeaderSection' : 'FooterSection'}.` });
267
269
  }
268
270
  function receivesInstanceId(node) {
@@ -0,0 +1,4 @@
1
+ /** Nothing changes a rule once it's defined. */
2
+ export function defineProjectRule(rule) {
3
+ return Object.freeze({ ...rule, scope: 'project' });
4
+ }
@@ -0,0 +1,2 @@
1
+ export { defineProjectRule } from "./define-project-rule.js";
2
+ export const projectRuleRegistry = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/origami-lint",
3
- "version": "1.34.2",
3
+ "version": "1.34.4",
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": {
@@ -51,5 +51,5 @@
51
51
  ]
52
52
  }
53
53
  },
54
- "falconPackageHash": "b4530d28f7fd3cf6e883a2f9ab79852ec1f94c67e1fa6b2ae68ace9a"
54
+ "falconPackageHash": "dae194e28b366afdeb725197b97b0f1659346d4dbe5cea8e1659dab4"
55
55
  }