correctly 0.0.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/AGENTS.md +8 -0
- package/LICENSE +373 -0
- package/README.md +24 -0
- package/dist/cli.mjs +294 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/core.d.mts +183 -0
- package/dist/core.d.mts.map +1 -0
- package/dist/core.mjs +2 -0
- package/dist/engine-BgWBnb1A.mjs +746 -0
- package/dist/engine-BgWBnb1A.mjs.map +1 -0
- package/dist/lsp.mjs +315 -0
- package/dist/lsp.mjs.map +1 -0
- package/docs/guide.md +114 -0
- package/examples/basic/correctly.config.json +10 -0
- package/examples/basic/project.json +1 -0
- package/examples/basic/project.jsonc +5 -0
- package/examples/basic/project.schema.json +10 -0
- package/package.json +61 -0
- package/src/cli/check.ts +83 -0
- package/src/cli/main.ts +86 -0
- package/src/cli/readable.ts +295 -0
- package/src/cli/vsix.ts +86 -0
- package/src/core/config.schema.json +47 -0
- package/src/core/config.ts +200 -0
- package/src/core/diagnostics.ts +114 -0
- package/src/core/engine.ts +247 -0
- package/src/core/index.ts +10 -0
- package/src/core/parse.ts +168 -0
- package/src/core/schemas.ts +379 -0
- package/src/core/types.ts +56 -0
- package/src/lsp/hints.ts +82 -0
- package/src/lsp/server.ts +265 -0
- package/src/lsp/workspace.ts +132 -0
- package/src/vscode/README.md +7 -0
- package/src/vscode/extension.ts +33 -0
- package/src/vscode/package.json +43 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "name": "Correctly", "color": "blue" }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"required": ["name"],
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"properties": {
|
|
7
|
+
"name": { "type": "string", "description": "The project name" },
|
|
8
|
+
"color": { "enum": ["red", "blue"], "description": "A primary color" }
|
|
9
|
+
}
|
|
10
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "correctly",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Shared JSON Schema validation for CI and editors",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/jeremybanka/correctly.git",
|
|
8
|
+
"directory": "packages/correctly"
|
|
9
|
+
},
|
|
10
|
+
"license": "MPL-2.0",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/core.d.mts",
|
|
15
|
+
"import": "./dist/core.mjs"
|
|
16
|
+
},
|
|
17
|
+
"./config.schema.json": "./src/core/config.schema.json"
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"correctly": "./dist/cli.mjs",
|
|
21
|
+
"correctly-lsp": "./dist/lsp.mjs"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"src",
|
|
26
|
+
"docs",
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"README.md",
|
|
29
|
+
"AGENTS.md",
|
|
30
|
+
"examples"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "vp pack",
|
|
34
|
+
"build:vsix": "node src/cli/vsix.ts",
|
|
35
|
+
"test": "vp test",
|
|
36
|
+
"test:public": "vp test tests/public",
|
|
37
|
+
"test:private": "vp test tests/private",
|
|
38
|
+
"test:watch": "vp test watch"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"ajv": "8.20.0",
|
|
42
|
+
"ajv-formats": "3.0.1",
|
|
43
|
+
"jsonc-parser": "3.3.1",
|
|
44
|
+
"picomatch": "4.0.7",
|
|
45
|
+
"tinyglobby": "0.2.17",
|
|
46
|
+
"vscode-json-languageservice": "5.7.2",
|
|
47
|
+
"vscode-languageserver": "10.1.2",
|
|
48
|
+
"vscode-languageserver-textdocument": "1.0.15"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "26.6.3",
|
|
52
|
+
"@types/picomatch": "4.0.3",
|
|
53
|
+
"@types/vscode": "1.138.0",
|
|
54
|
+
"@vscode/vsce": "4.0.0",
|
|
55
|
+
"rolldown": "1.2.11",
|
|
56
|
+
"vscode-languageclient": "10.1.2"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=22.18.0"
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/cli/check.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { glob } from "tinyglobby"
|
|
4
|
+
import {
|
|
5
|
+
discoverConfig,
|
|
6
|
+
exclusions,
|
|
7
|
+
isIncluded,
|
|
8
|
+
loadProject,
|
|
9
|
+
} from "../core/config.ts"
|
|
10
|
+
import { Engine } from "../core/engine.ts"
|
|
11
|
+
import { failure, type Report } from "../core/types.ts"
|
|
12
|
+
|
|
13
|
+
export type CheckOptions = {
|
|
14
|
+
cwd?: string
|
|
15
|
+
config?: string
|
|
16
|
+
offline?: boolean
|
|
17
|
+
files?: string[]
|
|
18
|
+
onRead?: (file: string, text: string) => void
|
|
19
|
+
}
|
|
20
|
+
export async function check(options: CheckOptions = {}): Promise<Report> {
|
|
21
|
+
const report: Report = {
|
|
22
|
+
reportVersion: 1,
|
|
23
|
+
config: null,
|
|
24
|
+
files: [],
|
|
25
|
+
failures: [],
|
|
26
|
+
summary: {
|
|
27
|
+
checked: 0,
|
|
28
|
+
schemaCovered: 0,
|
|
29
|
+
syntaxOnly: 0,
|
|
30
|
+
invalid: 0,
|
|
31
|
+
failures: 0,
|
|
32
|
+
},
|
|
33
|
+
exitCode: 0,
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const cwd = path.resolve(options.cwd ?? process.cwd())
|
|
37
|
+
const configPath = options.config
|
|
38
|
+
? path.resolve(cwd, options.config)
|
|
39
|
+
: await discoverConfig(cwd)
|
|
40
|
+
report.config = configPath
|
|
41
|
+
const project = await loadProject(configPath)
|
|
42
|
+
const engine = new Engine(
|
|
43
|
+
project,
|
|
44
|
+
options.offline === undefined ? {} : { offline: options.offline },
|
|
45
|
+
)
|
|
46
|
+
await engine.prepare()
|
|
47
|
+
const files = options.files?.length
|
|
48
|
+
? options.files.map((f) => path.resolve(cwd, f))
|
|
49
|
+
: await glob(project.config.files ?? ["**/*.json", "**/*.jsonc"], {
|
|
50
|
+
cwd: project.root,
|
|
51
|
+
ignore: exclusions(project),
|
|
52
|
+
absolute: true,
|
|
53
|
+
dot: true,
|
|
54
|
+
onlyFiles: true,
|
|
55
|
+
})
|
|
56
|
+
for (const file of [...new Set(files)].sort()) {
|
|
57
|
+
try {
|
|
58
|
+
const included = isIncluded(project, file)
|
|
59
|
+
const text = included ? await readFile(file, "utf8") : ""
|
|
60
|
+
if (included) options.onRead?.(file, text)
|
|
61
|
+
report.files.push(await engine.validate(file, text))
|
|
62
|
+
} catch (error) {
|
|
63
|
+
report.failures.push(failure(error, file))
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
report.failures.push(failure(error))
|
|
68
|
+
}
|
|
69
|
+
const checked = report.files.filter((f) => f.coverage !== "excluded")
|
|
70
|
+
report.summary = {
|
|
71
|
+
checked: checked.length,
|
|
72
|
+
schemaCovered: checked.filter((f) => f.coverage === "schema").length,
|
|
73
|
+
syntaxOnly: checked.filter((f) => f.coverage === "syntax-only").length,
|
|
74
|
+
invalid: checked.filter((f) => f.diagnostics.length > 0).length,
|
|
75
|
+
failures:
|
|
76
|
+
report.failures.length +
|
|
77
|
+
report.files.reduce((n, f) => n + f.failures.length, 0),
|
|
78
|
+
}
|
|
79
|
+
report.exitCode = report.summary.failures ? 2 : report.summary.invalid ? 1 : 0
|
|
80
|
+
return report
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { readableReport } from "./readable.ts"
|
package/src/cli/main.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "node:util"
|
|
3
|
+
import { check, readableReport } from "./check.ts"
|
|
4
|
+
import { failure, type Report } from "../core/types.ts"
|
|
5
|
+
|
|
6
|
+
const HELP = `correctly check [files...] [--config path] [--offline] [--format readable|json]
|
|
7
|
+
correctly-lsp --stdio
|
|
8
|
+
|
|
9
|
+
Validate JSON/JSONC using associations in correctly.config.json.
|
|
10
|
+
Exit 0: valid; 1: document errors; 2: configuration, schema, or execution errors.
|
|
11
|
+
Formatting does not affect validation. See the package guide for configuration.
|
|
12
|
+
`
|
|
13
|
+
|
|
14
|
+
export async function run(
|
|
15
|
+
args: string[],
|
|
16
|
+
cwd = process.cwd(),
|
|
17
|
+
): Promise<{ output: string; exitCode: number }> {
|
|
18
|
+
let json =
|
|
19
|
+
args.includes("--format=json") ||
|
|
20
|
+
args.some((arg, i) => arg === "--format" && args[i + 1] === "json")
|
|
21
|
+
try {
|
|
22
|
+
const { values, positionals } = parseArgs({
|
|
23
|
+
args,
|
|
24
|
+
allowPositionals: true,
|
|
25
|
+
strict: true,
|
|
26
|
+
options: {
|
|
27
|
+
config: { type: "string", short: "c" },
|
|
28
|
+
offline: { type: "boolean" },
|
|
29
|
+
format: { type: "string", default: "readable" },
|
|
30
|
+
help: { type: "boolean", short: "h" },
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
json = values.format === "json"
|
|
34
|
+
if (values.help || positionals.length === 0)
|
|
35
|
+
return { output: HELP, exitCode: 0 }
|
|
36
|
+
if (positionals[0] !== "check")
|
|
37
|
+
throw new Error(`Unknown command: ${positionals[0]}`)
|
|
38
|
+
if (values.format !== "json" && values.format !== "readable")
|
|
39
|
+
throw new Error(`Unknown report format: ${values.format}`)
|
|
40
|
+
const sources = new Map<string, string>()
|
|
41
|
+
const report = await check({
|
|
42
|
+
cwd,
|
|
43
|
+
files: positionals.slice(1),
|
|
44
|
+
...(values.config ? { config: values.config } : {}),
|
|
45
|
+
...(values.offline === undefined ? {} : { offline: values.offline }),
|
|
46
|
+
...(json
|
|
47
|
+
? {}
|
|
48
|
+
: {
|
|
49
|
+
onRead: (file: string, text: string) => {
|
|
50
|
+
sources.set(file, text)
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
})
|
|
54
|
+
return {
|
|
55
|
+
output: json
|
|
56
|
+
? JSON.stringify(report, null, 2)
|
|
57
|
+
: readableReport(report, { cwd, sources }),
|
|
58
|
+
exitCode: report.exitCode,
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
const report: Report = {
|
|
62
|
+
reportVersion: 1,
|
|
63
|
+
config: null,
|
|
64
|
+
files: [],
|
|
65
|
+
failures: [failure(error)],
|
|
66
|
+
summary: {
|
|
67
|
+
checked: 0,
|
|
68
|
+
schemaCovered: 0,
|
|
69
|
+
syntaxOnly: 0,
|
|
70
|
+
invalid: 0,
|
|
71
|
+
failures: 1,
|
|
72
|
+
},
|
|
73
|
+
exitCode: 2,
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
output: json
|
|
77
|
+
? JSON.stringify(report, null, 2)
|
|
78
|
+
: readableReport(report, { cwd }),
|
|
79
|
+
exitCode: 2,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const result = await run(process.argv.slice(2))
|
|
85
|
+
process.stdout.write(`${result.output}\n`)
|
|
86
|
+
process.exitCode = result.exitCode
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { fileURLToPath } from "node:url"
|
|
3
|
+
import { styleText } from "node:util"
|
|
4
|
+
import type { Diagnostic, Failure, FileResult, Report } from "../core/types.ts"
|
|
5
|
+
import { diagnosticViews, type DiagnosticView } from "../core/diagnostics.ts"
|
|
6
|
+
|
|
7
|
+
export type ReadableOptions = {
|
|
8
|
+
cwd?: string
|
|
9
|
+
sources?: ReadonlyMap<string, string>
|
|
10
|
+
color?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Node's automatic color detection respects the output stream and NO_COLOR,
|
|
14
|
+
// NODE_DISABLE_COLORS, and FORCE_COLOR. Use the same palette as Lasertag.
|
|
15
|
+
function createStyler(color?: boolean) {
|
|
16
|
+
const apply = (format: Parameters<typeof styleText>[0], text: string) =>
|
|
17
|
+
color === false
|
|
18
|
+
? text
|
|
19
|
+
: styleText(format, text, {
|
|
20
|
+
stream: process.stdout,
|
|
21
|
+
...(color === true ? { validateStream: false } : {}),
|
|
22
|
+
})
|
|
23
|
+
return {
|
|
24
|
+
bold: (text: string) => apply("bold", text),
|
|
25
|
+
bad: (text: string) => apply(["bold", "red"], text),
|
|
26
|
+
caret: (text: string) => apply(["bold", "yellow"], text),
|
|
27
|
+
code: (text: string) => apply("cyan", text),
|
|
28
|
+
dim: (text: string) => apply("dim", text),
|
|
29
|
+
success: (text: string) => apply(["bold", "green"], text),
|
|
30
|
+
warning: (text: string) => apply(["bold", "yellow"], text),
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
type Styler = ReturnType<typeof createStyler>
|
|
34
|
+
|
|
35
|
+
function displayPath(cwd: string, file: string): string {
|
|
36
|
+
const relative = path.relative(cwd, file)
|
|
37
|
+
return relative &&
|
|
38
|
+
relative !== ".." &&
|
|
39
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
40
|
+
!path.isAbsolute(relative)
|
|
41
|
+
? relative
|
|
42
|
+
: file
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function displaySchema(cwd: string, uri: string): string {
|
|
46
|
+
const url = new URL(uri)
|
|
47
|
+
if (url.protocol !== "file:") return uri
|
|
48
|
+
return `${displayPath(cwd, fileURLToPath(url))}${url.hash}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function countLabel(count: number, label: string): string {
|
|
52
|
+
return `${count} ${label}${count === 1 ? "" : "s"}`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function lastSelectedLine(diagnostic: Diagnostic): number {
|
|
56
|
+
const { start, end } = diagnostic.range
|
|
57
|
+
return Math.max(
|
|
58
|
+
start.line,
|
|
59
|
+
end.line - (end.line > start.line && end.character === 0 ? 1 : 0),
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Like Lasertag, keep one context line on either side and collapse long ranges
|
|
64
|
+
// to their first/last lines. Tabs are expanded equally in source and carets.
|
|
65
|
+
function excerpt(
|
|
66
|
+
source: string[],
|
|
67
|
+
diagnostic: Diagnostic,
|
|
68
|
+
diagnosedLines: ReadonlySet<number>,
|
|
69
|
+
styler: Styler,
|
|
70
|
+
) {
|
|
71
|
+
const start = diagnostic.range.start.line
|
|
72
|
+
const end = Math.min(lastSelectedLine(diagnostic), source.length - 1)
|
|
73
|
+
if (start < 0 || start >= source.length) return undefined
|
|
74
|
+
const visible: (number | undefined)[] = []
|
|
75
|
+
if (start > 0 && !diagnosedLines.has(start - 1)) visible.push(start - 1)
|
|
76
|
+
visible.push(start)
|
|
77
|
+
if (end > start + 1) visible.push(undefined)
|
|
78
|
+
if (end > start) visible.push(end)
|
|
79
|
+
if (
|
|
80
|
+
end + 1 < source.length &&
|
|
81
|
+
!diagnosedLines.has(end + 1) &&
|
|
82
|
+
(end + 1 < source.length - 1 || source[end + 1] !== "")
|
|
83
|
+
)
|
|
84
|
+
visible.push(end + 1)
|
|
85
|
+
const width = Math.max(
|
|
86
|
+
...visible.map((line) =>
|
|
87
|
+
line === undefined ? 1 : String(line + 1).length,
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
const output: string[] = []
|
|
91
|
+
for (const line of visible) {
|
|
92
|
+
if (line === undefined) {
|
|
93
|
+
output.push(styler.dim(`${" ".repeat(width)} │ …`))
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
const text = source[line]!
|
|
97
|
+
const selected = line >= start && line <= end
|
|
98
|
+
const expanded = text.replaceAll("\t", " ").trimEnd()
|
|
99
|
+
output.push(
|
|
100
|
+
`${styler.dim(`${String(line + 1).padStart(width)} │`)} ${selected ? expanded : styler.dim(expanded)}`,
|
|
101
|
+
)
|
|
102
|
+
if (!selected) continue
|
|
103
|
+
const from = line === start ? diagnostic.range.start.character : 0
|
|
104
|
+
const to =
|
|
105
|
+
line === diagnostic.range.end.line
|
|
106
|
+
? diagnostic.range.end.character
|
|
107
|
+
: text.length
|
|
108
|
+
const caretStart = text.slice(0, from).replaceAll("\t", " ").length
|
|
109
|
+
const caretWidth = Math.max(
|
|
110
|
+
1,
|
|
111
|
+
text.slice(from, to).replaceAll("\t", " ").length,
|
|
112
|
+
)
|
|
113
|
+
output.push(
|
|
114
|
+
`${styler.dim(`${" ".repeat(width)} │`)} ${" ".repeat(caretStart)}${styler.caret("^".repeat(caretWidth))}`,
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
return { lines: output, width }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
type FileGroup = { file: string; result?: FileResult; failures: Failure[] }
|
|
121
|
+
|
|
122
|
+
function renderDiagnostic(
|
|
123
|
+
issue: DiagnosticView,
|
|
124
|
+
prefix: string,
|
|
125
|
+
last: boolean,
|
|
126
|
+
source: string[] | undefined,
|
|
127
|
+
diagnosedLines: ReadonlySet<number>,
|
|
128
|
+
styler: Styler,
|
|
129
|
+
shownRegions = new Set<string>(),
|
|
130
|
+
): string[] {
|
|
131
|
+
const branch = last ? "└─" : "├─"
|
|
132
|
+
const continuation = `${prefix}${last ? " " : "│ "}`
|
|
133
|
+
const output = [
|
|
134
|
+
`${styler.dim(`${prefix}${branch}`)} ${styler.dim(`${issue.range.start.line + 1}:${issue.range.start.character + 1}`)} ${styler.code(issue.code)}`,
|
|
135
|
+
]
|
|
136
|
+
const region = source
|
|
137
|
+
? excerpt(source, issue, diagnosedLines, styler)
|
|
138
|
+
: undefined
|
|
139
|
+
const regionKey = `${issue.offset}:${issue.length}`
|
|
140
|
+
if (region && !shownRegions.has(regionKey))
|
|
141
|
+
output.push(
|
|
142
|
+
...region.lines.map((line) => `${styler.dim(continuation)}${line}`),
|
|
143
|
+
)
|
|
144
|
+
shownRegions.add(regionKey)
|
|
145
|
+
const indentation = `${continuation}${" ".repeat(region ? region.width + 1 : 0)}`
|
|
146
|
+
output.push(
|
|
147
|
+
`${styler.dim(continuation)}${" ".repeat(region ? region.width + 1 : 0)}${styler.dim("╰─")} ${issue.message}`,
|
|
148
|
+
)
|
|
149
|
+
for (const [index, branch] of issue.branches.entries()) {
|
|
150
|
+
const finalBranch = index === issue.branches.length - 1
|
|
151
|
+
output.push(
|
|
152
|
+
`${styler.dim(`${indentation}${finalBranch ? "└─" : "├─"}`)} ${styler.bold(branch.label)}`,
|
|
153
|
+
)
|
|
154
|
+
const children = branch.diagnostics.toSorted((a, b) => a.offset - b.offset)
|
|
155
|
+
for (const [childIndex, child] of children.entries())
|
|
156
|
+
output.push(
|
|
157
|
+
...renderDiagnostic(
|
|
158
|
+
child,
|
|
159
|
+
`${indentation}${finalBranch ? " " : "│ "}`,
|
|
160
|
+
childIndex === children.length - 1,
|
|
161
|
+
source,
|
|
162
|
+
diagnosedLines,
|
|
163
|
+
styler,
|
|
164
|
+
shownRegions,
|
|
165
|
+
),
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
return output
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function fileSection(
|
|
172
|
+
group: FileGroup,
|
|
173
|
+
options: ReadableOptions,
|
|
174
|
+
styler: Styler,
|
|
175
|
+
): string {
|
|
176
|
+
const cwd = options.cwd ?? process.cwd()
|
|
177
|
+
const diagnostics = diagnosticViews(group.result?.diagnostics ?? []).toSorted(
|
|
178
|
+
(a, b) => a.offset - b.offset,
|
|
179
|
+
)
|
|
180
|
+
const counts = [
|
|
181
|
+
...(diagnostics.length ? [countLabel(diagnostics.length, "error")] : []),
|
|
182
|
+
...(group.failures.length
|
|
183
|
+
? [countLabel(group.failures.length, "failure")]
|
|
184
|
+
: []),
|
|
185
|
+
]
|
|
186
|
+
const status =
|
|
187
|
+
counts.join(", ") ||
|
|
188
|
+
(group.result?.coverage === "excluded" ? "excluded" : "valid")
|
|
189
|
+
const statusColor = counts.length
|
|
190
|
+
? styler.bad
|
|
191
|
+
: group.result?.coverage === "excluded"
|
|
192
|
+
? styler.dim
|
|
193
|
+
: styler.success
|
|
194
|
+
let header = `${styler.bold(displayPath(cwd, group.file))} ${statusColor(status)}`
|
|
195
|
+
if (group.result) {
|
|
196
|
+
const { mode, coverage, association } = group.result
|
|
197
|
+
const format = mode.toUpperCase()
|
|
198
|
+
const schema = association?.schema
|
|
199
|
+
const description =
|
|
200
|
+
coverage === "excluded"
|
|
201
|
+
? format
|
|
202
|
+
: schema
|
|
203
|
+
? `${format} against ${association.name} (${displaySchema(cwd, schema)})`
|
|
204
|
+
: `${format} syntax only (${association ? `association: ${association.name}` : "no schema association"})`
|
|
205
|
+
header += ` ${styler.dim(`· ${description}`)}`
|
|
206
|
+
}
|
|
207
|
+
const output = [header]
|
|
208
|
+
const text = options.sources?.get(group.file)
|
|
209
|
+
const source = text === undefined ? undefined : text.split(/\r\n|\r|\n/)
|
|
210
|
+
const diagnosedLines = new Set<number>()
|
|
211
|
+
if (source)
|
|
212
|
+
for (const diagnostic of group.result?.diagnostics ?? []) {
|
|
213
|
+
for (
|
|
214
|
+
let line = diagnostic.range.start.line;
|
|
215
|
+
line <= Math.min(lastSelectedLine(diagnostic), source.length - 1);
|
|
216
|
+
line++
|
|
217
|
+
)
|
|
218
|
+
diagnosedLines.add(line)
|
|
219
|
+
}
|
|
220
|
+
const issues: (DiagnosticView | Failure)[] = [
|
|
221
|
+
...diagnostics,
|
|
222
|
+
...group.failures,
|
|
223
|
+
]
|
|
224
|
+
for (const [index, issue] of issues.entries()) {
|
|
225
|
+
const last = index === issues.length - 1
|
|
226
|
+
const branch = last ? "└─" : "├─"
|
|
227
|
+
if ("range" in issue) {
|
|
228
|
+
output.push(
|
|
229
|
+
...renderDiagnostic(issue, "", last, source, diagnosedLines, styler),
|
|
230
|
+
)
|
|
231
|
+
} else
|
|
232
|
+
output.push(
|
|
233
|
+
`${styler.dim(branch)} ${styler.bad(issue.code)}: ${issue.message}`,
|
|
234
|
+
)
|
|
235
|
+
if (!last) output.push(styler.dim("│"))
|
|
236
|
+
}
|
|
237
|
+
return output.join("\n")
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function readableReport(
|
|
241
|
+
report: Report,
|
|
242
|
+
options: ReadableOptions = {},
|
|
243
|
+
): string {
|
|
244
|
+
const styler = createStyler(options.color)
|
|
245
|
+
const groups = new Map<string, FileGroup>()
|
|
246
|
+
for (const result of report.files)
|
|
247
|
+
groups.set(result.file, {
|
|
248
|
+
file: result.file,
|
|
249
|
+
result,
|
|
250
|
+
failures: [...result.failures],
|
|
251
|
+
})
|
|
252
|
+
const globalFailures: Failure[] = []
|
|
253
|
+
for (const error of report.failures) {
|
|
254
|
+
if (!error.file) {
|
|
255
|
+
globalFailures.push(error)
|
|
256
|
+
continue
|
|
257
|
+
}
|
|
258
|
+
let group = groups.get(error.file)
|
|
259
|
+
if (!group) {
|
|
260
|
+
group = { file: error.file, failures: [] }
|
|
261
|
+
groups.set(error.file, group)
|
|
262
|
+
}
|
|
263
|
+
group.failures.push(error)
|
|
264
|
+
}
|
|
265
|
+
const output = [...groups.values()]
|
|
266
|
+
.toSorted((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0))
|
|
267
|
+
.map((group) => fileSection(group, options, styler))
|
|
268
|
+
for (const error of globalFailures)
|
|
269
|
+
output.push(`${styler.bad(error.code)}: ${error.message}`)
|
|
270
|
+
const errors = report.files.reduce(
|
|
271
|
+
(count, file) => count + diagnosticViews(file.diagnostics).length,
|
|
272
|
+
0,
|
|
273
|
+
)
|
|
274
|
+
const s = report.summary
|
|
275
|
+
const status =
|
|
276
|
+
report.exitCode === 2
|
|
277
|
+
? `▲ Check failed with ${countLabel(s.failures, "failure")}`
|
|
278
|
+
: report.exitCode === 1
|
|
279
|
+
? `▲ Check found ${countLabel(errors, "error")} in ${countLabel(s.invalid, "file")}`
|
|
280
|
+
: "✓ Check passed"
|
|
281
|
+
output.push(
|
|
282
|
+
[
|
|
283
|
+
styler.dim("─".repeat(56)),
|
|
284
|
+
"",
|
|
285
|
+
(report.exitCode === 2
|
|
286
|
+
? styler.bad
|
|
287
|
+
: report.exitCode === 1
|
|
288
|
+
? styler.warning
|
|
289
|
+
: styler.success)(status),
|
|
290
|
+
"",
|
|
291
|
+
`${s.checked} checked, ${s.schemaCovered} schema-covered, ${s.syntaxOnly} syntax-only, ${s.invalid} invalid, ${s.failures} failures`,
|
|
292
|
+
].join("\n"),
|
|
293
|
+
)
|
|
294
|
+
return output.join("\n\n")
|
|
295
|
+
}
|
package/src/cli/vsix.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { spawn } from "node:child_process"
|
|
2
|
+
import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises"
|
|
3
|
+
import { createRequire } from "node:module"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
6
|
+
import { rolldown } from "rolldown"
|
|
7
|
+
|
|
8
|
+
export async function buildVsix(
|
|
9
|
+
packageRoot: string,
|
|
10
|
+
outdir: string,
|
|
11
|
+
): Promise<string> {
|
|
12
|
+
const buildRoot = path.join(outdir, ".correctly-vsix")
|
|
13
|
+
await rm(buildRoot, { force: true, recursive: true })
|
|
14
|
+
await mkdir(path.join(buildRoot, "dist"), { recursive: true })
|
|
15
|
+
for (const [entry, outfile] of [
|
|
16
|
+
["vscode/extension", "extension"],
|
|
17
|
+
["lsp/server", "server"],
|
|
18
|
+
]) {
|
|
19
|
+
const bundle = await rolldown({
|
|
20
|
+
input: path.join(packageRoot, `src/${entry}.ts`),
|
|
21
|
+
platform: "node",
|
|
22
|
+
resolve: { mainFields: ["module", "main"] },
|
|
23
|
+
external: ["vscode"],
|
|
24
|
+
onLog: (level, log, handler) =>
|
|
25
|
+
handler(log.code === "UNRESOLVED_IMPORT" ? "error" : level, log),
|
|
26
|
+
})
|
|
27
|
+
try {
|
|
28
|
+
await bundle.write({
|
|
29
|
+
file: path.join(buildRoot, `dist/${outfile}.mjs`),
|
|
30
|
+
format: "esm",
|
|
31
|
+
sourcemap: true,
|
|
32
|
+
})
|
|
33
|
+
} finally {
|
|
34
|
+
await bundle.close()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const pkg = JSON.parse(
|
|
38
|
+
await readFile(path.join(packageRoot, "package.json"), "utf8"),
|
|
39
|
+
) as { version: string }
|
|
40
|
+
const manifest = JSON.parse(
|
|
41
|
+
await readFile(path.join(packageRoot, "src/vscode/package.json"), "utf8"),
|
|
42
|
+
) as { version: string }
|
|
43
|
+
manifest.version = pkg.version
|
|
44
|
+
await writeFile(
|
|
45
|
+
path.join(buildRoot, "package.json"),
|
|
46
|
+
`${JSON.stringify(manifest, null, 2)}\n`,
|
|
47
|
+
)
|
|
48
|
+
await cp(path.join(packageRoot, "LICENSE"), path.join(buildRoot, "LICENSE"))
|
|
49
|
+
await cp(
|
|
50
|
+
path.join(packageRoot, "src/vscode/README.md"),
|
|
51
|
+
path.join(buildRoot, "README.md"),
|
|
52
|
+
)
|
|
53
|
+
const require = createRequire(import.meta.url)
|
|
54
|
+
const vsceRoot = path.dirname(require.resolve("@vscode/vsce/package.json"))
|
|
55
|
+
const destination = path.join(outdir, `Correctly-${pkg.version}.vsix`)
|
|
56
|
+
const child = spawn(
|
|
57
|
+
process.execPath,
|
|
58
|
+
[
|
|
59
|
+
path.join(vsceRoot, "vsce"),
|
|
60
|
+
"package",
|
|
61
|
+
"--no-dependencies",
|
|
62
|
+
"--allow-missing-repository",
|
|
63
|
+
"--out",
|
|
64
|
+
destination,
|
|
65
|
+
],
|
|
66
|
+
{ cwd: buildRoot, stdio: "inherit" },
|
|
67
|
+
)
|
|
68
|
+
const code = await new Promise<number | null>((resolve, reject) => {
|
|
69
|
+
child.on("error", reject)
|
|
70
|
+
child.on("close", resolve)
|
|
71
|
+
})
|
|
72
|
+
if (code !== 0) throw new Error(`VSCE failed with exit code ${code}`)
|
|
73
|
+
return destination
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (
|
|
77
|
+
process.argv[1] &&
|
|
78
|
+
pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url
|
|
79
|
+
) {
|
|
80
|
+
const packageRoot = fileURLToPath(new URL("../../", import.meta.url))
|
|
81
|
+
const output = await buildVsix(
|
|
82
|
+
packageRoot,
|
|
83
|
+
path.resolve(packageRoot, "../../artifacts"),
|
|
84
|
+
)
|
|
85
|
+
process.stdout.write(`${output}\n`)
|
|
86
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"additionalProperties": false,
|
|
5
|
+
"required": ["associations"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"$schema": { "type": "string" },
|
|
8
|
+
"files": { "$ref": "#/definitions/patterns" },
|
|
9
|
+
"exclude": { "$ref": "#/definitions/patterns" },
|
|
10
|
+
"associations": {
|
|
11
|
+
"type": "array",
|
|
12
|
+
"items": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"additionalProperties": false,
|
|
15
|
+
"required": ["files", "schema"],
|
|
16
|
+
"properties": {
|
|
17
|
+
"name": { "type": "string", "minLength": 1 },
|
|
18
|
+
"files": { "$ref": "#/definitions/patterns" },
|
|
19
|
+
"schema": { "type": ["string", "null"], "minLength": 1 },
|
|
20
|
+
"mode": { "enum": ["json", "jsonc"] }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"remote": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"additionalProperties": false,
|
|
27
|
+
"properties": {
|
|
28
|
+
"offline": { "type": "boolean" },
|
|
29
|
+
"cacheDir": { "type": "string", "minLength": 1 },
|
|
30
|
+
"timeoutMs": { "type": "integer", "minimum": 1, "maximum": 60000 },
|
|
31
|
+
"maxBytes": { "type": "integer", "minimum": 1, "maximum": 16777216 },
|
|
32
|
+
"maxRequests": { "type": "integer", "minimum": 1, "maximum": 256 }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"definitions": {
|
|
37
|
+
"patterns": {
|
|
38
|
+
"type": "array",
|
|
39
|
+
"minItems": 1,
|
|
40
|
+
"items": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"minLength": 1,
|
|
43
|
+
"pattern": "^(?![/!])(?![A-Za-z]:)(?!.*(?:^|/)\\.\\.(?:/|$)).+"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|