correctly 0.0.0 → 0.1.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 +6 -2
- package/README.md +21 -15
- package/dist/ajv.d.mts +11 -0
- package/dist/ajv.d.mts.map +1 -0
- package/dist/ajv.mjs +221 -0
- package/dist/ajv.mjs.map +1 -0
- package/dist/check-BI7U3ZLc.mjs +88 -0
- package/dist/check-BI7U3ZLc.mjs.map +1 -0
- package/dist/cli.mjs +14 -62
- package/dist/cli.mjs.map +1 -1
- package/dist/config-Bx1-XXeo.d.mts +189 -0
- package/dist/config-Bx1-XXeo.d.mts.map +1 -0
- package/dist/config-Cb6o4YnJ.mjs +348 -0
- package/dist/config-Cb6o4YnJ.mjs.map +1 -0
- package/dist/core.d.mts +27 -125
- package/dist/core.d.mts.map +1 -1
- package/dist/core.mjs +5 -2
- package/dist/diagnostics-BqD1WOy4.mjs +70 -0
- package/dist/diagnostics-BqD1WOy4.mjs.map +1 -0
- package/dist/engine-VST0hoSk.mjs +111 -0
- package/dist/engine-VST0hoSk.mjs.map +1 -0
- package/dist/extensions-ClclWslY.d.mts +11 -0
- package/dist/extensions-ClclWslY.d.mts.map +1 -0
- package/dist/hints-HvSIhuMM.mjs +57 -0
- package/dist/hints-HvSIhuMM.mjs.map +1 -0
- package/dist/lsp.mjs +40 -91
- package/dist/lsp.mjs.map +1 -1
- package/dist/renovate.d.mts +5 -0
- package/dist/renovate.d.mts.map +1 -0
- package/dist/renovate.mjs +11 -0
- package/dist/renovate.mjs.map +1 -0
- package/dist/schemas-UxCtIdo2.mjs +265 -0
- package/dist/schemas-UxCtIdo2.mjs.map +1 -0
- package/dist/session-DCFav56x.mjs +113 -0
- package/dist/session-DCFav56x.mjs.map +1 -0
- package/dist/worker.mjs +80 -0
- package/dist/worker.mjs.map +1 -0
- package/docs/guide.md +166 -34
- package/examples/basic/correctly.config.ts +13 -0
- package/package.json +69 -60
- package/src/cli/check.ts +41 -11
- package/src/cli/main.ts +3 -2
- package/src/cli/readable.ts +24 -5
- package/src/cli/vsix.ts +1 -0
- package/src/core/adapters.ts +54 -0
- package/src/core/config.ts +201 -64
- package/src/core/engine.ts +118 -218
- package/src/core/extensions.ts +144 -0
- package/src/core/index.ts +3 -0
- package/src/core/parse.ts +26 -2
- package/src/core/parsers.ts +22 -0
- package/src/core/schema-walk.ts +43 -0
- package/src/core/schemas.ts +21 -41
- package/src/core/types.ts +33 -6
- package/src/extensions/renovate.ts +4 -0
- package/src/lsp/hints.ts +22 -12
- package/src/lsp/server.ts +9 -2
- package/src/lsp/workspace.ts +49 -54
- package/src/runtime/session.ts +173 -0
- package/src/runtime/worker.ts +99 -0
- package/src/validators/ajv.ts +257 -0
- package/src/vscode/README.md +1 -1
- package/src/vscode/extension.ts +2 -4
- package/src/vscode/package.json +9 -2
- package/dist/engine-BgWBnb1A.mjs +0 -746
- package/dist/engine-BgWBnb1A.mjs.map +0 -1
- package/examples/basic/correctly.config.json +0 -10
- package/src/core/config.schema.json +0 -47
package/src/core/config.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { readFile, stat } from "node:fs/promises"
|
|
2
2
|
import path from "node:path"
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
4
|
-
import {
|
|
4
|
+
import { cli } from "comline"
|
|
5
5
|
import picomatch from "picomatch"
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import { CorrectlyError, type Association
|
|
6
|
+
import type { Parser, Validator } from "./adapters.ts"
|
|
7
|
+
import { defaultParser } from "./parsers.ts"
|
|
8
|
+
import { CorrectlyError, type Association } from "./types.ts"
|
|
9
9
|
|
|
10
|
-
export const CONFIG_NAME = "correctly.config.
|
|
10
|
+
export const CONFIG_NAME = "correctly.config.ts"
|
|
11
11
|
export const DEFAULT_EXCLUDES = [
|
|
12
12
|
"**/node_modules/**",
|
|
13
13
|
"**/.git/**",
|
|
@@ -15,22 +15,23 @@ export const DEFAULT_EXCLUDES = [
|
|
|
15
15
|
"**/artifacts/**",
|
|
16
16
|
"**/.correctly-cache/**",
|
|
17
17
|
]
|
|
18
|
+
export type RemoteOptions = {
|
|
19
|
+
offline?: boolean
|
|
20
|
+
cacheDir?: string
|
|
21
|
+
timeoutMs?: number
|
|
22
|
+
maxBytes?: number
|
|
23
|
+
maxRequests?: number
|
|
24
|
+
}
|
|
18
25
|
export type ProjectConfig = {
|
|
19
26
|
files?: string[]
|
|
20
27
|
exclude?: string[]
|
|
21
28
|
associations: {
|
|
22
29
|
name?: string
|
|
23
30
|
files: string[]
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
parse?: Parser
|
|
32
|
+
validate: Validator | null
|
|
26
33
|
}[]
|
|
27
|
-
remote?:
|
|
28
|
-
offline?: boolean
|
|
29
|
-
cacheDir?: string
|
|
30
|
-
timeoutMs?: number
|
|
31
|
-
maxBytes?: number
|
|
32
|
-
maxRequests?: number
|
|
33
|
-
}
|
|
34
|
+
remote?: RemoteOptions
|
|
34
35
|
}
|
|
35
36
|
export type Project = {
|
|
36
37
|
configPath: string
|
|
@@ -39,10 +40,144 @@ export type Project = {
|
|
|
39
40
|
}
|
|
40
41
|
export type ReadText = (uri: string) => Promise<string>
|
|
41
42
|
export const readText: ReadText = (uri) => readFile(fileURLToPath(uri), "utf8")
|
|
42
|
-
const validateConfig = new Ajv({ allErrors: true, strict: true }).compile(
|
|
43
|
-
configSchema,
|
|
44
|
-
)
|
|
45
43
|
|
|
44
|
+
function object(
|
|
45
|
+
value: unknown,
|
|
46
|
+
location: string,
|
|
47
|
+
keys?: string[],
|
|
48
|
+
): asserts value is Record<string, unknown> {
|
|
49
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
50
|
+
throw new CorrectlyError("config", `${location} must be an object`)
|
|
51
|
+
if (keys)
|
|
52
|
+
for (const key of Object.keys(value))
|
|
53
|
+
if (!keys.includes(key))
|
|
54
|
+
throw new CorrectlyError("config", `Unknown option ${location}.${key}`)
|
|
55
|
+
}
|
|
56
|
+
function string(value: unknown, location: string): asserts value is string {
|
|
57
|
+
if (typeof value !== "string" || !value.length)
|
|
58
|
+
throw new CorrectlyError("config", `${location} must be a nonempty string`)
|
|
59
|
+
}
|
|
60
|
+
function patterns(value: unknown, location: string): void {
|
|
61
|
+
if (!Array.isArray(value) || !value.length)
|
|
62
|
+
throw new CorrectlyError(
|
|
63
|
+
"config",
|
|
64
|
+
`${location} must be a nonempty array of relative patterns`,
|
|
65
|
+
)
|
|
66
|
+
for (const item of value) {
|
|
67
|
+
string(item, location)
|
|
68
|
+
if (/^(?:[!/]|[A-Za-z]:)|(?:^|\/)\.\.(?:\/|$)|\\/.test(item))
|
|
69
|
+
throw new CorrectlyError(
|
|
70
|
+
"config",
|
|
71
|
+
`${location}: use relative forward-slash patterns without '..' or leading '!': ${item}`,
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function defineConfig<T extends ProjectConfig>(config: T): T {
|
|
76
|
+
validateConfig(config)
|
|
77
|
+
return config
|
|
78
|
+
}
|
|
79
|
+
export function validateConfig(value: unknown): ProjectConfig {
|
|
80
|
+
object(value, "config", ["files", "exclude", "associations", "remote"])
|
|
81
|
+
for (const key of ["files", "exclude"])
|
|
82
|
+
if (value[key] !== undefined) patterns(value[key], key)
|
|
83
|
+
if (!Array.isArray(value.associations))
|
|
84
|
+
throw new CorrectlyError("config", "associations must be an array")
|
|
85
|
+
for (const [index, rule] of value.associations.entries()) {
|
|
86
|
+
const at = `associations[${index}]`
|
|
87
|
+
object(rule, at, ["name", "files", "parse", "validate"])
|
|
88
|
+
patterns(rule.files, `${at}.files`)
|
|
89
|
+
if (rule.name !== undefined) string(rule.name, `${at}.name`)
|
|
90
|
+
if (rule.parse !== undefined) {
|
|
91
|
+
object(rule.parse, `${at}.parse`)
|
|
92
|
+
string(rule.parse.id, `${at}.parse.id`)
|
|
93
|
+
string(rule.parse.valueModel, `${at}.parse.valueModel`)
|
|
94
|
+
if (typeof rule.parse.parse !== "function")
|
|
95
|
+
throw new CorrectlyError(
|
|
96
|
+
"config",
|
|
97
|
+
`${at}.parse must be a parser adapter`,
|
|
98
|
+
)
|
|
99
|
+
if (
|
|
100
|
+
rule.parse.editorLanguage !== undefined &&
|
|
101
|
+
rule.parse.editorLanguage !== "json" &&
|
|
102
|
+
rule.parse.editorLanguage !== "jsonc"
|
|
103
|
+
)
|
|
104
|
+
throw new CorrectlyError(
|
|
105
|
+
"config",
|
|
106
|
+
`${at}.parse.editorLanguage must be json or jsonc`,
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
if (rule.validate !== null) {
|
|
110
|
+
object(rule.validate, `${at}.validate`)
|
|
111
|
+
string(rule.validate.id, `${at}.validate.id`)
|
|
112
|
+
if (
|
|
113
|
+
typeof rule.validate.prepare !== "function" ||
|
|
114
|
+
!Array.isArray(rule.validate.accepts) ||
|
|
115
|
+
!rule.validate.accepts.length ||
|
|
116
|
+
rule.validate.accepts.some((v: unknown) => typeof v !== "string" || !v)
|
|
117
|
+
)
|
|
118
|
+
throw new CorrectlyError(
|
|
119
|
+
"config",
|
|
120
|
+
`${at}.validate must be a validator adapter with accepted value models`,
|
|
121
|
+
)
|
|
122
|
+
if (
|
|
123
|
+
rule.validate.register !== undefined &&
|
|
124
|
+
typeof rule.validate.register !== "function"
|
|
125
|
+
)
|
|
126
|
+
throw new CorrectlyError(
|
|
127
|
+
"config",
|
|
128
|
+
`${at}.validate.register must be a function`,
|
|
129
|
+
)
|
|
130
|
+
if (rule.validate.extensions !== undefined) {
|
|
131
|
+
if (!Array.isArray(rule.validate.extensions))
|
|
132
|
+
throw new CorrectlyError(
|
|
133
|
+
"config",
|
|
134
|
+
`${at}.validate.extensions must be an array`,
|
|
135
|
+
)
|
|
136
|
+
for (const extension of rule.validate.extensions) {
|
|
137
|
+
object(extension, `${at}.validate.extensions[]`)
|
|
138
|
+
string(extension.id, `${at}.validate.extensions[].id`)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (rule.validate.schema !== undefined)
|
|
142
|
+
string(rule.validate.schema, `${at}.validate.schema`)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (value.remote !== undefined) {
|
|
146
|
+
object(value.remote, "remote", [
|
|
147
|
+
"offline",
|
|
148
|
+
"cacheDir",
|
|
149
|
+
"timeoutMs",
|
|
150
|
+
"maxBytes",
|
|
151
|
+
"maxRequests",
|
|
152
|
+
])
|
|
153
|
+
if (
|
|
154
|
+
value.remote.offline !== undefined &&
|
|
155
|
+
typeof value.remote.offline !== "boolean"
|
|
156
|
+
)
|
|
157
|
+
throw new CorrectlyError("config", "remote.offline must be a boolean")
|
|
158
|
+
if (value.remote.cacheDir !== undefined)
|
|
159
|
+
string(value.remote.cacheDir, "remote.cacheDir")
|
|
160
|
+
for (const [key, max] of [
|
|
161
|
+
["timeoutMs", 60000],
|
|
162
|
+
["maxBytes", 16777216],
|
|
163
|
+
["maxRequests", 256],
|
|
164
|
+
] as const) {
|
|
165
|
+
const number = value.remote[key]
|
|
166
|
+
if (
|
|
167
|
+
number !== undefined &&
|
|
168
|
+
(typeof number !== "number" ||
|
|
169
|
+
!Number.isInteger(number) ||
|
|
170
|
+
number < 1 ||
|
|
171
|
+
number > max)
|
|
172
|
+
)
|
|
173
|
+
throw new CorrectlyError(
|
|
174
|
+
"config",
|
|
175
|
+
`remote.${key} must be an integer between 1 and ${max}`,
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return value as ProjectConfig
|
|
180
|
+
}
|
|
46
181
|
export function contains(root: string, file: string): boolean {
|
|
47
182
|
const relative = path.relative(root, file)
|
|
48
183
|
return (
|
|
@@ -51,21 +186,17 @@ export function contains(root: string, file: string): boolean {
|
|
|
51
186
|
!path.isAbsolute(relative)
|
|
52
187
|
)
|
|
53
188
|
}
|
|
54
|
-
|
|
55
189
|
export async function discoverConfig(
|
|
56
190
|
start: string,
|
|
57
191
|
boundary?: string,
|
|
58
|
-
virtualFiles: ReadonlySet<string> = new Set(),
|
|
59
192
|
): Promise<string> {
|
|
60
193
|
let directory = path.resolve(start)
|
|
61
194
|
if (boundary && !contains(boundary, directory))
|
|
62
195
|
throw new CorrectlyError("config", `Path is outside workspace ${boundary}`)
|
|
63
196
|
while (true) {
|
|
64
197
|
const candidate = path.join(directory, CONFIG_NAME)
|
|
65
|
-
if (virtualFiles.has(candidate)) return candidate
|
|
66
198
|
try {
|
|
67
|
-
|
|
68
|
-
if (info.isFile()) return candidate
|
|
199
|
+
if ((await stat(candidate)).isFile()) return candidate
|
|
69
200
|
} catch (error) {
|
|
70
201
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error
|
|
71
202
|
}
|
|
@@ -78,48 +209,46 @@ export async function discoverConfig(
|
|
|
78
209
|
`No ${CONFIG_NAME} found from ${start}${boundary ? ` within ${boundary}` : ""}`,
|
|
79
210
|
)
|
|
80
211
|
}
|
|
81
|
-
|
|
82
|
-
export async function loadProject(
|
|
83
|
-
configPath: string,
|
|
84
|
-
read: ReadText = readText,
|
|
85
|
-
): Promise<Project> {
|
|
212
|
+
/** Comline owns module loading and invokes our runtime contract validation. */
|
|
213
|
+
export async function loadProject(configPath: string): Promise<Project> {
|
|
86
214
|
const resolved = path.resolve(configPath)
|
|
87
|
-
let text: string
|
|
88
215
|
try {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
216
|
+
if (path.extname(resolved) !== ".ts")
|
|
217
|
+
throw new Error("Correctly configuration must be a TypeScript .ts module")
|
|
218
|
+
if (!(await stat(resolved)).isFile())
|
|
219
|
+
throw new Error("Configuration must be a file")
|
|
220
|
+
const schema = {
|
|
221
|
+
"~standard": {
|
|
222
|
+
version: 1 as const,
|
|
223
|
+
vendor: "correctly",
|
|
224
|
+
validate: (value: unknown) => ({ value: validateConfig(value) }),
|
|
225
|
+
jsonSchema: {
|
|
226
|
+
input: () => ({ type: "object" }),
|
|
227
|
+
output: () => ({ type: "object" }),
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
}
|
|
231
|
+
const parse = cli({
|
|
232
|
+
cliName: "correctly",
|
|
233
|
+
discoverConfigPath: () => resolved,
|
|
234
|
+
routeOptions: {
|
|
235
|
+
"": {
|
|
236
|
+
description: "Correctly configuration",
|
|
237
|
+
optionsSchema: schema,
|
|
238
|
+
optionConfigs: {},
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
})
|
|
242
|
+
const config = validateConfig(
|
|
243
|
+
parse([process.execPath, "correctly"]).inputs.opts,
|
|
102
244
|
)
|
|
103
|
-
|
|
104
|
-
|
|
245
|
+
return { configPath: resolved, root: path.dirname(resolved), config }
|
|
246
|
+
} catch (error) {
|
|
105
247
|
throw new CorrectlyError(
|
|
106
248
|
"config",
|
|
107
|
-
|
|
249
|
+
`Cannot load ${resolved}: ${error instanceof Error ? error.message : String(error)}`,
|
|
108
250
|
)
|
|
109
251
|
}
|
|
110
|
-
const config = parsed.value as ProjectConfig
|
|
111
|
-
for (const pattern of [
|
|
112
|
-
...(config.files ?? []),
|
|
113
|
-
...(config.exclude ?? []),
|
|
114
|
-
...config.associations.flatMap((a) => a.files),
|
|
115
|
-
]) {
|
|
116
|
-
if (pattern.includes("\\"))
|
|
117
|
-
throw new CorrectlyError(
|
|
118
|
-
"config",
|
|
119
|
-
`Use forward slashes in patterns: ${pattern}`,
|
|
120
|
-
)
|
|
121
|
-
}
|
|
122
|
-
return { configPath: resolved, root: path.dirname(resolved), config }
|
|
123
252
|
}
|
|
124
253
|
|
|
125
254
|
export function schemaUri(value: string, root: string): string {
|
|
@@ -183,18 +312,26 @@ export function associationFor(
|
|
|
183
312
|
index--
|
|
184
313
|
) {
|
|
185
314
|
const rule = project.config.associations[index]!
|
|
186
|
-
if (picomatch(rule.files, { dot: true })(relative))
|
|
315
|
+
if (picomatch(rule.files, { dot: true })(relative))
|
|
187
316
|
return {
|
|
188
317
|
index,
|
|
189
318
|
name: rule.name ?? `association ${index + 1}`,
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
319
|
+
mode: (rule.parse ?? defaultParser(file)).id,
|
|
320
|
+
schema: rule.validate?.schema
|
|
321
|
+
? schemaUri(rule.validate.schema, project.root)
|
|
322
|
+
: null,
|
|
323
|
+
validator: rule.validate?.id ?? null,
|
|
324
|
+
...(rule.validate?.extensions
|
|
325
|
+
? {
|
|
326
|
+
extensions: rule.validate.extensions.map(
|
|
327
|
+
(extension) => extension.id,
|
|
328
|
+
),
|
|
329
|
+
}
|
|
330
|
+
: {}),
|
|
193
331
|
}
|
|
194
|
-
}
|
|
195
332
|
}
|
|
196
333
|
return null
|
|
197
334
|
}
|
|
198
|
-
export function modeFor(file: string):
|
|
199
|
-
return
|
|
335
|
+
export function modeFor(file: string): string {
|
|
336
|
+
return defaultParser(file).id
|
|
200
337
|
}
|