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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schemas-UxCtIdo2.mjs","names":[],"sources":["../src/core/schema-walk.ts","../src/core/schemas.ts"],"sourcesContent":["import { pointer } from \"./parse.ts\"\n\n/** Only schema positions: defaults, enums, examples and other data stay opaque. */\nexport function* schemaChildren(\n\tschema: Record<string, unknown>,\n): Generator<[string, unknown]> {\n\tfor (const key of [\n\t\t\"additionalItems\",\n\t\t\"additionalProperties\",\n\t\t\"contains\",\n\t\t\"propertyNames\",\n\t\t\"not\",\n\t\t\"if\",\n\t\t\"then\",\n\t\t\"else\",\n\t\t\"unevaluatedProperties\",\n\t\t\"unevaluatedItems\",\n\t\t\"contentSchema\",\n\t]) {\n\t\tif (schema[key] !== undefined) yield [pointer([key]), schema[key]]\n\t}\n\tfor (const key of [\n\t\t\"properties\",\n\t\t\"patternProperties\",\n\t\t\"definitions\",\n\t\t\"$defs\",\n\t\t\"dependentSchemas\",\n\t\t\"dependencies\",\n\t]) {\n\t\tconst map = schema[key]\n\t\tif (typeof map === \"object\" && map !== null && !Array.isArray(map))\n\t\t\tfor (const [name, child] of Object.entries(map))\n\t\t\t\tyield [pointer([key, name]), child]\n\t}\n\tfor (const key of [\"allOf\", \"anyOf\", \"oneOf\", \"prefixItems\", \"items\"]) {\n\t\tconst children = schema[key]\n\t\tif (Array.isArray(children)) {\n\t\t\tfor (const [index, child] of children.entries())\n\t\t\t\tyield [pointer([key, index]), child]\n\t\t} else if (key === \"items\" && children !== undefined)\n\t\t\tyield [pointer([key]), children]\n\t}\n}\n","import { createHash, randomUUID } from \"node:crypto\"\nimport { mkdir, readFile, rename, writeFile } from \"node:fs/promises\"\nimport path from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\nimport type { AnySchema, SchemaObject } from \"ajv\"\nimport { parseDocument } from \"./parse.ts\"\nimport { readText, type Project, type ReadText } from \"./config.ts\"\nimport { CorrectlyError } from \"./types.ts\"\nimport { schemaChildren } from \"./schema-walk.ts\"\n\nexport type Dialect = \"draft7\" | \"2020-12\"\nexport const DIALECTS = {\n\tdraft7: \"http://json-schema.org/draft-07/schema#\",\n\t\"2020-12\": \"https://json-schema.org/draft/2020-12/schema\",\n} as const\nconst VOCABULARIES = new Set(\n\t[\n\t\t\"core\",\n\t\t\"applicator\",\n\t\t\"unevaluated\",\n\t\t\"validation\",\n\t\t\"meta-data\",\n\t\t\"format-annotation\",\n\t\t\"content\",\n\t].map((name) => `https://json-schema.org/draft/2020-12/vocab/${name}`),\n)\n\nfunction isObject(value: unknown): value is SchemaObject {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value)\n}\nfunction dialect(value: unknown, inherited: Dialect): Dialect {\n\tif (value === undefined) return inherited\n\tif (typeof value !== \"string\")\n\t\tthrow new CorrectlyError(\"schema\", \"Schema $schema must be a string\")\n\tconst id = value.replace(/#$/, \"\")\n\tif (\n\t\tid === \"http://json-schema.org/draft-07/schema\" ||\n\t\tid === \"https://json-schema.org/draft-07/schema\"\n\t)\n\t\treturn \"draft7\"\n\tif (id === DIALECTS[\"2020-12\"]) return \"2020-12\"\n\tthrow new CorrectlyError(\n\t\t\"unsupported-dialect\",\n\t\t`Unsupported schema dialect: ${value}; supported: draft 7, draft 2020-12`,\n\t)\n}\n\nexport type SchemaResource = {\n\turi: string\n\tschema: AnySchema\n\tdialect: Dialect\n\tpointer?: string\n}\ntype CacheEntry = {\n\turi: string\n\ttext: string\n\tetag?: string\n\tlastModified?: string\n}\nexport type StoreOptions = {\n\toffline?: boolean\n\tread?: ReadText\n\tfetch?: typeof fetch\n\tsignal?: AbortSignal\n}\n\nexport class SchemaStore {\n\tprivate readonly options: StoreOptions\n\treadonly resources = new Map<string, SchemaResource>()\n\tprivate readonly pending = new Map<string, Promise<SchemaResource>>()\n\tprivate readonly cacheDir: string\n\tprivate readonly offline: boolean\n\tprivate readonly timeoutMs: number\n\tprivate readonly maxBytes: number\n\tprivate readonly maxRequests: number\n\tprivate readonly read: ReadText\n\tprivate readonly request: typeof fetch\n\tprivate requests = 0\n\tconstructor(\n\t\tproject: Pick<Project, \"root\"> & {\n\t\t\tconfig: Pick<Project[\"config\"], \"remote\">\n\t\t},\n\t\toptions: StoreOptions = {},\n\t) {\n\t\tthis.options = options\n\t\tconst remote = project.config.remote\n\t\tthis.cacheDir = path.resolve(\n\t\t\tproject.root,\n\t\t\tremote?.cacheDir ?? \".correctly-cache\",\n\t\t)\n\t\tthis.offline = options.offline ?? remote?.offline ?? false\n\t\tthis.timeoutMs = remote?.timeoutMs ?? 5000\n\t\tthis.maxBytes = remote?.maxBytes ?? 2 * 1024 * 1024\n\t\tthis.maxRequests = remote?.maxRequests ?? 64\n\t\tthis.read = options.read ?? readText\n\t\tthis.request = options.fetch ?? fetch\n\t}\n\n\tasync load(\n\t\tvalue: string,\n\t\tinherited: Dialect = \"draft7\",\n\t): Promise<SchemaResource> {\n\t\tthis.options.signal?.throwIfAborted()\n\t\tconst url = new URL(value)\n\t\turl.hash = \"\"\n\t\tconst uri = url.href\n\t\tconst known = this.resources.get(uri)\n\t\tif (known) return known\n\t\tlet promise = this.pending.get(uri)\n\t\tif (!promise) {\n\t\t\tpromise = this.loadResource(uri, inherited)\n\t\t\tthis.pending.set(uri, promise)\n\t\t}\n\t\treturn promise\n\t}\n\n\tprivate async loadResource(\n\t\turi: string,\n\t\tinherited: Dialect,\n\t): Promise<SchemaResource> {\n\t\tif (++this.requests > this.maxRequests)\n\t\t\tthrow new CorrectlyError(\n\t\t\t\t\"schema-limit\",\n\t\t\t\t`Schema resource limit (${this.maxRequests}) exceeded`,\n\t\t\t)\n\t\tlet text: string\n\t\tconst url = new URL(uri)\n\t\ttry {\n\t\t\tif (url.protocol === \"file:\") text = await this.read(uri)\n\t\t\telse if (url.protocol === \"https:\" || url.protocol === \"http:\")\n\t\t\t\ttext = await this.remote(uri)\n\t\t\telse\n\t\t\t\tthrow new CorrectlyError(\n\t\t\t\t\t\"schema\",\n\t\t\t\t\t`Unsupported schema protocol: ${url.protocol}`,\n\t\t\t\t)\n\t\t} catch (error) {\n\t\t\tif (error instanceof CorrectlyError) throw error\n\t\t\tthrow new CorrectlyError(\n\t\t\t\t\"schema-load\",\n\t\t\t\t`Cannot load schema ${uri}: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t)\n\t\t}\n\t\tthis.options.signal?.throwIfAborted()\n\t\tif (Buffer.byteLength(text) > this.maxBytes)\n\t\t\tthrow new CorrectlyError(\n\t\t\t\t\"schema-limit\",\n\t\t\t\t`Schema ${uri} exceeds ${this.maxBytes} bytes`,\n\t\t\t)\n\t\tconst parsed = parseDocument(text, \"json\")\n\t\tif (parsed.diagnostics.length) {\n\t\t\tconst d = parsed.diagnostics[0]!\n\t\t\tthrow new CorrectlyError(\n\t\t\t\t\"schema\",\n\t\t\t\t`Invalid schema JSON at ${uri}:${d.range.start.line + 1}:${d.range.start.character + 1}: ${d.message}`,\n\t\t\t)\n\t\t}\n\t\tif (typeof parsed.value !== \"boolean\" && !isObject(parsed.value))\n\t\t\tthrow new CorrectlyError(\n\t\t\t\t\"schema\",\n\t\t\t\t`Schema must be an object or boolean: ${uri}`,\n\t\t\t)\n\t\tconst rootDialect = dialect(\n\t\t\tisObject(parsed.value) ? parsed.value.$schema : undefined,\n\t\t\tinherited,\n\t\t)\n\t\tconst schema = parsed.value as AnySchema\n\t\tconst resource: SchemaResource = { uri, schema, dialect: rootDialect }\n\t\t// Normalize schema identifiers only, never document data or annotation values.\n\t\tthis.normalize(schema, uri, rootDialect, resource)\n\t\tif (isObject(schema) && schema.$id === undefined) schema.$id = uri\n\t\tthis.register(uri, resource)\n\t\treturn resource\n\t}\n\n\tprivate register(uri: string, resource: SchemaResource) {\n\t\tconst previous = this.resources.get(uri)\n\t\tif (previous && previous.schema !== resource.schema)\n\t\t\tthrow new CorrectlyError(\"schema\", `Duplicate schema identifier: ${uri}`)\n\t\tthis.resources.set(uri, resource)\n\t}\n\n\tprivate normalize(\n\t\tschema: unknown,\n\t\tbase: string,\n\t\texpected: Dialect,\n\t\troot: SchemaResource,\n\t\tlocation = \"\",\n\t) {\n\t\tif (!isObject(schema)) return\n\t\tconst ownDialect = dialect(schema.$schema, expected)\n\t\tif (ownDialect !== expected)\n\t\t\tthrow new CorrectlyError(\n\t\t\t\t\"unsupported-dialect\",\n\t\t\t\t`Mixed schema dialects at ${base} are not supported`,\n\t\t\t)\n\t\tif (schema.$schema !== undefined) schema.$schema = DIALECTS[ownDialect]\n\t\tif (schema.$async !== undefined)\n\t\t\tthrow new CorrectlyError(\n\t\t\t\t\"schema\",\n\t\t\t\t`Asynchronous data validation is not supported: ${base}`,\n\t\t\t)\n\t\tif (schema.$vocabulary !== undefined) {\n\t\t\tif (!isObject(schema.$vocabulary))\n\t\t\t\tthrow new CorrectlyError(\n\t\t\t\t\t\"schema\",\n\t\t\t\t\t`$vocabulary must be an object: ${base}`,\n\t\t\t\t)\n\t\t\tfor (const [vocabulary, required] of Object.entries(schema.$vocabulary)) {\n\t\t\t\tif (typeof required !== \"boolean\")\n\t\t\t\t\tthrow new CorrectlyError(\n\t\t\t\t\t\t\"schema\",\n\t\t\t\t\t\t`$vocabulary values must be boolean: ${base}`,\n\t\t\t\t\t)\n\t\t\t\tif (required && !VOCABULARIES.has(vocabulary))\n\t\t\t\t\tthrow new CorrectlyError(\n\t\t\t\t\t\t\"unsupported-vocabulary\",\n\t\t\t\t\t\t`Unsupported required vocabulary: ${vocabulary}`,\n\t\t\t\t\t)\n\t\t\t}\n\t\t}\n\t\tlet scope = base\n\t\tif (schema.$id !== undefined) {\n\t\t\tif (typeof schema.$id !== \"string\")\n\t\t\t\tthrow new CorrectlyError(\"schema\", `$id must be a string: ${base}`)\n\t\t\tscope = new URL(schema.$id, base).href\n\t\t\tschema.$id = scope\n\t\t\tthis.register(scope.replace(/#$/, \"\"), {\n\t\t\t\t...root,\n\t\t\t\tschema,\n\t\t\t\tpointer: location,\n\t\t\t})\n\t\t}\n\t\tfor (const anchor of [schema.$anchor, schema.$dynamicAnchor]) {\n\t\t\tif (typeof anchor === \"string\")\n\t\t\t\tthis.register(new URL(`#${anchor}`, scope).href, {\n\t\t\t\t\t...root,\n\t\t\t\t\tschema,\n\t\t\t\t\tpointer: location,\n\t\t\t\t})\n\t\t}\n\t\tfor (const [path, child] of schemaChildren(schema))\n\t\t\tthis.normalize(child, scope, expected, root, location + path)\n\t}\n\n\tprivate async remote(uri: string): Promise<string> {\n\t\tconst cacheFile = path.join(\n\t\t\tthis.cacheDir,\n\t\t\t`${createHash(\"sha256\").update(uri).digest(\"hex\")}.json`,\n\t\t)\n\t\tlet cached: CacheEntry | undefined\n\t\ttry {\n\t\t\tconst raw = await readFile(cacheFile, \"utf8\")\n\t\t\tconst entry: unknown = JSON.parse(raw)\n\t\t\tif (\n\t\t\t\t!isObject(entry) ||\n\t\t\t\tentry.uri !== uri ||\n\t\t\t\ttypeof entry.text !== \"string\"\n\t\t\t)\n\t\t\t\tthrow new Error(\"invalid cache entry\")\n\t\t\tcached = entry as CacheEntry\n\t\t} catch (error) {\n\t\t\tif ((error as NodeJS.ErrnoException).code !== \"ENOENT\")\n\t\t\t\tthrow new CorrectlyError(\n\t\t\t\t\t\"schema-cache\",\n\t\t\t\t\t`Cannot read schema cache ${cacheFile}: ${String(error)}`,\n\t\t\t\t)\n\t\t}\n\t\tif (this.offline) {\n\t\t\tif (!cached)\n\t\t\t\tthrow new CorrectlyError(\n\t\t\t\t\t\"offline-miss\",\n\t\t\t\t\t`Schema is not cached in offline mode: ${uri}`,\n\t\t\t\t)\n\t\t\treturn cached.text\n\t\t}\n\t\tconst timeout = AbortSignal.timeout(this.timeoutMs)\n\t\tconst signal = this.options.signal\n\t\t\t? AbortSignal.any([this.options.signal, timeout])\n\t\t\t: timeout\n\t\tconst headers: Record<string, string> = {}\n\t\tif (cached?.etag) headers[\"If-None-Match\"] = cached.etag\n\t\tif (cached?.lastModified) headers[\"If-Modified-Since\"] = cached.lastModified\n\t\tlet current = uri\n\t\tfor (let redirects = 0; redirects <= 5; redirects++) {\n\t\t\tconst response = await this.request(current, {\n\t\t\t\tsignal,\n\t\t\t\theaders,\n\t\t\t\tredirect: \"manual\",\n\t\t\t})\n\t\t\tif ([301, 302, 303, 307, 308].includes(response.status)) {\n\t\t\t\tawait response.body?.cancel()\n\t\t\t\tconst location = response.headers.get(\"location\")\n\t\t\t\tif (!location)\n\t\t\t\t\tthrow new CorrectlyError(\n\t\t\t\t\t\t\"schema-load\",\n\t\t\t\t\t\t`Redirect without location: ${current}`,\n\t\t\t\t\t)\n\t\t\t\tcurrent = new URL(location, current).href\n\t\t\t\tif (![\"http:\", \"https:\"].includes(new URL(current).protocol))\n\t\t\t\t\tthrow new CorrectlyError(\n\t\t\t\t\t\t\"schema-load\",\n\t\t\t\t\t\t`Unsupported redirect: ${current}`,\n\t\t\t\t\t)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif (response.status === 304 && cached) {\n\t\t\t\tawait response.body?.cancel()\n\t\t\t\treturn cached.text\n\t\t\t}\n\t\t\tif (!response.ok || !response.body) {\n\t\t\t\tawait response.body?.cancel()\n\t\t\t\tthrow new CorrectlyError(\n\t\t\t\t\t\"schema-load\",\n\t\t\t\t\t`HTTP ${response.status} loading schema ${uri}`,\n\t\t\t\t)\n\t\t\t}\n\t\t\tconst reader = response.body.getReader()\n\t\t\tconst chunks: Uint8Array[] = []\n\t\t\tlet size = 0\n\t\t\ttry {\n\t\t\t\twhile (true) {\n\t\t\t\t\tconst { value, done } = await reader.read()\n\t\t\t\t\tif (done) break\n\t\t\t\t\tsize += value.byteLength\n\t\t\t\t\tif (size > this.maxBytes)\n\t\t\t\t\t\tthrow new CorrectlyError(\n\t\t\t\t\t\t\t\"schema-limit\",\n\t\t\t\t\t\t\t`Schema ${uri} exceeds ${this.maxBytes} bytes`,\n\t\t\t\t\t\t)\n\t\t\t\t\tchunks.push(value)\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\tawait reader.cancel()\n\t\t\t}\n\t\t\tconst text = Buffer.concat(chunks).toString(\"utf8\")\n\t\t\tconst entry: CacheEntry = { uri, text }\n\t\t\tconst etag = response.headers.get(\"etag\")\n\t\t\tconst lastModified = response.headers.get(\"last-modified\")\n\t\t\tif (etag) entry.etag = etag\n\t\t\tif (lastModified) entry.lastModified = lastModified\n\t\t\tsignal.throwIfAborted()\n\t\t\tawait mkdir(this.cacheDir, { recursive: true })\n\t\t\tconst temporary = `${cacheFile}.${randomUUID()}.tmp`\n\t\t\tawait writeFile(temporary, JSON.stringify(entry))\n\t\t\tawait rename(temporary, cacheFile)\n\t\t\treturn text\n\t\t}\n\t\tthrow new CorrectlyError(\n\t\t\t\"schema-limit\",\n\t\t\t`Too many redirects loading schema ${uri}`,\n\t\t)\n\t}\n}\n\nexport function localSchemaPath(uri: string): string | undefined {\n\tconst url = new URL(uri)\n\treturn url.protocol === \"file:\" ? fileURLToPath(url) : undefined\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { m as CorrectlyError } from "./config-Cb6o4YnJ.mjs";
|
|
2
|
+
import { Worker } from "node:worker_threads";
|
|
3
|
+
//#region src/runtime/session.ts
|
|
4
|
+
/** Functions stay in this worker, which is replaced to invalidate native module caches. */
|
|
5
|
+
var ProjectSession = class {
|
|
6
|
+
worker;
|
|
7
|
+
pending = /* @__PURE__ */ new Map();
|
|
8
|
+
next = 0;
|
|
9
|
+
closed = false;
|
|
10
|
+
disposal;
|
|
11
|
+
ready;
|
|
12
|
+
constructor(configPath, options = {}) {
|
|
13
|
+
const source = import.meta.url.endsWith(".ts");
|
|
14
|
+
this.worker = new Worker(new URL(source ? "./worker.ts" : "./worker.mjs", import.meta.url), {
|
|
15
|
+
stdout: true,
|
|
16
|
+
stderr: true
|
|
17
|
+
});
|
|
18
|
+
this.worker.stdout?.on("data", (chunk) => process.stderr.write(chunk));
|
|
19
|
+
this.worker.stderr?.on("data", (chunk) => process.stderr.write(chunk));
|
|
20
|
+
this.worker.on("message", (message) => {
|
|
21
|
+
if (message.dependency) {
|
|
22
|
+
options.onDependency?.(message.dependency.uri, message.dependency.kind);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const pending = this.pending.get(message.id);
|
|
26
|
+
if (!pending) return;
|
|
27
|
+
this.pending.delete(message.id);
|
|
28
|
+
if (message.error) pending.reject(new CorrectlyError(message.error.code, message.error.message, message.error.details));
|
|
29
|
+
else pending.resolve(message.value);
|
|
30
|
+
if (!this.pending.size) this.worker.unref();
|
|
31
|
+
});
|
|
32
|
+
this.worker.on("error", (error) => this.fail(new CorrectlyError("config", `Project worker failed: ${error instanceof Error ? error.message : String(error)}`)));
|
|
33
|
+
this.worker.on("exit", (code) => this.fail(new CorrectlyError("config", `Project worker exited (${code})`)));
|
|
34
|
+
this.ready = this.request("init", {
|
|
35
|
+
configPath,
|
|
36
|
+
offline: options.offline,
|
|
37
|
+
buffers: options.buffers ?? []
|
|
38
|
+
});
|
|
39
|
+
this.ready.catch(() => {});
|
|
40
|
+
}
|
|
41
|
+
fail(error) {
|
|
42
|
+
this.closed = true;
|
|
43
|
+
for (const pending of this.pending.values()) pending.reject(error);
|
|
44
|
+
this.pending.clear();
|
|
45
|
+
}
|
|
46
|
+
request(method, params) {
|
|
47
|
+
if (this.closed) return Promise.reject(new CorrectlyError("execution", "Project worker has been closed"));
|
|
48
|
+
const id = this.next++;
|
|
49
|
+
this.worker.ref();
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
this.pending.set(id, {
|
|
52
|
+
resolve: (value) => resolve(value),
|
|
53
|
+
reject
|
|
54
|
+
});
|
|
55
|
+
this.worker.postMessage({
|
|
56
|
+
id,
|
|
57
|
+
method,
|
|
58
|
+
params
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async validate(file, text) {
|
|
63
|
+
await this.ready;
|
|
64
|
+
return this.request("validate", {
|
|
65
|
+
file,
|
|
66
|
+
text
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
async complete(document, position) {
|
|
70
|
+
await this.ready;
|
|
71
|
+
return this.request("complete", {
|
|
72
|
+
uri: document.uri,
|
|
73
|
+
text: document.getText(),
|
|
74
|
+
language: document.languageId,
|
|
75
|
+
version: document.version,
|
|
76
|
+
position
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async hover(document, position) {
|
|
80
|
+
await this.ready;
|
|
81
|
+
return this.request("hover", {
|
|
82
|
+
uri: document.uri,
|
|
83
|
+
text: document.getText(),
|
|
84
|
+
language: document.languageId,
|
|
85
|
+
version: document.version,
|
|
86
|
+
position
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
async check(params) {
|
|
90
|
+
await this.ready;
|
|
91
|
+
return this.request("check", params);
|
|
92
|
+
}
|
|
93
|
+
dispose() {
|
|
94
|
+
return this.disposal ??= (async () => {
|
|
95
|
+
const finished = this.closed ? Promise.resolve() : this.request("dispose", {}).catch(() => {});
|
|
96
|
+
this.closed = true;
|
|
97
|
+
let timer;
|
|
98
|
+
try {
|
|
99
|
+
await Promise.race([finished, new Promise((resolve) => {
|
|
100
|
+
timer = setTimeout(resolve, 250);
|
|
101
|
+
})]);
|
|
102
|
+
} finally {
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
this.fail(new CorrectlyError("execution", "Project configuration was invalidated"));
|
|
105
|
+
await this.worker.terminate();
|
|
106
|
+
}
|
|
107
|
+
})();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
//#endregion
|
|
111
|
+
export { ProjectSession as t };
|
|
112
|
+
|
|
113
|
+
//# sourceMappingURL=session-DCFav56x.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-DCFav56x.mjs","names":[],"sources":["../src/runtime/session.ts"],"sourcesContent":["import { Worker } from \"node:worker_threads\"\nimport type { TextDocument } from \"vscode-languageserver-textdocument\"\nimport type { CompletionList, Hover } from \"vscode-json-languageservice\"\nimport {\n\tCorrectlyError,\n\ttype Failure,\n\ttype FileResult,\n\ttype Position,\n\ttype Report,\n} from \"../core/types.ts\"\n\nexport type SessionOptions = {\n\toffline?: boolean\n\tbuffers?: [string, string][]\n\tonDependency?: (uri: string, kind: \"module\" | \"resource\") => void\n}\nexport type CheckResult = { report: Report; sources: [string, string][] }\n/** Functions stay in this worker, which is replaced to invalidate native module caches. */\nexport class ProjectSession {\n\tprivate readonly worker: Worker\n\tprivate readonly pending = new Map<\n\t\tnumber,\n\t\t{ resolve(value: unknown): void; reject(error: Error): void }\n\t>()\n\tprivate next = 0\n\tprivate closed = false\n\tprivate disposal: Promise<void> | undefined\n\treadonly ready: Promise<void>\n\tconstructor(configPath: string, options: SessionOptions = {}) {\n\t\tconst source = import.meta.url.endsWith(\".ts\")\n\t\tthis.worker = new Worker(\n\t\t\tnew URL(source ? \"./worker.ts\" : \"./worker.mjs\", import.meta.url),\n\t\t\t{ stdout: true, stderr: true },\n\t\t)\n\t\tthis.worker.stdout?.on(\"data\", (chunk: Buffer) =>\n\t\t\tprocess.stderr.write(chunk),\n\t\t)\n\t\tthis.worker.stderr?.on(\"data\", (chunk: Buffer) =>\n\t\t\tprocess.stderr.write(chunk),\n\t\t)\n\t\tthis.worker.on(\n\t\t\t\"message\",\n\t\t\t(message: {\n\t\t\t\tid?: number\n\t\t\t\tvalue?: unknown\n\t\t\t\terror?: Failure\n\t\t\t\tdependency?: { uri: string; kind: \"module\" | \"resource\" }\n\t\t\t}) => {\n\t\t\t\tif (message.dependency) {\n\t\t\t\t\toptions.onDependency?.(\n\t\t\t\t\t\tmessage.dependency.uri,\n\t\t\t\t\t\tmessage.dependency.kind,\n\t\t\t\t\t)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst pending = this.pending.get(message.id!)\n\t\t\t\tif (!pending) return\n\t\t\t\tthis.pending.delete(message.id!)\n\t\t\t\tif (message.error)\n\t\t\t\t\tpending.reject(\n\t\t\t\t\t\tnew CorrectlyError(\n\t\t\t\t\t\t\tmessage.error.code,\n\t\t\t\t\t\t\tmessage.error.message,\n\t\t\t\t\t\t\tmessage.error.details,\n\t\t\t\t\t\t),\n\t\t\t\t\t)\n\t\t\t\telse pending.resolve(message.value)\n\t\t\t\tif (!this.pending.size) this.worker.unref()\n\t\t\t},\n\t\t)\n\t\tthis.worker.on(\"error\", (error) =>\n\t\t\tthis.fail(\n\t\t\t\tnew CorrectlyError(\n\t\t\t\t\t\"config\",\n\t\t\t\t\t`Project worker failed: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t),\n\t\t\t),\n\t\t)\n\t\tthis.worker.on(\"exit\", (code) =>\n\t\t\tthis.fail(\n\t\t\t\tnew CorrectlyError(\"config\", `Project worker exited (${code})`),\n\t\t\t),\n\t\t)\n\t\tthis.ready = this.request<void>(\"init\", {\n\t\t\tconfigPath,\n\t\t\toffline: options.offline,\n\t\t\tbuffers: options.buffers ?? [],\n\t\t})\n\t\t// Avoid an unhandled rejection if a project is invalidated before its consumer awaits it.\n\t\tvoid this.ready.catch(() => {})\n\t}\n\tprivate fail(error: Error) {\n\t\tthis.closed = true\n\t\tfor (const pending of this.pending.values()) pending.reject(error)\n\t\tthis.pending.clear()\n\t}\n\tprivate request<T>(method: string, params: unknown): Promise<T> {\n\t\tif (this.closed)\n\t\t\treturn Promise.reject(\n\t\t\t\tnew CorrectlyError(\"execution\", \"Project worker has been closed\"),\n\t\t\t)\n\t\tconst id = this.next++\n\t\tthis.worker.ref()\n\t\treturn new Promise<T>((resolve, reject) => {\n\t\t\tthis.pending.set(id, { resolve: (value) => resolve(value as T), reject })\n\t\t\tthis.worker.postMessage({ id, method, params })\n\t\t})\n\t}\n\tasync validate(file: string, text: string): Promise<FileResult> {\n\t\tawait this.ready\n\t\treturn this.request(\"validate\", { file, text })\n\t}\n\tasync complete(\n\t\tdocument: TextDocument,\n\t\tposition: Position,\n\t): Promise<CompletionList | null> {\n\t\tawait this.ready\n\t\treturn this.request(\"complete\", {\n\t\t\turi: document.uri,\n\t\t\ttext: document.getText(),\n\t\t\tlanguage: document.languageId,\n\t\t\tversion: document.version,\n\t\t\tposition,\n\t\t})\n\t}\n\tasync hover(\n\t\tdocument: TextDocument,\n\t\tposition: Position,\n\t): Promise<Hover | null> {\n\t\tawait this.ready\n\t\treturn this.request(\"hover\", {\n\t\t\turi: document.uri,\n\t\t\ttext: document.getText(),\n\t\t\tlanguage: document.languageId,\n\t\t\tversion: document.version,\n\t\t\tposition,\n\t\t})\n\t}\n\tasync check(params: {\n\t\tcwd: string\n\t\tfiles?: string[]\n\t\tsources: boolean\n\t}): Promise<CheckResult> {\n\t\tawait this.ready\n\t\treturn this.request(\"check\", params)\n\t}\n\tdispose(): Promise<void> {\n\t\treturn (this.disposal ??= (async () => {\n\t\t\tconst finished = this.closed\n\t\t\t\t? Promise.resolve()\n\t\t\t\t: this.request(\"dispose\", {}).catch(() => {})\n\t\t\tthis.closed = true\n\t\t\tlet timer: ReturnType<typeof setTimeout> | undefined\n\t\t\ttry {\n\t\t\t\tawait Promise.race([\n\t\t\t\t\tfinished,\n\t\t\t\t\tnew Promise<void>((resolve) => {\n\t\t\t\t\t\ttimer = setTimeout(resolve, 250)\n\t\t\t\t\t}),\n\t\t\t\t])\n\t\t\t} finally {\n\t\t\t\tclearTimeout(timer)\n\t\t\t\tthis.fail(\n\t\t\t\t\tnew CorrectlyError(\n\t\t\t\t\t\t\"execution\",\n\t\t\t\t\t\t\"Project configuration was invalidated\",\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t\tawait this.worker.terminate()\n\t\t\t}\n\t\t})())\n\t}\n}\n"],"mappings":"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"}
|
package/dist/worker.mjs
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { d as readText, h as failure, l as loadProject } from "./config-Cb6o4YnJ.mjs";
|
|
2
|
+
import { n as checkProject } from "./check-BI7U3ZLc.mjs";
|
|
3
|
+
import { t as Hints } from "./hints-HvSIhuMM.mjs";
|
|
4
|
+
import { t as Engine } from "./engine-VST0hoSk.mjs";
|
|
5
|
+
import { registerHooks } from "node:module";
|
|
6
|
+
import { parentPort } from "node:worker_threads";
|
|
7
|
+
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
8
|
+
//#region src/runtime/worker.ts
|
|
9
|
+
if (!parentPort) throw new Error("Correctly project runtime requires a worker");
|
|
10
|
+
const port = parentPort;
|
|
11
|
+
let engine;
|
|
12
|
+
const hints = new Hints();
|
|
13
|
+
const controller = new AbortController();
|
|
14
|
+
const dependencies = /* @__PURE__ */ new Set();
|
|
15
|
+
function dependency(uri, kind) {
|
|
16
|
+
const key = `${kind}:${uri}`;
|
|
17
|
+
if (!uri.startsWith("file:") || dependencies.has(key)) return;
|
|
18
|
+
dependencies.add(key);
|
|
19
|
+
port.postMessage({ dependency: {
|
|
20
|
+
uri,
|
|
21
|
+
kind
|
|
22
|
+
} });
|
|
23
|
+
}
|
|
24
|
+
registerHooks({ resolve(specifier, context, next) {
|
|
25
|
+
if (context.parentURL && (specifier.startsWith(".") || specifier.startsWith("file:"))) dependency(new URL(specifier, context.parentURL).href, "module");
|
|
26
|
+
const result = next(specifier, context);
|
|
27
|
+
dependency(result.url, "module");
|
|
28
|
+
return result;
|
|
29
|
+
} });
|
|
30
|
+
port.on("message", (message) => {
|
|
31
|
+
(async () => {
|
|
32
|
+
const p = message.params;
|
|
33
|
+
if (message.method === "dispose") {
|
|
34
|
+
controller.abort();
|
|
35
|
+
await engine?.dispose();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (message.method === "init") {
|
|
39
|
+
const buffers = new Map(p.buffers);
|
|
40
|
+
const project = await loadProject(p.configPath);
|
|
41
|
+
engine = new Engine(project, {
|
|
42
|
+
signal: controller.signal,
|
|
43
|
+
read: (uri) => buffers.has(uri) ? Promise.resolve(buffers.get(uri)) : readText(uri),
|
|
44
|
+
...p.offline === void 0 ? {} : { offline: p.offline },
|
|
45
|
+
onDependency: (uri) => dependency(uri, "resource")
|
|
46
|
+
});
|
|
47
|
+
await engine.prepare();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (!engine) throw new Error("Project is not initialized");
|
|
51
|
+
if (message.method === "validate") return engine.validate(p.file, p.text);
|
|
52
|
+
if (message.method === "check") {
|
|
53
|
+
const sources = [];
|
|
54
|
+
return {
|
|
55
|
+
report: await checkProject(engine, {
|
|
56
|
+
cwd: p.cwd,
|
|
57
|
+
...p.files ? { files: p.files } : {},
|
|
58
|
+
...p.sources ? { onRead: (file, text) => {
|
|
59
|
+
sources.push([file, text]);
|
|
60
|
+
} } : {}
|
|
61
|
+
}),
|
|
62
|
+
sources
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const document = TextDocument.create(p.uri, p.language, p.version, p.text);
|
|
66
|
+
if (message.method === "complete") return hints.complete(engine, document, p.position);
|
|
67
|
+
if (message.method === "hover") return hints.hover(engine, document, p.position);
|
|
68
|
+
throw new Error(`Unknown project operation: ${message.method}`);
|
|
69
|
+
})().then((value) => port.postMessage({
|
|
70
|
+
id: message.id,
|
|
71
|
+
value
|
|
72
|
+
}), (error) => port.postMessage({
|
|
73
|
+
id: message.id,
|
|
74
|
+
error: failure(error)
|
|
75
|
+
}));
|
|
76
|
+
});
|
|
77
|
+
//#endregion
|
|
78
|
+
export {};
|
|
79
|
+
|
|
80
|
+
//# sourceMappingURL=worker.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.mjs","names":[],"sources":["../src/runtime/worker.ts"],"sourcesContent":["import { parentPort } from \"node:worker_threads\"\nimport { registerHooks } from \"node:module\"\nimport { TextDocument } from \"vscode-languageserver-textdocument\"\nimport { loadProject, readText } from \"../core/config.ts\"\nimport { Engine } from \"../core/engine.ts\"\nimport { failure, type Position } from \"../core/types.ts\"\nimport { Hints } from \"../lsp/hints.ts\"\nimport { checkProject } from \"../cli/check.ts\"\n\nif (!parentPort) throw new Error(\"Correctly project runtime requires a worker\")\nconst port = parentPort\nlet engine: Engine | undefined\nconst hints = new Hints()\nconst controller = new AbortController()\nconst dependencies = new Set<string>()\nfunction dependency(uri: string, kind: \"module\" | \"resource\") {\n\tconst key = `${kind}:${uri}`\n\tif (!uri.startsWith(\"file:\") || dependencies.has(key)) return\n\tdependencies.add(key)\n\tport.postMessage({ dependency: { uri, kind } })\n}\n// Comline owns loading; hooks only observe dependencies, including failed relative imports.\nregisterHooks({\n\tresolve(specifier, context, next) {\n\t\tif (\n\t\t\tcontext.parentURL &&\n\t\t\t(specifier.startsWith(\".\") || specifier.startsWith(\"file:\"))\n\t\t)\n\t\t\tdependency(new URL(specifier, context.parentURL).href, \"module\")\n\t\tconst result = next(specifier, context)\n\t\tdependency(result.url, \"module\")\n\t\treturn result\n\t},\n})\nport.on(\n\t\"message\",\n\t(message: {\n\t\tid: number\n\t\tmethod: string\n\t\tparams: Record<string, unknown>\n\t}) => {\n\t\tvoid (async () => {\n\t\t\tconst p = message.params\n\t\t\tif (message.method === \"dispose\") {\n\t\t\t\tcontroller.abort()\n\t\t\t\tawait engine?.dispose()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (message.method === \"init\") {\n\t\t\t\tconst buffers = new Map(p.buffers as [string, string][])\n\t\t\t\tconst project = await loadProject(p.configPath as string)\n\t\t\t\tengine = new Engine(project, {\n\t\t\t\t\tsignal: controller.signal,\n\t\t\t\t\tread: (uri) =>\n\t\t\t\t\t\tbuffers.has(uri)\n\t\t\t\t\t\t\t? Promise.resolve(buffers.get(uri)!)\n\t\t\t\t\t\t\t: readText(uri),\n\t\t\t\t\t...(p.offline === undefined ? {} : { offline: p.offline as boolean }),\n\t\t\t\t\tonDependency: (uri) => dependency(uri, \"resource\"),\n\t\t\t\t})\n\t\t\t\tawait engine.prepare()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!engine) throw new Error(\"Project is not initialized\")\n\t\t\tif (message.method === \"validate\")\n\t\t\t\treturn engine.validate(p.file as string, p.text as string)\n\t\t\tif (message.method === \"check\") {\n\t\t\t\tconst sources: [string, string][] = []\n\t\t\t\tconst report = await checkProject(engine, {\n\t\t\t\t\tcwd: p.cwd as string,\n\t\t\t\t\t...(p.files ? { files: p.files as string[] } : {}),\n\t\t\t\t\t...(p.sources\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\tonRead: (file: string, text: string) => {\n\t\t\t\t\t\t\t\t\tsources.push([file, text])\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t: {}),\n\t\t\t\t})\n\t\t\t\treturn { report, sources }\n\t\t\t}\n\t\t\tconst document = TextDocument.create(\n\t\t\t\tp.uri as string,\n\t\t\t\tp.language as string,\n\t\t\t\tp.version as number,\n\t\t\t\tp.text as string,\n\t\t\t)\n\t\t\tif (message.method === \"complete\")\n\t\t\t\treturn hints.complete(engine, document, p.position as Position)\n\t\t\tif (message.method === \"hover\")\n\t\t\t\treturn hints.hover(engine, document, p.position as Position)\n\t\t\tthrow new Error(`Unknown project operation: ${message.method}`)\n\t\t})().then(\n\t\t\t(value) => port.postMessage({ id: message.id, value }),\n\t\t\t(error: unknown) =>\n\t\t\t\tport.postMessage({ id: message.id, error: failure(error) }),\n\t\t)\n\t},\n)\n"],"mappings":";;;;;;;;AASA,IAAI,CAAC,YAAY,MAAM,IAAI,MAAM,6CAA6C;AAC9E,MAAM,OAAO;AACb,IAAI;AACJ,MAAM,QAAQ,IAAI,MAAM;AACxB,MAAM,aAAa,IAAI,gBAAgB;AACvC,MAAM,+BAAe,IAAI,IAAY;AACrC,SAAS,WAAW,KAAa,MAA6B;CAC7D,MAAM,MAAM,GAAG,KAAK,GAAG;CACvB,IAAI,CAAC,IAAI,WAAW,OAAO,KAAK,aAAa,IAAI,GAAG,GAAG;CACvD,aAAa,IAAI,GAAG;CACpB,KAAK,YAAY,EAAE,YAAY;EAAE;EAAK;CAAK,EAAE,CAAC;AAC/C;AAEA,cAAc,EACb,QAAQ,WAAW,SAAS,MAAM;CACjC,IACC,QAAQ,cACP,UAAU,WAAW,GAAG,KAAK,UAAU,WAAW,OAAO,IAE1D,WAAW,IAAI,IAAI,WAAW,QAAQ,SAAS,CAAC,CAAC,MAAM,QAAQ;CAChE,MAAM,SAAS,KAAK,WAAW,OAAO;CACtC,WAAW,OAAO,KAAK,QAAQ;CAC/B,OAAO;AACR,EACD,CAAC;AACD,KAAK,GACJ,YACC,YAIK;CACL,CAAM,YAAY;EACjB,MAAM,IAAI,QAAQ;EAClB,IAAI,QAAQ,WAAW,WAAW;GACjC,WAAW,MAAM;GACjB,MAAM,QAAQ,QAAQ;GACtB;EACD;EACA,IAAI,QAAQ,WAAW,QAAQ;GAC9B,MAAM,UAAU,IAAI,IAAI,EAAE,OAA6B;GACvD,MAAM,UAAU,MAAM,YAAY,EAAE,UAAoB;GACxD,SAAS,IAAI,OAAO,SAAS;IAC5B,QAAQ,WAAW;IACnB,OAAO,QACN,QAAQ,IAAI,GAAG,IACZ,QAAQ,QAAQ,QAAQ,IAAI,GAAG,CAAE,IACjC,SAAS,GAAG;IAChB,GAAI,EAAE,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,EAAE,QAAmB;IACnE,eAAe,QAAQ,WAAW,KAAK,UAAU;GAClD,CAAC;GACD,MAAM,OAAO,QAAQ;GACrB;EACD;EACA,IAAI,CAAC,QAAQ,MAAM,IAAI,MAAM,4BAA4B;EACzD,IAAI,QAAQ,WAAW,YACtB,OAAO,OAAO,SAAS,EAAE,MAAgB,EAAE,IAAc;EAC1D,IAAI,QAAQ,WAAW,SAAS;GAC/B,MAAM,UAA8B,CAAC;GAYrC,OAAO;IAAE,QAAA,MAXY,aAAa,QAAQ;KACzC,KAAK,EAAE;KACP,GAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAkB,IAAI,CAAC;KAChD,GAAI,EAAE,UACH,EACA,SAAS,MAAc,SAAiB;MACvC,QAAQ,KAAK,CAAC,MAAM,IAAI,CAAC;KAC1B,EACD,IACC,CAAC;IACL,CAAC;IACgB;GAAQ;EAC1B;EACA,MAAM,WAAW,aAAa,OAC7B,EAAE,KACF,EAAE,UACF,EAAE,SACF,EAAE,IACH;EACA,IAAI,QAAQ,WAAW,YACtB,OAAO,MAAM,SAAS,QAAQ,UAAU,EAAE,QAAoB;EAC/D,IAAI,QAAQ,WAAW,SACtB,OAAO,MAAM,MAAM,QAAQ,UAAU,EAAE,QAAoB;EAC5D,MAAM,IAAI,MAAM,8BAA8B,QAAQ,QAAQ;CAC/D,EAAA,CAAG,CAAC,CAAC,MACH,UAAU,KAAK,YAAY;EAAE,IAAI,QAAQ;EAAI;CAAM,CAAC,IACpD,UACA,KAAK,YAAY;EAAE,IAAI,QAAQ;EAAI,OAAO,QAAQ,KAAK;CAAE,CAAC,CAC5D;AACD,CACD"}
|
package/docs/guide.md
CHANGED
|
@@ -2,46 +2,51 @@
|
|
|
2
2
|
|
|
3
3
|
## One configuration for CI and editors
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Install Correctly in the project (`pnpm add -D correctly`) and create `correctly.config.ts`:
|
|
6
6
|
|
|
7
|
-
```
|
|
8
|
-
{
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
```ts
|
|
8
|
+
import { defineConfig, json, jsonc } from "correctly"
|
|
9
|
+
import { ajv } from "correctly/validators/ajv"
|
|
10
|
+
|
|
11
|
+
export default defineConfig({
|
|
12
|
+
files: ["config/**/*.json", "config/**/*.jsonc"],
|
|
13
|
+
exclude: ["config/generated/**"],
|
|
14
|
+
associations: [
|
|
12
15
|
{
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
name: "Application",
|
|
17
|
+
files: ["config/**"],
|
|
18
|
+
parse: json(),
|
|
19
|
+
validate: ajv({ schema: "schemas/application.json" }),
|
|
16
20
|
},
|
|
17
|
-
{
|
|
18
|
-
"name": "Local settings",
|
|
19
|
-
"files": ["config/local.json"],
|
|
20
|
-
"mode": "jsonc",
|
|
21
|
-
"schema": null
|
|
22
|
-
}
|
|
21
|
+
{ files: ["config/local.json"], parse: jsonc(), validate: null },
|
|
23
22
|
],
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
}
|
|
23
|
+
remote: {
|
|
24
|
+
offline: false,
|
|
25
|
+
cacheDir: ".correctly-cache",
|
|
26
|
+
timeoutMs: 5000,
|
|
27
|
+
maxBytes: 2097152,
|
|
28
|
+
maxRequests: 64,
|
|
29
|
+
},
|
|
30
|
+
})
|
|
32
31
|
```
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
TypeScript is the only configuration format. Comline 0.9.0 loads the default-exported object, including imported parser, validator, and extension implementations. `defineConfig` preserves inference and checks the runtime contract; configuration loaded without it receives the same checks. Unknown options and malformed adapters fail visibly. There is no configuration JSON Schema or string registry of plugins. Import packages installed in the project or relative modules with explicit extensions. Configuration uses Node's native erasable TypeScript support (Node 22.18 or later), not a separate transpiler: no TypeScript path aliases, enums requiring transformation, top-level await, asynchronous configuration exports, or configuration factory functions. Adapter preparation and validation may be asynchronous.
|
|
34
|
+
|
|
35
|
+
Configuration and its imports are executable project code. CLI invocation opts into executing that code. The VS Code extension is disabled in untrusted workspaces; other LSP clients must establish trust before starting the server. Both CLI and LSP execute the same configuration in a project worker. Workers provide module-cache isolation and termination, not a security sandbox. Project logging is redirected to stderr so JSON stdout and the LSP transport remain clean.
|
|
36
|
+
|
|
37
|
+
## Migrating from JSON configuration
|
|
38
|
+
|
|
39
|
+
Rename `correctly.config.json` to `correctly.config.ts`, export a `defineConfig` object, replace `mode: "jsonc"` with `parse: jsonc()`, and replace `schema: path` with `validate: ajv({ schema: path })`. Syntax-only associations use `validate: null`. Move extension choices inside `ajv({ extensions: [...] })` and pass imported implementations. Remove configuration `$schema` links and references to `correctly/config.schema.json`, which is no longer exported. Explicit `--config` accepts a `.ts` module; legacy JSON configuration is rejected. Machine consumers must accept report version 2, including custom validator coverage.
|
|
35
40
|
|
|
36
41
|
## Discovery, paths and precedence
|
|
37
42
|
|
|
38
43
|
`correctly check` discovers the nearest configuration by searching upward from the invocation directory. `--config path` overrides discovery and resolves from that directory. CLI discovery selects one project; invoke once per independent workspace root. Explicit document paths also resolve from the invocation directory and must be included by that project's patterns.
|
|
39
44
|
|
|
40
|
-
Editors choose the longest workspace root containing a file, then search upward from the file's directory, stopping at that root. Nested configurations override parent configurations completely. Sibling roots remain independent. Missing configurations become editor diagnostics.
|
|
45
|
+
Editors choose the longest workspace root containing a file, then search upward from the file's directory, stopping at that root. Nested configurations override parent configurations completely. Sibling roots remain independent. Missing configurations become editor diagnostics. Executable configuration and imported modules must be saved before discovery or reload. Unsaved edits to those files never execute. Unsaved data and JSON Schema buffers still affect validation.
|
|
41
46
|
|
|
42
47
|
File patterns, exclusions, local schema paths, and cache paths resolve from the configuration directory. Patterns use forward slashes, can match dotfiles, and must be relative without `..` or leading `!`. Use `exclude` instead of negated patterns. `files` defaults to `**/*.json` and `**/*.jsonc`. Built-in exclusions cover `node_modules`, `.git`, `dist`, `artifacts`, and `.correctly-cache`; configured cache directories inside the project are also excluded. User exclusions add to these defaults. Excluded files are skipped.
|
|
43
48
|
|
|
44
|
-
Associations are ordered: the **last matching rule wins**.
|
|
49
|
+
Associations are ordered: the **last matching rule wins**. `parse` selects a parser implementation; omitting it selects JSONC for `.jsonc` and strict JSON otherwise. `validate: null` explicitly requests syntax-only coverage. Unassociated included documents are also syntax-checked and reported as `syntax-only`; missing coverage does not fail the command. Reports include the matched index, name, optional schema URI, parser ID (`mode`), validator ID, extension IDs and coverage. Association patterns do not expand the project's `files` list: include custom file extensions there as well.
|
|
45
50
|
|
|
46
51
|
## Parsing and validation
|
|
47
52
|
|
|
@@ -53,6 +58,128 @@ Ajv provides authoritative validation in both CLI and LSP. Separate instances su
|
|
|
53
58
|
|
|
54
59
|
Descriptive Microsoft schema extensions, such as `markdownDescription`, `enumDescriptions`, and `defaultSnippets`, are treated as annotations. No values are coerced, no defaults are inserted, and no properties are removed. Formatting is outside validation.
|
|
55
60
|
|
|
61
|
+
## Schema extensions
|
|
62
|
+
|
|
63
|
+
Extensions are imported implementations passed to an individual Ajv validator. First-party extensions use exactly the same contract as third-party extensions. Importing one makes it available without enabling it globally:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { defineConfig, json } from "correctly"
|
|
67
|
+
import { ajv } from "correctly/validators/ajv"
|
|
68
|
+
import { schemars } from "@correctlyjs/schemars/ajv"
|
|
69
|
+
import { renovate } from "correctly/extensions/renovate"
|
|
70
|
+
|
|
71
|
+
export default defineConfig({
|
|
72
|
+
associations: [
|
|
73
|
+
{
|
|
74
|
+
files: ["turbo.json"],
|
|
75
|
+
parse: json(),
|
|
76
|
+
validate: ajv({
|
|
77
|
+
schema: "https://turbo.build/schema.json",
|
|
78
|
+
extensions: [schemars({ version: "0.8.22" })],
|
|
79
|
+
}),
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
files: ["renovate.json"],
|
|
83
|
+
validate: ajv({
|
|
84
|
+
schema: "schemas/renovate.json",
|
|
85
|
+
extensions: [renovate()],
|
|
86
|
+
}),
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Use a separate association and the appropriate schema path for each tool, for example Oxlint's `node_modules/oxlint/configuration_schema.json`. An extension applies to that validator's entire schema reference graph. A referenced resource does not inherit extension choices from another association. The same schema URI can use different extension sets. Prepared validators are cached by adapter object identity within a project session, never by display ID or schema URI alone. Reusing the same adapter object reuses its prepared state. The last matching association wins completely; extension lists never merge. Empty or omitted lists enable none. IDs are diagnostic metadata, not package specifications or lookup keys. Strings and duplicate IDs within one validator fail.
|
|
93
|
+
|
|
94
|
+
Extensions provide synchronous format assertions or annotation-only keywords. Ordinary type, range, pattern, and other schema constraints still apply. Two enabled extensions cannot own the same format or annotation; annotations cannot override an existing keyword. A format implementation can explicitly replace a baseline implementation. Extension order does not resolve conflicts. Existing Microsoft editor annotations remain supported by the baseline.
|
|
95
|
+
|
|
96
|
+
`renovate()` permits only the annotation keyword `x-renovate-version`; its value is metadata and never affects validation. It does not declare compatibility with a specific Renovate release.
|
|
97
|
+
|
|
98
|
+
### Schemars extension
|
|
99
|
+
|
|
100
|
+
Install `@correctlyjs/schemars` alongside `correctly` and import `schemars` from `@correctlyjs/schemars/ajv`. It is released independently; a new upstream Schemars version requires an extension release. The CLI, LSP, and VSIX load the implementation installed in the project.
|
|
101
|
+
|
|
102
|
+
Select an exact reviewed release, such as `schemars({ version: "0.8.22" })`, or an inclusive closed range within one compatibility era, such as `schemars({ version: ">=0.8.15 <=0.8.22" })`. The extension's npm version and the upstream Schemars version selected here are independent. Its peer dependency declares the supported Correctly API versions. The package root exports `schemarsEras` and the `SchemarsEra`, `SchemarsVersion`, and `SchemarsVersionSelection` types, all derived from the literal TypeScript catalog. Invalid selections fail type checking and runtime validation. `schemars()` returns the `AjvExtension` type exported by `correctly/validators/ajv`. Core contains installation suggestions only; it does not bundle or depend on the extension.
|
|
103
|
+
|
|
104
|
+
See the [extension guide](https://github.com/jeremybanka/correctly/blob/main/packages/schemars/docs/guide.md) for the reviewed releases, exhaustive format inventory, numeric precision policies, and other limits.
|
|
105
|
+
|
|
106
|
+
### Missing extension diagnostics
|
|
107
|
+
|
|
108
|
+
Correctly checks format support before compiling each loaded schema resource, including nested schema positions and unused definitions. A format name with no enabled assertion produces `extension-required`, exit 2, and the same actionable message in the editor. Annotation values such as defaults, enums, and examples are never inspected as schemas. No unsupported format is silently ignored. In particular, ajv-formats' annotation-only `password` and `binary` entries cannot attest to values and require an extension. Its OpenAPI numeric names (`int32`, `int64`, `float`, `double`) now require explicit extension selection too, so their width policy is not implicit.
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
extension-required: This schema needs an extension.
|
|
112
|
+
│ Format "uint64" has no enabled validator; validation cannot proceed.
|
|
113
|
+
│ Schema: file:///project/schema.json#/properties/limit/format
|
|
114
|
+
│ For a Schemars-generated schema, install @correctlyjs/schemars and import { schemars } from "@correctlyjs/schemars/ajv". Select the schema's reviewed Schemars version with schemars({ version: "..." }) in this Ajv validator's extensions in correctly.config.ts.
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
A known provider is suggested with a package and import path; select the schema's reviewed upstream version explicitly. An unknown format receives guidance to enable an implementation, without inventing a package to install. JSON failures additionally expose `details: { schemaUri, schemaPointer, format, suggestedExtension? }`. The pointer identifies the `format` keyword in the schema resource. Ordinary validation failures remain exit 1; unknown keywords and invalid schemas continue to fail strict compilation.
|
|
118
|
+
|
|
119
|
+
### Authoring an extension
|
|
120
|
+
|
|
121
|
+
The first-party implementations use the same exported `AjvExtension` interface available to consumers:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { defineConfig } from "correctly"
|
|
125
|
+
import { ajv, type AjvExtension } from "correctly/validators/ajv"
|
|
126
|
+
|
|
127
|
+
const company: AjvExtension = {
|
|
128
|
+
id: "company@1",
|
|
129
|
+
formats: {
|
|
130
|
+
"ticket-id": {
|
|
131
|
+
type: "string",
|
|
132
|
+
validate: (value: string) => /^TASK-[1-9][0-9]*$/.test(value),
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
annotations: ["x-company-description"],
|
|
136
|
+
}
|
|
137
|
+
export default defineConfig({
|
|
138
|
+
associations: [
|
|
139
|
+
{
|
|
140
|
+
files: ["tickets.json"],
|
|
141
|
+
validate: ajv({ schema: "schemas/tickets.json", extensions: [company] }),
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
})
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Import the implementation from a local module or npm package in real projects. The CLI, LSP, and VSIX run those same implementations. Format assertions must be synchronous functions returning booleans; numeric definitions must declare `type: "number"`. RegExp definitions, async format validators, mutation keywords, and arbitrary Ajv plugins are outside this interface. A different validator adapter can expose its own options and extension contract.
|
|
148
|
+
|
|
149
|
+
## Parser and validator adapters
|
|
150
|
+
|
|
151
|
+
`Parser` and `Validator` are structural TypeScript interfaces exported from `correctly`. An association composes their instances; no global registration is required.
|
|
152
|
+
|
|
153
|
+
- A parser declares `id` and `valueModel`, and implements `parse(text, { file, signal })`. It returns `value`, `diagnostics`, and `locate(pointer, key?)`, plus optional `rawNumber(pointer)`. Spans are UTF-16 source offsets/lengths. JSON's locator falls back to the nearest containing node for missing properties; `rawNumber` returns only exact numeric nodes, preserving the original lexeme. Its parsed values still use binary64. Custom lossless parsers must declare their own value model rather than claiming `json` compatibility.
|
|
154
|
+
- A validator declares `id` and `accepts` (value model names). `prepare(context)` returns a reusable validator with `validate({ file, text, parsed, signal })`, optional editor support, and optional `dispose()`. Validation returns Correctly diagnostics; `diagnostic(text, code, message, pointer, offset, length)` builds one with LSP ranges. Syntax errors prevent value validation. Throws become execution failures. Parser/validator value-model mismatches are configuration failures (`adapter-incompatible`, exit 2).
|
|
155
|
+
- Context supplies `root`, `configPath`, remote policy, `read(uri)`, optional `fetch` and abort signal, and `watch(uri)`. Use `read` for dependency-tracked resources or `watch` for resources read another way. Optional `register(context)` runs for all associations before preparation, letting Ajv register named schema IDs. `context.services` is a project-local map for shared adapter resources; versioned `Symbol.for` keys allow compatible copies of a package to share a service without process-global state.
|
|
156
|
+
- Preparation occurs once per adapter object per project session, including unmatched associations. The same schema URI with different adapter instances compiles independently. Fresh workers on reload discard native module caches, validators and editor services together. The runtime aborts its signal and allows disposal up to 250 ms before terminating the worker; disposal cannot indefinitely block editor reload.
|
|
157
|
+
- Hints require both parser `editorLanguage: "json" | "jsonc"` and prepared validator `editor: { kind: "json-schema", uri, readSchema(uri) }`. Ajv explicitly provides this capability. A custom validator without it produces diagnostics but no schema completions or hover. A different language needs its own future hint capability; merely returning JSON-like values does not claim JSON source mapping.
|
|
158
|
+
|
|
159
|
+
For example, a custom validator can consume the built-in parser without using Ajv:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
import { defineConfig, diagnostic, json, type Validator } from "correctly"
|
|
163
|
+
|
|
164
|
+
const objectOnly: Validator = {
|
|
165
|
+
id: "object-only",
|
|
166
|
+
accepts: ["json"],
|
|
167
|
+
prepare: () => ({
|
|
168
|
+
validate: ({ text, parsed }) =>
|
|
169
|
+
parsed.value !== null &&
|
|
170
|
+
typeof parsed.value === "object" &&
|
|
171
|
+
!Array.isArray(parsed.value)
|
|
172
|
+
? []
|
|
173
|
+
: [diagnostic(text, "object-only", "Expected an object")],
|
|
174
|
+
}),
|
|
175
|
+
}
|
|
176
|
+
export default defineConfig({
|
|
177
|
+
associations: [
|
|
178
|
+
{ files: ["settings.json"], parse: json(), validate: objectOnly },
|
|
179
|
+
],
|
|
180
|
+
})
|
|
181
|
+
```
|
|
182
|
+
|
|
56
183
|
## References and cache
|
|
57
184
|
|
|
58
185
|
Schema associations accept local paths, `file:` URIs, and HTTP(S) URLs, optionally with pointer or anchor fragments. Relative references resolve from their schema resource's `$id`, or its retrieval URI when there is no identifier. Relative `$id` values are resolved from their parent resource. Configured schema resources are registered before compilation, so identifiers can refer to other configured local schemas without needing to host them. References to unloaded identifiers use the loader. Cycles and nested resources are resolved by Ajv. Only supported dialect graphs are accepted.
|
|
@@ -61,13 +188,13 @@ Remote requests have a total timeout per resource, a byte limit, at most five re
|
|
|
61
188
|
|
|
62
189
|
`correctly check --offline` prevents all HTTP requests; a missing or corrupt cached schema fails with exit 2. Warm the cache with an online check, retain the cache directory in CI, then validate offline. Local references still work offline. Cache location is explicit and portable when kept with the project's CI cache. Schema resource size limits also apply to local schemas.
|
|
63
190
|
|
|
64
|
-
Editor watchers refresh configurations and
|
|
191
|
+
Editor watchers refresh saved configurations, imported modules (including transitive imports and missing relative import candidates), package manifests/lockfiles, and local schema dependencies. Dependencies outside workspace roots receive explicit watchers. Unsaved schema edits also invalidate validators and hints. A failed reload replaces the previous session with visible failures; old validators never continue silently. Relevant schema/configuration changes and server restart refresh remote schemas. Cache writes and unrelated JSON changes do not trigger reloads. Remote servers do not send change notifications, so remote changes require one of those refresh triggers. Cancellation and document version/generation guards prevent older work from publishing diagnostics or hints after newer edits. Closing a buffer returns it to disk contents; closing a document clears its diagnostics.
|
|
65
192
|
|
|
66
193
|
## CLI and report contract
|
|
67
194
|
|
|
68
195
|
```sh
|
|
69
196
|
correctly check
|
|
70
|
-
correctly check --config config/correctly.config.
|
|
197
|
+
correctly check --config config/correctly.config.ts
|
|
71
198
|
correctly check config/application.json --format json
|
|
72
199
|
correctly check --offline
|
|
73
200
|
correctly-lsp --stdio
|
|
@@ -75,7 +202,7 @@ correctly-lsp --stdio
|
|
|
75
202
|
|
|
76
203
|
Exit 0 means no validation or infrastructure failures; exit 1 means at least one invalid document; exit 2 means a configuration/schema/loading/execution failure, which takes priority over document errors. All configured associations are prepared even when their patterns match no documents, preventing unused broken schema maps from going unnoticed. Readable output identifies every file's coverage and selected association. JSON output is a single object without progress text or a filtering workaround.
|
|
77
204
|
|
|
78
|
-
Readable output follows Lasertag's file sections: a heading with the file name, error/failure counts, parsing mode, and selected schema; diagnostics sorted by source position; and numbered source excerpts with carets marking each affected range.
|
|
205
|
+
Readable output follows Lasertag's file sections: a heading with the file name, error/failure counts, parsing mode, and selected schema; diagnostics sorted by source position; and numbered source excerpts with carets marking each affected range. Syntax-only files explicitly say `syntax only`; custom validators without a schema report their validator ID. Excerpts use the exact text validated, expand tabs to four spaces, and include one context line on each side unless that line has another diagnostic. Long ranges show their first and last lines with an ellipsis between them. Paths inside the current directory are relative; other paths remain absolute.
|
|
79
206
|
|
|
80
207
|
Missing required properties point to the containing object, while the diagnostic's JSON pointer names the missing property. Enum violations list the allowed values as JSON, including values from referenced schemas. `const` violations include the required value, including `null`, `false`, arrays, and objects. Property-name failures highlight the offending key, including the underlying pattern, enum, or constant reason. `uniqueItems` highlights the later duplicate and names the earlier item by its JSON pointer; array indices remain zero-based.
|
|
81
208
|
|
|
@@ -93,11 +220,11 @@ data/project.json 1 error · JSON against Project (schemas/project.schema.jso
|
|
|
93
220
|
|
|
94
221
|
Readable reports use terminal colors automatically: bold file names, red error counts, cyan diagnostic codes, yellow carets, and dim context/gutters. Success summaries are green. Set `NO_COLOR=1` or `FORCE_COLOR=0` to disable colors, or `FORCE_COLOR=1` to force them when piping output. JSON reports never include ANSI styling.
|
|
95
222
|
|
|
96
|
-
`reportVersion:
|
|
223
|
+
`reportVersion: 2` contains `config`, sorted `files`, top-level `failures`, `summary`, and `exitCode`. Coverage is `schema`, `validated` (a custom validator without a schema URI), `syntax-only`, or `excluded`. `summary.validated` counts both validator-backed categories; `schemaCovered` is the subset with schema coverage, and `syntaxOnly` counts syntax-only files. `association.validator` is an adapter ID or null, and `mode` is a parser ID rather than a closed enum. Each file has `file`, `mode`, `coverage`, `association`, `diagnostics`, and `failures`. Diagnostics contain `code`, `message`, `pointer`, `offset`, `length`, and `range`; failures contain `code`, `message`, optionally `file`, and optional structured `details` for an extension requirement. Paths are absolute. The JSON report retains the complete flat diagnostic list in validator order, including every grouped reason. A grouped reason may additionally have `context: { parent, label }`: `parent` is the zero-based index of its summary in that same file's `diagnostics` array; `label` names its branch. Do not reorder that array before resolving context indices. The exported `diagnosticViews` helper projects it into the same hierarchy used by CLI and LSP, and `diagnosticDetails` lists the nested reasons. Messages reflect the pinned validator/parser versions and Correctly's message enrichment; consumers should use codes and pointers rather than parsing message prose.
|
|
97
224
|
|
|
98
225
|
## Editor installation and shared APIs
|
|
99
226
|
|
|
100
|
-
From the repository, run `pnpm build:vsix`, then `code --install-extension artifacts/Correctly-0.0.0.vsix`. The universal VSIX bundles the client
|
|
227
|
+
From the repository, run `pnpm build:vsix`, then `code --install-extension artifacts/Correctly-0.0.0.vsix`. The extension requires VS Code 1.105 or later: its [pinned Electron 37.6.0 runtime](https://github.com/microsoft/vscode/blob/1.105.0/.npmrc) includes [Node 22.19](https://releases.electronjs.org/release/v37.6.0), which supports native TypeScript configuration loading. The universal VSIX bundles the client, server and project worker. Configuration imports resolve from the project, so install Correctly and any adapter dependencies there. Open a trusted workspace with a configuration to activate it. All file documents can reach the server; project patterns select which ones are checked. Missing configurations produce diagnostics for JSON/JSONC; unrelated languages without a configuration remain quiet. The extension's **Correctly: Restart Server** command refreshes its lifecycle. Other LSP clients launch `correctly-lsp --stdio` and must send workspace folders and file-change notifications for automatic disk refresh. No formatter or automatic fixes are registered.
|
|
101
228
|
|
|
102
229
|
Microsoft's JSON language service supplies completions and hover. Its AST is retained with traversal methods bound to the original document; a selection view disables automatic embedded `$schema` selection. Ajv remains the validation authority. Built-in VS Code JSON features may independently provide suggestions/diagnostics; set `json.validate.enable` to `false` to use only Correctly validation, if desired.
|
|
103
230
|
|
|
@@ -106,9 +233,14 @@ The package root exports `loadProject`, `Engine`, `SchemaStore`, parser/location
|
|
|
106
233
|
```ts
|
|
107
234
|
import { Engine, loadProject } from "correctly"
|
|
108
235
|
|
|
109
|
-
const engine = new Engine(await loadProject("/project/correctly.config.
|
|
236
|
+
const engine = new Engine(await loadProject("/project/correctly.config.ts"))
|
|
110
237
|
await engine.prepare()
|
|
111
|
-
|
|
238
|
+
try {
|
|
239
|
+
const result = await engine.validate("/project/config/app.json", sourceText)
|
|
240
|
+
console.log(result)
|
|
241
|
+
} finally {
|
|
242
|
+
await engine.dispose()
|
|
243
|
+
}
|
|
112
244
|
```
|
|
113
245
|
|
|
114
|
-
The core owns association
|
|
246
|
+
The core owns association and adapter lifecycle without depending on Ajv or a JSON AST. `loadProject` is a one-shot Comline load using native module caching; long-running embedders must create fresh worker/module contexts when configuration imports change. CLI and LSP already do this. Built-in parsers cover JSON/JSONC; other parsers can compose today through the public interface, provided they define value semantics and source mappings precisely.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from "correctly"
|
|
2
|
+
import { ajv } from "correctly/validators/ajv"
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
files: ["project.json", "project.jsonc"],
|
|
6
|
+
associations: [
|
|
7
|
+
{
|
|
8
|
+
name: "Project",
|
|
9
|
+
files: ["project.*"],
|
|
10
|
+
validate: ajv({ schema: "project.schema.json" }),
|
|
11
|
+
},
|
|
12
|
+
],
|
|
13
|
+
})
|