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.
Files changed (68) hide show
  1. package/AGENTS.md +6 -2
  2. package/README.md +21 -15
  3. package/dist/ajv.d.mts +11 -0
  4. package/dist/ajv.d.mts.map +1 -0
  5. package/dist/ajv.mjs +221 -0
  6. package/dist/ajv.mjs.map +1 -0
  7. package/dist/check-BI7U3ZLc.mjs +88 -0
  8. package/dist/check-BI7U3ZLc.mjs.map +1 -0
  9. package/dist/cli.mjs +14 -62
  10. package/dist/cli.mjs.map +1 -1
  11. package/dist/config-Bx1-XXeo.d.mts +189 -0
  12. package/dist/config-Bx1-XXeo.d.mts.map +1 -0
  13. package/dist/config-Cb6o4YnJ.mjs +348 -0
  14. package/dist/config-Cb6o4YnJ.mjs.map +1 -0
  15. package/dist/core.d.mts +27 -125
  16. package/dist/core.d.mts.map +1 -1
  17. package/dist/core.mjs +5 -2
  18. package/dist/diagnostics-BqD1WOy4.mjs +70 -0
  19. package/dist/diagnostics-BqD1WOy4.mjs.map +1 -0
  20. package/dist/engine-VST0hoSk.mjs +111 -0
  21. package/dist/engine-VST0hoSk.mjs.map +1 -0
  22. package/dist/extensions-ClclWslY.d.mts +11 -0
  23. package/dist/extensions-ClclWslY.d.mts.map +1 -0
  24. package/dist/hints-HvSIhuMM.mjs +57 -0
  25. package/dist/hints-HvSIhuMM.mjs.map +1 -0
  26. package/dist/lsp.mjs +40 -91
  27. package/dist/lsp.mjs.map +1 -1
  28. package/dist/renovate.d.mts +5 -0
  29. package/dist/renovate.d.mts.map +1 -0
  30. package/dist/renovate.mjs +11 -0
  31. package/dist/renovate.mjs.map +1 -0
  32. package/dist/schemas-UxCtIdo2.mjs +265 -0
  33. package/dist/schemas-UxCtIdo2.mjs.map +1 -0
  34. package/dist/session-DCFav56x.mjs +113 -0
  35. package/dist/session-DCFav56x.mjs.map +1 -0
  36. package/dist/worker.mjs +80 -0
  37. package/dist/worker.mjs.map +1 -0
  38. package/docs/guide.md +166 -34
  39. package/examples/basic/correctly.config.ts +13 -0
  40. package/package.json +69 -60
  41. package/src/cli/check.ts +41 -11
  42. package/src/cli/main.ts +3 -2
  43. package/src/cli/readable.ts +24 -5
  44. package/src/cli/vsix.ts +1 -0
  45. package/src/core/adapters.ts +54 -0
  46. package/src/core/config.ts +201 -64
  47. package/src/core/engine.ts +118 -218
  48. package/src/core/extensions.ts +144 -0
  49. package/src/core/index.ts +3 -0
  50. package/src/core/parse.ts +26 -2
  51. package/src/core/parsers.ts +22 -0
  52. package/src/core/schema-walk.ts +43 -0
  53. package/src/core/schemas.ts +21 -41
  54. package/src/core/types.ts +33 -6
  55. package/src/extensions/renovate.ts +4 -0
  56. package/src/lsp/hints.ts +22 -12
  57. package/src/lsp/server.ts +9 -2
  58. package/src/lsp/workspace.ts +49 -54
  59. package/src/runtime/session.ts +173 -0
  60. package/src/runtime/worker.ts +99 -0
  61. package/src/validators/ajv.ts +257 -0
  62. package/src/vscode/README.md +1 -1
  63. package/src/vscode/extension.ts +2 -4
  64. package/src/vscode/package.json +9 -2
  65. package/dist/engine-BgWBnb1A.mjs +0 -746
  66. package/dist/engine-BgWBnb1A.mjs.map +0 -1
  67. package/examples/basic/correctly.config.json +0 -10
  68. package/src/core/config.schema.json +0 -47
@@ -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 { Ajv } from "ajv"
4
+ import { cli } from "comline"
5
5
  import picomatch from "picomatch"
6
- import configSchema from "./config.schema.json" with { type: "json" }
7
- import { parseDocument } from "./parse.ts"
8
- import { CorrectlyError, type Association, type Mode } from "./types.ts"
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.json"
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
- schema: string | null
25
- mode?: Mode
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
- const info = await stat(candidate)
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
- text = await read(pathToFileURL(resolved).href)
90
- } catch (error) {
91
- throw new CorrectlyError(
92
- "config",
93
- `Cannot read configuration ${resolved}: ${String(error)}`,
94
- )
95
- }
96
- const parsed = parseDocument(text, "json")
97
- if (parsed.diagnostics.length) {
98
- const d = parsed.diagnostics[0]!
99
- throw new CorrectlyError(
100
- "config",
101
- `${resolved}:${d.range.start.line + 1}:${d.range.start.character + 1}: ${d.message}`,
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
- if (!validateConfig(parsed.value)) {
245
+ return { configPath: resolved, root: path.dirname(resolved), config }
246
+ } catch (error) {
105
247
  throw new CorrectlyError(
106
248
  "config",
107
- `${resolved}: ${validateConfig.errors?.map((e) => `${e.instancePath || "/"} ${e.message}`).join("; ")}`,
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
- schema:
191
- rule.schema === null ? null : schemaUri(rule.schema, project.root),
192
- mode: rule.mode ?? modeFor(file),
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): Mode {
199
- return path.extname(file).toLowerCase() === ".jsonc" ? "jsonc" : "json"
335
+ export function modeFor(file: string): string {
336
+ return defaultParser(file).id
200
337
  }