@wix/origami-lint 1.34.17 → 1.34.19
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/bi/names.js +3 -0
- package/dist/bi/read-stats.js +27 -0
- package/dist/bi/stats.js +43 -0
- package/dist/history/changed-files.js +20 -11
- package/dist/history/last-passing.js +9 -6
- package/dist/lint.js +48 -6
- package/dist/project-rules-runner/run-project-rules.js +5 -3
- package/dist/rules-adapter/oxlint/adapt-rule.js +6 -2
- package/dist/rules-adapter/oxlint/run-rules.js +15 -9
- package/dist/rules-adapter/utils.js +14 -3
- package/dist/rules-api/rules-api.js +26 -2
- package/dist/shared/paths.js +6 -7
- package/package.json +3 -3
package/dist/bi/names.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
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) && validPage(s.page) && validKind(s.kind) && Number.isSafeInteger(s.violations) && s.violations >= 0 && count(s.duration_ms))
|
|
12
|
+
|| !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
|
+
return;
|
|
14
|
+
for (const model of stats.models) {
|
|
15
|
+
run.recordRow('step', 'build_page_model', { item_type: model.kind, item_id: model.page,
|
|
16
|
+
step_details: { components: model.components, css_files: model.css_files } }, {
|
|
17
|
+
success: !model.error_name, failure: model.error_name ? { error_name: model.error_name } : undefined, duration_ms: model.duration_ms,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
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 } }, {
|
|
22
|
+
success: statsRow.violations === 0, duration_ms: statsRow.duration_ms,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch { /* Missing, truncated or malformed stats never affect lint. */ }
|
|
27
|
+
}
|
package/dist/bi/stats.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// The oxlint process collects only ids, counts and timings. No BI sender runs in the plugin.
|
|
2
|
+
import { writeFileSync } from 'node:fs';
|
|
3
|
+
import { GLOBAL_COMPONENTS_FOLDER } from "../shared/site-files-tree-mapping.js";
|
|
4
|
+
import { readStructure } from "../page-model/read-structure.js";
|
|
5
|
+
const rules = [];
|
|
6
|
+
export const models = [];
|
|
7
|
+
const pages = new Map();
|
|
8
|
+
let registered = false;
|
|
9
|
+
/** Resolve the same root component id the page model reads; never use its path as a BI id. */
|
|
10
|
+
export function pageId(filename, text, program) {
|
|
11
|
+
let page = pages.get(filename);
|
|
12
|
+
if (!pages.has(filename)) {
|
|
13
|
+
const { components } = readStructure(filename, text, program);
|
|
14
|
+
const id = Object.values(components).find(component => component.parentKey === null)?.id;
|
|
15
|
+
page = id && /^[\w-]+$/.test(id) ? id : undefined;
|
|
16
|
+
pages.set(filename, page);
|
|
17
|
+
}
|
|
18
|
+
return page;
|
|
19
|
+
}
|
|
20
|
+
/** Called once per rule and file by the adapter. Passing rules retain their zero count. */
|
|
21
|
+
export function startRuleStats(rule, filename, text, program) {
|
|
22
|
+
const destination = process.env.ORIGAMI_LINT_STATS;
|
|
23
|
+
if (!destination)
|
|
24
|
+
return;
|
|
25
|
+
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
|
+
const kind = filename.startsWith(`${GLOBAL_COMPONENTS_FOLDER}/`) ? 'component' : 'page';
|
|
36
|
+
const stats = { rule, kind, page: pageId(filename, text, program), violations: 0, duration_ms: 0 };
|
|
37
|
+
rules.push(stats);
|
|
38
|
+
return stats;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Which files changed since the last passing run. Now = the tree hash of the whole folder, committed or not,
|
|
2
2
|
// taken with a private index file, so your staging area, branches and history are never touched.
|
|
3
|
+
import { trackSync } from "../bi/names.js";
|
|
3
4
|
import { mkdirSync } from 'node:fs';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import { log } from "../shared/log.js";
|
|
@@ -10,15 +11,23 @@ const PRIVATE_INDEX = { GIT_INDEX_FILE: path.join(LINT_DIR, 'index') };
|
|
|
10
11
|
/** `--full`: compare with nothing. `ORIGAMI_LINT_BASE=main`: compare with main instead of the last passing run. */
|
|
11
12
|
export function getChangedFiles() {
|
|
12
13
|
const base = process.env.ORIGAMI_LINT_BASE;
|
|
13
|
-
const now = takeSnapshot();
|
|
14
|
-
const before =
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
const now = trackSync('step', 'take_snapshot', {}, () => takeSnapshot());
|
|
15
|
+
const before = trackSync('step', 'find_compare_version', {}, row => {
|
|
16
|
+
const before = findVersionToCompareWith(base);
|
|
17
|
+
row?.update({ step_details: { compare_mode: before.mode, lint_version_changed: before.lintVersionChanged } });
|
|
18
|
+
return before.version;
|
|
19
|
+
});
|
|
20
|
+
const changedFiles = trackSync('step', 'diff_changed_files', {}, row => {
|
|
21
|
+
const changedFiles = {
|
|
22
|
+
now,
|
|
23
|
+
since: before && (base ?? before.slice(0, 7)),
|
|
24
|
+
comparedWithBase: Boolean(base),
|
|
25
|
+
changed: before ? gitList(['diff', '--name-only', '-z', '--no-renames', before, now]) : null,
|
|
26
|
+
all: gitList(['ls-tree', '-r', '-z', '--name-only', now]),
|
|
27
|
+
};
|
|
28
|
+
row?.update({ step_details: { changed_files: changedFiles.changed?.length ?? changedFiles.all.length, all_files: changedFiles.all.length } });
|
|
29
|
+
return changedFiles;
|
|
30
|
+
});
|
|
22
31
|
logChangedFiles(changedFiles);
|
|
23
32
|
return changedFiles;
|
|
24
33
|
}
|
|
@@ -29,9 +38,9 @@ function takeSnapshot() {
|
|
|
29
38
|
}
|
|
30
39
|
function findVersionToCompareWith(base) {
|
|
31
40
|
if (process.argv.includes('--full'))
|
|
32
|
-
return null;
|
|
41
|
+
return { version: null, mode: 'full', lintVersionChanged: false };
|
|
33
42
|
if (base)
|
|
34
|
-
return git(['rev-parse', base]);
|
|
43
|
+
return { version: git(['rev-parse', base]), mode: 'base', lintVersionChanged: false };
|
|
35
44
|
return lastPassingVersion();
|
|
36
45
|
}
|
|
37
46
|
function logChangedFiles({ since, changed }) {
|
|
@@ -7,24 +7,27 @@ import { git } from "./utils.js";
|
|
|
7
7
|
const LAST_PASSING_REF = 'refs/origami/last-valid';
|
|
8
8
|
const LAST_PASSING_FILE = path.join(LINT_DIR, 'last-passing.json');
|
|
9
9
|
const LINT_VERSION = JSON.parse(readFileSync(LINT_PACKAGE_FILE, 'utf8')).version;
|
|
10
|
-
/**
|
|
10
|
+
/** No version when nothing passed yet, or another lint version passed it (its rules may differ). */
|
|
11
11
|
export function lastPassingVersion() {
|
|
12
12
|
const snapshot = lastPassingSnapshot();
|
|
13
13
|
if (!snapshot)
|
|
14
|
-
return null;
|
|
14
|
+
return { version: null, mode: 'none', lintVersionChanged: false };
|
|
15
15
|
const passedWith = lastPassingLintVersion();
|
|
16
16
|
if (passedWith === LINT_VERSION)
|
|
17
|
-
return snapshot;
|
|
17
|
+
return { version: snapshot, mode: 'last_passing', lintVersionChanged: false };
|
|
18
18
|
log(`the lint changed since the last passing run (${passedWith ?? 'unknown'} → ${LINT_VERSION})`);
|
|
19
|
-
return null;
|
|
19
|
+
return { version: null, mode: 'none', lintVersionChanged: true };
|
|
20
20
|
}
|
|
21
21
|
/** The next run compares with the version that just passed. */
|
|
22
22
|
export function markPassing({ now, comparedWithBase }) {
|
|
23
|
-
if (comparedWithBase)
|
|
24
|
-
|
|
23
|
+
if (comparedWithBase) {
|
|
24
|
+
log('not saved as passing: compared with ORIGAMI_LINT_BASE');
|
|
25
|
+
return 'not_saved_base';
|
|
26
|
+
}
|
|
25
27
|
git(['update-ref', LAST_PASSING_REF, now]);
|
|
26
28
|
writeFileSync(LAST_PASSING_FILE, `${JSON.stringify({ lintVersion: LINT_VERSION }, null, 2)}\n`);
|
|
27
29
|
log('saved as passing: the next run compares with now');
|
|
30
|
+
return 'saved';
|
|
28
31
|
}
|
|
29
32
|
function lastPassingSnapshot() {
|
|
30
33
|
try {
|
package/dist/lint.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createWorkspaceRun, sendAndExit } from '@wix/origami-bi';
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { readFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
4
6
|
// Origami lint: runs the project rules on the site's own files, then lints the pages whose files changed since the last
|
|
5
7
|
// passing run. See lint/README.md.
|
|
6
8
|
import { getChangedFiles, markPassing } from "./history/index.js";
|
|
@@ -8,15 +10,55 @@ import { runRules } from "./rules-adapter/index.js";
|
|
|
8
10
|
import { findPagesToLint } from "./page-model/index.js";
|
|
9
11
|
import { runProjectRules } from "./project-rules-runner/index.js";
|
|
10
12
|
import { checkStartedAtSiteRoot, LINT_PACKAGE_FILE } from "./shared/paths.js";
|
|
13
|
+
import { GLOBAL_COMPONENTS_FOLDER, THEME_FILE } from "./shared/site-files-tree-mapping.js";
|
|
14
|
+
import { track, trackSync } from "./bi/names.js";
|
|
15
|
+
import { readStats } from "./bi/read-stats.js";
|
|
11
16
|
const run = createWorkspaceRun({ command: 'yarn lint', toolVersion: JSON.parse(readFileSync(LINT_PACKAGE_FILE, 'utf8')).version });
|
|
12
17
|
await run.trackCommand(async () => {
|
|
13
|
-
|
|
18
|
+
run.commandRow.update({ step_details: { is_full: process.argv.includes('--full'), has_base: Boolean(process.env.ORIGAMI_LINT_BASE) } });
|
|
19
|
+
await track('step', 'check_site_root', {}, async (row) => {
|
|
20
|
+
const problem = checkStartedAtSiteRoot();
|
|
21
|
+
if (problem) {
|
|
22
|
+
row?.end(false, { error_name: problem.error_name });
|
|
23
|
+
await sendAndExit(1);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
14
26
|
const changedFiles = getChangedFiles();
|
|
15
|
-
const projectPassed =
|
|
16
|
-
|
|
17
|
-
|
|
27
|
+
const projectPassed = trackSync('step', 'run_project_rules', {}, row => {
|
|
28
|
+
const passed = runProjectRules(changedFiles.all, { parentRow: row });
|
|
29
|
+
row?.end(passed);
|
|
30
|
+
return passed;
|
|
31
|
+
});
|
|
32
|
+
const pagesToLint = trackSync('step', 'find_pages_to_lint', {}, row => {
|
|
33
|
+
const pages = findPagesToLint(changedFiles);
|
|
34
|
+
const globals = pages.filter(file => file.startsWith(`${GLOBAL_COMPONENTS_FOLDER}/`)).length;
|
|
35
|
+
row?.update({ step_details: { pages: pages.length - globals, global_components: globals, theme_changed: changedFiles.changed === null || changedFiles.changed.includes(THEME_FILE) } });
|
|
36
|
+
return pages;
|
|
37
|
+
});
|
|
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
|
+
}
|
|
56
|
+
});
|
|
18
57
|
const passed = projectPassed && pagesPassed;
|
|
19
58
|
if (passed)
|
|
20
|
-
|
|
59
|
+
trackSync('step', 'mark_passing', {}, row => {
|
|
60
|
+
const outcome = markPassing(changedFiles);
|
|
61
|
+
row?.update({ step_details: { outcome } });
|
|
62
|
+
});
|
|
21
63
|
await sendAndExit(passed ? 0 : 1);
|
|
22
64
|
});
|
|
@@ -1,5 +1,3 @@
|
|
|
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
1
|
import { readFileSync } from 'node:fs';
|
|
4
2
|
import path from 'node:path';
|
|
5
3
|
import { projectRuleRegistry } from "../rules/project/index.js";
|
|
@@ -7,10 +5,11 @@ import { log } from "../shared/log.js";
|
|
|
7
5
|
import { ROOT } from "../shared/paths.js";
|
|
8
6
|
import { createProjectModel } from "../project-model/index.js";
|
|
9
7
|
/** 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 } = {}) {
|
|
8
|
+
export function runProjectRules(files, { rules = projectRuleRegistry, readFile = readFromSite, print = console.log, parentRow } = {}) {
|
|
11
9
|
log(`${rules.length} project rules to run`);
|
|
12
10
|
let passed = true;
|
|
13
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' } });
|
|
14
13
|
let problems;
|
|
15
14
|
try {
|
|
16
15
|
problems = rule.check(createProjectModel(rule.name, files, readFile));
|
|
@@ -18,9 +17,12 @@ export function runProjectRules(files, { rules = projectRuleRegistry, readFile =
|
|
|
18
17
|
catch (error) {
|
|
19
18
|
// 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
19
|
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
21
|
passed = false;
|
|
22
22
|
continue;
|
|
23
23
|
}
|
|
24
|
+
row?.update({ step_details: { violations: problems.length } });
|
|
25
|
+
row?.end(problems.length === 0);
|
|
24
26
|
for (const problem of problems) {
|
|
25
27
|
print(formatProblem(rule, problem));
|
|
26
28
|
if ((rule.severity ?? 'error') === 'error')
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// An origami rule as an oxlint rule: its callbacks are oxlint's, so the adapter only adds what every callback gets.
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { startRuleStats } from "../../bi/stats.js";
|
|
1
4
|
import { getOrCreateRulesApi } from "../../rules-api/index.js";
|
|
2
5
|
import { createReport, createToolCallbacks } from "../utils.js";
|
|
3
6
|
export function toOxlintRule(rule) {
|
|
@@ -5,8 +8,9 @@ export function toOxlintRule(rule) {
|
|
|
5
8
|
meta: { type: 'problem' },
|
|
6
9
|
// oxlint calls this once per rule per file. The file's rules API is shared by every rule (getOrCreateRulesApi caches it).
|
|
7
10
|
create: (context) => {
|
|
8
|
-
const
|
|
9
|
-
|
|
11
|
+
const stats = startRuleStats(rule.name, path.relative(context.cwd, context.filename), context.sourceCode.text, context.sourceCode.ast);
|
|
12
|
+
const report = createReport((problem) => context.report(problem), stats);
|
|
13
|
+
return createToolCallbacks(rule, { rulesApi: () => getOrCreateRulesApi(toLintedFile(context)), report, context, stats });
|
|
10
14
|
},
|
|
11
15
|
};
|
|
12
16
|
}
|
|
@@ -9,14 +9,20 @@ const OXLINT_BIN = path.join(path.dirname(createRequire(import.meta.url).resolve
|
|
|
9
9
|
// .ts in the origami repo, .js once built.
|
|
10
10
|
const OXLINT_CONFIG = path.join(import.meta.dirname, `origami-rules.config${path.extname(import.meta.filename)}`);
|
|
11
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) {
|
|
13
|
-
const
|
|
14
|
-
log(passed ? 'rules passed' : 'rules failed');
|
|
15
|
-
return
|
|
12
|
+
export function runRules(jsxFiles, statsFile) {
|
|
13
|
+
const result = runOxlint(jsxFiles, statsFile);
|
|
14
|
+
log(result.passed ? 'rules passed' : 'rules failed');
|
|
15
|
+
return result;
|
|
16
16
|
}
|
|
17
|
-
function runOxlint(files) {
|
|
18
|
-
if (files.length === 0)
|
|
19
|
-
return true;
|
|
20
|
-
|
|
21
|
-
|
|
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' };
|
|
22
28
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
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. */
|
|
2
|
-
export function createReport(send) {
|
|
2
|
+
export function createReport(send, stats) {
|
|
3
3
|
const reported = new Set();
|
|
4
4
|
return (problem) => {
|
|
5
5
|
const { start, end } = problem.loc;
|
|
@@ -7,14 +7,25 @@ export function createReport(send) {
|
|
|
7
7
|
if (reported.has(key))
|
|
8
8
|
return;
|
|
9
9
|
reported.add(key);
|
|
10
|
+
if (stats)
|
|
11
|
+
stats.violations++;
|
|
10
12
|
send(problem);
|
|
11
13
|
};
|
|
12
14
|
}
|
|
13
15
|
/** A rule's callbacks as the tool calls them, with the node only: every entry of rule.callbacks, so there's no list of names to keep. */
|
|
14
|
-
export function createToolCallbacks(rule, { rulesApi, report, context }) {
|
|
16
|
+
export function createToolCallbacks(rule, { rulesApi, report, context, stats }) {
|
|
15
17
|
return Object.fromEntries(Object.entries(rule.callbacks).map(([name, callback]) => {
|
|
16
18
|
// The tool calls each callback with the node its name is for, as Callbacks types it.
|
|
17
19
|
const check = callback;
|
|
18
|
-
return [name, (node) =>
|
|
20
|
+
return [name, (node) => {
|
|
21
|
+
const started = stats ? performance.now() : 0;
|
|
22
|
+
try {
|
|
23
|
+
check({ node, get rulesApi() { return rulesApi(); }, report, context });
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
if (stats)
|
|
27
|
+
stats.duration_ms += performance.now() - started;
|
|
28
|
+
}
|
|
29
|
+
}];
|
|
19
30
|
}));
|
|
20
31
|
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// and reading the file's own code that more than one rule needs (what type a tag is, its name, its id, its slot, its children, the elements in its attributes, where a name is imported from, the name an import is exported under, what is TypeScript, the props an arrow destructures).
|
|
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
|
+
import { GLOBAL_COMPONENTS_FOLDER } from "../shared/site-files-tree-mapping.js";
|
|
6
|
+
import { models, pageId } from "../bi/stats.js";
|
|
5
7
|
import { readPage } from "../page-model/index.js";
|
|
6
8
|
import { getAttributeElements as getHeldElements, getChildren, getId, getImport, getImportKind, getImportedName, getPropNames, getSlot, getTagName, getType, hasDataAttributes, isTypeScript } from "../shared/utils.js";
|
|
7
9
|
import { readBreakpointName, readCustomMedia, readThemeTokens } from "../shared/css.js";
|
|
@@ -55,8 +57,30 @@ const rulesApis = new Map();
|
|
|
55
57
|
/** This file's rules API: its page is built on the first call for the file (CSS and theme from disk), then shared by every rule. */
|
|
56
58
|
export function getOrCreateRulesApi(file) {
|
|
57
59
|
let rulesApi = rulesApis.get(file.filename);
|
|
58
|
-
if (!rulesApi)
|
|
59
|
-
|
|
60
|
+
if (!rulesApi) {
|
|
61
|
+
const started = performance.now();
|
|
62
|
+
let errorName;
|
|
63
|
+
try {
|
|
64
|
+
rulesApi = createRulesApi(readPage(file.cwd, { jsxFile: getRelativeJsxPath(file), text: file.text, program: file.program }));
|
|
65
|
+
rulesApis.set(file.filename, rulesApi);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
errorName = error instanceof Error && error.name === 'CssSyntaxError' ? 'css_syntax_error' : 'unknown_error';
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
if (process.env.ORIGAMI_LINT_STATS) {
|
|
73
|
+
try {
|
|
74
|
+
const kind = getRelativeJsxPath(file).startsWith(`${GLOBAL_COMPONENTS_FOLDER}/`) ? 'component' : 'page';
|
|
75
|
+
models.push({ kind, page: pageId(getRelativeJsxPath(file), file.text, file.program), duration_ms: performance.now() - started,
|
|
76
|
+
components: rulesApi ? Object.keys(rulesApi.page.components).length : 0,
|
|
77
|
+
css_files: rulesApi ? Object.values(rulesApi.page.css.files).filter(Boolean).length : 0,
|
|
78
|
+
...(errorName ? { error_name: errorName } : {}) });
|
|
79
|
+
}
|
|
80
|
+
catch { /* Stats must never fail lint. */ }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
60
84
|
return rulesApi;
|
|
61
85
|
}
|
|
62
86
|
/** The tool gives an absolute path; pages are keyed by the path from the site root. */
|
package/dist/shared/paths.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { sendAndExit } from '@wix/origami-bi';
|
|
2
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
/** The site: the folder the lint was started from (`yarn lint` at the site's root). */
|
|
@@ -9,20 +8,20 @@ export const LINT_PACKAGE_FILE = path.resolve(import.meta.dirname, '../../packag
|
|
|
9
8
|
const LINT_PACKAGE = 'origami-lint';
|
|
10
9
|
const LINT_PACKAGE_NAME = `@wix/${LINT_PACKAGE}`;
|
|
11
10
|
/** The lint runs only from the folder whose package.json adds it, which holds the site's git repository. */
|
|
12
|
-
export
|
|
11
|
+
export function checkStartedAtSiteRoot() {
|
|
13
12
|
const problem = siteRootProblem();
|
|
14
13
|
if (!problem)
|
|
15
|
-
return;
|
|
16
|
-
console.error(`${LINT_PACKAGE}: ${problem}`);
|
|
17
|
-
|
|
14
|
+
return null;
|
|
15
|
+
console.error(`${LINT_PACKAGE}: ${problem.message}`);
|
|
16
|
+
return problem;
|
|
18
17
|
}
|
|
19
18
|
function siteRootProblem() {
|
|
20
19
|
const packageFile = path.join(ROOT, 'package.json');
|
|
21
20
|
if (!existsSync(packageFile) || !addsTheLint(JSON.parse(readFileSync(packageFile, 'utf8')))) {
|
|
22
|
-
return `run it from the site's root, the folder whose package.json has ${LINT_PACKAGE_NAME} (started in ${ROOT})
|
|
21
|
+
return { error_name: 'no_lint_package', message: `run it from the site's root, the folder whose package.json has ${LINT_PACKAGE_NAME} (started in ${ROOT})` };
|
|
23
22
|
}
|
|
24
23
|
if (!existsSync(path.join(ROOT, '.git'))) {
|
|
25
|
-
return `no git repository in ${ROOT}. The lint keeps the last passing version in the site's repository: run git init here
|
|
24
|
+
return { error_name: 'no_git', message: `no git repository in ${ROOT}. The lint keeps the last passing version in the site's repository: run git init here.` };
|
|
26
25
|
}
|
|
27
26
|
return null;
|
|
28
27
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/origami-lint",
|
|
3
|
-
"version": "1.34.
|
|
3
|
+
"version": "1.34.19",
|
|
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": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"oxc-parser": "^0.151.0",
|
|
26
26
|
"oxlint": "^1.39.0",
|
|
27
27
|
"postcss": "^8.5.3",
|
|
28
|
-
"@wix/origami-bi": "0.
|
|
28
|
+
"@wix/origami-bi": "0.8.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/node": "^22.0.0",
|
|
@@ -52,5 +52,5 @@
|
|
|
52
52
|
]
|
|
53
53
|
}
|
|
54
54
|
},
|
|
55
|
-
"falconPackageHash": "
|
|
55
|
+
"falconPackageHash": "791d9d5c3c5c2e15912796c5bfa7def4c696084174385a57d767c143"
|
|
56
56
|
}
|