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
@@ -0,0 +1,99 @@
1
+ import { parentPort } from "node:worker_threads"
2
+ import { registerHooks } from "node:module"
3
+ import { TextDocument } from "vscode-languageserver-textdocument"
4
+ import { loadProject, readText } from "../core/config.ts"
5
+ import { Engine } from "../core/engine.ts"
6
+ import { failure, type Position } from "../core/types.ts"
7
+ import { Hints } from "../lsp/hints.ts"
8
+ import { checkProject } from "../cli/check.ts"
9
+
10
+ if (!parentPort) throw new Error("Correctly project runtime requires a worker")
11
+ const port = parentPort
12
+ let engine: Engine | undefined
13
+ const hints = new Hints()
14
+ const controller = new AbortController()
15
+ const dependencies = new Set<string>()
16
+ function dependency(uri: string, kind: "module" | "resource") {
17
+ const key = `${kind}:${uri}`
18
+ if (!uri.startsWith("file:") || dependencies.has(key)) return
19
+ dependencies.add(key)
20
+ port.postMessage({ dependency: { uri, kind } })
21
+ }
22
+ // Comline owns loading; hooks only observe dependencies, including failed relative imports.
23
+ registerHooks({
24
+ resolve(specifier, context, next) {
25
+ if (
26
+ context.parentURL &&
27
+ (specifier.startsWith(".") || specifier.startsWith("file:"))
28
+ )
29
+ dependency(new URL(specifier, context.parentURL).href, "module")
30
+ const result = next(specifier, context)
31
+ dependency(result.url, "module")
32
+ return result
33
+ },
34
+ })
35
+ port.on(
36
+ "message",
37
+ (message: {
38
+ id: number
39
+ method: string
40
+ params: Record<string, unknown>
41
+ }) => {
42
+ void (async () => {
43
+ const p = message.params
44
+ if (message.method === "dispose") {
45
+ controller.abort()
46
+ await engine?.dispose()
47
+ return
48
+ }
49
+ if (message.method === "init") {
50
+ const buffers = new Map(p.buffers as [string, string][])
51
+ const project = await loadProject(p.configPath as string)
52
+ engine = new Engine(project, {
53
+ signal: controller.signal,
54
+ read: (uri) =>
55
+ buffers.has(uri)
56
+ ? Promise.resolve(buffers.get(uri)!)
57
+ : readText(uri),
58
+ ...(p.offline === undefined ? {} : { offline: p.offline as boolean }),
59
+ onDependency: (uri) => dependency(uri, "resource"),
60
+ })
61
+ await engine.prepare()
62
+ return
63
+ }
64
+ if (!engine) throw new Error("Project is not initialized")
65
+ if (message.method === "validate")
66
+ return engine.validate(p.file as string, p.text as string)
67
+ if (message.method === "check") {
68
+ const sources: [string, string][] = []
69
+ const report = await checkProject(engine, {
70
+ cwd: p.cwd as string,
71
+ ...(p.files ? { files: p.files as string[] } : {}),
72
+ ...(p.sources
73
+ ? {
74
+ onRead: (file: string, text: string) => {
75
+ sources.push([file, text])
76
+ },
77
+ }
78
+ : {}),
79
+ })
80
+ return { report, sources }
81
+ }
82
+ const document = TextDocument.create(
83
+ p.uri as string,
84
+ p.language as string,
85
+ p.version as number,
86
+ p.text as string,
87
+ )
88
+ if (message.method === "complete")
89
+ return hints.complete(engine, document, p.position as Position)
90
+ if (message.method === "hover")
91
+ return hints.hover(engine, document, p.position as Position)
92
+ throw new Error(`Unknown project operation: ${message.method}`)
93
+ })().then(
94
+ (value) => port.postMessage({ id: message.id, value }),
95
+ (error: unknown) =>
96
+ port.postMessage({ id: message.id, error: failure(error) }),
97
+ )
98
+ },
99
+ )
@@ -0,0 +1,257 @@
1
+ import {
2
+ Ajv,
3
+ type ErrorObject,
4
+ type ValidateFunction,
5
+ type SchemaObject,
6
+ } from "ajv"
7
+ import { Ajv2020 } from "ajv/dist/2020.js"
8
+ import addFormats from "ajv-formats"
9
+ import { schemaUri } from "../core/config.ts"
10
+ import { diagnostic, pointer, type ParsedDocument } from "../core/parse.ts"
11
+ import { DIALECTS, SchemaStore } from "../core/schemas.ts"
12
+ import { validationContexts } from "../core/diagnostics.ts"
13
+ import {
14
+ knownExtensions,
15
+ installExtensions,
16
+ assertFormats,
17
+ type AjvExtension,
18
+ } from "../core/extensions.ts"
19
+ import {
20
+ CorrectlyError,
21
+ isCorrectlyError,
22
+ type Diagnostic,
23
+ } from "../core/types.ts"
24
+ import type { Validator, ValidationContext } from "../core/adapters.ts"
25
+
26
+ const ANNOTATIONS = [
27
+ "markdownDescription",
28
+ "enumDescriptions",
29
+ "markdownEnumDescriptions",
30
+ "defaultSnippets",
31
+ "errorMessage",
32
+ "patternErrorMessage",
33
+ "deprecationMessage",
34
+ "suggestSortText",
35
+ "doNotSuggest",
36
+ "allowComments",
37
+ "allowTrailingCommas",
38
+ "deprecated",
39
+ "$vocabulary",
40
+ ]
41
+
42
+ function registerRootAnchors(ajv: Ajv, schema: unknown, uri: string) {
43
+ if (typeof schema !== "object" || schema === null) return
44
+ const object = schema as SchemaObject
45
+ for (const anchor of [object.$anchor, object.$dynamicAnchor]) {
46
+ if (typeof anchor === "string")
47
+ ajv.refs[new URL(`#${anchor}`, object.$id ?? uri).href] =
48
+ object.$id ?? uri
49
+ }
50
+ }
51
+
52
+ function validationMessage(error: ErrorObject): string {
53
+ if (error.keyword === "anyOf")
54
+ return "must match at least one alternative; none matched"
55
+ if (
56
+ error.keyword === "if" &&
57
+ ["then", "else"].includes(error.params.failingKeyword as string)
58
+ )
59
+ return `must satisfy the ${error.params.failingKeyword} branch of the conditional schema`
60
+ if (error.keyword === "uniqueItems") {
61
+ const { i, j } = error.params
62
+ if (Number.isInteger(i) && Number.isInteger(j))
63
+ return `duplicates item at ${error.instancePath}${pointer([Math.min(i, j)])}; array items must be unique`
64
+ }
65
+ if (error.keyword === "enum" && Array.isArray(error.params.allowedValues))
66
+ return `must be one of: ${error.params.allowedValues.map((value: unknown) => JSON.stringify(value)).join(", ")}`
67
+ if (error.keyword === "const" && Object.hasOwn(error.params, "allowedValue"))
68
+ return `must equal ${JSON.stringify(error.params.allowedValue)}`
69
+ if (error.keyword === "oneOf") {
70
+ if (error.params.passingSchemas === null)
71
+ return "must match exactly one alternative; none matched"
72
+ if (Array.isArray(error.params.passingSchemas))
73
+ return `must match exactly one alternative; multiple matched (alternatives ${error.params.passingSchemas.map((index: number) => index + 1).join(", ")})`
74
+ }
75
+ return error.message ?? "Schema violation"
76
+ }
77
+
78
+ function validationDiagnostic(
79
+ text: string,
80
+ parsed: ParsedDocument,
81
+ error: ErrorObject,
82
+ ): Diagnostic {
83
+ let jsonPointer = error.instancePath
84
+ if (
85
+ error.keyword === "uniqueItems" &&
86
+ Number.isInteger(error.params.i) &&
87
+ Number.isInteger(error.params.j)
88
+ )
89
+ jsonPointer += pointer([Math.max(error.params.i, error.params.j)])
90
+ const property: unknown =
91
+ error.params.additionalProperty ??
92
+ error.params.unevaluatedProperty ??
93
+ error.params.missingProperty ??
94
+ error.params.propertyName ??
95
+ error.propertyName
96
+ if (typeof property === "string") jsonPointer += pointer([property])
97
+ const node = parsed.locate(
98
+ jsonPointer,
99
+ error.keyword === "additionalProperties" ||
100
+ error.keyword === "unevaluatedProperties" ||
101
+ error.keyword === "propertyNames" ||
102
+ typeof error.propertyName === "string",
103
+ )
104
+ const rangeNode =
105
+ error.keyword === "required" && error.propertyName === undefined
106
+ ? parsed.locate(error.instancePath)
107
+ : node
108
+ const length =
109
+ error.keyword === "required" && error.propertyName === undefined
110
+ ? 1
111
+ : rangeNode?.length
112
+ return diagnostic(
113
+ text,
114
+ `schema/${error.keyword}`,
115
+ `${jsonPointer || "/"}: ${validationMessage(error)}`,
116
+ jsonPointer,
117
+ rangeNode?.offset ?? 0,
118
+ length ?? 1,
119
+ )
120
+ }
121
+
122
+ async function compile(
123
+ store: SchemaStore,
124
+ uri: string,
125
+ extensions: readonly AjvExtension[],
126
+ ): Promise<ValidateFunction> {
127
+ try {
128
+ const resource = await store.load(uri)
129
+ const options = {
130
+ allErrors: true,
131
+ strictSchema: true,
132
+ strictTypes: false,
133
+ strictTuples: false,
134
+ strictRequired: false,
135
+ coerceTypes: false,
136
+ useDefaults: false,
137
+ removeAdditional: false,
138
+ validateSchema: true,
139
+ loadSchema: async (ref: string) => {
140
+ const loaded = await store.load(ref, resource.dialect)
141
+ if (loaded.dialect !== resource.dialect)
142
+ throw new CorrectlyError(
143
+ "unsupported-dialect",
144
+ `Mixed schema dialects: ${uri} references ${ref}`,
145
+ )
146
+ assertFormats(
147
+ ajv,
148
+ loaded.schema,
149
+ loaded.uri,
150
+ knownExtensions,
151
+ loaded.pointer,
152
+ )
153
+ registerRootAnchors(ajv, loaded.schema, loaded.uri)
154
+ return loaded.schema as SchemaObject
155
+ },
156
+ }
157
+ const ajv =
158
+ resource.dialect === "2020-12" ? new Ajv2020(options) : new Ajv(options)
159
+ // CommonJS package exports retain a callable default at runtime.
160
+ const formats = addFormats as unknown as (instance: Ajv) => void
161
+ formats(ajv)
162
+ // ajv-formats also ships OpenAPI numeric names. Their width semantics
163
+ // belong to an explicitly selected extension, not the baseline.
164
+ for (const name of ["int32", "int64", "float", "double"])
165
+ delete ajv.formats[name]
166
+ for (const keyword of ANNOTATIONS)
167
+ if (!ajv.RULES.keywords[keyword]) ajv.addKeyword({ keyword, valid: true })
168
+ // Ajv indexes plain anchors during reference resolution, but does not
169
+ // register the annotation keyword in its strict vocabulary.
170
+ if (resource.dialect === "2020-12")
171
+ ajv.addKeyword({
172
+ keyword: "$anchor",
173
+ schemaType: "string",
174
+ valid: true,
175
+ })
176
+ installExtensions(ajv, extensions)
177
+ assertFormats(
178
+ ajv,
179
+ resource.schema,
180
+ resource.uri,
181
+ knownExtensions,
182
+ resource.pointer,
183
+ )
184
+ ajv.addSchema(resource.schema, resource.uri)
185
+ registerRootAnchors(ajv, resource.schema, resource.uri)
186
+ const validate = await ajv.compileAsync({
187
+ $schema: DIALECTS[resource.dialect],
188
+ $ref: uri,
189
+ })
190
+ return validate as ValidateFunction
191
+ } catch (error) {
192
+ if (isCorrectlyError(error)) throw error
193
+ throw new CorrectlyError(
194
+ "schema",
195
+ `Cannot compile schema ${uri}: ${error instanceof Error ? error.message : String(error)}`,
196
+ )
197
+ }
198
+ }
199
+
200
+ const storeKey = Symbol.for("correctly/ajv/schema-store@1")
201
+ function schemaStore(context: ValidationContext): SchemaStore {
202
+ let store = context.services.get(storeKey) as SchemaStore | undefined
203
+ if (!store) {
204
+ store = new SchemaStore(
205
+ {
206
+ root: context.root,
207
+ config: context.remote ? { remote: context.remote } : {},
208
+ },
209
+ {
210
+ read: context.read,
211
+ ...(context.fetch ? { fetch: context.fetch } : {}),
212
+ ...(context.signal ? { signal: context.signal } : {}),
213
+ },
214
+ )
215
+ context.services.set(storeKey, store)
216
+ }
217
+ return store
218
+ }
219
+ export type AjvOptions = {
220
+ schema: string
221
+ extensions?: readonly AjvExtension[]
222
+ }
223
+ export function ajv(options: AjvOptions): Validator {
224
+ const extensions = [...(options.extensions ?? [])]
225
+ return {
226
+ id: "ajv",
227
+ accepts: ["json"],
228
+ schema: options.schema,
229
+ extensions,
230
+ register: async (context) => {
231
+ await schemaStore(context).load(schemaUri(options.schema, context.root))
232
+ },
233
+ prepare: async (context) => {
234
+ const store = schemaStore(context)
235
+ const uri = schemaUri(options.schema, context.root)
236
+ const validate = await compile(store, uri, extensions)
237
+ return {
238
+ validate: ({ text, parsed }) =>
239
+ validate(parsed.value)
240
+ ? []
241
+ : validationContexts(
242
+ validate.errors ?? [],
243
+ (validate.errors ?? []).map((error) =>
244
+ validationDiagnostic(text, parsed, error),
245
+ ),
246
+ ),
247
+ editor: {
248
+ kind: "json-schema",
249
+ uri,
250
+ readSchema: async (uri) =>
251
+ JSON.stringify((await store.load(uri)).schema),
252
+ },
253
+ }
254
+ },
255
+ }
256
+ }
257
+ export type { AjvExtension } from "../core/extensions.ts"
@@ -1,6 +1,6 @@
1
1
  # Correctly for VS Code
