@jigs.design/compiler 0.1.2 → 0.3.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/README.md +7 -7
- package/dist/block-hoist.d.ts +2 -0
- package/dist/check-code.d.ts +4 -2
- package/dist/check-code.js +5 -5
- package/dist/codegen.d.ts +2 -0
- package/dist/codegen.js +4 -4
- package/dist/compile.d.ts +24 -18
- package/dist/compile.js +140 -120
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/offsets.d.ts +9 -0
- package/dist/offsets.js +14 -1
- package/dist/patch.d.ts +21 -9
- package/dist/patch.js +80 -34
- package/dist/signature.d.ts +2 -2
- package/dist/signature.js +6 -6
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# @jigs.design/compiler
|
|
2
2
|
|
|
3
|
-
Today's Jigs compiler: the parser, the checker against a catalog, the code generator,
|
|
3
|
+
Today's Jigs compiler: the parser, the checker against a catalog, the code generator, jig libraries, and the SEARCH/REPLACE edit helpers. `@jigs.design/server` and `@jigs.design/prompt` use it. You import it for tooling, not to run a session.
|
|
4
4
|
|
|
5
|
-
It parses embedded code with [acorn](https://github.com/acornjs/acorn) in strict mode, the mode programs run in, and checks names and the JavaScript subset. It has no built-in catalog. Every caller passes one, and its prop kinds are boolean, enum, handler, number, rows and string.
|
|
5
|
+
Its diagnostics are the `Diagnostic` type from `@jigs.design/protocol`, the shape the server sends; it imports that package's types and nothing else from it. It parses embedded code with [acorn](https://github.com/acornjs/acorn) in strict mode, the mode programs run in, and checks names and the JavaScript subset. It has no built-in catalog. Every caller passes one, and its prop kinds are boolean, enum, handler, number, rows and string.
|
|
6
6
|
|
|
7
|
-
The [language
|
|
7
|
+
It evolves in place, behind the same server API. The [language plan's record](../../.agent-docs/notes/language-v2-2026-10.md) says what each phase changed, and `.agent-docs/deferred.md` what is set aside.
|
|
8
8
|
|
|
9
9
|
## Exports
|
|
10
10
|
|
|
@@ -12,18 +12,18 @@ The [language RFC](../../docs/language-v2/rfc.md) plans its next changes. It evo
|
|
|
12
12
|
| ---------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
13
13
|
| `parseJigs` | Source to a document tree. Tolerates a prefix cut off mid-stream |
|
|
14
14
|
| `compileJigs` | Checks a document against a catalog and generates its program |
|
|
15
|
-
| `compileLibrary` | Compiles
|
|
16
|
-
| `
|
|
15
|
+
| `compileLibrary` | Compiles jigs into a library a document can use |
|
|
16
|
+
| `jigSignature` | A jig's props and slot, as the prompt lists them |
|
|
17
17
|
| `isEditReply`, `parseEdits`, `applyEdits`, `SEARCH_MARKER` | Recognise and apply a reply made of SEARCH/REPLACE edits |
|
|
18
18
|
| `CompileCatalog`, `ComponentSpec`, `PropSpec` | What the compiler checks against |
|
|
19
19
|
|
|
20
20
|
## Source
|
|
21
21
|
|
|
22
22
|
- `src/parse.ts`, `src/ast.ts`: the parser and its tree.
|
|
23
|
-
- `src/compile.ts`: checking,
|
|
23
|
+
- `src/compile.ts`: checking, jig libraries and diagnostics.
|
|
24
24
|
- `src/script.ts`, `src/check-code.ts`: embedded code read with acorn, and the checks on it.
|
|
25
25
|
- `src/suggest.ts`: the nearest name, for "did you mean".
|
|
26
26
|
- `src/codegen.ts`: the JavaScript the sandbox runs.
|
|
27
27
|
- `src/patch.ts`: SEARCH/REPLACE edits.
|
|
28
|
-
- `src/spec.ts`, `src/signature.ts`: catalog specs and
|
|
28
|
+
- `src/spec.ts`, `src/signature.ts`: catalog specs and jig signatures.
|
|
29
29
|
- `src/test-catalog.ts`: the catalog the tests run against.
|
package/dist/check-code.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import "./ast.js";
|
|
2
|
+
import { Finding } from "./compile.js";
|
|
3
|
+
import "./block-hoist.js";
|
|
2
4
|
import { ProcedureCall } from "./script.js";
|
|
3
5
|
//#region src/check-code.d.ts
|
|
4
6
|
/** A procedure call in the document, by line, for the server to check against its host. */
|
|
@@ -10,7 +12,7 @@ export interface ProcedureUse extends Omit<ProcedureCall, "end" | "offset"> {
|
|
|
10
12
|
start?: number;
|
|
11
13
|
}
|
|
12
14
|
/** Where a diagnostic points: a line, and a range in the source when the code has one. */
|
|
13
|
-
type Place = Pick<
|
|
15
|
+
type Place = Pick<Finding, "end" | "line" | "start">;
|
|
14
16
|
/** A step of `DataRead.path` into each element of an array, as an `<each>` item takes. */
|
|
15
17
|
export declare const ELEMENT = "[]";
|
|
16
18
|
/**
|
package/dist/check-code.js
CHANGED
|
@@ -277,18 +277,18 @@ function readBody(statements, define, outer) {
|
|
|
277
277
|
* Checks every piece of code in the page and its defines, adding to
|
|
278
278
|
* `diagnostics`, and returns the procedure calls it saw, in source order.
|
|
279
279
|
*/
|
|
280
|
-
function checkCode(tree, defines, pageStatements,
|
|
280
|
+
function checkCode(tree, defines, pageStatements, findings, hoists, dropped = /* @__PURE__ */ new Set()) {
|
|
281
281
|
const procedures = [];
|
|
282
282
|
const reads = [];
|
|
283
283
|
const report = (place, code, message, fix) => {
|
|
284
|
-
const
|
|
284
|
+
const finding = {
|
|
285
285
|
code,
|
|
286
286
|
...place,
|
|
287
287
|
message,
|
|
288
|
-
severity: "
|
|
288
|
+
severity: "error"
|
|
289
289
|
};
|
|
290
|
-
if (fix !== void 0)
|
|
291
|
-
|
|
290
|
+
if (fix !== void 0) finding.fix = fix;
|
|
291
|
+
findings.push(finding);
|
|
292
292
|
};
|
|
293
293
|
const checkSnippet = (snippet, body, scope) => {
|
|
294
294
|
const facts = factsOf(snippet);
|
package/dist/codegen.d.ts
CHANGED
package/dist/codegen.js
CHANGED
|
@@ -62,15 +62,15 @@ const fragment = (children) => `__jigs.jsx(${[
|
|
|
62
62
|
const documentLines = { line: String };
|
|
63
63
|
const libraryLines = { line: () => "__jigsAt" };
|
|
64
64
|
function generateProgram(statements, tree, defines = []) {
|
|
65
|
-
const
|
|
65
|
+
const findings = [];
|
|
66
66
|
const constants = {};
|
|
67
67
|
let nextConstant = 0;
|
|
68
68
|
const invalid = (code, line, message) => {
|
|
69
|
-
|
|
69
|
+
findings.push({
|
|
70
70
|
code,
|
|
71
71
|
line,
|
|
72
72
|
message,
|
|
73
|
-
severity: "
|
|
73
|
+
severity: "error"
|
|
74
74
|
});
|
|
75
75
|
};
|
|
76
76
|
/** Statement lines, run in order, plus a `let` for every name they declare. */
|
|
@@ -195,7 +195,7 @@ function generateProgram(statements, tree, defines = []) {
|
|
|
195
195
|
const functions = defines.map((def) => genDefine(def));
|
|
196
196
|
const children = genChildren(tree, documentLines);
|
|
197
197
|
return {
|
|
198
|
-
|
|
198
|
+
findings,
|
|
199
199
|
output: {
|
|
200
200
|
code: [
|
|
201
201
|
"\"use strict\";",
|
package/dist/compile.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { ExprNode, PropValue, Statement, TextNode } from "./ast.js";
|
|
|
3
3
|
import { CompileCatalog } from "./spec.js";
|
|
4
4
|
import { ProgramOutput } from "./codegen.js";
|
|
5
5
|
import { DataRead, ProcedureUse } from "./check-code.js";
|
|
6
|
+
import { Diagnostic, Severity } from "@jigs.design/protocol";
|
|
6
7
|
//#region src/compile.d.ts
|
|
7
8
|
type DiagnosticCode = "coerced" | "handle_field" | "hook_placement" | "invalid_control" | "invalid_define" | "invalid_expression" | "invalid_literal" | "invalid_statement" | "missing_declarations" | "missing_prop" | "mutate_in_render" | "outer_reference" | "query_options" | "repaired" | "shadows_library" | "unclosed" | "undefined_compare" | "undefined_reference" | "unexpected_children" | "unknown_component" | "unknown_prop" | "unstable_input" | "unsupported_syntax" | "use_before_declare";
|
|
8
9
|
/** Replace `[start, end)` of the source with `text`. */
|
|
@@ -21,20 +22,25 @@ export interface Fix {
|
|
|
21
22
|
/** What the fix does, such as "Replace with `q.status === "pending"`". */
|
|
22
23
|
title: string;
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
+
/**
|
|
26
|
+
* A diagnostic while the compiler works: flat offsets into the source being
|
|
27
|
+
* compiled, and a fix as offset edits, so `applyFixes` can apply it. The
|
|
28
|
+
* result reports each as a `Diagnostic`.
|
|
29
|
+
*/
|
|
30
|
+
export interface Finding {
|
|
25
31
|
code: DiagnosticCode;
|
|
26
|
-
/** The 1-based column of `start` on its line. */
|
|
27
|
-
column?: number;
|
|
28
32
|
/** Just past what the diagnostic is about, as an offset into the source. */
|
|
29
33
|
end?: number;
|
|
30
34
|
fix?: Fix;
|
|
31
35
|
/** The line of `start` when there is one. */
|
|
32
36
|
line: number;
|
|
33
37
|
message: string;
|
|
34
|
-
severity:
|
|
38
|
+
severity: Severity;
|
|
35
39
|
/** The first character of what the diagnostic is about, as an offset into the source. */
|
|
36
40
|
start?: number;
|
|
37
41
|
}
|
|
42
|
+
/** What `compileJigs` reports: the shape the server sends and the eval results store. */
|
|
43
|
+
type Diagnostic$1 = Diagnostic<DiagnosticCode>;
|
|
38
44
|
interface RenderElement {
|
|
39
45
|
children: Array<RenderNode>;
|
|
40
46
|
component: string;
|
|
@@ -104,7 +110,7 @@ export interface CompiledDefine {
|
|
|
104
110
|
name: string;
|
|
105
111
|
origin: "doc" | "library";
|
|
106
112
|
/**
|
|
107
|
-
* The procedure calls in a library
|
|
113
|
+
* The procedure calls in a library jig's source, by its lines. Empty
|
|
108
114
|
* for a document's own define: those are on the document's result.
|
|
109
115
|
*/
|
|
110
116
|
procedures: ReadonlyArray<ProcedureUse>;
|
|
@@ -124,28 +130,28 @@ export interface Program {
|
|
|
124
130
|
tree: Array<RenderNode>;
|
|
125
131
|
}
|
|
126
132
|
/** A reusable component written in Jigs: source with exactly one `<define>`. */
|
|
127
|
-
export interface
|
|
133
|
+
export interface Jig {
|
|
128
134
|
name: string;
|
|
129
135
|
source: string;
|
|
130
136
|
}
|
|
131
|
-
/**
|
|
137
|
+
/** Jigs compiled against one catalog, ready to use in documents. */
|
|
132
138
|
export type Library = ReadonlyMap<string, CompiledDefine>;
|
|
133
139
|
export interface CompileOptions {
|
|
134
140
|
/** The components a document may use. */
|
|
135
141
|
catalog: CompileCatalog;
|
|
136
|
-
/**
|
|
142
|
+
/** Jigs in scope, from `compileLibrary` with the same catalog. */
|
|
137
143
|
library?: Library;
|
|
138
144
|
}
|
|
139
145
|
export interface CompileResult {
|
|
140
146
|
defines: ReadonlyArray<CompiledDefine>;
|
|
141
|
-
diagnostics: Array<Diagnostic>;
|
|
142
|
-
/** The
|
|
147
|
+
diagnostics: Array<Diagnostic$1>;
|
|
148
|
+
/** The jigs this document was compiled against. */
|
|
143
149
|
library: ReadonlyMap<string, CompiledDefine>;
|
|
144
150
|
output: ProgramOutput;
|
|
145
151
|
/**
|
|
146
152
|
* Every `useQuery`, `useMutation` and `useAction` call in the parsed
|
|
147
153
|
* code of the document and its defines, in line order: nothing in
|
|
148
|
-
* prose or in string literals.
|
|
154
|
+
* prose or in string literals. Library jigs' calls are on the library's
|
|
149
155
|
* defines.
|
|
150
156
|
*/
|
|
151
157
|
procedures: ReadonlyArray<ProcedureUse>;
|
|
@@ -159,19 +165,19 @@ export interface CompileResult {
|
|
|
159
165
|
source: string;
|
|
160
166
|
}
|
|
161
167
|
/**
|
|
162
|
-
* Compiles `
|
|
163
|
-
* before it. Throws when a
|
|
164
|
-
*
|
|
168
|
+
* Compiles `jigs` against `catalog`, in order: a jig may use the ones
|
|
169
|
+
* before it. Throws when a jig has any diagnostic. Cached per catalog and
|
|
170
|
+
* jig list.
|
|
165
171
|
*/
|
|
166
|
-
export declare function compileLibrary(
|
|
172
|
+
export declare function compileLibrary(jigs: ReadonlyArray<Jig>, catalog: CompileCatalog): Library;
|
|
167
173
|
/**
|
|
168
|
-
* Compiles a document.
|
|
174
|
+
* Compiles a document. An error that carries a fix is applied rather than
|
|
169
175
|
* sent to the model: the fixed source is compiled again, and when it has
|
|
170
176
|
* exactly the problems left that the fixes did not cover, its program is
|
|
171
|
-
* used and those
|
|
177
|
+
* used and those errors become `info`, as a quoted number on a number prop
|
|
172
178
|
* already is. Diagnostics, procedures and their spans stay those of `source`,
|
|
173
179
|
* which is what the model wrote and what the session keeps.
|
|
174
180
|
*/
|
|
175
181
|
export declare function compileJigs(source: string, options: CompileOptions): CompileResult;
|
|
176
182
|
//#endregion
|
|
177
|
-
export {};
|
|
183
|
+
export { Diagnostic$1 as Diagnostic };
|
package/dist/compile.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { analyze } from "./script.js";
|
|
2
2
|
import { generateProgram, isBindingName } from "./codegen.js";
|
|
3
|
-
import {
|
|
3
|
+
import { lineAt, rangeIn, sourceRange, spanIn } from "./offsets.js";
|
|
4
4
|
import { closest, nearest } from "./suggest.js";
|
|
5
5
|
import { checkCode } from "./check-code.js";
|
|
6
6
|
import { parseJigs } from "./parse.js";
|
|
@@ -58,7 +58,7 @@ function hookMisuse(code, kind, block) {
|
|
|
58
58
|
return null;
|
|
59
59
|
}
|
|
60
60
|
const NO_LIBRARY = /* @__PURE__ */ new Map();
|
|
61
|
-
/** Libraries by catalog, then by
|
|
61
|
+
/** Libraries by catalog, then by jig list. */
|
|
62
62
|
const libraryCache = /* @__PURE__ */ new WeakMap();
|
|
63
63
|
function propSpec(spec, name) {
|
|
64
64
|
const props = spec.props;
|
|
@@ -109,37 +109,37 @@ function treeHasSlot(nodes) {
|
|
|
109
109
|
}
|
|
110
110
|
return false;
|
|
111
111
|
}
|
|
112
|
-
function readDefine(node,
|
|
112
|
+
function readDefine(node, findings, libraryNames, components) {
|
|
113
113
|
const nameProp = node.props.find((prop) => prop.name === "name");
|
|
114
114
|
const name = nameProp !== void 0 && nameProp.shorthand !== true && nameProp.value.kind === "literal" && typeof nameProp.value.value === "string" ? nameProp.value.value : null;
|
|
115
115
|
if (name === null) {
|
|
116
|
-
|
|
116
|
+
findings.push({
|
|
117
117
|
code: "invalid_define",
|
|
118
118
|
...placeOf(node),
|
|
119
119
|
message: `<define> needs name="componentName"; it was removed`,
|
|
120
|
-
severity: "
|
|
120
|
+
severity: "error"
|
|
121
121
|
});
|
|
122
122
|
return null;
|
|
123
123
|
}
|
|
124
124
|
if (componentSpec(components, name) !== void 0 || RESERVED_TAGS.has(name)) {
|
|
125
|
-
|
|
125
|
+
findings.push({
|
|
126
126
|
code: "invalid_define",
|
|
127
127
|
...placeOf(node),
|
|
128
128
|
message: `<define name="${name}"> clashes with <${name}>; it was removed`,
|
|
129
|
-
severity: "
|
|
129
|
+
severity: "error"
|
|
130
130
|
});
|
|
131
131
|
return null;
|
|
132
132
|
}
|
|
133
133
|
if (!isBindingName(name)) {
|
|
134
|
-
|
|
134
|
+
findings.push({
|
|
135
135
|
code: "invalid_define",
|
|
136
136
|
...placeOf(node),
|
|
137
137
|
message: `<define name="${name}"> is not a usable component name; it was removed`,
|
|
138
|
-
severity: "
|
|
138
|
+
severity: "error"
|
|
139
139
|
});
|
|
140
140
|
return null;
|
|
141
141
|
}
|
|
142
|
-
if (libraryNames.has(name))
|
|
142
|
+
if (libraryNames.has(name)) findings.push({
|
|
143
143
|
code: "shadows_library",
|
|
144
144
|
...placeOf(node),
|
|
145
145
|
message: `<define name="${name}"> overrides the library component`,
|
|
@@ -150,38 +150,38 @@ function readDefine(node, diagnostics, libraryNames, components) {
|
|
|
150
150
|
for (const prop of node.props) {
|
|
151
151
|
if (prop === nameProp) continue;
|
|
152
152
|
if (prop.name === "key") {
|
|
153
|
-
|
|
153
|
+
findings.push({
|
|
154
154
|
code: "invalid_define",
|
|
155
155
|
...placeOf(node, prop),
|
|
156
156
|
message: `<define name="${name}"> prop "key" is reserved for the instance; it was removed`,
|
|
157
|
-
severity: "
|
|
157
|
+
severity: "error"
|
|
158
158
|
});
|
|
159
159
|
continue;
|
|
160
160
|
}
|
|
161
161
|
if (prop.name === "name") {
|
|
162
|
-
|
|
162
|
+
findings.push({
|
|
163
163
|
code: "invalid_define",
|
|
164
164
|
...placeOf(node, prop),
|
|
165
165
|
message: `<define name="${name}"> cannot take a prop called "name", which names the define itself; call it "title" or "label". It was removed`,
|
|
166
|
-
severity: "
|
|
166
|
+
severity: "error"
|
|
167
167
|
});
|
|
168
168
|
continue;
|
|
169
169
|
}
|
|
170
170
|
if (prop.name === "children" || !isBindingName(prop.name)) {
|
|
171
|
-
|
|
171
|
+
findings.push({
|
|
172
172
|
code: "invalid_define",
|
|
173
173
|
...placeOf(node, prop),
|
|
174
174
|
message: `<define name="${name}"> prop "${prop.name}" is not a usable name; it was removed`,
|
|
175
|
-
severity: "
|
|
175
|
+
severity: "error"
|
|
176
176
|
});
|
|
177
177
|
continue;
|
|
178
178
|
}
|
|
179
179
|
if (seen.has(prop.name)) {
|
|
180
|
-
|
|
180
|
+
findings.push({
|
|
181
181
|
code: "invalid_define",
|
|
182
182
|
...placeOf(node, prop),
|
|
183
183
|
message: `<define name="${name}"> prop "${prop.name}" is duplicated; the later one was removed`,
|
|
184
|
-
severity: "
|
|
184
|
+
severity: "error"
|
|
185
185
|
});
|
|
186
186
|
continue;
|
|
187
187
|
}
|
|
@@ -355,13 +355,13 @@ function placeOfCode(code) {
|
|
|
355
355
|
};
|
|
356
356
|
}
|
|
357
357
|
const codeAt = (value) => value?.kind === "expr" ? value.at : void 0;
|
|
358
|
-
function validateControl(node, name, children, statements,
|
|
359
|
-
const
|
|
360
|
-
|
|
358
|
+
function validateControl(node, name, children, statements, findings) {
|
|
359
|
+
const reject = (code, message, prop) => {
|
|
360
|
+
findings.push({
|
|
361
361
|
code,
|
|
362
362
|
...placeOf(node, prop),
|
|
363
363
|
message,
|
|
364
|
-
severity: "
|
|
364
|
+
severity: "error"
|
|
365
365
|
});
|
|
366
366
|
};
|
|
367
367
|
const props = new Map(node.props.map((prop) => [prop.name, prop.value]));
|
|
@@ -369,21 +369,21 @@ function validateControl(node, name, children, statements, diagnostics) {
|
|
|
369
369
|
if (controlProps[name].some((allowed) => allowed === prop.name)) continue;
|
|
370
370
|
const fix = name === "each" && prop.name === "key" ? dropAttribute(node, index) : null;
|
|
371
371
|
if (fix === null) {
|
|
372
|
-
|
|
372
|
+
reject("unknown_prop", `<${name}> has no prop "${prop.name}"`, prop);
|
|
373
373
|
continue;
|
|
374
374
|
}
|
|
375
|
-
|
|
375
|
+
findings.push({
|
|
376
376
|
code: "unknown_prop",
|
|
377
377
|
...placeOf(node, prop),
|
|
378
378
|
fix,
|
|
379
379
|
message: "<each> has no prop \"key\": items are keyed by their own `id` or `key`",
|
|
380
|
-
severity: "
|
|
380
|
+
severity: "error"
|
|
381
381
|
});
|
|
382
382
|
}
|
|
383
383
|
if (name === "if") {
|
|
384
384
|
const test = asCode(props.get("test"));
|
|
385
385
|
if (test === null) {
|
|
386
|
-
|
|
386
|
+
reject("invalid_control", "<if> needs test={condition}; it was removed");
|
|
387
387
|
return [];
|
|
388
388
|
}
|
|
389
389
|
const testAt = codeAt(props.get("test"));
|
|
@@ -399,14 +399,14 @@ function validateControl(node, name, children, statements, diagnostics) {
|
|
|
399
399
|
const items = asCode(props.get("of"));
|
|
400
400
|
const itemsAt = codeAt(props.get("of"));
|
|
401
401
|
if (items === null) {
|
|
402
|
-
|
|
402
|
+
reject("invalid_control", "<each> needs of={list}; it was removed");
|
|
403
403
|
return [];
|
|
404
404
|
}
|
|
405
405
|
const binding = (prop, fallback) => {
|
|
406
406
|
const value = props.get(prop);
|
|
407
407
|
if (value === void 0) return fallback;
|
|
408
408
|
if (value.kind === "literal" && isBindingName(String(value.value))) return String(value.value);
|
|
409
|
-
|
|
409
|
+
reject("invalid_control", `<each ${prop}> must be a plain variable name like "item"; using ${fallback === null ? "none" : `"${fallback}"`}`);
|
|
410
410
|
return fallback;
|
|
411
411
|
};
|
|
412
412
|
return [{
|
|
@@ -422,25 +422,25 @@ function validateControl(node, name, children, statements, diagnostics) {
|
|
|
422
422
|
}
|
|
423
423
|
function validateSlot(node, state) {
|
|
424
424
|
if (!state.inDefine) {
|
|
425
|
-
state.
|
|
425
|
+
state.findings.push({
|
|
426
426
|
code: "invalid_define",
|
|
427
427
|
...placeOf(node),
|
|
428
428
|
message: "<slot> is only allowed inside <define>; it was removed",
|
|
429
|
-
severity: "
|
|
429
|
+
severity: "error"
|
|
430
430
|
});
|
|
431
431
|
return [];
|
|
432
432
|
}
|
|
433
|
-
for (const prop of node.props) state.
|
|
433
|
+
for (const prop of node.props) state.findings.push({
|
|
434
434
|
code: "unknown_prop",
|
|
435
435
|
...placeOf(node, prop),
|
|
436
436
|
message: `<slot> has no prop "${prop.name}"`,
|
|
437
|
-
severity: "
|
|
437
|
+
severity: "error"
|
|
438
438
|
});
|
|
439
|
-
if (node.children.length > 0) state.
|
|
439
|
+
if (node.children.length > 0) state.findings.push({
|
|
440
440
|
code: "unexpected_children",
|
|
441
441
|
...placeOf(node),
|
|
442
442
|
message: "<slot> takes no children; they were dropped",
|
|
443
|
-
severity: "
|
|
443
|
+
severity: "error"
|
|
444
444
|
});
|
|
445
445
|
return [{
|
|
446
446
|
line: node.line,
|
|
@@ -448,15 +448,15 @@ function validateSlot(node, state) {
|
|
|
448
448
|
}];
|
|
449
449
|
}
|
|
450
450
|
/** Keeps the first `<slot/>` that survived validation and drops the rest. */
|
|
451
|
-
function trimExtraSlots(nodes, defineName,
|
|
451
|
+
function trimExtraSlots(nodes, defineName, findings, seen) {
|
|
452
452
|
return nodes.flatMap((node) => {
|
|
453
453
|
if (node.type === "slot") {
|
|
454
454
|
if (seen.count > 0) {
|
|
455
|
-
|
|
455
|
+
findings.push({
|
|
456
456
|
code: "invalid_define",
|
|
457
457
|
...placeOf(node),
|
|
458
458
|
message: `<${defineName}> already has a <slot>; this one was removed`,
|
|
459
|
-
severity: "
|
|
459
|
+
severity: "error"
|
|
460
460
|
});
|
|
461
461
|
return [];
|
|
462
462
|
}
|
|
@@ -465,7 +465,7 @@ function trimExtraSlots(nodes, defineName, diagnostics, seen) {
|
|
|
465
465
|
}
|
|
466
466
|
if (node.type === "call" || node.type === "each" || node.type === "element" || node.type === "if") return [{
|
|
467
467
|
...node,
|
|
468
|
-
children: trimExtraSlots(node.children, defineName,
|
|
468
|
+
children: trimExtraSlots(node.children, defineName, findings, seen)
|
|
469
469
|
}];
|
|
470
470
|
return [node];
|
|
471
471
|
});
|
|
@@ -480,11 +480,11 @@ function validateCall(node, def, state, scope) {
|
|
|
480
480
|
for (const prop of node.props) {
|
|
481
481
|
if (prop.name === "key") {
|
|
482
482
|
if (prop.value.kind === "literal" && typeof prop.value.value !== "string" && typeof prop.value.value !== "number") {
|
|
483
|
-
state.
|
|
483
|
+
state.findings.push({
|
|
484
484
|
code: "invalid_literal",
|
|
485
485
|
...placeOf(node, prop),
|
|
486
486
|
message: `<${def.name} key={${JSON.stringify(prop.value.value)}}> must be a string or number`,
|
|
487
|
-
severity: "
|
|
487
|
+
severity: "error"
|
|
488
488
|
});
|
|
489
489
|
continue;
|
|
490
490
|
}
|
|
@@ -494,17 +494,17 @@ function validateCall(node, def, state, scope) {
|
|
|
494
494
|
const spec = known.get(prop.name);
|
|
495
495
|
if (spec === void 0) {
|
|
496
496
|
const near = nearest(prop.name, known.keys(), 1)[0];
|
|
497
|
-
state.
|
|
497
|
+
state.findings.push({
|
|
498
498
|
code: "unknown_prop",
|
|
499
499
|
...placeOf(node, prop),
|
|
500
500
|
message: near === void 0 ? `<${def.name}> has no prop "${prop.name}"` : `<${def.name}> has no prop "${prop.name}"; did you mean ${near}?`,
|
|
501
|
-
severity: "
|
|
501
|
+
severity: "error"
|
|
502
502
|
});
|
|
503
503
|
continue;
|
|
504
504
|
}
|
|
505
505
|
if (spec.kind === "number" && prop.value.kind === "literal" && typeof prop.value.value === "string" && NUMERIC.test(prop.value.value)) {
|
|
506
506
|
const value = Number(prop.value.value);
|
|
507
|
-
state.
|
|
507
|
+
state.findings.push({
|
|
508
508
|
code: "coerced",
|
|
509
509
|
...placeOf(node, prop),
|
|
510
510
|
message: `<${def.name} ${writtenProp(prop)}> is text; it was read as ${prop.name}={${String(value)}}. Write numbers in braces`,
|
|
@@ -517,27 +517,27 @@ function validateCall(node, def, state, scope) {
|
|
|
517
517
|
continue;
|
|
518
518
|
}
|
|
519
519
|
if (prop.value.kind === "literal" && spec.kind !== null && typeof prop.value.value !== spec.kind) {
|
|
520
|
-
state.
|
|
520
|
+
state.findings.push({
|
|
521
521
|
code: "invalid_literal",
|
|
522
522
|
...placeOf(node, prop),
|
|
523
523
|
message: `<${def.name} ${writtenProp(prop)}> does not match a ${spec.kind} value`,
|
|
524
|
-
severity: "
|
|
524
|
+
severity: "error"
|
|
525
525
|
});
|
|
526
526
|
continue;
|
|
527
527
|
}
|
|
528
528
|
props[prop.name] = prop.value;
|
|
529
529
|
}
|
|
530
|
-
for (const spec of def.props) if (spec.required && !Object.hasOwn(props, spec.name)) state.
|
|
530
|
+
for (const spec of def.props) if (spec.required && !Object.hasOwn(props, spec.name)) state.findings.push({
|
|
531
531
|
code: "missing_prop",
|
|
532
532
|
...placeOf(node),
|
|
533
533
|
message: `<${def.name}> is missing required prop "${spec.name}"`,
|
|
534
|
-
severity: "
|
|
534
|
+
severity: "error"
|
|
535
535
|
});
|
|
536
|
-
if (!def.hasSlot && def.incomplete !== true && children.length > 0) state.
|
|
536
|
+
if (!def.hasSlot && def.incomplete !== true && children.length > 0) state.findings.push({
|
|
537
537
|
code: "unexpected_children",
|
|
538
538
|
...placeOf(node),
|
|
539
539
|
message: `<${def.name}> has no <slot>, so its children were dropped`,
|
|
540
|
-
severity: "
|
|
540
|
+
severity: "error"
|
|
541
541
|
});
|
|
542
542
|
return [{
|
|
543
543
|
children: def.hasSlot ? children : [],
|
|
@@ -562,14 +562,14 @@ function validateNodes(nodes, state, scope) {
|
|
|
562
562
|
if (misuse !== null) {
|
|
563
563
|
const inBlock = scope.block === "each" || scope.block === "if";
|
|
564
564
|
const hoist = inBlock ? state.hoists.byStart.get(node.start) : void 0;
|
|
565
|
-
const
|
|
565
|
+
const finding = {
|
|
566
566
|
code: inBlock ? "invalid_statement" : "hook_placement",
|
|
567
567
|
...placeOfCode(node),
|
|
568
568
|
message: `${misuse.message}${hoist?.reason === null || hoist?.reason === void 0 ? "" : `; it can't move out by itself, as it ${hoist.reason}`}. The statement was removed`,
|
|
569
|
-
severity: "
|
|
569
|
+
severity: "error"
|
|
570
570
|
};
|
|
571
|
-
if (hoist?.fix)
|
|
572
|
-
state.
|
|
571
|
+
if (hoist?.fix) finding.fix = hoist.fix;
|
|
572
|
+
state.findings.push(finding);
|
|
573
573
|
for (const name of misuse.declared) state.dropped.add(name);
|
|
574
574
|
return [];
|
|
575
575
|
}
|
|
@@ -585,11 +585,11 @@ function validateNodes(nodes, state, scope) {
|
|
|
585
585
|
if (node.type === "expr") {
|
|
586
586
|
const misuse = hookMisuse(node.code, "expression", scope.block);
|
|
587
587
|
if (misuse !== null) {
|
|
588
|
-
state.
|
|
588
|
+
state.findings.push({
|
|
589
589
|
code: "hook_placement",
|
|
590
590
|
...placeOfCode(node),
|
|
591
591
|
message: `${misuse.message}. The expression was removed`,
|
|
592
|
-
severity: "
|
|
592
|
+
severity: "error"
|
|
593
593
|
});
|
|
594
594
|
return [];
|
|
595
595
|
}
|
|
@@ -597,11 +597,11 @@ function validateNodes(nodes, state, scope) {
|
|
|
597
597
|
}
|
|
598
598
|
if (node.type !== "element") return [node];
|
|
599
599
|
if (node.name === "define") {
|
|
600
|
-
state.
|
|
600
|
+
state.findings.push({
|
|
601
601
|
code: "invalid_define",
|
|
602
602
|
...placeOf(node),
|
|
603
603
|
message: "<define> is only allowed at the top level; it was removed",
|
|
604
|
-
severity: "
|
|
604
|
+
severity: "error"
|
|
605
605
|
});
|
|
606
606
|
return [];
|
|
607
607
|
}
|
|
@@ -612,18 +612,18 @@ function validateNodes(nodes, state, scope) {
|
|
|
612
612
|
block: node.name,
|
|
613
613
|
statements
|
|
614
614
|
});
|
|
615
|
-
return validateControl(node, node.name, children, statements, state.
|
|
615
|
+
return validateControl(node, node.name, children, statements, state.findings);
|
|
616
616
|
}
|
|
617
617
|
const defined = state.defines.get(node.name);
|
|
618
618
|
if (defined !== void 0) return validateCall(node, defined, state, scope);
|
|
619
619
|
const children = validateNodes(node.children, state, scope);
|
|
620
620
|
const spec = componentSpec(state.catalog, node.name);
|
|
621
621
|
if (spec === void 0) {
|
|
622
|
-
state.
|
|
622
|
+
state.findings.push({
|
|
623
623
|
code: "unknown_component",
|
|
624
624
|
...placeOf(node),
|
|
625
625
|
message: unknownComponentMessage(node.name, state.defines, state.catalog),
|
|
626
|
-
severity: "
|
|
626
|
+
severity: "error"
|
|
627
627
|
});
|
|
628
628
|
return children;
|
|
629
629
|
}
|
|
@@ -634,29 +634,29 @@ function validateNodes(nodes, state, scope) {
|
|
|
634
634
|
const propDefinition = propSpec(spec, prop.name);
|
|
635
635
|
if (propDefinition === void 0) {
|
|
636
636
|
const wrap = flexWrap(node, prop, state.catalog);
|
|
637
|
-
const
|
|
637
|
+
const finding = {
|
|
638
638
|
code: "unknown_prop",
|
|
639
639
|
...placeOf(node, prop),
|
|
640
640
|
message: wrap === null ? unknownPropMessage(component, prop.name, spec.props) : `<${component}> has no prop "flex"; wrap it in <box ${wrap.written}>`,
|
|
641
|
-
severity: "
|
|
641
|
+
severity: "error"
|
|
642
642
|
};
|
|
643
|
-
if (wrap !== null)
|
|
644
|
-
state.
|
|
643
|
+
if (wrap !== null) finding.fix = wrap.fix;
|
|
644
|
+
state.findings.push(finding);
|
|
645
645
|
continue;
|
|
646
646
|
}
|
|
647
647
|
const misuse = prop.value.kind === "expr" ? hookMisuse(prop.value.code, "expression", scope.block) : null;
|
|
648
648
|
if (misuse !== null) {
|
|
649
|
-
state.
|
|
649
|
+
state.findings.push({
|
|
650
650
|
code: "hook_placement",
|
|
651
651
|
...placeOf(node, prop),
|
|
652
652
|
message: `<${component} ${prop.name}={…}>: ${misuse.message}. The prop was removed`,
|
|
653
|
-
severity: "
|
|
653
|
+
severity: "error"
|
|
654
654
|
});
|
|
655
655
|
continue;
|
|
656
656
|
}
|
|
657
657
|
const coerced = coerceNumber(propDefinition, prop.value);
|
|
658
658
|
if (coerced !== null && coerced.kind === "literal") {
|
|
659
|
-
state.
|
|
659
|
+
state.findings.push({
|
|
660
660
|
code: "coerced",
|
|
661
661
|
...placeOf(node, prop),
|
|
662
662
|
message: `<${component} ${writtenProp(prop)}> is text; it was read as ${prop.name}={${String(coerced.value)}}. Write numbers in braces`,
|
|
@@ -666,22 +666,22 @@ function validateNodes(nodes, state, scope) {
|
|
|
666
666
|
continue;
|
|
667
667
|
}
|
|
668
668
|
if (prop.value.kind === "literal" && !isValidValue(propDefinition, prop.value.value)) {
|
|
669
|
-
state.
|
|
669
|
+
state.findings.push({
|
|
670
670
|
code: "invalid_literal",
|
|
671
671
|
...placeOf(node, prop),
|
|
672
672
|
message: `<${component} ${writtenProp(prop)}> does not match ${describe(propDefinition, prop.value.value)}`,
|
|
673
|
-
severity: "
|
|
673
|
+
severity: "error"
|
|
674
674
|
});
|
|
675
675
|
continue;
|
|
676
676
|
}
|
|
677
677
|
props[prop.name] = prop.value;
|
|
678
678
|
}
|
|
679
679
|
const allowsChildren = spec.children !== "none";
|
|
680
|
-
if (!allowsChildren && children.length > 0) state.
|
|
680
|
+
if (!allowsChildren && children.length > 0) state.findings.push({
|
|
681
681
|
code: "unexpected_children",
|
|
682
682
|
...placeOf(node),
|
|
683
683
|
message: `<${component}> takes no children; they were dropped`,
|
|
684
|
-
severity: "
|
|
684
|
+
severity: "error"
|
|
685
685
|
});
|
|
686
686
|
return [{
|
|
687
687
|
children: allowsChildren ? children : [],
|
|
@@ -738,7 +738,7 @@ function emittedDefines(tree, docDefines, available) {
|
|
|
738
738
|
}
|
|
739
739
|
/**
|
|
740
740
|
* Static call sites in the document tree and in document-local define bodies.
|
|
741
|
-
* A tag inside a library
|
|
741
|
+
* A tag inside a library jig is not counted again.
|
|
742
742
|
*/
|
|
743
743
|
function countUses(tree, docDefines) {
|
|
744
744
|
const counts = /* @__PURE__ */ new Map();
|
|
@@ -756,7 +756,7 @@ function countUses(tree, docDefines) {
|
|
|
756
756
|
}
|
|
757
757
|
function compileDocument(source, library, components) {
|
|
758
758
|
const document = parseJigs(source);
|
|
759
|
-
const
|
|
759
|
+
const findings = [...document.repairs.map((repair) => ({
|
|
760
760
|
code: "repaired",
|
|
761
761
|
...placeOf(repair),
|
|
762
762
|
message: repair.message,
|
|
@@ -765,7 +765,7 @@ function compileDocument(source, library, components) {
|
|
|
765
765
|
code: "unclosed",
|
|
766
766
|
...placeOf(error),
|
|
767
767
|
message: error.message,
|
|
768
|
-
severity: "
|
|
768
|
+
severity: "error"
|
|
769
769
|
}))];
|
|
770
770
|
const libraryNames = new Set(library.keys());
|
|
771
771
|
const docDefines = [];
|
|
@@ -774,14 +774,14 @@ function compileDocument(source, library, components) {
|
|
|
774
774
|
const docNames = /* @__PURE__ */ new Set();
|
|
775
775
|
for (const node of document.children) {
|
|
776
776
|
if (node.type === "element" && node.name === "define") {
|
|
777
|
-
const def = readDefine(node,
|
|
777
|
+
const def = readDefine(node, findings, libraryNames, components);
|
|
778
778
|
if (def === null) continue;
|
|
779
779
|
if (docNames.has(def.name)) {
|
|
780
|
-
|
|
780
|
+
findings.push({
|
|
781
781
|
code: "invalid_define",
|
|
782
782
|
...placeOf(node),
|
|
783
783
|
message: `<define name="${def.name}"> is already defined; this one was removed`,
|
|
784
|
-
severity: "
|
|
784
|
+
severity: "error"
|
|
785
785
|
});
|
|
786
786
|
continue;
|
|
787
787
|
}
|
|
@@ -801,8 +801,8 @@ function compileDocument(source, library, components) {
|
|
|
801
801
|
catalog: components,
|
|
802
802
|
defineName: null,
|
|
803
803
|
defines: available,
|
|
804
|
-
diagnostics,
|
|
805
804
|
dropped,
|
|
805
|
+
findings,
|
|
806
806
|
hoists,
|
|
807
807
|
inDefine: false,
|
|
808
808
|
ordinals: /* @__PURE__ */ new Map()
|
|
@@ -813,15 +813,15 @@ function compileDocument(source, library, components) {
|
|
|
813
813
|
catalog: components,
|
|
814
814
|
defineName: def.name,
|
|
815
815
|
defines: available,
|
|
816
|
-
diagnostics,
|
|
817
816
|
dropped,
|
|
817
|
+
findings,
|
|
818
818
|
hoists,
|
|
819
819
|
inDefine: true,
|
|
820
820
|
ordinals: /* @__PURE__ */ new Map()
|
|
821
821
|
}, {
|
|
822
822
|
block: "define",
|
|
823
823
|
statements
|
|
824
|
-
}), def.name,
|
|
824
|
+
}), def.name, findings, { count: 0 });
|
|
825
825
|
def.statements = statements;
|
|
826
826
|
}
|
|
827
827
|
const statements = [];
|
|
@@ -831,11 +831,11 @@ function compileDocument(source, library, components) {
|
|
|
831
831
|
statements.push(statement);
|
|
832
832
|
continue;
|
|
833
833
|
}
|
|
834
|
-
|
|
834
|
+
findings.push({
|
|
835
835
|
code: "hook_placement",
|
|
836
836
|
...placeOfCode(statement),
|
|
837
837
|
message: `${misuse.message}. The statement was removed`,
|
|
838
|
-
severity: "
|
|
838
|
+
severity: "error"
|
|
839
839
|
});
|
|
840
840
|
for (const name of misuse.declared) dropped.add(name);
|
|
841
841
|
}
|
|
@@ -843,17 +843,14 @@ function compileDocument(source, library, components) {
|
|
|
843
843
|
block: null,
|
|
844
844
|
statements
|
|
845
845
|
});
|
|
846
|
-
const uses = checkCode(tree, docDefines, statements,
|
|
846
|
+
const uses = checkCode(tree, docDefines, statements, findings, hoists, dropped);
|
|
847
847
|
const generated = generateProgram(statements, tree, emittedDefines(tree, docDefines, available));
|
|
848
|
-
|
|
849
|
-
for (const
|
|
850
|
-
|
|
851
|
-
diagnostic.column = columnAt(source, diagnostic.start);
|
|
852
|
-
}
|
|
853
|
-
diagnostics.sort((a, b) => a.line - b.line);
|
|
848
|
+
findings.push(...generated.findings);
|
|
849
|
+
for (const finding of findings) if (finding.start !== void 0) finding.line = lineAt(source, finding.start);
|
|
850
|
+
findings.sort((a, b) => a.line - b.line);
|
|
854
851
|
return {
|
|
855
852
|
defines: docDefines,
|
|
856
|
-
|
|
853
|
+
findings,
|
|
857
854
|
library,
|
|
858
855
|
output: generated.output,
|
|
859
856
|
procedures: uses.procedures,
|
|
@@ -868,14 +865,14 @@ function compileDocument(source, library, components) {
|
|
|
868
865
|
}
|
|
869
866
|
function buildLibrary(list, components) {
|
|
870
867
|
const built = /* @__PURE__ */ new Map();
|
|
871
|
-
for (const
|
|
872
|
-
if (built.has(
|
|
873
|
-
const result = compileDocument(
|
|
874
|
-
const problem = result.
|
|
875
|
-
if (problem !== void 0) throw new Error(`
|
|
868
|
+
for (const jig of list) {
|
|
869
|
+
if (built.has(jig.name)) throw new Error(`Jig name "${jig.name}" is duplicated`);
|
|
870
|
+
const result = compileDocument(jig.source, built, components);
|
|
871
|
+
const problem = result.findings[0];
|
|
872
|
+
if (problem !== void 0) throw new Error(`Jig "${jig.name}" failed to compile: line ${problem.line}: ${problem.message}`);
|
|
876
873
|
const def = result.defines[0];
|
|
877
|
-
if (result.defines.length !== 1 || def === void 0) throw new Error(`
|
|
878
|
-
if (def.name !==
|
|
874
|
+
if (result.defines.length !== 1 || def === void 0) throw new Error(`Jig "${jig.name}" must contain exactly one <define>`);
|
|
875
|
+
if (def.name !== jig.name) throw new Error(`Jig "${jig.name}" defines <${def.name}> instead`);
|
|
879
876
|
built.set(def.name, {
|
|
880
877
|
...def,
|
|
881
878
|
origin: "library",
|
|
@@ -885,17 +882,17 @@ function buildLibrary(list, components) {
|
|
|
885
882
|
return built;
|
|
886
883
|
}
|
|
887
884
|
/**
|
|
888
|
-
* Compiles `
|
|
889
|
-
* before it. Throws when a
|
|
890
|
-
*
|
|
885
|
+
* Compiles `jigs` against `catalog`, in order: a jig may use the ones
|
|
886
|
+
* before it. Throws when a jig has any diagnostic. Cached per catalog and
|
|
887
|
+
* jig list.
|
|
891
888
|
*/
|
|
892
|
-
function compileLibrary(
|
|
893
|
-
const
|
|
894
|
-
libraryCache.set(catalog,
|
|
895
|
-
const existing =
|
|
889
|
+
function compileLibrary(jigs, catalog) {
|
|
890
|
+
const byJigs = libraryCache.get(catalog) ?? /* @__PURE__ */ new WeakMap();
|
|
891
|
+
libraryCache.set(catalog, byJigs);
|
|
892
|
+
const existing = byJigs.get(jigs);
|
|
896
893
|
if (existing !== void 0) return existing;
|
|
897
|
-
const built = buildLibrary(
|
|
898
|
-
|
|
894
|
+
const built = buildLibrary(jigs, catalog);
|
|
895
|
+
byJigs.set(jigs, built);
|
|
899
896
|
return built;
|
|
900
897
|
}
|
|
901
898
|
/** `source` with the fixes' edits applied. */
|
|
@@ -917,34 +914,57 @@ function applyFixes(source, fixes) {
|
|
|
917
914
|
text
|
|
918
915
|
};
|
|
919
916
|
}
|
|
920
|
-
const problems = (
|
|
917
|
+
const problems = (findings) => findings.filter((finding) => finding.severity === "error").length;
|
|
918
|
+
/** `finding` as a `Diagnostic`, its spans counted in `source`. */
|
|
919
|
+
function toDiagnostic(source, finding) {
|
|
920
|
+
const diagnostic = {
|
|
921
|
+
code: finding.code,
|
|
922
|
+
message: finding.message,
|
|
923
|
+
severity: finding.severity,
|
|
924
|
+
span: spanIn(source, finding)
|
|
925
|
+
};
|
|
926
|
+
if (finding.fix !== void 0) diagnostic.fix = {
|
|
927
|
+
edits: finding.fix.edits.map((edit) => ({
|
|
928
|
+
span: rangeIn(source, edit.start, edit.end),
|
|
929
|
+
text: edit.text
|
|
930
|
+
})),
|
|
931
|
+
title: finding.fix.title
|
|
932
|
+
};
|
|
933
|
+
return diagnostic;
|
|
934
|
+
}
|
|
935
|
+
/** `document`'s result, reporting `findings` as diagnostics of `source`. */
|
|
936
|
+
function withDiagnostics({ findings: _, ...result }, source, findings) {
|
|
937
|
+
return {
|
|
938
|
+
...result,
|
|
939
|
+
diagnostics: findings.map((finding) => toDiagnostic(source, finding))
|
|
940
|
+
};
|
|
941
|
+
}
|
|
921
942
|
/**
|
|
922
|
-
* Compiles a document.
|
|
943
|
+
* Compiles a document. An error that carries a fix is applied rather than
|
|
923
944
|
* sent to the model: the fixed source is compiled again, and when it has
|
|
924
945
|
* exactly the problems left that the fixes did not cover, its program is
|
|
925
|
-
* used and those
|
|
946
|
+
* used and those errors become `info`, as a quoted number on a number prop
|
|
926
947
|
* already is. Diagnostics, procedures and their spans stay those of `source`,
|
|
927
948
|
* which is what the model wrote and what the session keeps.
|
|
928
949
|
*/
|
|
929
950
|
function compileJigs(source, options) {
|
|
930
951
|
const library = options.library ?? NO_LIBRARY;
|
|
931
952
|
const result = compileDocument(source, library, options.catalog);
|
|
932
|
-
const fixes = new Set(result.
|
|
933
|
-
if (fixes.size === 0) return result;
|
|
953
|
+
const fixes = new Set(result.findings.flatMap((finding) => finding.severity === "error" && finding.fix !== void 0 ? [finding.fix] : []));
|
|
954
|
+
if (fixes.size === 0) return withDiagnostics(result, source, result.findings);
|
|
934
955
|
const patched = applyFixes(source, [...fixes]);
|
|
935
956
|
const fixed = compileDocument(patched.text, library, options.catalog);
|
|
936
|
-
const covered = result.
|
|
937
|
-
if (problems(fixed.
|
|
938
|
-
return {
|
|
957
|
+
const covered = result.findings.filter((finding) => finding.fix !== void 0 && patched.applied.has(finding.fix)).length;
|
|
958
|
+
if (problems(fixed.findings) !== problems(result.findings) - covered) return withDiagnostics(result, source, result.findings);
|
|
959
|
+
return withDiagnostics({
|
|
939
960
|
...fixed,
|
|
940
|
-
diagnostics: result.diagnostics.map((diagnostic) => diagnostic.fix !== void 0 && patched.applied.has(diagnostic.fix) ? {
|
|
941
|
-
...diagnostic,
|
|
942
|
-
message: `${diagnostic.message}; the compiler made that change`,
|
|
943
|
-
severity: "info"
|
|
944
|
-
} : diagnostic),
|
|
945
961
|
procedures: result.procedures,
|
|
946
962
|
reads: result.reads
|
|
947
|
-
}
|
|
963
|
+
}, source, result.findings.map((finding) => finding.fix !== void 0 && patched.applied.has(finding.fix) ? {
|
|
964
|
+
...finding,
|
|
965
|
+
message: `${finding.message}; the compiler made that change`,
|
|
966
|
+
severity: "info"
|
|
967
|
+
} : finding));
|
|
948
968
|
}
|
|
949
969
|
//#endregion
|
|
950
970
|
export { compileJigs, compileLibrary };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { columnAt, lineAt } from "./offsets.js";
|
|
1
|
+
import { columnAt, lineAt, rangeIn, spanIn } from "./offsets.js";
|
|
2
2
|
import { JigsNode, Statement } from "./ast.js";
|
|
3
3
|
import { CompileCatalog, ComponentSpec, PropSpec } from "./spec.js";
|
|
4
4
|
import { ProgramOutput } from "./codegen.js";
|
|
5
|
-
import { CompileOptions, CompileResult, CompiledDefine, Diagnostic, Library, Program,
|
|
5
|
+
import { CompileOptions, CompileResult, CompiledDefine, Diagnostic, Jig, Library, Program, RenderNode, compileJigs, compileLibrary } from "./compile.js";
|
|
6
6
|
import { DataRead, ELEMENT, ProcedureUse } from "./check-code.js";
|
|
7
7
|
import { Hoisted, hoistDeclarations } from "./hoist.js";
|
|
8
8
|
import { parseJigs } from "./parse.js";
|
|
9
9
|
import { nearest } from "./suggest.js";
|
|
10
|
-
import {
|
|
10
|
+
import { jigSignature } from "./signature.js";
|
|
11
11
|
import { Edit, EditFailure, EditFailureReason, SEARCH_MARKER, applyEdits, editBreakage, isEditReply, parseEdits } from "./patch.js";
|
|
12
|
-
export { type CompileCatalog, type CompileOptions, type CompileResult, type CompiledDefine, type ComponentSpec, type DataRead, type Diagnostic, ELEMENT, type Edit, type EditFailure, type EditFailureReason, type Hoisted, type JigsNode, type Library, type ProcedureUse, type Program, type ProgramOutput, type PropSpec, type
|
|
12
|
+
export { type CompileCatalog, type CompileOptions, type CompileResult, type CompiledDefine, type ComponentSpec, type DataRead, type Diagnostic, ELEMENT, type Edit, type EditFailure, type EditFailureReason, type Hoisted, type Jig, type JigsNode, type Library, type ProcedureUse, type Program, type ProgramOutput, type PropSpec, type RenderNode, SEARCH_MARKER, type Statement, applyEdits, columnAt, compileJigs, compileLibrary, editBreakage, hoistDeclarations, isEditReply, jigSignature, lineAt, nearest, parseEdits, parseJigs, rangeIn, spanIn };
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { columnAt, lineAt } from "./offsets.js";
|
|
1
|
+
import { columnAt, lineAt, rangeIn, spanIn } from "./offsets.js";
|
|
2
2
|
import { nearest } from "./suggest.js";
|
|
3
3
|
import { ELEMENT } from "./check-code.js";
|
|
4
4
|
import { parseJigs } from "./parse.js";
|
|
5
5
|
import { hoistDeclarations } from "./hoist.js";
|
|
6
6
|
import { compileJigs, compileLibrary } from "./compile.js";
|
|
7
|
-
import {
|
|
7
|
+
import { jigSignature } from "./signature.js";
|
|
8
8
|
import { SEARCH_MARKER, applyEdits, editBreakage, isEditReply, parseEdits } from "./patch.js";
|
|
9
|
-
export { ELEMENT, SEARCH_MARKER, applyEdits, columnAt, compileJigs, compileLibrary, editBreakage, hoistDeclarations, isEditReply, lineAt, nearest, parseEdits, parseJigs,
|
|
9
|
+
export { ELEMENT, SEARCH_MARKER, applyEdits, columnAt, compileJigs, compileLibrary, editBreakage, hoistDeclarations, isEditReply, jigSignature, lineAt, nearest, parseEdits, parseJigs, rangeIn, spanIn };
|
package/dist/offsets.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Span } from "@jigs.design/protocol";
|
|
1
2
|
//#region src/offsets.d.ts
|
|
2
3
|
/**
|
|
3
4
|
* Where text the parser cut out sits in the source. Each entry is
|
|
@@ -15,4 +16,12 @@ export interface Range {
|
|
|
15
16
|
export declare function columnAt(source: string, offset: number): number;
|
|
16
17
|
/** The 1-based line of `offset` in `source`. */
|
|
17
18
|
export declare function lineAt(source: string, offset: number): number;
|
|
19
|
+
/** The span of `[start, end)` in `source`: its line, column and range. */
|
|
20
|
+
export declare function rangeIn(source: string, start: number, end: number): Span;
|
|
21
|
+
/** The span of `place` in `source`: its line, and its range and column when it has one. */
|
|
22
|
+
export declare function spanIn(source: string, place: {
|
|
23
|
+
end?: number;
|
|
24
|
+
line: number;
|
|
25
|
+
start?: number;
|
|
26
|
+
}): Span;
|
|
18
27
|
//#endregion
|
package/dist/offsets.js
CHANGED
|
@@ -36,5 +36,18 @@ function lineAt(source, offset) {
|
|
|
36
36
|
for (let newline = source.indexOf("\n"); newline !== -1 && newline < offset; newline = source.indexOf("\n", newline + 1)) line++;
|
|
37
37
|
return line;
|
|
38
38
|
}
|
|
39
|
+
/** The span of `[start, end)` in `source`: its line, column and range. */
|
|
40
|
+
function rangeIn(source, start, end) {
|
|
41
|
+
return {
|
|
42
|
+
column: columnAt(source, start),
|
|
43
|
+
end,
|
|
44
|
+
line: lineAt(source, start),
|
|
45
|
+
start
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** The span of `place` in `source`: its line, and its range and column when it has one. */
|
|
49
|
+
function spanIn(source, place) {
|
|
50
|
+
return place.start === void 0 || place.end === void 0 ? { line: place.line } : rangeIn(source, place.start, place.end);
|
|
51
|
+
}
|
|
39
52
|
//#endregion
|
|
40
|
-
export { columnAt, extendMap, lineAt, sliceMap, sourceOffset, sourceRange };
|
|
53
|
+
export { columnAt, extendMap, lineAt, rangeIn, sliceMap, sourceOffset, sourceRange, spanIn };
|
package/dist/patch.d.ts
CHANGED
|
@@ -13,11 +13,13 @@ export declare function isEditReply(text: string): boolean | null;
|
|
|
13
13
|
export declare function parseEdits(text: string): Array<Edit>;
|
|
14
14
|
/**
|
|
15
15
|
* Why an edit was rejected. `missing`: its SEARCH is not in the document.
|
|
16
|
-
* `ambiguous`: its SEARCH is there more than once. `
|
|
17
|
-
*
|
|
18
|
-
* the
|
|
16
|
+
* `ambiguous`: its SEARCH is there more than once. `overlapping`: its SEARCH
|
|
17
|
+
* spans lines an earlier edit of the same reply also changes. `malformed`:
|
|
18
|
+
* the block itself is broken. `broken`: it applied, but the result needs
|
|
19
|
+
* parser repairs the document did not (see `editBreakage`); the server
|
|
20
|
+
* reports this one.
|
|
19
21
|
*/
|
|
20
|
-
export type EditFailureReason = "ambiguous" | "broken" | "malformed" | "missing";
|
|
22
|
+
export type EditFailureReason = "ambiguous" | "broken" | "malformed" | "missing" | "overlapping";
|
|
21
23
|
export interface EditFailure {
|
|
22
24
|
/** For `broken`: the repairs the edited document needs and the base did not. */
|
|
23
25
|
breakage?: ReadonlyArray<{
|
|
@@ -25,8 +27,13 @@ export interface EditFailure {
|
|
|
25
27
|
message: string;
|
|
26
28
|
}>;
|
|
27
29
|
failed: Edit;
|
|
28
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* For `ambiguous`: the line each match starts on. For `overlapping`: the
|
|
32
|
+
* line the earlier edit's SEARCH starts on, then this one's.
|
|
33
|
+
*/
|
|
29
34
|
lines?: ReadonlyArray<number>;
|
|
35
|
+
/** For `overlapping`: the earlier edit whose lines this one also changes. */
|
|
36
|
+
overlaps?: Edit;
|
|
30
37
|
reason: EditFailureReason;
|
|
31
38
|
}
|
|
32
39
|
export type ApplyResult = (EditFailure & {
|
|
@@ -36,10 +43,15 @@ export type ApplyResult = (EditFailure & {
|
|
|
36
43
|
text: string;
|
|
37
44
|
};
|
|
38
45
|
/**
|
|
39
|
-
* Applies
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
46
|
+
* Applies a reply's edits to the document it was written against. Every
|
|
47
|
+
* SEARCH is located in that document first, exactly or ignoring
|
|
48
|
+
* indentation, and must be there once; the replacements are then made from
|
|
49
|
+
* the end, so one edit's REPLACE never moves or breaks another's anchor. An
|
|
50
|
+
* empty SEARCH appends. A SEARCH that is not in the document, or that spans
|
|
51
|
+
* lines an earlier edit's SEARCH spans, applies to the text as the edits
|
|
52
|
+
* before it leave it when it is there once, as edits written one after
|
|
53
|
+
* another; the edits after it anchor to that text. Otherwise it fails as
|
|
54
|
+
* `missing` or `overlapping`. Stops at the first edit that fails.
|
|
43
55
|
*/
|
|
44
56
|
export declare function applyEdits(base: string, edits: ReadonlyArray<Edit>): ApplyResult;
|
|
45
57
|
/**
|
package/dist/patch.js
CHANGED
|
@@ -40,18 +40,39 @@ function parseEdits(text) {
|
|
|
40
40
|
}
|
|
41
41
|
return edits;
|
|
42
42
|
}
|
|
43
|
-
/**
|
|
44
|
-
function findLoose(
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
/** Every line range of `text` matching `search` when leading and trailing whitespace is ignored. */
|
|
44
|
+
function findLoose(text, search) {
|
|
45
|
+
const lines = text.split("\n");
|
|
46
|
+
const target = search.split("\n").map((line) => line.trim());
|
|
47
|
+
const offsets = [];
|
|
48
|
+
let offset = 0;
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
offsets.push(offset);
|
|
51
|
+
offset += line.length + 1;
|
|
52
|
+
}
|
|
53
|
+
const ranges = [];
|
|
54
|
+
for (let start = 0; start + target.length <= lines.length; start++) {
|
|
55
|
+
const last = start + target.length - 1;
|
|
56
|
+
if (target.every((line, index) => lines[start + index]?.trim() === line)) ranges.push({
|
|
57
|
+
end: (offsets[last] ?? 0) + (lines[last]?.length ?? 0),
|
|
58
|
+
start: offsets[start] ?? 0
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return ranges;
|
|
49
62
|
}
|
|
50
|
-
/** Every
|
|
63
|
+
/** Every range at which `search` occurs in `text`, without overlaps. */
|
|
51
64
|
function findExact(text, search) {
|
|
52
|
-
const
|
|
53
|
-
for (let index = text.indexOf(search); index !== -1; index = text.indexOf(search, index + search.length))
|
|
54
|
-
|
|
65
|
+
const ranges = [];
|
|
66
|
+
for (let index = text.indexOf(search); index !== -1; index = text.indexOf(search, index + search.length)) ranges.push({
|
|
67
|
+
end: index + search.length,
|
|
68
|
+
start: index
|
|
69
|
+
});
|
|
70
|
+
return ranges;
|
|
71
|
+
}
|
|
72
|
+
/** Where `search` is in `text`: exactly if it is there, else ignoring indentation. */
|
|
73
|
+
function locate(text, search) {
|
|
74
|
+
const exact = findExact(text, search);
|
|
75
|
+
return exact.length > 0 ? exact : findLoose(text, search);
|
|
55
76
|
}
|
|
56
77
|
const lineAt = (text, index) => text.slice(0, index).split("\n").length;
|
|
57
78
|
const MARKERS = /* @__PURE__ */ new Set([
|
|
@@ -61,13 +82,31 @@ const MARKERS = /* @__PURE__ */ new Set([
|
|
|
61
82
|
]);
|
|
62
83
|
const hasMarker = (text) => text.split("\n").some((line) => MARKERS.has(line.trim()));
|
|
63
84
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
|
|
67
|
-
|
|
85
|
+
* `text` with the anchored replacements made from the end, so none moves
|
|
86
|
+
* another's range, then the appends in order.
|
|
87
|
+
*/
|
|
88
|
+
function applyAnchored(text, anchored, appended) {
|
|
89
|
+
let result = text;
|
|
90
|
+
const fromEnd = anchored.toSorted((a, b) => b.range.start - a.range.start);
|
|
91
|
+
for (const place of fromEnd) result = result.slice(0, place.range.start) + place.edit.replace + result.slice(place.range.end);
|
|
92
|
+
for (const replace of appended) result = `${result.trimEnd()}\n${replace}`;
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Applies a reply's edits to the document it was written against. Every
|
|
97
|
+
* SEARCH is located in that document first, exactly or ignoring
|
|
98
|
+
* indentation, and must be there once; the replacements are then made from
|
|
99
|
+
* the end, so one edit's REPLACE never moves or breaks another's anchor. An
|
|
100
|
+
* empty SEARCH appends. A SEARCH that is not in the document, or that spans
|
|
101
|
+
* lines an earlier edit's SEARCH spans, applies to the text as the edits
|
|
102
|
+
* before it leave it when it is there once, as edits written one after
|
|
103
|
+
* another; the edits after it anchor to that text. Otherwise it fails as
|
|
104
|
+
* `missing` or `overlapping`. Stops at the first edit that fails.
|
|
68
105
|
*/
|
|
69
106
|
function applyEdits(base, edits) {
|
|
70
107
|
let text = base;
|
|
108
|
+
let anchored = [];
|
|
109
|
+
let appended = [];
|
|
71
110
|
for (const edit of edits) {
|
|
72
111
|
if (hasMarker(edit.replace)) return {
|
|
73
112
|
failed: edit,
|
|
@@ -75,42 +114,49 @@ function applyEdits(base, edits) {
|
|
|
75
114
|
reason: "malformed"
|
|
76
115
|
};
|
|
77
116
|
if (edit.search.trim() === "") {
|
|
78
|
-
|
|
117
|
+
appended.push(edit.replace);
|
|
79
118
|
continue;
|
|
80
119
|
}
|
|
81
|
-
const
|
|
82
|
-
if (
|
|
120
|
+
const ranges = locate(text, edit.search);
|
|
121
|
+
if (ranges.length > 1) return {
|
|
83
122
|
failed: edit,
|
|
84
|
-
lines:
|
|
123
|
+
lines: ranges.map((range) => lineAt(text, range.start)),
|
|
85
124
|
ok: false,
|
|
86
125
|
reason: "ambiguous"
|
|
87
126
|
};
|
|
88
|
-
const [
|
|
89
|
-
|
|
90
|
-
|
|
127
|
+
const [range] = ranges;
|
|
128
|
+
const overlap = range === void 0 ? void 0 : anchored.find((other) => range.start < other.range.end && other.range.start < range.end);
|
|
129
|
+
if (range !== void 0 && overlap === void 0) {
|
|
130
|
+
anchored.push({
|
|
131
|
+
edit,
|
|
132
|
+
range
|
|
133
|
+
});
|
|
91
134
|
continue;
|
|
92
135
|
}
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
136
|
+
const edited = applyAnchored(text, anchored, appended);
|
|
137
|
+
const later = locate(edited, edit.search);
|
|
138
|
+
const [found] = later;
|
|
139
|
+
if (found === void 0 || later.length > 1) return range === void 0 || overlap === void 0 ? {
|
|
97
140
|
failed: edit,
|
|
98
|
-
lines: starts.map((start) => start + 1),
|
|
99
141
|
ok: false,
|
|
100
|
-
reason: "
|
|
101
|
-
}
|
|
102
|
-
const [start] = starts;
|
|
103
|
-
if (start === void 0) return {
|
|
142
|
+
reason: "missing"
|
|
143
|
+
} : {
|
|
104
144
|
failed: edit,
|
|
145
|
+
lines: [lineAt(text, overlap.range.start), lineAt(text, range.start)],
|
|
105
146
|
ok: false,
|
|
106
|
-
|
|
147
|
+
overlaps: overlap.edit,
|
|
148
|
+
reason: "overlapping"
|
|
107
149
|
};
|
|
108
|
-
|
|
109
|
-
|
|
150
|
+
text = edited;
|
|
151
|
+
anchored = [{
|
|
152
|
+
edit,
|
|
153
|
+
range: found
|
|
154
|
+
}];
|
|
155
|
+
appended = [];
|
|
110
156
|
}
|
|
111
157
|
return {
|
|
112
158
|
ok: true,
|
|
113
|
-
text
|
|
159
|
+
text: applyAnchored(text, anchored, appended)
|
|
114
160
|
};
|
|
115
161
|
}
|
|
116
162
|
/** Diagnostic codes the parser reports when it changed the source to make it parse. */
|
package/dist/signature.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Jig } from "./compile.js";
|
|
2
2
|
//#region src/signature.d.ts
|
|
3
3
|
/** `<statCard label value delta={0} note="">…</statCard>` when the define has a slot. */
|
|
4
|
-
export declare function
|
|
4
|
+
export declare function jigSignature(jig: Jig): string;
|
|
5
5
|
//#endregion
|
package/dist/signature.js
CHANGED
|
@@ -14,12 +14,12 @@ function hasSlot(nodes) {
|
|
|
14
14
|
return false;
|
|
15
15
|
}
|
|
16
16
|
/** `<statCard label value delta={0} note="">…</statCard>` when the define has a slot. */
|
|
17
|
-
function
|
|
18
|
-
const define = parseJigs(
|
|
19
|
-
if (define === void 0) return `<${
|
|
17
|
+
function jigSignature(jig) {
|
|
18
|
+
const define = parseJigs(jig.source).children.find((node) => node.type === "element" && node.name === "define");
|
|
19
|
+
if (define === void 0) return `<${jig.name}>`;
|
|
20
20
|
const attrs = define.props.filter((prop) => prop.name !== "name").map((prop) => formatAttr(prop));
|
|
21
|
-
const open = attrs.length === 0 ? `<${
|
|
22
|
-
return hasSlot(define.children) ? `${open}…</${
|
|
21
|
+
const open = attrs.length === 0 ? `<${jig.name}>` : `<${jig.name} ${attrs.join(" ")}>`;
|
|
22
|
+
return hasSlot(define.children) ? `${open}…</${jig.name}>` : open;
|
|
23
23
|
}
|
|
24
24
|
//#endregion
|
|
25
|
-
export {
|
|
25
|
+
export { jigSignature };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jigs.design/compiler",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Today's Jigs compiler: parser, checker against a catalog, code generator,
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Today's Jigs compiler: parser, checker against a catalog, code generator, jig libraries, and the edit helpers. The v2 rewrite replaces it.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"access": "public"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"acorn": "8.19.0"
|
|
28
|
+
"acorn": "8.19.0",
|
|
29
|
+
"@jigs.design/protocol": "0.3.0"
|
|
29
30
|
}
|
|
30
31
|
}
|