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.
@@ -0,0 +1 @@
1
+ { "name": "Correctly", "color": "blue" }
@@ -0,0 +1,5 @@
1
+ {
2
+ // Comments and trailing commas are allowed in JSONC mode.
3
+ "name": "Correctly",
4
+ "color": "red",
5
+ }
@@ -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
+ }
@@ -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"
@@ -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
+ }
@@ -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
+ }