@lightdash/cli 0.2192.0 → 0.2193.1
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/.tsbuildinfo +1 -1
- package/dist/ajv.d.ts.map +1 -1
- package/dist/ajv.js +6 -1
- package/dist/handlers/lint/ajvToSarif.d.ts +66 -0
- package/dist/handlers/lint/ajvToSarif.d.ts.map +1 -0
- package/dist/handlers/lint/ajvToSarif.js +222 -0
- package/dist/handlers/lint/sarifFormatter.d.ts +14 -0
- package/dist/handlers/lint/sarifFormatter.d.ts.map +1 -0
- package/dist/handlers/lint/sarifFormatter.js +111 -0
- package/dist/handlers/lint.d.ts +8 -0
- package/dist/handlers/lint.d.ts.map +1 -0
- package/dist/handlers/lint.js +250 -0
- package/dist/index.js +16 -0
- package/package.json +4 -3
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.lintHandler = lintHandler;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const common_1 = require("@lightdash/common");
|
|
6
|
+
const chalk_1 = tslib_1.__importDefault(require("chalk"));
|
|
7
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
8
|
+
const path = tslib_1.__importStar(require("path"));
|
|
9
|
+
const YAML = tslib_1.__importStar(require("yaml"));
|
|
10
|
+
const ajv_1 = require("../ajv");
|
|
11
|
+
const ajvToSarif_1 = require("./lint/ajvToSarif");
|
|
12
|
+
const sarifFormatter_1 = require("./lint/sarifFormatter");
|
|
13
|
+
const validateChartSchema = ajv_1.ajv.compile(common_1.chartAsCodeSchema);
|
|
14
|
+
const validateDashboardSchema = ajv_1.ajv.compile(common_1.dashboardAsCodeSchema);
|
|
15
|
+
/**
|
|
16
|
+
* Find all YAML and JSON files in a path (file or directory).
|
|
17
|
+
* If a file path is provided, returns it if it's a .yml/.yaml/.json file.
|
|
18
|
+
* If a directory path is provided, recursively searches for all such files.
|
|
19
|
+
*/
|
|
20
|
+
function findLightdashCodeFiles(inputPath) {
|
|
21
|
+
const files = [];
|
|
22
|
+
// Check if the path is a file or directory
|
|
23
|
+
const stats = fs.statSync(inputPath);
|
|
24
|
+
if (stats.isFile()) {
|
|
25
|
+
// Single file case - check if it's a valid extension
|
|
26
|
+
const isYaml = inputPath.endsWith('.yml') || inputPath.endsWith('.yaml');
|
|
27
|
+
const isJson = inputPath.endsWith('.json');
|
|
28
|
+
if (isYaml || isJson) {
|
|
29
|
+
files.push(inputPath);
|
|
30
|
+
}
|
|
31
|
+
return files;
|
|
32
|
+
}
|
|
33
|
+
// Directory case - walk recursively
|
|
34
|
+
function walk(currentPath) {
|
|
35
|
+
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
38
|
+
if (entry.isDirectory()) {
|
|
39
|
+
// Skip node_modules, .git, etc.
|
|
40
|
+
if (!entry.name.startsWith('.') &&
|
|
41
|
+
entry.name !== 'node_modules' &&
|
|
42
|
+
entry.name !== 'target') {
|
|
43
|
+
walk(fullPath);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else if (entry.isFile()) {
|
|
47
|
+
const isYaml = entry.name.endsWith('.yml') || entry.name.endsWith('.yaml');
|
|
48
|
+
const isJson = entry.name.endsWith('.json');
|
|
49
|
+
if (isYaml || isJson) {
|
|
50
|
+
files.push(fullPath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
walk(inputPath);
|
|
56
|
+
return files;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Build a map of JSON paths to their line/column positions in the source YAML/JSON file.
|
|
60
|
+
*
|
|
61
|
+
* This creates a Map<string, {line, column}> by traversing the YAML Abstract Syntax Tree (AST).
|
|
62
|
+
* For each YAML node encountered, we store its location keyed by its JSON path (e.g., '/metricQuery/filters').
|
|
63
|
+
*
|
|
64
|
+
* IMPORTANT: The map stores locations for ACTUAL YAML KEYS that exist in the file.
|
|
65
|
+
* It does NOT contain entries for:
|
|
66
|
+
* - Root path '/' (there's no root key in YAML)
|
|
67
|
+
* - Missing required properties that don't exist in the file
|
|
68
|
+
*
|
|
69
|
+
* @param fileContent - The raw YAML or JSON file content
|
|
70
|
+
* @param isJson - Whether the file is JSON (true) or YAML (false)
|
|
71
|
+
* @returns Object containing parsed data and the location map
|
|
72
|
+
*/
|
|
73
|
+
function buildLocationMap(fileContent, isJson) {
|
|
74
|
+
const locationMap = new Map();
|
|
75
|
+
if (isJson) {
|
|
76
|
+
// For JSON, parse normally (location map not populated - could be enhanced later)
|
|
77
|
+
const data = JSON.parse(fileContent);
|
|
78
|
+
return { data, locationMap };
|
|
79
|
+
}
|
|
80
|
+
// Parse YAML with the 'yaml' package to access the Abstract Syntax Tree (AST)
|
|
81
|
+
const doc = YAML.parseDocument(fileContent);
|
|
82
|
+
function traverse(node, jsonPath) {
|
|
83
|
+
if (!node)
|
|
84
|
+
return;
|
|
85
|
+
// Store location for this node
|
|
86
|
+
if (node.range) {
|
|
87
|
+
const [start] = node.range;
|
|
88
|
+
const lines = fileContent.substring(0, start).split('\n');
|
|
89
|
+
const line = lines.length;
|
|
90
|
+
const column = lines[lines.length - 1].length + 1;
|
|
91
|
+
locationMap.set(jsonPath, { line, column });
|
|
92
|
+
}
|
|
93
|
+
if (YAML.isMap(node)) {
|
|
94
|
+
for (const pair of node.items) {
|
|
95
|
+
if (YAML.isScalar(pair.key)) {
|
|
96
|
+
const key = String(pair.key.value);
|
|
97
|
+
const childPath = jsonPath
|
|
98
|
+
? `${jsonPath}/${key}`
|
|
99
|
+
: `/${key}`;
|
|
100
|
+
traverse(pair.value, childPath);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else if (YAML.isSeq(node)) {
|
|
105
|
+
for (let i = 0; i < node.items.length; i += 1) {
|
|
106
|
+
const childPath = `${jsonPath}/${i}`;
|
|
107
|
+
traverse(node.items[i], childPath);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
traverse(doc.contents, '');
|
|
112
|
+
// Convert to plain JS object for validation
|
|
113
|
+
const data = doc.toJS();
|
|
114
|
+
return { data, locationMap };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Validate a single YAML or JSON file
|
|
118
|
+
*/
|
|
119
|
+
function validateFile(filePath) {
|
|
120
|
+
try {
|
|
121
|
+
// Read and parse file
|
|
122
|
+
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
123
|
+
const isJson = filePath.endsWith('.json');
|
|
124
|
+
const { data, locationMap } = buildLocationMap(fileContent, isJson);
|
|
125
|
+
if (!data || typeof data !== 'object') {
|
|
126
|
+
return {
|
|
127
|
+
filePath,
|
|
128
|
+
valid: false,
|
|
129
|
+
// Skip non-object files as they're not lightdash code
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const dataObj = data;
|
|
133
|
+
// Check if this is a chart (has version and metricQuery)
|
|
134
|
+
if (dataObj.version === 1 && dataObj.metricQuery && !dataObj.tiles) {
|
|
135
|
+
const valid = validateChartSchema(data);
|
|
136
|
+
if (!valid && validateChartSchema.errors) {
|
|
137
|
+
return {
|
|
138
|
+
filePath,
|
|
139
|
+
valid: false,
|
|
140
|
+
errors: validateChartSchema.errors,
|
|
141
|
+
fileContent,
|
|
142
|
+
locationMap,
|
|
143
|
+
type: 'chart',
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return { filePath, valid: true, type: 'chart' };
|
|
147
|
+
}
|
|
148
|
+
// Check if this is a dashboard (has version and tiles)
|
|
149
|
+
if (dataObj.version === 1 && dataObj.tiles) {
|
|
150
|
+
const valid = validateDashboardSchema(data);
|
|
151
|
+
if (!valid && validateDashboardSchema.errors) {
|
|
152
|
+
return {
|
|
153
|
+
filePath,
|
|
154
|
+
valid: false,
|
|
155
|
+
errors: validateDashboardSchema.errors,
|
|
156
|
+
fileContent,
|
|
157
|
+
locationMap,
|
|
158
|
+
type: 'dashboard',
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return { filePath, valid: true, type: 'dashboard' };
|
|
162
|
+
}
|
|
163
|
+
// Not a lightdash code file
|
|
164
|
+
return {
|
|
165
|
+
filePath,
|
|
166
|
+
valid: true, // Don't report non-lightdash files as errors
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
// Parsing error - skip this file
|
|
171
|
+
return {
|
|
172
|
+
filePath,
|
|
173
|
+
valid: false,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function lintHandler(options) {
|
|
178
|
+
const searchPath = path.resolve(options.path || process.cwd());
|
|
179
|
+
const outputFormat = options.format || 'cli';
|
|
180
|
+
// Check if path exists
|
|
181
|
+
if (!fs.existsSync(searchPath)) {
|
|
182
|
+
throw new Error(`Path does not exist: ${searchPath}`);
|
|
183
|
+
}
|
|
184
|
+
if (outputFormat === 'cli') {
|
|
185
|
+
console.log(chalk_1.default.dim(`Searching for Lightdash Code files in: ${searchPath}\n`));
|
|
186
|
+
}
|
|
187
|
+
// Find all YAML/JSON files
|
|
188
|
+
const codeFiles = findLightdashCodeFiles(searchPath);
|
|
189
|
+
if (codeFiles.length === 0) {
|
|
190
|
+
if (outputFormat === 'cli') {
|
|
191
|
+
console.log(chalk_1.default.yellow('No YAML/JSON files found in the specified path.'));
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (options.verbose && outputFormat === 'cli') {
|
|
196
|
+
console.log(chalk_1.default.dim(`Found ${codeFiles.length} YAML/JSON files\n`));
|
|
197
|
+
}
|
|
198
|
+
// Validate each file
|
|
199
|
+
const results = [];
|
|
200
|
+
for (const file of codeFiles) {
|
|
201
|
+
const result = validateFile(file);
|
|
202
|
+
// Only track Lightdash Code files (views, explores, charts, dashboards)
|
|
203
|
+
if (result.type) {
|
|
204
|
+
results.push(result);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (results.length === 0) {
|
|
208
|
+
if (outputFormat === 'cli') {
|
|
209
|
+
console.log(chalk_1.default.yellow('No Lightdash Code files found.'));
|
|
210
|
+
console.log(chalk_1.default.dim('Charts must have version: 1 + metricQuery, dashboards must have version: 1 + tiles'));
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// Convert to SARIF format
|
|
215
|
+
const invalidResults = results.filter((r) => !r.valid);
|
|
216
|
+
const validCount = results.length - invalidResults.length;
|
|
217
|
+
// Build SARIF report from invalid results
|
|
218
|
+
const sarifResults = invalidResults
|
|
219
|
+
.filter((r) => r.errors && r.fileContent && r.type)
|
|
220
|
+
.map((r) => ({
|
|
221
|
+
filePath: r.filePath,
|
|
222
|
+
errors: r.errors,
|
|
223
|
+
fileContent: r.fileContent,
|
|
224
|
+
locationMap: r.locationMap,
|
|
225
|
+
schemaType: r.type,
|
|
226
|
+
}));
|
|
227
|
+
const sarifLog = (0, ajvToSarif_1.createSarifReport)(sarifResults);
|
|
228
|
+
// Output based on format
|
|
229
|
+
if (outputFormat === 'json') {
|
|
230
|
+
console.log(JSON.stringify(sarifLog, null, 2));
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
// CLI format
|
|
234
|
+
const summary = (0, sarifFormatter_1.getSarifSummary)(sarifLog);
|
|
235
|
+
if (!summary.hasErrors) {
|
|
236
|
+
console.log(chalk_1.default.green('\n✓ All Lightdash Code files are valid!\n'));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
// Show summary
|
|
240
|
+
console.log(chalk_1.default.bold(`\nValidated ${results.length} Lightdash Code files:`));
|
|
241
|
+
console.log(chalk_1.default.green(` ✓ ${validCount} valid`));
|
|
242
|
+
console.log(chalk_1.default.red(` ✗ ${summary.totalFiles} invalid`));
|
|
243
|
+
// Show formatted errors (starts with newline, so we don't need extra spacing)
|
|
244
|
+
console.log((0, sarifFormatter_1.formatSarifForCli)(sarifLog, searchPath));
|
|
245
|
+
}
|
|
246
|
+
// Exit with error if there were validation failures
|
|
247
|
+
if (invalidResults.length > 0) {
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ const diagnostics_1 = require("./handlers/diagnostics");
|
|
|
14
14
|
const download_1 = require("./handlers/download");
|
|
15
15
|
const generate_1 = require("./handlers/generate");
|
|
16
16
|
const generateExposures_1 = require("./handlers/generateExposures");
|
|
17
|
+
const lint_1 = require("./handlers/lint");
|
|
17
18
|
const login_1 = require("./handlers/login");
|
|
18
19
|
const preview_1 = require("./handlers/preview");
|
|
19
20
|
const renameHandler_1 = require("./handlers/renameHandler");
|
|
@@ -373,6 +374,21 @@ ${styles.bold('Examples:')}
|
|
|
373
374
|
.option('--defer', 'dbt property. Resolve unselected nodes by deferring to the manifest within the --state directory.', undefined)
|
|
374
375
|
.option('--no-defer', 'dbt property. Do not resolve unselected nodes by deferring to the manifest within the --state directory.', undefined)
|
|
375
376
|
.action(diagnostics_1.diagnosticsHandler);
|
|
377
|
+
commander_1.program
|
|
378
|
+
.command('lint')
|
|
379
|
+
.description('Validates Lightdash Code files (charts, dashboards) against JSON schemas')
|
|
380
|
+
.addHelpText('after', `
|
|
381
|
+
${styles.bold('Examples:')}
|
|
382
|
+
${styles.title('⚡')}️lightdash ${styles.bold('lint')} ${styles.secondary('-- validates all Lightdash Code files in current directory')}
|
|
383
|
+
${styles.title('⚡')}️lightdash ${styles.bold('lint')} --path ./chart.yml ${styles.secondary('-- validates a single chart file')}
|
|
384
|
+
${styles.title('⚡')}️lightdash ${styles.bold('lint')} --path ./lightdash ${styles.secondary('-- validates files in a specific directory')}
|
|
385
|
+
${styles.title('⚡')}️lightdash ${styles.bold('lint')} --verbose ${styles.secondary('-- shows detailed validation output')}
|
|
386
|
+
${styles.title('⚡')}️lightdash ${styles.bold('lint')} --format json ${styles.secondary('-- outputs results in SARIF JSON format')}
|
|
387
|
+
`)
|
|
388
|
+
.option('-p, --path <path>', 'Path to a file or directory to lint (defaults to current directory)', undefined)
|
|
389
|
+
.option('--verbose', 'Show detailed output', false)
|
|
390
|
+
.option('-f, --format <format>', 'Output format: cli (default) or json (SARIF format)', 'cli')
|
|
391
|
+
.action(lint_1.lintHandler);
|
|
376
392
|
const errorHandler = (err) => {
|
|
377
393
|
// Use error message with fallback for safety
|
|
378
394
|
const errorMessage = (0, common_1.getErrorMessage)(err) || 'An unexpected error occurred';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightdash/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2193.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"bin": {
|
|
6
6
|
"lightdash": "dist/index.js"
|
|
@@ -33,8 +33,9 @@
|
|
|
33
33
|
"parse-node-version": "^2.0.0",
|
|
34
34
|
"unique-names-generator": "^4.7.1",
|
|
35
35
|
"uuid": "^11.0.3",
|
|
36
|
-
"
|
|
37
|
-
"@lightdash/
|
|
36
|
+
"yaml": "^2.7.0",
|
|
37
|
+
"@lightdash/warehouses": "0.2193.1",
|
|
38
|
+
"@lightdash/common": "0.2193.1"
|
|
38
39
|
},
|
|
39
40
|
"description": "Lightdash CLI tool",
|
|
40
41
|
"devDependencies": {
|