@salesforce-ux/slds-linter 0.1.9 → 0.2.0-alpha.2
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 +93 -3
- package/build/commands/emit.js +1 -1
- package/build/commands/lint.js +12 -38
- package/build/commands/report.js +26 -36
- package/build/executor/__tests__/executor.test.js +189 -0
- package/build/executor/index.d.ts +19 -0
- package/build/executor/index.js +78 -0
- package/build/index.js +1 -1
- package/build/services/artifact-processor.d.ts +6 -0
- package/build/services/artifact-processor.js +37 -0
- package/build/services/config.resolver.js +1 -1
- package/build/services/report-generator.d.ts +22 -2
- package/build/services/report-generator.js +70 -44
- package/build/types/index.d.ts +43 -19
- package/build/utils/config-utils.d.ts +33 -0
- package/build/utils/config-utils.js +68 -0
- package/build/utils/lintResultsUtil.d.ts +2 -1
- package/build/utils/lintResultsUtil.js +18 -3
- package/package.json +8 -6
- package/build/utils/cli-args.d.ts +0 -4
- package/build/utils/cli-args.js +0 -43
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
SLDS Linter provides custom linting rules built for Salesforce Lightning Design System 2 (SLDS 2 beta). Lint your components against SLDS 2 best practices to ensure they adhere to the latest styling standards.
|
|
6
6
|
|
|
7
|
-
SLDS Linter checks your Aura and Lightning web components
|
|
7
|
+
SLDS Linter checks your Aura and Lightning web components' CSS and markup files to identify styling issues that you can fix for SLDS 2 compatibility. SLDS Linter helps you maintain consistent styling and identify common issues with custom Lightning components.
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
@@ -78,7 +78,7 @@ In your project root directory, follow these steps.
|
|
|
78
78
|
|
|
79
79
|
### Troubleshoot SARIF Viewer Navigation
|
|
80
80
|
|
|
81
|
-
If the SARIF viewer doesn
|
|
81
|
+
If the SARIF viewer doesn't automatically go to the line of code when you click on an error or warning, follow these steps.
|
|
82
82
|
|
|
83
83
|
1. In the SARIF viewer pop-up window, click Locate.
|
|
84
84
|
2. In the file explorer or code editor, locate the file.
|
|
@@ -103,6 +103,7 @@ These options are available on SLDS Linter commands.
|
|
|
103
103
|
| `--config-stylelint <path>` | Path to stylelint config file | `lint`, `report`|
|
|
104
104
|
| `--config-eslint <path>` | Path to eslint config file | `lint`, `report`|
|
|
105
105
|
| `--editor <editor>` | Editor to open files with (e.g., vscode, atom, sublime). Defaults to vscode | `lint` |
|
|
106
|
+
| `--format <type>` | Output format (sarif, csv). Defaults to sarif | `report` |
|
|
106
107
|
|
|
107
108
|
To view help for these options, add `--help` to each command. For example, run `npx @salesforce-ux/slds-linter lint --help` to see which options you can use with `lint`.
|
|
108
109
|
|
|
@@ -167,4 +168,93 @@ Linting all `.html` and `.cmp` files:
|
|
|
167
168
|
npx @salesforce-ux/slds-linter lint "**/*.{html,cmp}"
|
|
168
169
|
```
|
|
169
170
|
|
|
170
|
-
For any questions or issues, open an issue in this repository.
|
|
171
|
+
For any questions or issues, open an issue in this repository.
|
|
172
|
+
|
|
173
|
+
## Node.js API
|
|
174
|
+
|
|
175
|
+
The SLDS Linter provides a programmatic API for Node.js applications. This allows you to integrate the linter directly into your build process or other tools without relying on the command-line interface.
|
|
176
|
+
|
|
177
|
+
### Installation
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
npm install @salesforce-ux/slds-linter
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Usage
|
|
184
|
+
|
|
185
|
+
```javascript
|
|
186
|
+
import { lint, report } from '@salesforce-ux/slds-linter/executor';
|
|
187
|
+
|
|
188
|
+
// Lint files in a directory
|
|
189
|
+
const results = await lint({
|
|
190
|
+
directory: './src',
|
|
191
|
+
fix: false // Set to true to auto-fix issues where possible
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
console.log(`Found ${results.length} files with issues`);
|
|
195
|
+
|
|
196
|
+
// Generate a report
|
|
197
|
+
const reportStream = await report({
|
|
198
|
+
directory: './src',
|
|
199
|
+
format: 'sarif'
|
|
200
|
+
}, results); // Pass the lint results as second parameter
|
|
201
|
+
|
|
202
|
+
// Process the report stream
|
|
203
|
+
let reportData = '';
|
|
204
|
+
reportStream.on('data', chunk => {
|
|
205
|
+
reportData += chunk;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
reportStream.on('end', () => {
|
|
209
|
+
console.log('Report:', reportData);
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### API Reference
|
|
214
|
+
|
|
215
|
+
The Node.js API provides the following methods:
|
|
216
|
+
|
|
217
|
+
#### `lint(options)`
|
|
218
|
+
|
|
219
|
+
Lints files for SLDS compliance.
|
|
220
|
+
|
|
221
|
+
**Options:**
|
|
222
|
+
- `directory`: Path to directory to scan for files to lint
|
|
223
|
+
- `fix`: Boolean indicating whether to automatically fix issues when possible
|
|
224
|
+
- `configStylelint`: Path to custom stylelint configuration file
|
|
225
|
+
- `configEslint`: Path to custom eslint configuration file
|
|
226
|
+
|
|
227
|
+
**Returns:** Promise resolving to an array of result objects containing linting issues
|
|
228
|
+
|
|
229
|
+
#### `report(options)`
|
|
230
|
+
|
|
231
|
+
Generates a report of linting issues.
|
|
232
|
+
|
|
233
|
+
**Options:**
|
|
234
|
+
- `directory`: Path to directory to scan (if `results` is not provided)
|
|
235
|
+
- `format`: Report format ('sarif' or 'csv')
|
|
236
|
+
- `configStylelint`: Path to custom stylelint configuration file (if `directory` is used)
|
|
237
|
+
- `configEslint`: Path to custom eslint configuration file (if `directory` is used)
|
|
238
|
+
|
|
239
|
+
**Parameters:**
|
|
240
|
+
- `options`: Configuration options as detailed above
|
|
241
|
+
- `results`: Optional array of lint results (from `lint()`). If not provided, will run lint on the specified directory
|
|
242
|
+
|
|
243
|
+
**Returns:** A readable stream containing the report data
|
|
244
|
+
|
|
245
|
+
### Utility Functions
|
|
246
|
+
|
|
247
|
+
The package also provides utility functions to help with configuration:
|
|
248
|
+
|
|
249
|
+
#### `normalizeConfig(options)`
|
|
250
|
+
|
|
251
|
+
Normalizes configuration options with default values. Import from `@salesforce-ux/slds-linter/utils/config-utils`.
|
|
252
|
+
|
|
253
|
+
**Options:** Same as the options for `lint()` or `report()`
|
|
254
|
+
|
|
255
|
+
**Returns:** A normalized configuration object with default values applied
|
|
256
|
+
|
|
257
|
+
### Examples
|
|
258
|
+
|
|
259
|
+
For complete examples, see the [examples directory](./examples/).
|
|
260
|
+
|
package/build/commands/emit.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/commands/emit.ts
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { Logger } from "../utils/logger.js";
|
|
4
|
-
import { normalizeCliOptions } from "../utils/
|
|
4
|
+
import { normalizeCliOptions } from "../utils/config-utils.js";
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_ESLINT_CONFIG_PATH,
|
|
7
7
|
DEFAULT_STYLELINT_CONFIG_PATH
|
package/build/commands/lint.js
CHANGED
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
import { Option } from "commander";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { printLintResults } from "../utils/lintResultsUtil.js";
|
|
5
|
-
import { normalizeCliOptions,
|
|
5
|
+
import { normalizeCliOptions, normalizeDirectoryPath } from "../utils/config-utils.js";
|
|
6
6
|
import { Logger } from "../utils/logger.js";
|
|
7
|
-
import { FileScanner } from "../services/file-scanner.js";
|
|
8
|
-
import { StyleFilePatterns, ComponentFilePatterns } from "../services/file-patterns.js";
|
|
9
|
-
import { LintRunner } from "../services/lint-runner.js";
|
|
10
7
|
import { DEFAULT_ESLINT_CONFIG_PATH, DEFAULT_STYLELINT_CONFIG_PATH } from "../services/config.resolver.js";
|
|
8
|
+
import { lint } from "../executor/index.js";
|
|
11
9
|
function registerLintCommand(program) {
|
|
12
10
|
program.command("lint").aliases(["lint:styles", "lint:components"]).configureHelp({
|
|
13
11
|
commandUsage: () => {
|
|
@@ -22,54 +20,30 @@ function registerLintCommand(program) {
|
|
|
22
20
|
configEslint: DEFAULT_ESLINT_CONFIG_PATH
|
|
23
21
|
});
|
|
24
22
|
if (directory) {
|
|
25
|
-
normalizedOptions.directory =
|
|
23
|
+
normalizedOptions.directory = normalizeDirectoryPath(directory);
|
|
26
24
|
} else if (options.directory) {
|
|
27
25
|
Logger.newLine().warning(chalk.yellow(
|
|
28
26
|
`WARNING: --directory, -d option is deprecated. Supply as argument instead.
|
|
29
27
|
Example: npx @salesforce-ux/slds-linter lint ${options.directory}`
|
|
30
28
|
));
|
|
31
29
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
patterns: StyleFilePatterns,
|
|
35
|
-
batchSize: 100
|
|
36
|
-
});
|
|
37
|
-
const totalStyleFiles = styleFileBatches.reduce((sum, batch) => sum + batch.length, 0);
|
|
38
|
-
Logger.info(chalk.blue(`Found ${totalStyleFiles} style file(s). Running stylelint...`));
|
|
39
|
-
Logger.newLine().info(chalk.blue(`Running stylelint${normalizedOptions.fix ? " with autofix" : ""}...`));
|
|
40
|
-
const styleResults = await LintRunner.runLinting(styleFileBatches, "style", {
|
|
41
|
-
fix: normalizedOptions.fix,
|
|
42
|
-
configPath: normalizedOptions.configStylelint
|
|
43
|
-
});
|
|
44
|
-
printLintResults(styleResults, normalizedOptions.editor);
|
|
45
|
-
const styleErrorCount = styleResults.reduce((sum, r) => sum + r.errors.length, 0);
|
|
46
|
-
const styleWarningCount = styleResults.reduce((sum, r) => sum + r.warnings.length, 0);
|
|
47
|
-
Logger.newLine().info(chalk.blue("Scanning component files..."));
|
|
48
|
-
const componentFileBatches = await FileScanner.scanFiles(normalizedOptions.directory, {
|
|
49
|
-
patterns: ComponentFilePatterns,
|
|
50
|
-
batchSize: 100
|
|
51
|
-
});
|
|
52
|
-
const totalComponentFiles = componentFileBatches.reduce((sum, batch) => sum + batch.length, 0);
|
|
53
|
-
Logger.info(chalk.blue(`Found ${totalComponentFiles} component file(s). Running eslint...
|
|
54
|
-
`));
|
|
55
|
-
Logger.info(chalk.blue(`Running linting${normalizedOptions.fix ? " with autofix" : ""}...`));
|
|
56
|
-
const componentResults = await LintRunner.runLinting(componentFileBatches, "component", {
|
|
30
|
+
const lintResults = await lint({
|
|
31
|
+
directory: normalizedOptions.directory,
|
|
57
32
|
fix: normalizedOptions.fix,
|
|
58
|
-
|
|
33
|
+
configStylelint: normalizedOptions.configStylelint,
|
|
34
|
+
configEslint: normalizedOptions.configEslint
|
|
59
35
|
});
|
|
60
|
-
printLintResults(
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const totalErrors = styleErrorCount + componentErrorCount;
|
|
64
|
-
const totalWarnings = styleWarningCount + componentWarningCount;
|
|
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);
|
|
65
39
|
Logger.info(
|
|
66
40
|
`
|
|
67
|
-
${chalk.red(`${
|
|
41
|
+
${chalk.red(`${errorCount} error${errorCount !== 1 ? "s" : ""}`)} ${chalk.yellow(`${warningCount} warning${warningCount !== 1 ? "s" : ""}`)}`
|
|
68
42
|
);
|
|
69
43
|
const elapsedTime = ((Date.now() - startTime) / 1e3).toFixed(2);
|
|
70
44
|
Logger.success(chalk.green(`
|
|
71
45
|
Linting completed in ${elapsedTime} seconds.`));
|
|
72
|
-
process.exit(
|
|
46
|
+
process.exit(errorCount > 0 ? 1 : 0);
|
|
73
47
|
} catch (error) {
|
|
74
48
|
Logger.error(chalk.red(`Failed to complete linting: ${error.message}`));
|
|
75
49
|
process.exit(1);
|
package/build/commands/report.js
CHANGED
|
@@ -3,13 +3,11 @@ import { Option } from "commander";
|
|
|
3
3
|
import path from "path";
|
|
4
4
|
import ora from "ora";
|
|
5
5
|
import chalk from "chalk";
|
|
6
|
-
import
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import { normalizeCliOptions, normalizeDirectoryPath } from "../utils/config-utils.js";
|
|
7
8
|
import { Logger } from "../utils/logger.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { LintRunner } from "../services/lint-runner.js";
|
|
11
|
-
import { ReportGenerator, CsvReportGenerator } from "../services/report-generator.js";
|
|
12
|
-
import { DEFAULT_ESLINT_CONFIG_PATH, DEFAULT_STYLELINT_CONFIG_PATH, LINTER_CLI_VERSION } from "../services/config.resolver.js";
|
|
9
|
+
import { DEFAULT_ESLINT_CONFIG_PATH, DEFAULT_STYLELINT_CONFIG_PATH } from "../services/config.resolver.js";
|
|
10
|
+
import { report, lint } from "../executor/index.js";
|
|
13
11
|
function registerReportCommand(program) {
|
|
14
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) => {
|
|
15
13
|
const spinner = ora("Starting report generation...");
|
|
@@ -19,7 +17,7 @@ function registerReportCommand(program) {
|
|
|
19
17
|
configEslint: DEFAULT_ESLINT_CONFIG_PATH
|
|
20
18
|
});
|
|
21
19
|
if (directory) {
|
|
22
|
-
normalizedOptions.directory =
|
|
20
|
+
normalizedOptions.directory = normalizeDirectoryPath(directory);
|
|
23
21
|
} else if (options.directory) {
|
|
24
22
|
Logger.newLine().warning(chalk.yellow(
|
|
25
23
|
`WARNING: --directory, -d option is deprecated. Supply as argument instead.
|
|
@@ -27,41 +25,33 @@ function registerReportCommand(program) {
|
|
|
27
25
|
));
|
|
28
26
|
}
|
|
29
27
|
spinner.start();
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
const reportFormat = normalizedOptions.format?.toLowerCase() || "sarif";
|
|
29
|
+
const lintResults = await lint({
|
|
30
|
+
directory: normalizedOptions.directory,
|
|
31
|
+
configStylelint: normalizedOptions.configStylelint,
|
|
32
|
+
configEslint: normalizedOptions.configEslint
|
|
34
33
|
});
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const componentFileBatches = await FileScanner.scanFiles(normalizedOptions.directory, {
|
|
40
|
-
patterns: ComponentFilePatterns,
|
|
41
|
-
batchSize: 100
|
|
42
|
-
});
|
|
43
|
-
const componentResults = await LintRunner.runLinting(componentFileBatches, "component", {
|
|
44
|
-
configPath: normalizedOptions.configEslint
|
|
45
|
-
});
|
|
46
|
-
const reportFormat = options.format.toLowerCase();
|
|
34
|
+
const reportStream = await report({
|
|
35
|
+
format: reportFormat
|
|
36
|
+
}, lintResults);
|
|
37
|
+
let outputFilePath;
|
|
47
38
|
if (reportFormat === "sarif") {
|
|
48
|
-
spinner.text = "
|
|
49
|
-
|
|
50
|
-
await ReportGenerator.generateSarifReport([...styleResults, ...componentResults], {
|
|
51
|
-
outputPath: combinedReportPath,
|
|
52
|
-
toolName: "slds-linter",
|
|
53
|
-
toolVersion: LINTER_CLI_VERSION
|
|
54
|
-
});
|
|
55
|
-
Logger.success(`SARIF report generated: ${combinedReportPath}
|
|
56
|
-
`);
|
|
39
|
+
spinner.text = "Saving SARIF report...";
|
|
40
|
+
outputFilePath = path.join(normalizedOptions.output, "slds-linter-report.sarif");
|
|
57
41
|
} else if (reportFormat === "csv") {
|
|
58
|
-
spinner.text = "
|
|
59
|
-
|
|
60
|
-
Logger.success(`CSV report generated: ${csvReportPath}
|
|
61
|
-
`);
|
|
42
|
+
spinner.text = "Saving CSV report...";
|
|
43
|
+
outputFilePath = path.join(normalizedOptions.output, "slds-linter-report.csv");
|
|
62
44
|
} else {
|
|
63
45
|
throw new Error(`Invalid format: ${reportFormat}. Supported formats: sarif, csv`);
|
|
64
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
|
+
`);
|
|
65
55
|
spinner.succeed("Report generation completed");
|
|
66
56
|
process.exit(0);
|
|
67
57
|
} catch (error) {
|
|
@@ -0,0 +1,189 @@
|
|
|
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 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
|
+
var init_executor = __esm({
|
|
102
|
+
"src/executor/index.ts"() {
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// src/executor/__tests__/executor.test.ts
|
|
107
|
+
import { lint as lint2, report as report2 } from "../index.js";
|
|
108
|
+
import { LintRunner as LintRunner2 } from "../../services/lint-runner.js";
|
|
109
|
+
import { FileScanner as FileScanner2 } from "../../services/file-scanner.js";
|
|
110
|
+
import { Readable as Readable2 } from "stream";
|
|
111
|
+
import { jest } from "@jest/globals.js";
|
|
112
|
+
var mockLintResult = {
|
|
113
|
+
filePath: "file1.css",
|
|
114
|
+
errors: [{ line: 1, column: 1, endColumn: 10, message: "Test error", ruleId: "test-rule", severity: 2 }],
|
|
115
|
+
warnings: []
|
|
116
|
+
};
|
|
117
|
+
jest.mock("../../services/lint-runner", () => {
|
|
118
|
+
return {
|
|
119
|
+
LintRunner: {
|
|
120
|
+
runLinting: jest.fn().mockImplementation(() => {
|
|
121
|
+
return Promise.resolve([mockLintResult]);
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
jest.mock("../../services/file-scanner", () => {
|
|
127
|
+
return {
|
|
128
|
+
FileScanner: {
|
|
129
|
+
scanFiles: jest.fn().mockImplementation(() => {
|
|
130
|
+
return Promise.resolve([["file1.css"]]);
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
jest.mock("fs/promises");
|
|
136
|
+
xdescribe("Executor functions", () => {
|
|
137
|
+
beforeEach(() => {
|
|
138
|
+
jest.clearAllMocks();
|
|
139
|
+
});
|
|
140
|
+
describe("lint", () => {
|
|
141
|
+
it("should scan directory and run linting when no files are provided", async () => {
|
|
142
|
+
const config = {
|
|
143
|
+
directory: "./src",
|
|
144
|
+
fix: true
|
|
145
|
+
};
|
|
146
|
+
const results = await lint2(config);
|
|
147
|
+
expect(FileScanner2.scanFiles).toHaveBeenCalledTimes(2);
|
|
148
|
+
expect(LintRunner2.runLinting).toHaveBeenCalledTimes(2);
|
|
149
|
+
expect(results).toHaveLength(2);
|
|
150
|
+
});
|
|
151
|
+
it("should use provided files and skip scanning when files are provided", async () => {
|
|
152
|
+
const config = {
|
|
153
|
+
files: ["file1.css", "component1.html"]
|
|
154
|
+
};
|
|
155
|
+
await lint2(config);
|
|
156
|
+
expect(FileScanner2.scanFiles).not.toHaveBeenCalled();
|
|
157
|
+
expect(LintRunner2.runLinting).toHaveBeenCalledTimes(2);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
describe("report", () => {
|
|
161
|
+
it("should return a readable stream", async () => {
|
|
162
|
+
const config = {
|
|
163
|
+
directory: "./src",
|
|
164
|
+
format: "sarif"
|
|
165
|
+
};
|
|
166
|
+
const stream = await report2(config);
|
|
167
|
+
expect(stream).toBeInstanceOf(Readable2);
|
|
168
|
+
});
|
|
169
|
+
it("should use lint results to generate a report", async () => {
|
|
170
|
+
const lintMock = jest.spyOn((init_executor(), __toCommonJS(executor_exports)), "lint").mockResolvedValue([mockLintResult]);
|
|
171
|
+
const config = {
|
|
172
|
+
directory: "./src",
|
|
173
|
+
format: "sarif"
|
|
174
|
+
};
|
|
175
|
+
await report2(config);
|
|
176
|
+
expect(lintMock).toHaveBeenCalledWith({
|
|
177
|
+
directory: "./src",
|
|
178
|
+
configStylelint: expect.any(String),
|
|
179
|
+
configEslint: expect.any(String)
|
|
180
|
+
});
|
|
181
|
+
lintMock.mockRestore();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
describe("Executor placeholder tests", () => {
|
|
186
|
+
it("should be implemented in the future", () => {
|
|
187
|
+
expect(true).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
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>;
|
|
@@ -0,0 +1,78 @@
|
|
|
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 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
|
+
export {
|
|
76
|
+
lint,
|
|
77
|
+
report
|
|
78
|
+
};
|
package/build/index.js
CHANGED
|
@@ -19,7 +19,7 @@ process.on("uncaughtException", (error) => {
|
|
|
19
19
|
var program = new Command();
|
|
20
20
|
program.name("npx @salesforce-ux/slds-linter@latest").showHelpAfterError();
|
|
21
21
|
function registerVersion() {
|
|
22
|
-
program.description("SLDS Linter CLI tool for linting styles and components").version("0.1
|
|
22
|
+
program.description("SLDS Linter CLI tool for linting styles and components").version("0.2.0-alpha.1");
|
|
23
23
|
}
|
|
24
24
|
registerLintCommand(program);
|
|
25
25
|
registerReportCommand(program);
|
|
@@ -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
|
+
};
|
|
@@ -5,7 +5,7 @@ var DEFAULT_ESLINT_CONFIG_PATH = resolvePath("@salesforce-ux/eslint-plugin-slds/
|
|
|
5
5
|
var DEFAULT_STYLELINT_CONFIG_PATH = resolvePath("@salesforce-ux/stylelint-plugin-slds/.stylelintrc.yml", import.meta);
|
|
6
6
|
var STYLELINT_VERSION = "16.14.1";
|
|
7
7
|
var ESLINT_VERSION = "8.57.1";
|
|
8
|
-
var LINTER_CLI_VERSION = "0.1
|
|
8
|
+
var LINTER_CLI_VERSION = "0.2.0-alpha.1";
|
|
9
9
|
var getRuleDescription = (ruleId) => {
|
|
10
10
|
const ruleIdWithoutNameSpace = `${ruleId}`.replace(/\@salesforce-ux\//, "");
|
|
11
11
|
return ruleMetadata(ruleIdWithoutNameSpace)?.ruleDesc || "--";
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import { LintResult } from '../types';
|
|
2
|
+
import { Readable } from 'stream';
|
|
2
3
|
export interface ReportOptions {
|
|
3
|
-
outputPath
|
|
4
|
+
outputPath?: string;
|
|
4
5
|
toolName: string;
|
|
5
6
|
toolVersion: string;
|
|
6
7
|
}
|
|
7
8
|
export declare class ReportGenerator {
|
|
8
9
|
/**
|
|
9
|
-
* Generate SARIF report from lint results
|
|
10
|
+
* Generate SARIF report from lint results with file output
|
|
10
11
|
*/
|
|
11
12
|
static generateSarifReport(results: LintResult[], options: ReportOptions): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Generate SARIF report as a stream without creating a file
|
|
15
|
+
*/
|
|
16
|
+
static generateSarifReportStream(results: LintResult[], options: ReportOptions): Promise<Readable>;
|
|
17
|
+
/**
|
|
18
|
+
* Build SARIF report data in memory
|
|
19
|
+
*/
|
|
20
|
+
static buildSarifReport(results: LintResult[], options: ReportOptions): Promise<any>;
|
|
12
21
|
/**
|
|
13
22
|
* Extract unique rules from results
|
|
14
23
|
*/
|
|
@@ -19,5 +28,16 @@ export declare class ReportGenerator {
|
|
|
19
28
|
private static addResultsToSarif;
|
|
20
29
|
}
|
|
21
30
|
export declare class CsvReportGenerator {
|
|
31
|
+
/**
|
|
32
|
+
* Generate CSV report and write to file
|
|
33
|
+
*/
|
|
22
34
|
static generate(results: any[]): Promise<string>;
|
|
35
|
+
/**
|
|
36
|
+
* Generate CSV string from lint results
|
|
37
|
+
*/
|
|
38
|
+
static generateCsvString(results: any[]): string;
|
|
39
|
+
/**
|
|
40
|
+
* Convert lint results to CSV-compatible data format
|
|
41
|
+
*/
|
|
42
|
+
private static convertResultsToCsvData;
|
|
23
43
|
}
|
|
@@ -6,18 +6,56 @@ import { SarifBuilder, SarifRunBuilder, SarifResultBuilder, SarifRuleBuilder } f
|
|
|
6
6
|
import { createWriteStream } from "fs";
|
|
7
7
|
import { JsonStreamStringify } from "json-stream-stringify";
|
|
8
8
|
import { getRuleDescription } from "./config.resolver.js";
|
|
9
|
-
import { parseText, replaceNamespaceinRules } from "../utils/lintResultsUtil.js";
|
|
9
|
+
import { parseText, replaceNamespaceinRules, transformedResults } from "../utils/lintResultsUtil.js";
|
|
10
|
+
import { processArtifacts } from "./artifact-processor.js";
|
|
10
11
|
var ReportGenerator = class {
|
|
11
12
|
/**
|
|
12
|
-
* Generate SARIF report from lint results
|
|
13
|
+
* Generate SARIF report from lint results with file output
|
|
13
14
|
*/
|
|
14
15
|
static async generateSarifReport(results, options) {
|
|
16
|
+
if (!options.outputPath) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const sarifReport = await this.buildSarifReport(results, options);
|
|
20
|
+
for (const run of sarifReport.runs) {
|
|
21
|
+
await processArtifacts(run.artifacts);
|
|
22
|
+
}
|
|
23
|
+
const outputDir = path.dirname(options.outputPath);
|
|
24
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
25
|
+
const writeStream = createWriteStream(options.outputPath);
|
|
26
|
+
const jsonStream = new JsonStreamStringify(sarifReport, null, 2);
|
|
27
|
+
await new Promise((resolve, reject) => {
|
|
28
|
+
jsonStream.pipe(writeStream).on("finish", resolve).on("error", reject);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Generate SARIF report as a stream without creating a file
|
|
33
|
+
*/
|
|
34
|
+
static async generateSarifReportStream(results, options) {
|
|
35
|
+
const sarifReport = await this.buildSarifReport(results, options);
|
|
36
|
+
return new JsonStreamStringify(sarifReport, null, 2);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Build SARIF report data in memory
|
|
40
|
+
*/
|
|
41
|
+
static async buildSarifReport(results, options) {
|
|
15
42
|
const builder = new SarifBuilder();
|
|
16
|
-
const runBuilder = new SarifRunBuilder(
|
|
43
|
+
const runBuilder = new SarifRunBuilder({
|
|
44
|
+
defaultSourceLanguage: "html"
|
|
45
|
+
}).initSimple({
|
|
17
46
|
toolDriverName: options.toolName,
|
|
18
47
|
toolDriverVersion: options.toolVersion,
|
|
19
48
|
url: "https://github.com/salesforce-ux/slds-linter"
|
|
20
49
|
});
|
|
50
|
+
runBuilder.run.properties = {
|
|
51
|
+
id: Number(Math.random().toString(10).substring(2, 10)),
|
|
52
|
+
version: options.toolVersion,
|
|
53
|
+
submissionDate: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
|
|
54
|
+
language: "html",
|
|
55
|
+
status: "accepted",
|
|
56
|
+
type: "source code"
|
|
57
|
+
};
|
|
58
|
+
runBuilder.run.tool.driver.organization = "Salesforce";
|
|
21
59
|
const rules = this.extractRules(results);
|
|
22
60
|
for (const rule of rules) {
|
|
23
61
|
const ruleBuilder = new SarifRuleBuilder().initSimple({
|
|
@@ -30,14 +68,7 @@ var ReportGenerator = class {
|
|
|
30
68
|
this.addResultsToSarif(runBuilder, result);
|
|
31
69
|
}
|
|
32
70
|
builder.addRun(runBuilder);
|
|
33
|
-
|
|
34
|
-
const outputDir = path.dirname(options.outputPath);
|
|
35
|
-
await fs.mkdir(outputDir, { recursive: true });
|
|
36
|
-
const writeStream = createWriteStream(options.outputPath);
|
|
37
|
-
const jsonStream = new JsonStreamStringify(sarifReport, null, 2);
|
|
38
|
-
await new Promise((resolve, reject) => {
|
|
39
|
-
jsonStream.pipe(writeStream).on("finish", resolve).on("error", reject);
|
|
40
|
-
});
|
|
71
|
+
return builder.buildSarifOutput();
|
|
41
72
|
}
|
|
42
73
|
/**
|
|
43
74
|
* Extract unique rules from results
|
|
@@ -78,38 +109,39 @@ var ReportGenerator = class {
|
|
|
78
109
|
* Add lint results to SARIF report
|
|
79
110
|
*/
|
|
80
111
|
static addResultsToSarif(runBuilder, lintResult) {
|
|
81
|
-
|
|
82
|
-
const resultBuilder = new SarifResultBuilder().initSimple(
|
|
83
|
-
ruleId: replaceNamespaceinRules(error.ruleId),
|
|
84
|
-
level: "error",
|
|
85
|
-
messageText: parseText(error.message),
|
|
86
|
-
fileUri: path.relative(process.cwd(), lintResult.filePath),
|
|
87
|
-
startLine: error.line,
|
|
88
|
-
startColumn: error.column,
|
|
89
|
-
endLine: error.line,
|
|
90
|
-
endColumn: error.endColumn
|
|
91
|
-
});
|
|
112
|
+
lintResult.errors.forEach((error) => {
|
|
113
|
+
const resultBuilder = new SarifResultBuilder().initSimple(transformedResults(lintResult, error, "error"));
|
|
92
114
|
runBuilder.addResult(resultBuilder);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const resultBuilder = new SarifResultBuilder().initSimple(
|
|
96
|
-
ruleId: replaceNamespaceinRules(warning.ruleId),
|
|
97
|
-
level: "warning",
|
|
98
|
-
messageText: parseText(warning.message),
|
|
99
|
-
fileUri: path.relative(process.cwd(), lintResult.filePath),
|
|
100
|
-
startLine: warning.line,
|
|
101
|
-
startColumn: warning.column,
|
|
102
|
-
endLine: warning.line,
|
|
103
|
-
endColumn: warning.endColumn
|
|
104
|
-
});
|
|
115
|
+
});
|
|
116
|
+
lintResult.warnings.forEach((warning) => {
|
|
117
|
+
const resultBuilder = new SarifResultBuilder().initSimple(transformedResults(lintResult, warning, "warning"));
|
|
105
118
|
runBuilder.addResult(resultBuilder);
|
|
106
|
-
}
|
|
119
|
+
});
|
|
107
120
|
}
|
|
108
121
|
};
|
|
109
122
|
var CsvReportGenerator = class {
|
|
123
|
+
/**
|
|
124
|
+
* Generate CSV report and write to file
|
|
125
|
+
*/
|
|
110
126
|
static async generate(results) {
|
|
127
|
+
const csvString = this.generateCsvString(results);
|
|
128
|
+
const csvReportPath = path.join(process.cwd(), "slds-linter-report.csv");
|
|
129
|
+
await writeFile(csvReportPath, csvString);
|
|
130
|
+
return csvReportPath;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Generate CSV string from lint results
|
|
134
|
+
*/
|
|
135
|
+
static generateCsvString(results) {
|
|
136
|
+
const csvData = this.convertResultsToCsvData(results);
|
|
137
|
+
return asString(csvData);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Convert lint results to CSV-compatible data format
|
|
141
|
+
*/
|
|
142
|
+
static convertResultsToCsvData(results) {
|
|
143
|
+
const cwd = process.cwd();
|
|
111
144
|
const csvConfig = mkConfig({
|
|
112
|
-
filename: "slds-linter-report",
|
|
113
145
|
fieldSeparator: ",",
|
|
114
146
|
quoteStrings: true,
|
|
115
147
|
decimalSeparator: ".",
|
|
@@ -117,8 +149,7 @@ var CsvReportGenerator = class {
|
|
|
117
149
|
useBom: true,
|
|
118
150
|
useKeysAsHeaders: true
|
|
119
151
|
});
|
|
120
|
-
const
|
|
121
|
-
const transformedResults = results.flatMap(
|
|
152
|
+
const transformedResults2 = results.flatMap(
|
|
122
153
|
(result) => [
|
|
123
154
|
...result.errors.map((error) => ({
|
|
124
155
|
"File Path": path.relative(cwd, result.filePath),
|
|
@@ -146,12 +177,7 @@ var CsvReportGenerator = class {
|
|
|
146
177
|
}))
|
|
147
178
|
]
|
|
148
179
|
);
|
|
149
|
-
|
|
150
|
-
const csvString = asString(csvData);
|
|
151
|
-
const csvReportPath = path.join(cwd, `${csvConfig.filename}.csv`);
|
|
152
|
-
return writeFile(csvReportPath, csvString).then(() => {
|
|
153
|
-
return csvReportPath;
|
|
154
|
-
});
|
|
180
|
+
return generateCsv(csvConfig)(transformedResults2);
|
|
155
181
|
}
|
|
156
182
|
};
|
|
157
183
|
export {
|
package/build/types/index.d.ts
CHANGED
|
@@ -1,30 +1,44 @@
|
|
|
1
|
-
export interface
|
|
1
|
+
export interface BaseConfig {
|
|
2
2
|
directory?: string;
|
|
3
|
-
|
|
4
|
-
fix?: boolean;
|
|
3
|
+
files?: string[];
|
|
5
4
|
configStylelint?: string;
|
|
6
5
|
configEslint?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* CLI options interface extends BaseConfig for shared properties
|
|
9
|
+
*/
|
|
10
|
+
export interface CliOptions extends BaseConfig {
|
|
11
|
+
output?: string;
|
|
12
|
+
fix?: boolean;
|
|
7
13
|
editor?: string;
|
|
8
14
|
format?: string;
|
|
9
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Configuration for linting operation in the Node API
|
|
18
|
+
* Extends the common base configuration
|
|
19
|
+
*/
|
|
20
|
+
export interface LintConfig extends BaseConfig {
|
|
21
|
+
fix?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Configuration for report generation in the Node API
|
|
25
|
+
* Extends the common base configuration
|
|
26
|
+
*/
|
|
27
|
+
export interface ReportConfig extends BaseConfig {
|
|
28
|
+
format?: 'sarif' | 'csv';
|
|
29
|
+
}
|
|
30
|
+
export interface LintResultEntry {
|
|
31
|
+
line: number;
|
|
32
|
+
column: number;
|
|
33
|
+
endColumn: number;
|
|
34
|
+
message: string;
|
|
35
|
+
ruleId: string;
|
|
36
|
+
severity: number;
|
|
37
|
+
}
|
|
10
38
|
export interface LintResult {
|
|
11
39
|
filePath: string;
|
|
12
|
-
errors: Array<
|
|
13
|
-
|
|
14
|
-
column: number;
|
|
15
|
-
endColumn: number;
|
|
16
|
-
message: string;
|
|
17
|
-
ruleId: string;
|
|
18
|
-
severity: number;
|
|
19
|
-
}>;
|
|
20
|
-
warnings: Array<{
|
|
21
|
-
line: number;
|
|
22
|
-
column: number;
|
|
23
|
-
endColumn: number;
|
|
24
|
-
message: string;
|
|
25
|
-
ruleId: string;
|
|
26
|
-
severity: number;
|
|
27
|
-
}>;
|
|
40
|
+
errors: Array<LintResultEntry>;
|
|
41
|
+
warnings: Array<LintResultEntry>;
|
|
28
42
|
}
|
|
29
43
|
export type ExitCode = 0 | 1 | 2;
|
|
30
44
|
export interface WorkerConfig {
|
|
@@ -49,3 +63,13 @@ export interface WorkerResult {
|
|
|
49
63
|
ruleId: string;
|
|
50
64
|
}>;
|
|
51
65
|
}
|
|
66
|
+
export interface SarifResultEntry {
|
|
67
|
+
level: any;
|
|
68
|
+
messageText: string;
|
|
69
|
+
ruleId: string;
|
|
70
|
+
fileUri?: string;
|
|
71
|
+
startLine?: number;
|
|
72
|
+
startColumn?: number;
|
|
73
|
+
endLine?: number;
|
|
74
|
+
endColumn?: number;
|
|
75
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { LintConfig, ReportConfig, CliOptions } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Normalize and validate a file path
|
|
4
|
+
* This function ensures that the provided path exists and is accessible
|
|
5
|
+
* If no path is provided, it defaults to the current working directory
|
|
6
|
+
*
|
|
7
|
+
* @param inputPath Input file path or undefined
|
|
8
|
+
* @returns Normalized and validated absolute path
|
|
9
|
+
* @throws Error if the path is invalid or inaccessible
|
|
10
|
+
*/
|
|
11
|
+
export declare function normalizeAndValidatePath(inputPath?: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Normalize directory path with special handling for glob patterns
|
|
14
|
+
* This function preserves glob patterns and resolves regular paths to absolute paths
|
|
15
|
+
* If no directory is provided, it defaults to the current working directory
|
|
16
|
+
*
|
|
17
|
+
* @param directory Input directory or glob pattern or undefined
|
|
18
|
+
* @returns Normalized directory path
|
|
19
|
+
*/
|
|
20
|
+
export declare function normalizeDirectoryPath(directory?: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Normalize configuration with appropriate defaults
|
|
23
|
+
* This function ensures all required options have valid values
|
|
24
|
+
* It applies provided defaults, then user-provided options, then normalizes paths
|
|
25
|
+
* Used by both CLI commands and Node API functions
|
|
26
|
+
*
|
|
27
|
+
* @param options Options to normalize
|
|
28
|
+
* @param defaultOptions Default options to apply
|
|
29
|
+
* @param isNodeApi Whether this is being called from the Node API
|
|
30
|
+
* @returns Normalized options with appropriate defaults
|
|
31
|
+
*/
|
|
32
|
+
export declare function normalizeCliOptions<T extends CliOptions | LintConfig | ReportConfig>(options: T, defaultOptions?: Partial<T>, isNodeApi?: boolean): T;
|
|
33
|
+
export declare const normalizeConfig: typeof normalizeCliOptions;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// src/utils/config-utils.ts
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { DEFAULT_ESLINT_CONFIG_PATH, DEFAULT_STYLELINT_CONFIG_PATH } from "../services/config.resolver.js";
|
|
4
|
+
import { isDynamicPattern } from "globby";
|
|
5
|
+
import { accessSync } from "fs";
|
|
6
|
+
import { Logger } from "./logger.js";
|
|
7
|
+
function normalizeAndValidatePath(inputPath) {
|
|
8
|
+
if (!inputPath) {
|
|
9
|
+
Logger.debug("No path provided, using current working directory");
|
|
10
|
+
return process.cwd();
|
|
11
|
+
}
|
|
12
|
+
const normalizedPath = path.resolve(inputPath);
|
|
13
|
+
Logger.debug(`Normalized path: ${normalizedPath}`);
|
|
14
|
+
try {
|
|
15
|
+
accessSync(normalizedPath);
|
|
16
|
+
return normalizedPath;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
const errorMessage = `Invalid path: ${inputPath}`;
|
|
19
|
+
Logger.error(errorMessage);
|
|
20
|
+
throw new Error(errorMessage);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function normalizeDirectoryPath(directory) {
|
|
24
|
+
if (!directory) {
|
|
25
|
+
Logger.debug("No directory provided, using current working directory");
|
|
26
|
+
return process.cwd();
|
|
27
|
+
}
|
|
28
|
+
if (isDynamicPattern(directory)) {
|
|
29
|
+
Logger.debug(`Detected glob pattern: ${directory}`);
|
|
30
|
+
return directory;
|
|
31
|
+
}
|
|
32
|
+
Logger.debug(`Normalizing directory path: ${directory}`);
|
|
33
|
+
return path.resolve(directory);
|
|
34
|
+
}
|
|
35
|
+
function normalizeCliOptions(options, defaultOptions = {}, isNodeApi = false) {
|
|
36
|
+
const baseDefaults = {
|
|
37
|
+
files: [],
|
|
38
|
+
configStylelint: isNodeApi ? DEFAULT_STYLELINT_CONFIG_PATH : "",
|
|
39
|
+
configEslint: isNodeApi ? DEFAULT_ESLINT_CONFIG_PATH : ""
|
|
40
|
+
};
|
|
41
|
+
if (!isNodeApi) {
|
|
42
|
+
Object.assign(baseDefaults, {
|
|
43
|
+
fix: false,
|
|
44
|
+
editor: "vscode",
|
|
45
|
+
format: "sarif"
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const normalizedOptions = {
|
|
49
|
+
...baseDefaults,
|
|
50
|
+
...defaultOptions,
|
|
51
|
+
...options,
|
|
52
|
+
directory: normalizeDirectoryPath(options.directory)
|
|
53
|
+
};
|
|
54
|
+
if (!isNodeApi) {
|
|
55
|
+
normalizedOptions.output = options.output ? normalizeAndValidatePath(options.output) : process.cwd();
|
|
56
|
+
}
|
|
57
|
+
if ("format" in options && !options.format && isNodeApi) {
|
|
58
|
+
normalizedOptions.format = "sarif";
|
|
59
|
+
}
|
|
60
|
+
return normalizedOptions;
|
|
61
|
+
}
|
|
62
|
+
var normalizeConfig = normalizeCliOptions;
|
|
63
|
+
export {
|
|
64
|
+
normalizeAndValidatePath,
|
|
65
|
+
normalizeCliOptions,
|
|
66
|
+
normalizeConfig,
|
|
67
|
+
normalizeDirectoryPath
|
|
68
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LintResult } from '../types';
|
|
1
|
+
import { LintResult, LintResultEntry, SarifResultEntry } from '../types';
|
|
2
2
|
/**
|
|
3
3
|
*
|
|
4
4
|
* @param id - Rule id
|
|
@@ -18,3 +18,4 @@ export declare function parseText(text: string): string;
|
|
|
18
18
|
* @param editor - The chosen editor for clickable links (e.g., "vscode", "atom", "sublime").
|
|
19
19
|
*/
|
|
20
20
|
export declare function printLintResults(results: LintResult[], editor: string): void;
|
|
21
|
+
export declare function transformedResults(lintResult: LintResult, entry: LintResultEntry, level: 'error' | 'warning'): SarifResultEntry;
|
|
@@ -7,12 +7,14 @@ function replaceNamespaceinRules(id) {
|
|
|
7
7
|
return id.includes("@salesforce-ux/") ? id.replace("@salesforce-ux/", "") : id;
|
|
8
8
|
}
|
|
9
9
|
function parseText(text) {
|
|
10
|
+
let safeText = text;
|
|
10
11
|
try {
|
|
11
12
|
const parsed = JSON.parse(text);
|
|
12
|
-
|
|
13
|
+
safeText = parsed.message || JSON.stringify(parsed);
|
|
13
14
|
} catch (error) {
|
|
14
|
-
|
|
15
|
+
safeText = text;
|
|
15
16
|
}
|
|
17
|
+
return safeText.endsWith(".") ? safeText : `${safeText}.`;
|
|
16
18
|
}
|
|
17
19
|
function printLintResults(results, editor) {
|
|
18
20
|
results.forEach((result) => {
|
|
@@ -48,8 +50,21 @@ function printLintResults(results, editor) {
|
|
|
48
50
|
}
|
|
49
51
|
});
|
|
50
52
|
}
|
|
53
|
+
function transformedResults(lintResult, entry, level) {
|
|
54
|
+
return {
|
|
55
|
+
ruleId: replaceNamespaceinRules(entry.ruleId),
|
|
56
|
+
level,
|
|
57
|
+
messageText: parseText(entry.message),
|
|
58
|
+
fileUri: path.relative(process.cwd(), lintResult.filePath),
|
|
59
|
+
startLine: entry.line,
|
|
60
|
+
startColumn: entry.column,
|
|
61
|
+
endLine: entry.line,
|
|
62
|
+
endColumn: entry.endColumn
|
|
63
|
+
};
|
|
64
|
+
}
|
|
51
65
|
export {
|
|
52
66
|
parseText,
|
|
53
67
|
printLintResults,
|
|
54
|
-
replaceNamespaceinRules
|
|
68
|
+
replaceNamespaceinRules,
|
|
69
|
+
transformedResults
|
|
55
70
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce-ux/slds-linter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-alpha.2",
|
|
4
4
|
"description": "SLDS Linter CLI tool for linting styles and components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lightning design system linter",
|
|
@@ -12,16 +12,18 @@
|
|
|
12
12
|
"author": "UXF Tooling Team",
|
|
13
13
|
"type": "module",
|
|
14
14
|
"main": "build/index.js",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./build/index.js",
|
|
17
|
+
"./executor": "./build/executor/index.js"
|
|
18
|
+
},
|
|
15
19
|
"files": [
|
|
16
20
|
"build/**",
|
|
17
21
|
"README.md"
|
|
18
22
|
],
|
|
19
|
-
"bin":
|
|
20
|
-
"slds-linter": "./build/index.js"
|
|
21
|
-
},
|
|
23
|
+
"bin": "./build/index.js",
|
|
22
24
|
"dependencies": {
|
|
23
|
-
"@salesforce-ux/eslint-plugin-slds": "0.
|
|
24
|
-
"@salesforce-ux/stylelint-plugin-slds": "0.
|
|
25
|
+
"@salesforce-ux/eslint-plugin-slds": "0.2.0-alpha.2",
|
|
26
|
+
"@salesforce-ux/stylelint-plugin-slds": "0.2.0-alpha.2",
|
|
25
27
|
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
|
26
28
|
"@typescript-eslint/parser": "^5.0.0",
|
|
27
29
|
"chalk": "^4.1.2",
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import { CliOptions } from "../types";
|
|
2
|
-
export declare function nomalizeAndValidatePath(inputPath?: string): string;
|
|
3
|
-
export declare function nomalizeDirPath(inputPath?: string): string;
|
|
4
|
-
export declare function normalizeCliOptions(options: CliOptions, defultOptions?: Partial<CliOptions>): Required<CliOptions>;
|
package/build/utils/cli-args.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
// src/utils/cli-args.ts
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { accessSync } from "fs";
|
|
4
|
-
import { isDynamicPattern } from "globby";
|
|
5
|
-
function nomalizeAndValidatePath(inputPath) {
|
|
6
|
-
if (!inputPath) {
|
|
7
|
-
return process.cwd();
|
|
8
|
-
}
|
|
9
|
-
const normalizedPath = path.resolve(inputPath);
|
|
10
|
-
try {
|
|
11
|
-
accessSync(normalizedPath);
|
|
12
|
-
return normalizedPath;
|
|
13
|
-
} catch (error) {
|
|
14
|
-
throw new Error(`Invalid path: ${inputPath}`);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
function nomalizeDirPath(inputPath) {
|
|
18
|
-
if (!inputPath) {
|
|
19
|
-
return process.cwd();
|
|
20
|
-
}
|
|
21
|
-
if (isDynamicPattern(inputPath)) {
|
|
22
|
-
return inputPath;
|
|
23
|
-
}
|
|
24
|
-
return nomalizeAndValidatePath(inputPath);
|
|
25
|
-
}
|
|
26
|
-
function normalizeCliOptions(options, defultOptions = {}) {
|
|
27
|
-
return {
|
|
28
|
-
fix: false,
|
|
29
|
-
editor: "vscode",
|
|
30
|
-
configStylelint: "",
|
|
31
|
-
configEslint: "",
|
|
32
|
-
format: "sarif",
|
|
33
|
-
...defultOptions,
|
|
34
|
-
...options,
|
|
35
|
-
directory: nomalizeDirPath(options.directory),
|
|
36
|
-
output: nomalizeAndValidatePath(options.output)
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
export {
|
|
40
|
-
nomalizeAndValidatePath,
|
|
41
|
-
nomalizeDirPath,
|
|
42
|
-
normalizeCliOptions
|
|
43
|
-
};
|