2
2
 
3
- Install the VSIX, add `correctly.config.json` to each workspace root, and open JSON or JSONC files. Correctly uses that same external configuration for validation, completion, and hover. It reads unsaved buffers and refreshes when configurations and schemas change.
3
+ Install Correctly and any imported adapters in the project, trust the workspace, install the VSIX, add `correctly.config.ts` to each workspace root, and open JSON or JSONC files. Correctly uses that same external configuration for validation, completion, and hover. It reads unsaved data and schema buffers. Executable configuration and imported modules reload only after saving. The extension is disabled in untrusted workspaces.
4
4
 
5
5
  The client and server are bundled; no npm install or separate schema map is needed. Run **Correctly: Restart Server** to restart the language server. JSON formatting remains the responsibility of your formatter.
6
6
 
@@ -3,6 +3,7 @@ import { LanguageClient, TransportKind } from "vscode-languageclient/node"
3
3
 
4
4
  let client: LanguageClient | undefined
5
5
  export async function activate(context: vscode.ExtensionContext) {
6
+ if (!vscode.workspace.isTrusted) return
6
7
  const watcher = vscode.workspace.createFileSystemWatcher("**/*")
7
8
  context.subscriptions.push(watcher)
8
9
  client = new LanguageClient(
@@ -13,10 +14,7 @@ export async function activate(context: vscode.ExtensionContext) {
13
14
  transport: TransportKind.stdio,
14
15
  },
15
16
  {
16
- documentSelector: [
17
- { scheme: "file", language: "json" },
18
- { scheme: "file", language: "jsonc" },
19
- ],
17
+ documentSelector: [{ scheme: "file" }],
20
18
  synchronize: { fileEvents: watcher },
21
19
  },
22
20
  )
@@ -32,10 +32,17 @@
32
32
  },
33
33
  "activationEvents": [
34
34
  "onLanguage:json",
35
- "onLanguage:jsonc"
35
+ "onLanguage:jsonc",
36
+ "workspaceContains:**/correctly.config.ts"
36
37
  ],
37
38
  "engines": {
38
- "vscode": "^1.100.0"
39
+ "vscode": "^1.105.0"
40
+ },
41
+ "capabilities": {
42
+ "untrustedWorkspaces": {
43
+ "supported": false,
44
+ "description": "Correctly evaluates the project TypeScript configuration and its imported parser, validator, and extension code."
45
+ }
39
46
  },
40
47
  "extensionKind": [
41
48
  "workspace"