@wix/origami-lint 1.34.35 → 1.34.37

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Offline lint for an exported Origami site. Each run lints only the pages whose files changed since the last passing
4
4
  run (every page when `theme.css` changed), and runs every rule on every component of those pages.
5
- The rules run as plain oxlint rules.
5
+ The rules run as plain ESLint rules, in the lint's own process.
6
6
 
7
7
  Published to the Wix registry (public npm later). How the lint works, its parts and how to write a rule:
8
8
  [lint/README.md](lint/README.md).
@@ -35,8 +35,8 @@ pnpm --filter @wix/origami-lint run lint
35
35
  ```
36
36
  lint/ the source (lint.ts is the entry point and the origami-lint bin)
37
37
  dist/ lint/ built to JavaScript: what the package publishes (Node won't run .ts inside node_modules)
38
- test/ rules on in-memory pages, CSS, the pages to lint, the oxlint adapter; history, the site root and a whole run on temporary repositories
38
+ test/ rules on in-memory pages, CSS, the pages to lint, the ESLint adapter; history, the site root and a whole run on temporary repositories
39
39
  ```
40
40
 
41
- The oxlint config is `lint/rules-adapter/oxlint/origami-rules.config.ts`, passed to oxlint with `-c`. It is not named
42
- `oxlint.config.ts`, so `oxlint .` in this repo doesn't load it as a nested config.
41
+ The ESLint config is built in `lint/rules-adapter/eslint/run-rules.ts` and passed to ESLint's Node API, so a site's
42
+ own `eslint.config.js` is never loaded. `pnpm run lint` (this package's own source) still runs oxlint.
package/dist/bi/stats.js CHANGED
@@ -1,11 +1,14 @@
1
- // The oxlint process collects only ids, counts and timings. No BI sender runs in the plugin.
2
- import { writeFileSync } from 'node:fs';
3
1
  import { GLOBAL_COMPONENTS_FOLDER } from "../shared/site-files-tree-mapping.js";
4
2
  import { readStructure } from "../page-model/read-structure.js";
5
3
  const rules = [];
6
4
  export const models = [];
7
5
  const pages = new Map();
8
- let registered = false;
6
+ let collecting = false;
7
+ /** BI is on: from now on, the rules and the page models collect their stats. */
8
+ export function collectStats() {
9
+ collecting = true;
10
+ }
11
+ export const isCollectingStats = () => collecting;
9
12
  /** Resolve the same root component id the page model reads; never use its path as a BI id. */
10
13
  export function pageId(filename, text, program) {
11
14
  let page = pages.get(filename);
@@ -19,19 +22,9 @@ export function pageId(filename, text, program) {
19
22
  }
20
23
  /** Called once per rule and file by the adapter. Passing rules retain their zero count. */
21
24
  export function startRuleStats(rule, checks, filename, text, program) {
22
- const destination = process.env.ORIGAMI_LINT_STATS;
23
- if (!destination)
25
+ if (!collecting)
24
26
  return;
25
27
  try {
26
- if (!registered) {
27
- registered = true;
28
- process.once('exit', () => {
29
- try {
30
- writeFileSync(destination, JSON.stringify({ rules, models }), { mode: 0o600 });
31
- }
32
- catch { /* Stats must never fail lint. */ }
33
- });
34
- }
35
28
  const kind = filename.startsWith(`${GLOBAL_COMPONENTS_FOLDER}/`) ? 'component' : 'page';
36
29
  const page = pageId(filename, text, program);
37
30
  const stats = { rule, checks: Object.fromEntries(checks.map(check => [check, 0])), kind, page, violations: 0, duration_ms: 0 };
@@ -42,3 +35,24 @@ export function startRuleStats(rule, checks, filename, text, program) {
42
35
  return undefined;
43
36
  }
44
37
  }
38
+ /** The collected stats as rows: a page model's build, then each rule on each file and its checks that found problems. Only ids, counts and timings, never a message or a path. */
39
+ export function recordStats(run) {
40
+ try {
41
+ for (const model of models) {
42
+ run.recordRow('step', 'build_page_model', { item_type: model.kind, item_id: model.page,
43
+ step_details: { components: model.components, css_files: model.css_files } }, {
44
+ success: !model.error_name, failure: model.error_name ? { error_name: model.error_name } : undefined, duration_ms: model.duration_ms,
45
+ });
46
+ }
47
+ for (const statsRow of rules) {
48
+ const row = run.recordRow('lint_rule', statsRow.rule, { item_type: statsRow.kind, item_id: statsRow.page, step_details: { violations: statsRow.violations } }, {
49
+ success: statsRow.violations === 0, duration_ms: statsRow.duration_ms,
50
+ });
51
+ for (const [check, violations] of Object.entries(statsRow.checks)) {
52
+ if (violations > 0)
53
+ run.recordRow('lint_rule', statsRow.rule, { item_type: statsRow.kind, item_id: statsRow.page, step_details: { check, violations } }, { success: false, duration_ms: 0 }, row);
54
+ }
55
+ }
56
+ }
57
+ catch { /* Stats must never fail lint. */ }
58
+ }
package/dist/lint.js CHANGED
@@ -1,8 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createWorkspaceRun, sendAndExit } from '@wix/origami-bi';
3
- import { readFileSync, rmSync } from 'node:fs';
4
- import { tmpdir } from 'node:os';
5
- import path from 'node:path';
3
+ import { readFileSync } from 'node:fs';
6
4
  // Origami lint: runs the project rules on the site's own files, then lints the pages whose files changed since the last
7
5
  // passing run. See lint/README.md.
8
6
  import { getChangedFiles, markPassing } from "./history/index.js";
@@ -12,7 +10,7 @@ import { runProjectRules } from "./project-rules-runner/index.js";
12
10
  import { checkStartedAtSiteRoot, LINT_PACKAGE_FILE } from "./shared/paths.js";
13
11
  import { GLOBAL_COMPONENTS_FOLDER, THEME_FILE } from "./shared/site-files-tree-mapping.js";
14
12
  import { track, trackSync } from "./bi/names.js";
15
- import { readStats } from "./bi/read-stats.js";
13
+ import { collectStats, recordStats } from "./bi/stats.js";
16
14
  const run = createWorkspaceRun({ command: 'yarn lint', toolVersion: JSON.parse(readFileSync(LINT_PACKAGE_FILE, 'utf8')).version });
17
15
  await run.trackCommand(async () => {
18
16
  run.commandRow.update({ step_details: { is_full: process.argv.includes('--full'), has_base: Boolean(process.env.ORIGAMI_LINT_BASE) } });
@@ -35,24 +33,15 @@ await run.trackCommand(async () => {
35
33
  row?.update({ step_details: { pages: pages.length - globals, global_components: globals, theme_changed: changedFiles.changed === null || changedFiles.changed.includes(THEME_FILE) } });
36
34
  return pages;
37
35
  });
38
- const statsFile = run.enabled ? path.join(tmpdir(), `origami-lint-${run.id}.json`) : undefined;
39
- const pagesPassed = trackSync('step', 'run_rules', { items_total: pagesToLint.length }, row => {
40
- try {
41
- const result = runRules(pagesToLint, statsFile);
42
- if (statsFile)
43
- readStats(statsFile, run);
44
- row?.update({ step_details: { exit_code: result.exitCode, signal: result.signal, outcome: result.outcome } });
45
- row?.end(result.passed);
46
- return result.passed;
47
- }
48
- finally {
49
- if (statsFile) {
50
- try {
51
- rmSync(statsFile, { force: true });
52
- }
53
- catch { /* Stats must never fail lint. */ }
54
- }
55
- }
36
+ if (run.enabled)
37
+ collectStats();
38
+ const pagesPassed = await track('step', 'run_rules', { items_total: pagesToLint.length }, async (row) => {
39
+ const result = await runRules(pagesToLint);
40
+ if (run.enabled)
41
+ recordStats(run);
42
+ row?.update({ step_details: { outcome: result.outcome } });
43
+ row?.end(result.passed);
44
+ return result.passed;
56
45
  });
57
46
  const passed = projectPassed && pagesPassed;
58
47
  if (passed)
@@ -1,9 +1,9 @@
1
1
  // structure.jsx (or .tsx) → components. Every JSX element inside Structure is a component.
2
2
  // Only what the lint tool can't give a rule: each component's key and parentKey, to connect its CSS. The rest is in the tool's syntax tree.
3
- import { parseSync } from 'oxc-parser';
3
+ import { parse } from '@typescript-eslint/parser';
4
4
  import { findDeclaration, isIdAttribute, getLiteralClassNames, getLiteralId, getNestingContext, createSourceReader, walk, emptyComponentCss } from "./utils.js";
5
- /** program: the same text's oxc syntax tree, when the lint tool already has it (oxlint's is the same shape); otherwise parsed here. */
6
- export function readStructure(jsxFile, text, program = parseSync(jsxFile, text).program) {
5
+ /** program: the same text's syntax tree, when the lint tool already has it (ESLint's, from the same parser); otherwise parsed here. */
6
+ export function readStructure(jsxFile, text, program = parse(text, { filePath: jsxFile, ecmaFeatures: { jsx: true }, range: true })) {
7
7
  const source = createSourceReader(text);
8
8
  const elements = findElements(program);
9
9
  return { components: toComponents(elements, assignKeys(elements, source), source) };
@@ -19,7 +19,7 @@ function findElements(program) {
19
19
  if (node.type !== 'JSXElement')
20
20
  return;
21
21
  const { parent, slot } = getNestingContext(ancestors);
22
- const siblings = `${parent?.start}|${slot}`;
22
+ const siblings = `${parent?.range[0]}|${slot}`;
23
23
  const index = siblingCounts.get(siblings) ?? 0;
24
24
  siblingCounts.set(siblings, index + 1);
25
25
  elements.push({ node: node, parent, slot, index });
@@ -46,7 +46,7 @@ function toComponents(elements, keys, source) {
46
46
  const key = keys.get(node);
47
47
  components[key] = {
48
48
  key,
49
- sourceStart: node.start,
49
+ sourceStart: node.range[0],
50
50
  id: getLiteralId(node),
51
51
  classNames: getLiteralClassNames(node),
52
52
  parentKey: parent ? keys.get(parent) : null,
@@ -1,6 +1,7 @@
1
1
  import { CSS_KINDS } from "./model.js";
2
2
  // ── syntax tree ──
3
- /** Calls visit(node, ancestors) for every node. Skips `parent`, which oxlint's syntax tree has (oxc-parser's doesn't). */
3
+ const isAstNode = (value) => typeof value?.type === 'string';
4
+ /** Calls visit(node, ancestors) for every node. Skips `parent`, which ESLint's syntax tree has (the parser's alone doesn't). */
4
5
  export function walk(node, ancestors, visit) {
5
6
  visit(node, ancestors);
6
7
  ancestors.push(node);
@@ -8,7 +9,7 @@ export function walk(node, ancestors, visit) {
8
9
  if (key === 'parent')
9
10
  continue;
10
11
  for (const child of Array.isArray(value) ? value : [value])
11
- if (typeof child?.type === 'string')
12
+ if (isAstNode(child))
12
13
  walk(child, ancestors, visit);
13
14
  }
14
15
  ancestors.pop();
@@ -68,8 +69,8 @@ export function createSourceReader(text) {
68
69
  return { line: line + 1, column: offset - lineStarts[line] };
69
70
  };
70
71
  return {
71
- code: (node) => text.slice(node.start, node.end).replace(/\s+/g, ' ').trim(),
72
- loc: (node) => ({ start: position(node.start), end: position(node.end) }),
72
+ code: (node) => text.slice(...node.range).replace(/\s+/g, ' ').trim(),
73
+ loc: (node) => ({ start: position(node.range[0]), end: position(node.range[1]) }),
73
74
  };
74
75
  }
75
76
  // ── CSS ──
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { projectRuleRegistry } from "../rules/project/index.js";
4
- import { log } from "../shared/log.js";
4
+ import { formatProblemLine, log } from "../shared/log.js";
5
5
  import { ROOT } from "../shared/paths.js";
6
6
  import { createProjectModel } from "../project-model/index.js";
7
7
  /** true when no error rule reported a problem, on the site of these files. Logs how many rules run, and whether they passed. */
@@ -38,9 +38,9 @@ export function runProjectRules(files, { rules = projectRuleRegistry, readFile =
38
38
  log(passed ? 'project rules passed' : 'project rules failed');
39
39
  return passed;
40
40
  }
41
- /** `file:line:1: error origami(name): message`, the shape oxlint prints the page rules' problems in. */
41
+ /** `file:line:1: error origami(name): message`, the shape the page rules' problems print in. */
42
42
  export function formatProblem({ name, severity = 'error' }, { file, line = 1, message }) {
43
- return `${file}:${line}:1: ${severity === 'error' ? 'error' : 'warning'} origami(${name}): ${message}`;
43
+ return formatProblemLine({ file, line, column: 1, severity: severity === 'error' ? 'error' : 'warning', rule: name, message });
44
44
  }
45
45
  function readFromSite(file) {
46
46
  try {
@@ -59,7 +59,7 @@ function isAllowedStatement({ node, rulesApi, report, context }) {
59
59
  }
60
60
  /** Skips a file with no root element (page-structure reports it) and a body that is TypeScript (hasNoTypeScript reports it). */
61
61
  function returnsElement({ node, rulesApi, report }) {
62
- const body = unwrap(node.body);
62
+ const { body } = node;
63
63
  if (body.type === 'JSXElement' || rulesApi.isTypeScript(body) || !rulesApi.getRoot())
64
64
  return;
65
65
  report({ check: 'invalid_structure_declaration', loc: node.body.loc, message: STRUCTURE_SHAPE });
@@ -107,7 +107,6 @@ function isStructureArrow(node) {
107
107
  const statement = node.parent.parent?.parent;
108
108
  return statement?.type === 'ExportNamedDeclaration' && isAtTop(statement) && structureArrowOf(statement) === node;
109
109
  }
110
- const unwrap = (node) => (node.type === 'ParenthesizedExpression' ? node.expression : node);
111
110
  function shortCode(code) {
112
111
  const [line] = code.trim().split('\n');
113
112
  return line.length > 40 ? `${line.slice(0, 40)}…` : line;
@@ -2,7 +2,7 @@
2
2
  // from the Wix font catalog in @wix/origami-fonts: no quotes, commas or fallbacks, the same case. The editor reads that
3
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
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
- // theme.css isn't a file oxlint lints, so the rule runs on each linted page (a theme change lints every page) and
5
+ // theme.css isn't a file ESLint 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
7
  import { CATALOG_FONTS, readFontShorthand, reportCssProblem } from "../../rules-api/index.js";
8
8
  import { defineRule } from "./define-rule.js";
@@ -1,21 +1,22 @@
1
- // An origami rule as an oxlint rule: its callbacks are oxlint's, so the adapter only adds what every callback gets.
1
+ // An origami rule as an ESLint rule: its callbacks are ESLint's, so the adapter only adds what every callback gets.
2
2
  import path from 'node:path';
3
3
  import { startRuleStats } from "../../bi/stats.js";
4
4
  import { getOrCreateRulesApi } from "../../rules-api/index.js";
5
5
  import { createReport, createToolCallbacks } from "../utils.js";
6
- export function toOxlintRule(rule) {
6
+ export function toEslintRule(rule) {
7
7
  return {
8
- meta: { type: 'problem' },
9
- // oxlint calls this once per rule per file. The file's rules API is shared by every rule (getOrCreateRulesApi caches it).
8
+ // ESLint reports by message id: one message, the rule's own text as its data.
9
+ meta: { type: 'problem', schema: [], messages: { problem: '{{message}}' } },
10
+ defaultOptions: [],
11
+ // ESLint calls this once per rule per file. The file's rules API is shared by every rule (getOrCreateRulesApi caches it).
10
12
  create: (context) => {
11
13
  const stats = startRuleStats(rule.name, rule.checks, path.relative(context.cwd, context.filename), context.sourceCode.text, context.sourceCode.ast);
12
- const report = createReport((problem) => context.report(problem), stats);
14
+ const report = createReport((problem) => context.report({ messageId: 'problem', data: { message: problem.message }, loc: problem.loc }), stats);
13
15
  return createToolCallbacks(rule, { rulesApi: () => getOrCreateRulesApi(toLintedFile(context)), report, context, stats });
14
16
  },
15
17
  };
16
18
  }
17
- /** oxlint already parsed the file: its syntax tree is reused instead of parsing it again. */
19
+ /** ESLint already parsed the file: its syntax tree is reused instead of parsing it again. */
18
20
  function toLintedFile({ cwd, filename, sourceCode }) {
19
- // oxlint's tree is oxc-parser's (oxlint runs on oxc), plus `parent`, `range` and `loc`. Their types are declared apart.
20
21
  return { cwd, filename, text: sourceCode.text, program: sourceCode.ast };
21
22
  }
@@ -1,7 +1,7 @@
1
- // The plugin oxlint loads when it starts: every rule as an oxlint rule.
1
+ // The plugin run-rules.ts gives ESLint: every rule as an ESLint rule.
2
2
  import { ruleRegistry } from "../../rules/index.js";
3
- import { toOxlintRule } from "./adapt-rule.js";
3
+ import { toEslintRule } from "./adapt-rule.js";
4
4
  export default {
5
5
  meta: { name: 'origami' },
6
- rules: Object.fromEntries(ruleRegistry.map((rule) => [rule.name, toOxlintRule(rule)])),
6
+ rules: Object.fromEntries(ruleRegistry.map((rule) => [rule.name, toEslintRule(rule)])),
7
7
  };
@@ -0,0 +1,60 @@
1
+ // Runs the rules with ESLint, in this process, on the pages' structure files: the plugin (eslint-plugin.ts) on a config
2
+ // built here, never the site's own ESLint config.
3
+ import path from 'node:path';
4
+ import * as parser from '@typescript-eslint/parser';
5
+ import { ESLint } from 'eslint';
6
+ import { ruleRegistry } from "../../rules/index.js";
7
+ import { formatProblemLine, log } from "../../shared/log.js";
8
+ import { ROOT } from "../../shared/paths.js";
9
+ import plugin from "./eslint-plugin.js";
10
+ /** Each rule on at its own severity. Warnings don't fail the lint. */
11
+ export const ruleSeverities = (rules) => Object.fromEntries(rules.map(({ name, severity = 'error' }) => [`origami/${name}`, severity]));
12
+ /** How ESLint reads a structure file: typescript-eslint's parser reads .jsx and .tsx alike; its tree is the one the rules' types are. */
13
+ export const STRUCTURE_FILES = {
14
+ files: ['**/*.jsx', '**/*.tsx'],
15
+ languageOptions: { parser, parserOptions: { ecmaFeatures: { jsx: true } } },
16
+ // An `eslint-disable` comment the site has for its own tools isn't a problem of the page.
17
+ linterOptions: { reportUnusedDisableDirectives: 'off' },
18
+ };
19
+ /** Every rule, on the structure files. */
20
+ export const CONFIG = [{
21
+ ...STRUCTURE_FILES,
22
+ plugins: { origami: plugin },
23
+ rules: ruleSeverities(ruleRegistry),
24
+ }];
25
+ /** jsxFiles: the pages' structure files, from the site root. Each problem printed as one line; passed when there was nothing to lint, or no error. */
26
+ export async function runRules(jsxFiles) {
27
+ const result = await runEslint(jsxFiles);
28
+ log(result.passed ? 'rules passed' : 'rules failed');
29
+ return result;
30
+ }
31
+ async function runEslint(files) {
32
+ if (files.length === 0)
33
+ return { passed: true, outcome: 'no_files' };
34
+ let results;
35
+ try {
36
+ results = await lintStructureFiles(files);
37
+ }
38
+ catch (error) {
39
+ // A rule that couldn't finish fails the run, like a project rule.
40
+ console.log(formatProblemLine({ file: '.', line: 1, column: 1, severity: 'error', message: `the page rules couldn't finish, so this run can't pass: ${error instanceof Error ? error.message : String(error)}` }));
41
+ return { passed: false, outcome: 'plugin_crash' };
42
+ }
43
+ for (const line of results.flatMap((result) => toProblemLines(result)))
44
+ console.log(line);
45
+ const passed = results.every((result) => result.errorCount === 0);
46
+ return { passed, outcome: passed ? 'passed' : 'violations' };
47
+ }
48
+ /** Every rule on these structure files (paths from cwd, the site root): ESLint's results, nothing printed. */
49
+ export function lintStructureFiles(files, cwd = ROOT) {
50
+ // ignore: false, so a page in a folder ESLint skips by default is linted too: these are the files to lint.
51
+ return new ESLint({ cwd, overrideConfigFile: true, overrideConfig: CONFIG, ignore: false }).lintFiles(files);
52
+ }
53
+ /** A file's problems as lines, from the site root. A parse error has no rule, and only its own text. */
54
+ export function toProblemLines({ filePath, messages }, cwd = ROOT) {
55
+ const file = path.relative(cwd, filePath).split(path.sep).join('/');
56
+ return messages.map(({ line, column, severity, ruleId, message }) => formatProblemLine({
57
+ file, line, column, severity: severity === 2 ? 'error' : 'warning',
58
+ rule: ruleId?.replace(/^origami\//, ''), message: ruleId ? message : message.replace(/^Parsing error: /, ''),
59
+ }));
60
+ }
@@ -1,3 +1,3 @@
1
- // Part 5 — rules adapter: runs the rules inside a lint tool (oxlint today). Everything that knows oxlint is in oxlint/.
2
- export { runRules } from "./oxlint/run-rules.js";
1
+ // Part 5 — rules adapter: runs the rules inside a lint tool (ESLint today). Everything that knows ESLint is in eslint/.
2
+ export { runRules } from "./eslint/run-rules.js";
3
3
  export { createReport, createToolCallbacks } from "./utils.js";
@@ -1,4 +1,4 @@
1
- /** The report a rule gets on a file: a violation found from both sides (parent and child) is reported once. Neither oxlint nor ESLint does that. */
1
+ /** The report a rule gets on a file: a violation found from both sides (parent and child) is reported once. ESLint doesn't do that. */
2
2
  export function createReport(send, stats) {
3
3
  const reported = new Set();
4
4
  return (problem) => {
@@ -3,7 +3,7 @@
3
3
  // One per file, shared by every rule on it. Only building its page reads from disk, once per file.
4
4
  import path from 'node:path';
5
5
  import { GLOBAL_COMPONENTS_FOLDER } from "../shared/site-files-tree-mapping.js";
6
- import { models, pageId } from "../bi/stats.js";
6
+ import { isCollectingStats, 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
9
  import { CONTENT_TARGET, readBreakpointName, readCustomMedia, readThemeFontTokens, readThemeTokens } from "../shared/css.js";
@@ -78,7 +78,7 @@ export function getOrCreateRulesApi(file) {
78
78
  throw error;
79
79
  }
80
80
  finally {
81
- if (process.env.ORIGAMI_LINT_STATS) {
81
+ if (isCollectingStats()) {
82
82
  try {
83
83
  const kind = getRelativeJsxPath(file).startsWith(`${GLOBAL_COMPONENTS_FOLDER}/`) ? 'component' : 'page';
84
84
  models.push({ kind, page: pageId(getRelativeJsxPath(file), file.text, file.program), duration_ms: performance.now() - started,
@@ -2,3 +2,7 @@
2
2
  export function log(message) {
3
3
  console.log(`origami-lint [${Math.round(performance.now())} ms]: ${message}`);
4
4
  }
5
+ /** `file:line:column: error origami(rule): message`, or `file:line:column: error: message` without a rule: one line per problem, the shape agents read. */
6
+ export function formatProblemLine({ file, line, column, severity, rule, message }) {
7
+ return `${file}:${line}:${column}: ${severity}${rule ? ` origami(${rule})` : ''}: ${message}`;
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/origami-lint",
3
- "version": "1.34.35",
3
+ "version": "1.34.37",
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": {
@@ -12,7 +12,7 @@
12
12
  "!**/*.tsbuildinfo"
13
13
  ],
14
14
  "engines": {
15
- "node": "^20.19.0 || >=22.12.0"
15
+ "node": "^20.19.0 || ^22.13.0 || >=24"
16
16
  },
17
17
  "scripts": {
18
18
  "build": "rm -rf dist && tsc -p . && tsc -p tsconfig.build.json",
@@ -21,15 +21,17 @@
21
21
  "test": "node --test 'test/*.test.ts'"
22
22
  },
23
23
  "dependencies": {
24
+ "@typescript-eslint/parser": "^8.71.2",
24
25
  "@wix/origami-fonts": "1.0.4",
25
- "oxc-parser": "^0.151.0",
26
- "oxlint": "^1.39.0",
26
+ "eslint": "^10.12.0",
27
27
  "postcss": "^8.5.3",
28
- "@wix/origami-bi": "0.14.0"
28
+ "typescript": "^5.8.3 || ~6.0.3",
29
+ "@wix/origami-bi": "0.15.0"
29
30
  },
30
31
  "devDependencies": {
31
32
  "@types/node": "^22.0.0",
32
- "typescript": "^6.0.3"
33
+ "@typescript-eslint/utils": "^8.71.2",
34
+ "oxlint": "^1.39.0"
33
35
  },
34
36
  "publishUnscoped": false,
35
37
  "publishConfig": {
@@ -52,5 +54,5 @@
52
54
  ]
53
55
  }
54
56
  },
55
- "falconPackageHash": "3167f248bc0c6e01893243829d375999c8ee55bc2d649ba40becbf55"
57
+ "falconPackageHash": "8b862c51f861c94c457797da1e14d586491490ca9c49def14ca39d1b"
56
58
  }
@@ -1,34 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { ruleRegistry } from "../rules/index.js";
3
- /** Best-effort transport, explicitly projecting the plugin's data so messages and paths cannot ride along. */
4
- export function readStats(file, run) {
5
- try {
6
- const stats = JSON.parse(readFileSync(file, 'utf8'));
7
- const validPage = (page) => page === undefined || typeof page === 'string' && /^[\w-]+$/.test(page);
8
- const validKind = (kind) => kind === 'page' || kind === 'component';
9
- const count = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0;
10
- if (!Array.isArray(stats.rules) || !Array.isArray(stats.models)
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))
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')))
16
- return;
17
- for (const model of stats.models) {
18
- run.recordRow('step', 'build_page_model', { item_type: model.kind, item_id: model.page,
19
- step_details: { components: model.components, css_files: model.css_files } }, {
20
- success: !model.error_name, failure: model.error_name ? { error_name: model.error_name } : undefined, duration_ms: model.duration_ms,
21
- });
22
- }
23
- for (const statsRow of stats.rules) {
24
- const row = run.recordRow('lint_rule', statsRow.rule, { item_type: statsRow.kind, item_id: statsRow.page, step_details: { violations: statsRow.violations } }, {
25
- success: statsRow.violations === 0, duration_ms: statsRow.duration_ms,
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
- }
31
- }
32
- }
33
- catch { /* Missing, truncated or malformed stats never affect lint. */ }
34
- }
@@ -1,12 +0,0 @@
1
- // Passed to oxlint with -c by run-rules.ts. Not named oxlint.config.ts, so `oxlint .` in the origami repo does not load it as a nested config.
2
- import path from 'node:path';
3
- import { defineConfig } from 'oxlint';
4
- import { ruleRegistry } from "../../rules/index.js";
5
- /** Each rule on at its own severity. oxlint exits 0 when it finds only warnings, so warnings don't fail the lint. */
6
- export const ruleSeverities = (rules) => Object.fromEntries(rules.map(({ name, severity = 'error' }) => [`origami/${name}`, severity]));
7
- export default defineConfig({
8
- // .ts in the origami repo, .js once built: the build rewrites imports, not this path.
9
- jsPlugins: [path.join(import.meta.dirname, `oxlint-plugin${path.extname(import.meta.filename)}`)],
10
- categories: { correctness: 'off' },
11
- rules: ruleSeverities(ruleRegistry),
12
- });
@@ -1,28 +0,0 @@
1
- // Runs the rules with oxlint on the pages' structure files. Its config loads the rules (through oxlint-plugin.ts).
2
- import { spawnSync } from 'node:child_process';
3
- import { createRequire } from 'node:module';
4
- import path from 'node:path';
5
- import { log } from "../../shared/log.js";
6
- import { ROOT } from "../../shared/paths.js";
7
- // oxlint is the lint's own dependency: found from here, wherever the package manager installed it.
8
- const OXLINT_BIN = path.join(path.dirname(createRequire(import.meta.url).resolve('oxlint/package.json')), 'bin/oxlint');
9
- // .ts in the origami repo, .js once built.
10
- const OXLINT_CONFIG = path.join(import.meta.dirname, `origami-rules.config${path.extname(import.meta.filename)}`);
11
- /** jsxFiles: the pages' structure files, from the site root. true when there was nothing to lint, or oxlint found no errors. */
12
- export function runRules(jsxFiles, statsFile) {
13
- const result = runOxlint(jsxFiles, statsFile);
14
- log(result.passed ? 'rules passed' : 'rules failed');
15
- return result;
16
- }
17
- function runOxlint(files, statsFile) {
18
- if (files.length === 0) {
19
- return { passed: true, exitCode: null, signal: null, outcome: 'no_files' };
20
- }
21
- const env = { ...process.env };
22
- delete env.ORIGAMI_LINT_STATS;
23
- if (statsFile)
24
- env.ORIGAMI_LINT_STATS = statsFile;
25
- const result = spawnSync(process.execPath, [OXLINT_BIN, '-c', OXLINT_CONFIG, ...files], { cwd: ROOT, stdio: 'inherit', env });
26
- return { passed: result.status === 0, exitCode: result.status, signal: result.signal,
27
- outcome: result.status === 0 ? 'passed' : result.status === 1 ? 'violations' : 'plugin_crash' };
28
- }