@salesforce-ux/slds-linter 0.2.0 → 0.2.2-alpha-2-alpha.0
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/build/commands/emit.d.ts +2 -0
- package/build/commands/emit.js +48 -0
- package/build/commands/lint.d.ts +2 -0
- package/build/commands/lint.js +55 -0
- package/build/commands/report.d.ts +2 -0
- package/build/commands/report.js +66 -0
- package/build/executor/__tests__/executor.test.js +216 -0
- package/build/executor/index.d.ts +20 -0
- package/build/executor/index.js +105 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +33 -0
- package/build/services/__tests__/file-scanner.test.js +47 -0
- package/build/services/artifact-processor.d.ts +6 -0
- package/build/services/artifact-processor.js +37 -0
- package/build/services/batch-processor.d.ts +29 -0
- package/build/services/batch-processor.js +84 -0
- package/build/services/config.resolver.d.ts +6 -0
- package/build/services/config.resolver.js +20 -0
- package/build/services/file-patterns.d.ts +3 -0
- package/build/services/file-patterns.js +21 -0
- package/build/services/file-scanner.d.ts +26 -0
- package/build/services/file-scanner.js +71 -0
- package/build/services/lint-runner.d.ts +17 -0
- package/build/services/lint-runner.js +69 -0
- package/build/services/report-generator.d.ts +43 -0
- package/build/services/report-generator.js +186 -0
- package/build/types/index.d.ts +75 -0
- package/build/types/index.js +0 -0
- package/build/utils/config-utils.d.ts +33 -0
- package/build/utils/config-utils.js +68 -0
- package/build/utils/editorLinkUtil.d.ts +21 -0
- package/build/utils/editorLinkUtil.js +21 -0
- package/build/utils/index.d.ts +2 -0
- package/build/utils/index.js +7 -0
- package/build/utils/lintResultsUtil.d.ts +21 -0
- package/build/utils/lintResultsUtil.js +70 -0
- package/build/utils/logger.d.ts +8 -0
- package/build/utils/logger.js +28 -0
- package/build/utils/nodeVersionUtil.d.ts +19 -0
- package/build/utils/nodeVersionUtil.js +42 -0
- package/build/workers/base.worker.d.ts +15 -0
- package/build/workers/base.worker.js +44 -0
- package/build/workers/eslint.worker.d.ts +1 -0
- package/build/workers/eslint.worker.js +49 -0
- package/build/workers/stylelint.worker.d.ts +1 -0
- package/build/workers/stylelint.worker.js +40 -0
- package/package.json +4 -4
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// src/commands/emit.ts
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { Logger } from "../utils/logger.js";
|
|
4
|
+
import { normalizeCliOptions } from "../utils/config-utils.js";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_ESLINT_CONFIG_PATH,
|
|
7
|
+
DEFAULT_STYLELINT_CONFIG_PATH
|
|
8
|
+
} from "../services/config.resolver.js";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { copyFile } from "fs/promises";
|
|
11
|
+
function registerEmitCommand(program) {
|
|
12
|
+
program.command("emit").description("Emits the configuration files used by slds-linter cli").option(
|
|
13
|
+
"-d, --directory <path>",
|
|
14
|
+
"Target directory to emit (defaults to current directory). Support glob patterns"
|
|
15
|
+
).action(async (options) => {
|
|
16
|
+
try {
|
|
17
|
+
Logger.info(chalk.blue("Emitting configuration files..."));
|
|
18
|
+
const normalizedOptions = normalizeCliOptions(options, {
|
|
19
|
+
configStylelint: DEFAULT_STYLELINT_CONFIG_PATH,
|
|
20
|
+
configEslint: DEFAULT_ESLINT_CONFIG_PATH
|
|
21
|
+
});
|
|
22
|
+
const destStyleConfigPath = path.join(
|
|
23
|
+
normalizedOptions.directory,
|
|
24
|
+
path.basename(normalizedOptions.configStylelint)
|
|
25
|
+
);
|
|
26
|
+
await copyFile(normalizedOptions.configStylelint, destStyleConfigPath);
|
|
27
|
+
Logger.success(chalk.green(`Stylelint configuration created at:
|
|
28
|
+
${destStyleConfigPath}
|
|
29
|
+
`));
|
|
30
|
+
const destESLintConfigPath = path.join(
|
|
31
|
+
normalizedOptions.directory,
|
|
32
|
+
path.basename(normalizedOptions.configEslint)
|
|
33
|
+
);
|
|
34
|
+
await copyFile(normalizedOptions.configEslint, destESLintConfigPath);
|
|
35
|
+
Logger.success(chalk.green(`ESLint configuration created at:
|
|
36
|
+
${destESLintConfigPath}
|
|
37
|
+
`));
|
|
38
|
+
} catch (error) {
|
|
39
|
+
Logger.error(
|
|
40
|
+
chalk.red(`Failed to emit configuration: ${error.message}`)
|
|
41
|
+
);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
registerEmitCommand
|
|
48
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// src/commands/lint.ts
|
|
2
|
+
import { Option } from "commander";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { printLintResults } from "../utils/lintResultsUtil.js";
|
|
5
|
+
import { normalizeCliOptions, normalizeDirectoryPath } from "../utils/config-utils.js";
|
|
6
|
+
import { Logger } from "../utils/logger.js";
|
|
7
|
+
import { DEFAULT_ESLINT_CONFIG_PATH, DEFAULT_STYLELINT_CONFIG_PATH } from "../services/config.resolver.js";
|
|
8
|
+
import { lint } from "../executor/index.js";
|
|
9
|
+
function registerLintCommand(program) {
|
|
10
|
+
program.command("lint").aliases(["lint:styles", "lint:components"]).configureHelp({
|
|
11
|
+
commandUsage: () => {
|
|
12
|
+
return `${program.name()} lint [directory] [options]`;
|
|
13
|
+
}
|
|
14
|
+
}).description("Run both style and component linting").argument("[directory]", "Target directory to scan (defaults to current directory). Support glob patterns").addOption(new Option("-d, --directory <path>", "Target directory to scan (defaults to current directory). Support glob patterns").hideHelp()).option("--fix", "Automatically fix problems").option("--config-stylelint <path>", "Path to stylelint config file").option("--config-eslint <path>", "Path to eslint config file").option("--editor <editor>", "Editor to open files with (e.g., vscode, atom, sublime). Defaults to vscode", "vscode").action(async (directory, options) => {
|
|
15
|
+
const startTime = Date.now();
|
|
16
|
+
try {
|
|
17
|
+
Logger.info(chalk.blue("Starting lint process..."));
|
|
18
|
+
const normalizedOptions = normalizeCliOptions(options, {
|
|
19
|
+
configStylelint: DEFAULT_STYLELINT_CONFIG_PATH,
|
|
20
|
+
configEslint: DEFAULT_ESLINT_CONFIG_PATH
|
|
21
|
+
});
|
|
22
|
+
if (directory) {
|
|
23
|
+
normalizedOptions.directory = normalizeDirectoryPath(directory);
|
|
24
|
+
} else if (options.directory) {
|
|
25
|
+
Logger.newLine().warning(chalk.yellow(
|
|
26
|
+
`WARNING: --directory, -d option is deprecated. Supply as argument instead.
|
|
27
|
+
Example: npx @salesforce-ux/slds-linter lint ${options.directory}`
|
|
28
|
+
));
|
|
29
|
+
}
|
|
30
|
+
const lintResults = await lint({
|
|
31
|
+
directory: normalizedOptions.directory,
|
|
32
|
+
fix: normalizedOptions.fix,
|
|
33
|
+
configStylelint: normalizedOptions.configStylelint,
|
|
34
|
+
configEslint: normalizedOptions.configEslint
|
|
35
|
+
});
|
|
36
|
+
printLintResults(lintResults, normalizedOptions.editor);
|
|
37
|
+
const errorCount = lintResults.reduce((sum, r) => sum + r.errors.length, 0);
|
|
38
|
+
const warningCount = lintResults.reduce((sum, r) => sum + r.warnings.length, 0);
|
|
39
|
+
Logger.info(
|
|
40
|
+
`
|
|
41
|
+
${chalk.red(`${errorCount} error${errorCount !== 1 ? "s" : ""}`)} ${chalk.yellow(`${warningCount} warning${warningCount !== 1 ? "s" : ""}`)}`
|
|
42
|
+
);
|
|
43
|
+
const elapsedTime = ((Date.now() - startTime) / 1e3).toFixed(2);
|
|
44
|
+
Logger.success(chalk.green(`
|
|
45
|
+
Linting completed in ${elapsedTime} seconds.`));
|
|
46
|
+
process.exit(errorCount > 0 ? 1 : 0);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
Logger.error(chalk.red(`Failed to complete linting: ${error.message}`));
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export {
|
|
54
|
+
registerLintCommand
|
|
55
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// src/commands/report.ts
|
|
2
|
+
import { Option } from "commander";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import { normalizeCliOptions, normalizeDirectoryPath } from "../utils/config-utils.js";
|
|
8
|
+
import { Logger } from "../utils/logger.js";
|
|
9
|
+
import { DEFAULT_ESLINT_CONFIG_PATH, DEFAULT_STYLELINT_CONFIG_PATH } from "../services/config.resolver.js";
|
|
10
|
+
import { report, lint } from "../executor/index.js";
|
|
11
|
+
function registerReportCommand(program) {
|
|
12
|
+
program.command("report").description("Generate report from linting results").argument("[directory]", "Target directory to scan (defaults to current directory). Support glob patterns").addOption(new Option("-d, --directory <path>", "Target directory to scan (defaults to current directory). Support glob patterns").hideHelp()).option("-o, --output <path>", "Output directory for reports (defaults to current directory)").option("--config-stylelint <path>", "Path to stylelint config file").option("--config-eslint <path>", "Path to eslint config file").addOption(new Option("--format <type>", "Output format").choices(["sarif", "csv"]).default("sarif")).action(async (directory, options) => {
|
|
13
|
+
const spinner = ora("Starting report generation...");
|
|
14
|
+
try {
|
|
15
|
+
const normalizedOptions = normalizeCliOptions(options, {
|
|
16
|
+
configStylelint: DEFAULT_STYLELINT_CONFIG_PATH,
|
|
17
|
+
configEslint: DEFAULT_ESLINT_CONFIG_PATH
|
|
18
|
+
});
|
|
19
|
+
if (directory) {
|
|
20
|
+
normalizedOptions.directory = normalizeDirectoryPath(directory);
|
|
21
|
+
} else if (options.directory) {
|
|
22
|
+
Logger.newLine().warning(chalk.yellow(
|
|
23
|
+
`WARNING: --directory, -d option is deprecated. Supply as argument instead.
|
|
24
|
+
Example: npx @salesforce-ux/slds-linter report ${options.directory}`
|
|
25
|
+
));
|
|
26
|
+
}
|
|
27
|
+
spinner.start();
|
|
28
|
+
const reportFormat = normalizedOptions.format?.toLowerCase() || "sarif";
|
|
29
|
+
const lintResults = await lint({
|
|
30
|
+
directory: normalizedOptions.directory,
|
|
31
|
+
configStylelint: normalizedOptions.configStylelint,
|
|
32
|
+
configEslint: normalizedOptions.configEslint
|
|
33
|
+
});
|
|
34
|
+
const reportStream = await report({
|
|
35
|
+
format: reportFormat
|
|
36
|
+
}, lintResults);
|
|
37
|
+
let outputFilePath;
|
|
38
|
+
if (reportFormat === "sarif") {
|
|
39
|
+
spinner.text = "Saving SARIF report...";
|
|
40
|
+
outputFilePath = path.join(normalizedOptions.output, "slds-linter-report.sarif");
|
|
41
|
+
} else if (reportFormat === "csv") {
|
|
42
|
+
spinner.text = "Saving CSV report...";
|
|
43
|
+
outputFilePath = path.join(normalizedOptions.output, "slds-linter-report.csv");
|
|
44
|
+
} else {
|
|
45
|
+
throw new Error(`Invalid format: ${reportFormat}. Supported formats: sarif, csv`);
|
|
46
|
+
}
|
|
47
|
+
const writeStream = fs.createWriteStream(outputFilePath);
|
|
48
|
+
reportStream.pipe(writeStream);
|
|
49
|
+
await new Promise((resolve, reject) => {
|
|
50
|
+
writeStream.on("finish", resolve);
|
|
51
|
+
writeStream.on("error", reject);
|
|
52
|
+
});
|
|
53
|
+
Logger.success(`${reportFormat.toUpperCase()} report generated: ${outputFilePath}
|
|
54
|
+
`);
|
|
55
|
+
spinner.succeed("Report generation completed");
|
|
56
|
+
process.exit(0);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
spinner?.fail("Report generation failed");
|
|
59
|
+
Logger.error(`Failed to generate report: ${error.message}`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
registerReportCommand
|
|
66
|
+
};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __esm = (fn, res) => function __init() {
|
|
6
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
7
|
+
};
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
21
|
+
|
|
22
|
+
// src/executor/index.ts
|
|
23
|
+
var executor_exports = {};
|
|
24
|
+
__export(executor_exports, {
|
|
25
|
+
lint: () => lint,
|
|
26
|
+
report: () => report
|
|
27
|
+
});
|
|
28
|
+
import { Readable } from "stream";
|
|
29
|
+
import { FileScanner } from "../services/file-scanner.js";
|
|
30
|
+
import { LintRunner } from "../services/lint-runner.js";
|
|
31
|
+
import { StyleFilePatterns, ComponentFilePatterns } from "../services/file-patterns.js";
|
|
32
|
+
import { ReportGenerator, CsvReportGenerator } from "../services/report-generator.js";
|
|
33
|
+
import { LINTER_CLI_VERSION } from "../services/config.resolver.js";
|
|
34
|
+
import { normalizeCliOptions } from "../utils/config-utils.js";
|
|
35
|
+
import { Logger } from "../utils/logger.js";
|
|
36
|
+
async function lint(config) {
|
|
37
|
+
try {
|
|
38
|
+
Logger.debug("Starting linting with Node API");
|
|
39
|
+
const normalizedConfig = normalizeCliOptions(config, {}, true);
|
|
40
|
+
const styleFiles = await FileScanner.scanFiles(normalizedConfig.directory, {
|
|
41
|
+
patterns: StyleFilePatterns,
|
|
42
|
+
batchSize: 100
|
|
43
|
+
});
|
|
44
|
+
const componentFiles = await FileScanner.scanFiles(normalizedConfig.directory, {
|
|
45
|
+
patterns: ComponentFilePatterns,
|
|
46
|
+
batchSize: 100
|
|
47
|
+
});
|
|
48
|
+
const lintOptions = {
|
|
49
|
+
fix: normalizedConfig.fix,
|
|
50
|
+
configPath: normalizedConfig.configStylelint
|
|
51
|
+
};
|
|
52
|
+
const styleResults = await LintRunner.runLinting(styleFiles, "style", {
|
|
53
|
+
...lintOptions,
|
|
54
|
+
configPath: normalizedConfig.configStylelint
|
|
55
|
+
});
|
|
56
|
+
const componentResults = await LintRunner.runLinting(componentFiles, "component", {
|
|
57
|
+
...lintOptions,
|
|
58
|
+
configPath: normalizedConfig.configEslint
|
|
59
|
+
});
|
|
60
|
+
const combinedResults = [...styleResults, ...componentResults];
|
|
61
|
+
return standardizeLintMessages(combinedResults);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
const errorMessage = `Linting failed: ${error.message}`;
|
|
64
|
+
Logger.error(errorMessage);
|
|
65
|
+
throw new Error(errorMessage);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function report(config, results) {
|
|
69
|
+
try {
|
|
70
|
+
Logger.debug("Starting report generation with Node API");
|
|
71
|
+
const normalizedConfig = normalizeCliOptions(config, {}, true);
|
|
72
|
+
const format = normalizedConfig.format || "sarif";
|
|
73
|
+
const lintResults = results || await lint({
|
|
74
|
+
directory: normalizedConfig.directory,
|
|
75
|
+
configStylelint: normalizedConfig.configStylelint,
|
|
76
|
+
configEslint: normalizedConfig.configEslint
|
|
77
|
+
});
|
|
78
|
+
switch (format) {
|
|
79
|
+
case "sarif":
|
|
80
|
+
return ReportGenerator.generateSarifReportStream(lintResults, {
|
|
81
|
+
toolName: "slds-linter",
|
|
82
|
+
toolVersion: LINTER_CLI_VERSION
|
|
83
|
+
});
|
|
84
|
+
case "csv":
|
|
85
|
+
const csvString = CsvReportGenerator.generateCsvString(lintResults);
|
|
86
|
+
const csvStream = new Readable();
|
|
87
|
+
csvStream.push(csvString);
|
|
88
|
+
csvStream.push(null);
|
|
89
|
+
return csvStream;
|
|
90
|
+
default:
|
|
91
|
+
const errorMessage = `Unsupported format: ${format}`;
|
|
92
|
+
Logger.error(errorMessage);
|
|
93
|
+
throw new Error(errorMessage);
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
const errorMessage = `Report generation failed: ${error.message}`;
|
|
97
|
+
Logger.error(errorMessage);
|
|
98
|
+
throw new Error(errorMessage);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function standardizeLintMessages(results) {
|
|
102
|
+
return results.map((result) => ({
|
|
103
|
+
...result,
|
|
104
|
+
errors: result.errors.map((entry) => {
|
|
105
|
+
let msgObj;
|
|
106
|
+
try {
|
|
107
|
+
msgObj = JSON.parse(entry.message);
|
|
108
|
+
if (typeof msgObj === "object" && "message" in msgObj) {
|
|
109
|
+
return { ...entry, ...msgObj };
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
return { ...entry, message: entry.message };
|
|
114
|
+
}),
|
|
115
|
+
warnings: result.warnings.map((entry) => {
|
|
116
|
+
let msgObj;
|
|
117
|
+
try {
|
|
118
|
+
msgObj = JSON.parse(entry.message);
|
|
119
|
+
if (typeof msgObj === "object" && "message" in msgObj) {
|
|
120
|
+
return { ...entry, ...msgObj };
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
return { ...entry, message: entry.message };
|
|
125
|
+
})
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
var init_executor = __esm({
|
|
129
|
+
"src/executor/index.ts"() {
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// src/executor/__tests__/executor.test.ts
|
|
134
|
+
import { lint as lint2, report as report2 } from "../index.js";
|
|
135
|
+
import { LintRunner as LintRunner2 } from "../../services/lint-runner.js";
|
|
136
|
+
import { FileScanner as FileScanner2 } from "../../services/file-scanner.js";
|
|
137
|
+
import { Readable as Readable2 } from "stream";
|
|
138
|
+
import { jest } from "@jest/globals.js";
|
|
139
|
+
var mockLintResult = {
|
|
140
|
+
filePath: "file1.css",
|
|
141
|
+
errors: [{ line: 1, column: 1, endColumn: 10, message: "Test error", ruleId: "test-rule", severity: 2 }],
|
|
142
|
+
warnings: []
|
|
143
|
+
};
|
|
144
|
+
jest.mock("../../services/lint-runner", () => {
|
|
145
|
+
return {
|
|
146
|
+
LintRunner: {
|
|
147
|
+
runLinting: jest.fn().mockImplementation(() => {
|
|
148
|
+
return Promise.resolve([mockLintResult]);
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
jest.mock("../../services/file-scanner", () => {
|
|
154
|
+
return {
|
|
155
|
+
FileScanner: {
|
|
156
|
+
scanFiles: jest.fn().mockImplementation(() => {
|
|
157
|
+
return Promise.resolve([["file1.css"]]);
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
jest.mock("fs/promises");
|
|
163
|
+
xdescribe("Executor functions", () => {
|
|
164
|
+
beforeEach(() => {
|
|
165
|
+
jest.clearAllMocks();
|
|
166
|
+
});
|
|
167
|
+
describe("lint", () => {
|
|
168
|
+
it("should scan directory and run linting when no files are provided", async () => {
|
|
169
|
+
const config = {
|
|
170
|
+
directory: "./src",
|
|
171
|
+
fix: true
|
|
172
|
+
};
|
|
173
|
+
const results = await lint2(config);
|
|
174
|
+
expect(FileScanner2.scanFiles).toHaveBeenCalledTimes(2);
|
|
175
|
+
expect(LintRunner2.runLinting).toHaveBeenCalledTimes(2);
|
|
176
|
+
expect(results).toHaveLength(2);
|
|
177
|
+
});
|
|
178
|
+
it("should use provided files and skip scanning when files are provided", async () => {
|
|
179
|
+
const config = {
|
|
180
|
+
files: ["file1.css", "component1.html"]
|
|
181
|
+
};
|
|
182
|
+
await lint2(config);
|
|
183
|
+
expect(FileScanner2.scanFiles).not.toHaveBeenCalled();
|
|
184
|
+
expect(LintRunner2.runLinting).toHaveBeenCalledTimes(2);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
describe("report", () => {
|
|
188
|
+
it("should return a readable stream", async () => {
|
|
189
|
+
const config = {
|
|
190
|
+
directory: "./src",
|
|
191
|
+
format: "sarif"
|
|
192
|
+
};
|
|
193
|
+
const stream = await report2(config);
|
|
194
|
+
expect(stream).toBeInstanceOf(Readable2);
|
|
195
|
+
});
|
|
196
|
+
it("should use lint results to generate a report", async () => {
|
|
197
|
+
const lintMock = jest.spyOn((init_executor(), __toCommonJS(executor_exports)), "lint").mockResolvedValue([mockLintResult]);
|
|
198
|
+
const config = {
|
|
199
|
+
directory: "./src",
|
|
200
|
+
format: "sarif"
|
|
201
|
+
};
|
|
202
|
+
await report2(config);
|
|
203
|
+
expect(lintMock).toHaveBeenCalledWith({
|
|
204
|
+
directory: "./src",
|
|
205
|
+
configStylelint: expect.any(String),
|
|
206
|
+
configEslint: expect.any(String)
|
|
207
|
+
});
|
|
208
|
+
lintMock.mockRestore();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
describe("Executor placeholder tests", () => {
|
|
213
|
+
it("should be implemented in the future", () => {
|
|
214
|
+
expect(true).toBe(true);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Readable } from 'stream';
|
|
2
|
+
import { LintResult, LintConfig, ReportConfig } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* Run linting on specified files or directory
|
|
5
|
+
*
|
|
6
|
+
* @param config Linting configuration options
|
|
7
|
+
* @returns Promise resolving to an array of lint results
|
|
8
|
+
* @throws Error if linting fails
|
|
9
|
+
*/
|
|
10
|
+
export declare function lint(config: LintConfig): Promise<LintResult[]>;
|
|
11
|
+
/**
|
|
12
|
+
* Generate a report from linting results
|
|
13
|
+
*
|
|
14
|
+
* @param config Report configuration options
|
|
15
|
+
* @param results Optional lint results (if not provided, will run lint)
|
|
16
|
+
* @returns A readable stream containing the report data
|
|
17
|
+
* @throws Error if report generation fails
|
|
18
|
+
*/
|
|
19
|
+
export declare function report(config: ReportConfig, results?: LintResult[]): Promise<Readable>;
|
|
20
|
+
export type { LintResult, LintResultEntry, LintConfig, ReportConfig, ExitCode, WorkerResult, SarifResultEntry } from '../types';
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// src/executor/index.ts
|
|
2
|
+
import { Readable } from "stream";
|
|
3
|
+
import { FileScanner } from "../services/file-scanner.js";
|
|
4
|
+
import { LintRunner } from "../services/lint-runner.js";
|
|
5
|
+
import { StyleFilePatterns, ComponentFilePatterns } from "../services/file-patterns.js";
|
|
6
|
+
import { ReportGenerator, CsvReportGenerator } from "../services/report-generator.js";
|
|
7
|
+
import { LINTER_CLI_VERSION } from "../services/config.resolver.js";
|
|
8
|
+
import { normalizeCliOptions } from "../utils/config-utils.js";
|
|
9
|
+
import { Logger } from "../utils/logger.js";
|
|
10
|
+
async function lint(config) {
|
|
11
|
+
try {
|
|
12
|
+
Logger.debug("Starting linting with Node API");
|
|
13
|
+
const normalizedConfig = normalizeCliOptions(config, {}, true);
|
|
14
|
+
const styleFiles = await FileScanner.scanFiles(normalizedConfig.directory, {
|
|
15
|
+
patterns: StyleFilePatterns,
|
|
16
|
+
batchSize: 100
|
|
17
|
+
});
|
|
18
|
+
const componentFiles = await FileScanner.scanFiles(normalizedConfig.directory, {
|
|
19
|
+
patterns: ComponentFilePatterns,
|
|
20
|
+
batchSize: 100
|
|
21
|
+
});
|
|
22
|
+
const lintOptions = {
|
|
23
|
+
fix: normalizedConfig.fix,
|
|
24
|
+
configPath: normalizedConfig.configStylelint
|
|
25
|
+
};
|
|
26
|
+
const styleResults = await LintRunner.runLinting(styleFiles, "style", {
|
|
27
|
+
...lintOptions,
|
|
28
|
+
configPath: normalizedConfig.configStylelint
|
|
29
|
+
});
|
|
30
|
+
const componentResults = await LintRunner.runLinting(componentFiles, "component", {
|
|
31
|
+
...lintOptions,
|
|
32
|
+
configPath: normalizedConfig.configEslint
|
|
33
|
+
});
|
|
34
|
+
const combinedResults = [...styleResults, ...componentResults];
|
|
35
|
+
return standardizeLintMessages(combinedResults);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
const errorMessage = `Linting failed: ${error.message}`;
|
|
38
|
+
Logger.error(errorMessage);
|
|
39
|
+
throw new Error(errorMessage);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function report(config, results) {
|
|
43
|
+
try {
|
|
44
|
+
Logger.debug("Starting report generation with Node API");
|
|
45
|
+
const normalizedConfig = normalizeCliOptions(config, {}, true);
|
|
46
|
+
const format = normalizedConfig.format || "sarif";
|
|
47
|
+
const lintResults = results || await lint({
|
|
48
|
+
directory: normalizedConfig.directory,
|
|
49
|
+
configStylelint: normalizedConfig.configStylelint,
|
|
50
|
+
configEslint: normalizedConfig.configEslint
|
|
51
|
+
});
|
|
52
|
+
switch (format) {
|
|
53
|
+
case "sarif":
|
|
54
|
+
return ReportGenerator.generateSarifReportStream(lintResults, {
|
|
55
|
+
toolName: "slds-linter",
|
|
56
|
+
toolVersion: LINTER_CLI_VERSION
|
|
57
|
+
});
|
|
58
|
+
case "csv":
|
|
59
|
+
const csvString = CsvReportGenerator.generateCsvString(lintResults);
|
|
60
|
+
const csvStream = new Readable();
|
|
61
|
+
csvStream.push(csvString);
|
|
62
|
+
csvStream.push(null);
|
|
63
|
+
return csvStream;
|
|
64
|
+
default:
|
|
65
|
+
const errorMessage = `Unsupported format: ${format}`;
|
|
66
|
+
Logger.error(errorMessage);
|
|
67
|
+
throw new Error(errorMessage);
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
const errorMessage = `Report generation failed: ${error.message}`;
|
|
71
|
+
Logger.error(errorMessage);
|
|
72
|
+
throw new Error(errorMessage);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function standardizeLintMessages(results) {
|
|
76
|
+
return results.map((result) => ({
|
|
77
|
+
...result,
|
|
78
|
+
errors: result.errors.map((entry) => {
|
|
79
|
+
let msgObj;
|
|
80
|
+
try {
|
|
81
|
+
msgObj = JSON.parse(entry.message);
|
|
82
|
+
if (typeof msgObj === "object" && "message" in msgObj) {
|
|
83
|
+
return { ...entry, ...msgObj };
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
}
|
|
87
|
+
return { ...entry, message: entry.message };
|
|
88
|
+
}),
|
|
89
|
+
warnings: result.warnings.map((entry) => {
|
|
90
|
+
let msgObj;
|
|
91
|
+
try {
|
|
92
|
+
msgObj = JSON.parse(entry.message);
|
|
93
|
+
if (typeof msgObj === "object" && "message" in msgObj) {
|
|
94
|
+
return { ...entry, ...msgObj };
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
}
|
|
98
|
+
return { ...entry, message: entry.message };
|
|
99
|
+
})
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
export {
|
|
103
|
+
lint,
|
|
104
|
+
report
|
|
105
|
+
};
|
package/build/index.d.ts
ADDED
package/build/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { registerLintCommand } from "./commands/lint.js";
|
|
6
|
+
import { registerReportCommand } from "./commands/report.js";
|
|
7
|
+
import { registerEmitCommand } from "./commands/emit.js";
|
|
8
|
+
import { Logger } from "./utils/logger.js";
|
|
9
|
+
import { validateNodeVersion } from "./utils/nodeVersionUtil.js";
|
|
10
|
+
validateNodeVersion();
|
|
11
|
+
process.on("unhandledRejection", (error) => {
|
|
12
|
+
Logger.error(`Unhandled rejection: ${error}`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
});
|
|
15
|
+
process.on("uncaughtException", (error) => {
|
|
16
|
+
Logger.error(`Uncaught exception: ${error}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
});
|
|
19
|
+
var program = new Command();
|
|
20
|
+
program.name("npx @salesforce-ux/slds-linter@latest").showHelpAfterError();
|
|
21
|
+
function registerVersion() {
|
|
22
|
+
program.description("SLDS Linter CLI tool for linting styles and components").version("0.2.2-alpha-2-alpha.0");
|
|
23
|
+
}
|
|
24
|
+
registerLintCommand(program);
|
|
25
|
+
registerReportCommand(program);
|
|
26
|
+
registerEmitCommand(program);
|
|
27
|
+
registerVersion();
|
|
28
|
+
program.configureHelp({
|
|
29
|
+
subcommandTerm: (cmd) => {
|
|
30
|
+
return cmd.name();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// src/services/__tests__/file-scanner.test.ts
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { FileScanner } from "../file-scanner.js";
|
|
4
|
+
import { StyleFilePatterns } from "../file-patterns.js";
|
|
5
|
+
import { mkdir, writeFile, rm } from "fs/promises";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
var __dirname = path.dirname(__filename);
|
|
9
|
+
describe("FileScanner", () => {
|
|
10
|
+
const testDir = path.join(__dirname, "fixtures");
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
await mkdir(testDir, { recursive: true });
|
|
13
|
+
await writeFile(
|
|
14
|
+
path.join(testDir, "test.css"),
|
|
15
|
+
"body { color: red; }"
|
|
16
|
+
);
|
|
17
|
+
await writeFile(
|
|
18
|
+
path.join(testDir, "test.scss"),
|
|
19
|
+
"$color: red;"
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
afterAll(async () => {
|
|
23
|
+
await rm(testDir, { recursive: true });
|
|
24
|
+
});
|
|
25
|
+
it("should scan and batch files correctly", async () => {
|
|
26
|
+
const options = {
|
|
27
|
+
patterns: StyleFilePatterns,
|
|
28
|
+
batchSize: 1
|
|
29
|
+
};
|
|
30
|
+
const batches = await FileScanner.scanFiles(testDir, options);
|
|
31
|
+
expect(batches).toHaveLength(2);
|
|
32
|
+
expect(batches[0]).toHaveLength(1);
|
|
33
|
+
expect(batches[1]).toHaveLength(1);
|
|
34
|
+
expect(batches[0][0]).toMatch(/test\.(css|scss)$/);
|
|
35
|
+
expect(batches[1][0]).toMatch(/test\.(css|scss)$/);
|
|
36
|
+
});
|
|
37
|
+
it("should handle invalid files gracefully", async () => {
|
|
38
|
+
const options = {
|
|
39
|
+
patterns: {
|
|
40
|
+
extensions: ["nonexistent"],
|
|
41
|
+
exclude: []
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const batches = await FileScanner.scanFiles(testDir, options);
|
|
45
|
+
expect(batches).toHaveLength(0);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// src/services/artifact-processor.ts
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import crypto from "crypto";
|
|
5
|
+
var BATCH_SIZE = 10;
|
|
6
|
+
async function processArtifacts(artifacts) {
|
|
7
|
+
const batches = Array.from(
|
|
8
|
+
{ length: Math.ceil(artifacts.length / BATCH_SIZE) },
|
|
9
|
+
(_, i) => artifacts.slice(i * BATCH_SIZE, (i + 1) * BATCH_SIZE)
|
|
10
|
+
);
|
|
11
|
+
for (const batch of batches) {
|
|
12
|
+
await Promise.all(
|
|
13
|
+
batch.map(async (artifact) => {
|
|
14
|
+
try {
|
|
15
|
+
artifact.sourceLanguage = "html";
|
|
16
|
+
if (!artifact.location?.uri) {
|
|
17
|
+
console.warn("Warning: Artifact missing location URI");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const filePath = path.join(process.cwd(), artifact.location.uri);
|
|
21
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
22
|
+
artifact.length = content.length;
|
|
23
|
+
const hash = crypto.createHash("sha256");
|
|
24
|
+
hash.update(content);
|
|
25
|
+
artifact.hashes = {
|
|
26
|
+
"sha-256": hash.digest("hex")
|
|
27
|
+
};
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.warn(`Warning: Could not process artifact ${artifact.location?.uri}: ${error.message}`);
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
processArtifacts
|
|
37
|
+
};
|