rooklang 0.3.4 → 0.4.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 +2 -0
- package/README.ru.md +2 -0
- package/dist/ast/declarations.d.ts +19 -3
- package/dist/ast/expressions.d.ts +4 -8
- package/dist/ast/statements.d.ts +3 -1
- package/dist/checker/assignments.d.ts +2 -0
- package/dist/checker/assignments.js +9 -3
- package/dist/checker/base.d.ts +8 -2
- package/dist/checker/base.js +3 -3
- package/dist/checker/builtins.js +7 -3
- package/dist/checker/channels.js +1 -2
- package/dist/checker/classes.js +7 -1
- package/dist/checker/closing.js +1 -1
- package/dist/checker/completion.d.ts +1 -1
- package/dist/checker/completion.js +2 -1
- package/dist/checker/component-bindings.d.ts +7 -0
- package/dist/checker/component-bindings.js +45 -0
- package/dist/checker/component-values.js +10 -7
- package/dist/checker/components.d.ts +2 -2
- package/dist/checker/components.js +8 -3
- package/dist/checker/context.d.ts +28 -5
- package/dist/checker/context.js +23 -0
- package/dist/checker/control-flow.d.ts +6 -5
- package/dist/checker/control-flow.js +48 -40
- package/dist/checker/declarations.d.ts +2 -2
- package/dist/checker/declarations.js +3 -2
- package/dist/checker/decorator-exports.js +4 -0
- package/dist/checker/decorator-includes.d.ts +9 -0
- package/dist/checker/decorator-includes.js +71 -0
- package/dist/checker/decorator-members.d.ts +8 -10
- package/dist/checker/decorator-members.js +28 -31
- package/dist/checker/decorator-wrappers.d.ts +1 -1
- package/dist/checker/decorator-wrappers.js +3 -2
- package/dist/checker/decorators.d.ts +14 -5
- package/dist/checker/decorators.js +52 -25
- package/dist/checker/expressions.d.ts +1 -1
- package/dist/checker/expressions.js +7 -6
- package/dist/checker/flow.d.ts +25 -3
- package/dist/checker/flow.js +48 -8
- package/dist/checker/functions.d.ts +2 -2
- package/dist/checker/functions.js +9 -5
- package/dist/checker/helpers.d.ts +4 -1
- package/dist/checker/helpers.js +7 -2
- package/dist/checker/imports.js +5 -2
- package/dist/checker/loops.d.ts +2 -0
- package/dist/checker/loops.js +27 -2
- package/dist/checker/markup.js +14 -2
- package/dist/checker/members.js +3 -1
- package/dist/checker/shared-state.d.ts +11 -0
- package/dist/checker/shared-state.js +37 -0
- package/dist/checker/statements.d.ts +5 -2
- package/dist/checker/statements.js +51 -22
- package/dist/checker/tagged-unions.d.ts +37 -0
- package/dist/checker/tagged-unions.js +132 -0
- package/dist/checker/untyped-globals.d.ts +1 -0
- package/dist/checker/untyped-globals.js +3 -1
- package/dist/codegen/attributes.d.ts +4 -0
- package/dist/codegen/attributes.js +24 -2
- package/dist/codegen/component-helpers.d.ts +6 -1
- package/dist/codegen/component-helpers.js +6 -1
- package/dist/codegen/component-props.d.ts +11 -2
- package/dist/codegen/component-props.js +50 -5
- package/dist/codegen/components.d.ts +6 -5
- package/dist/codegen/components.js +44 -21
- package/dist/codegen/control-flow.d.ts +2 -0
- package/dist/codegen/control-flow.js +23 -9
- package/dist/codegen/creation.js +5 -2
- package/dist/codegen/early-returns.d.ts +4 -3
- package/dist/codegen/early-returns.js +17 -5
- package/dist/codegen/emitter.d.ts +3 -0
- package/dist/codegen/emitter.js +18 -4
- package/dist/codegen/expressions.js +0 -2
- package/dist/codegen/functions.d.ts +11 -1
- package/dist/codegen/functions.js +31 -3
- package/dist/codegen/live.d.ts +60 -0
- package/dist/codegen/live.js +29 -0
- package/dist/codegen/mutations.d.ts +7 -0
- package/dist/codegen/mutations.js +43 -2
- package/dist/codegen/options.d.ts +2 -0
- package/dist/codegen/reactive.d.ts +4 -59
- package/dist/codegen/reactive.js +34 -72
- package/dist/codegen/runtime.js +6 -1
- package/dist/codegen/shared.d.ts +22 -0
- package/dist/codegen/shared.js +93 -0
- package/dist/codegen/simple-statements.d.ts +1 -1
- package/dist/codegen/statements.d.ts +5 -0
- package/dist/codegen/statements.js +35 -0
- package/dist/codegen/updates.d.ts +11 -3
- package/dist/codegen/updates.js +52 -4
- package/dist/codegen/web-components.d.ts +2 -2
- package/dist/codegen/web-components.js +2 -2
- package/dist/compile.js +6 -2
- package/dist/decorator-copy.d.ts +1 -1
- package/dist/decorator-copy.js +1 -0
- package/dist/decorator-names.d.ts +14 -0
- package/dist/decorator-names.js +56 -0
- package/dist/decorators.d.ts +2 -3
- package/dist/decorators.js +36 -14
- package/dist/inline-packages.d.ts +2 -1
- package/dist/inline-packages.js +9 -4
- package/dist/names.d.ts +4 -0
- package/dist/names.js +29 -0
- package/dist/parser/base.d.ts +1 -0
- package/dist/parser/base.js +1 -1
- package/dist/parser/control-flow.d.ts +5 -0
- package/dist/parser/control-flow.js +28 -2
- package/dist/parser/declarations.d.ts +5 -15
- package/dist/parser/declarations.js +15 -119
- package/dist/parser/decorators.d.ts +6 -2
- package/dist/parser/decorators.js +43 -19
- package/dist/parser/params.d.ts +21 -0
- package/dist/parser/params.js +143 -0
- package/dist/parser/simple-statements.d.ts +1 -0
- package/dist/parser/simple-statements.js +22 -6
- package/dist/parser/statements.d.ts +5 -2
- package/dist/parser/statements.js +18 -11
- package/dist/runtime/sources.js +3 -2
- package/dist/walk.js +3 -3
- package/package.json +2 -2
- package/src/runtime/decode.js +41 -10
- package/src/runtime/index.js +1 -0
- package/src/runtime/markup.js +27 -17
- package/src/runtime/shared.js +17 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { guardedByError } from "./error-results.js";
|
|
2
2
|
import { AssignmentChecker } from "./assignments.js";
|
|
3
|
+
import { placeOf } from "./context.js";
|
|
4
|
+
import { isTerminating } from "./flow.js";
|
|
3
5
|
import { isUntyped, isNumeric, ownStatements } from "./helpers.js";
|
|
4
6
|
import { func, hasZeroValue, isNullable, nonNull, nullable, typeToString, UNKNOWN, } from "./types.js";
|
|
5
|
-
// Statements: declarations of variables, blocks, `mount
|
|
7
|
+
// Statements: declarations of variables, blocks, `mount`, `effect`. Assignments and `return`
|
|
6
8
|
// are in assignments.ts.
|
|
7
9
|
export class StatementChecker extends AssignmentChecker {
|
|
8
10
|
checkStatement(node) {
|
|
@@ -92,30 +94,22 @@ export class StatementChecker extends AssignmentChecker {
|
|
|
92
94
|
* a handler. It cleans up with `defer`, so it returns nothing.
|
|
93
95
|
*/
|
|
94
96
|
checkEffect(node) {
|
|
95
|
-
|
|
96
|
-
if (statement.kind === 'ReturnStatement' && statement.values.length > 0) {
|
|
97
|
-
this.error('an effect returns nothing; clean up with defer, e.g. defer clearInterval(timer)', statement.values[0]);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
this.checkFunction([], func([], []), null, node.body, { closure: true });
|
|
97
|
+
this.checkDeferring(node.body, 'an effect');
|
|
101
98
|
}
|
|
102
99
|
/**
|
|
103
|
-
* `mount
|
|
104
|
-
*
|
|
100
|
+
* `mount { ... }`: a function that runs once the markup is in the document, like a handler. It
|
|
101
|
+
* cleans up with `defer`, as an effect does, so it returns nothing.
|
|
105
102
|
*/
|
|
106
103
|
checkMount(node) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
(result.kind === 'function' && result.required === 0));
|
|
115
|
-
if (!cleans) {
|
|
116
|
-
const returned = results.length === 1 ? typeToString(results[0]) : `${results.length} values`;
|
|
117
|
-
this.error(`mount() returns the function that cleans up, e.g. return () => clearInterval(timer), not ${returned}`, { start: node.start, end: node.start + 'mount'.length });
|
|
104
|
+
this.checkDeferring(node.body, 'mount');
|
|
105
|
+
}
|
|
106
|
+
checkDeferring(body, what) {
|
|
107
|
+
for (const statement of ownStatements(body)) {
|
|
108
|
+
if (statement.kind === 'ReturnStatement' && statement.values.length > 0) {
|
|
109
|
+
this.error(`${what} returns nothing; clean up with defer, e.g. defer clearInterval(timer)`, statement.values[0]);
|
|
110
|
+
}
|
|
118
111
|
}
|
|
112
|
+
this.checkFunction([], func([], []), null, body, { closure: true });
|
|
119
113
|
}
|
|
120
114
|
/**
|
|
121
115
|
* `delete scores[name]` removes a key of an object used as a map, `{ [string]: T }`, or a field
|
|
@@ -133,6 +127,8 @@ export class StatementChecker extends AssignmentChecker {
|
|
|
133
127
|
: null;
|
|
134
128
|
if (target.kind === 'IndexExpression')
|
|
135
129
|
this.checkValue(target.index);
|
|
130
|
+
// `delete user.boss` makes the field null again.
|
|
131
|
+
this.forget(target);
|
|
136
132
|
if (isUntyped(object))
|
|
137
133
|
return;
|
|
138
134
|
if (object.kind === 'array') {
|
|
@@ -180,7 +176,7 @@ export class StatementChecker extends AssignmentChecker {
|
|
|
180
176
|
binding.type = type;
|
|
181
177
|
const value = values.length === names.length ? values[i] : undefined;
|
|
182
178
|
if (value && isNullable(type) && !isNullable(this.typeOfChecked(value))) {
|
|
183
|
-
this.flow.set(binding, nonNull(type));
|
|
179
|
+
this.flow.set(placeOf(binding), nonNull(type));
|
|
184
180
|
}
|
|
185
181
|
});
|
|
186
182
|
if (node.keyword === 'const' && !declared)
|
|
@@ -207,7 +203,10 @@ export class StatementChecker extends AssignmentChecker {
|
|
|
207
203
|
initializerTypes(node, declared) {
|
|
208
204
|
const { names, values } = node;
|
|
209
205
|
let types;
|
|
210
|
-
if (
|
|
206
|
+
if (node.body) {
|
|
207
|
+
types = [this.derivedBlockType(names[0].name, node.body.body, declared)];
|
|
208
|
+
}
|
|
209
|
+
else if (values.length === 0) {
|
|
211
210
|
if (declared && !hasZeroValue(declared) && declared.kind !== 'unknown') {
|
|
212
211
|
const type = typeToString(declared);
|
|
213
212
|
this.error(`${type} has no zero value: give "${names[0].name}" a value or make it nullable with ${typeToString(nullable(declared))}`, node.type ?? node);
|
|
@@ -228,4 +227,34 @@ export class StatementChecker extends AssignmentChecker {
|
|
|
228
227
|
}
|
|
229
228
|
return types;
|
|
230
229
|
}
|
|
230
|
+
/** `derived total number { ... }`: the block runs like the body of a closure and returns the value. */
|
|
231
|
+
derivedBlockType(name, body, declared) {
|
|
232
|
+
const ends = isTerminating(body);
|
|
233
|
+
if (!ends) {
|
|
234
|
+
this.error(`derived "${name}" must return its value at the end of the block, e.g. return total`, {
|
|
235
|
+
start: body.end - 1,
|
|
236
|
+
end: body.end,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
const saved = this.derivedBlock;
|
|
240
|
+
const results = declared ? [declared] : null;
|
|
241
|
+
this.derivedBlock = name;
|
|
242
|
+
try {
|
|
243
|
+
const types = this.checkFunction([], func([], results ?? []), results, body, {
|
|
244
|
+
closure: true,
|
|
245
|
+
});
|
|
246
|
+
if (types.length === 1)
|
|
247
|
+
return types[0];
|
|
248
|
+
// A block that does not end with a return is reported above.
|
|
249
|
+
if (ends) {
|
|
250
|
+
this.error(types.length === 0
|
|
251
|
+
? `derived "${name}" returns no value: return it, e.g. return total`
|
|
252
|
+
: `derived "${name}" is one value, but its block returns ${types.length}`, body);
|
|
253
|
+
}
|
|
254
|
+
return UNKNOWN;
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
this.derivedBlock = saved;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
231
260
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type * as ast from '../ast.ts';
|
|
2
|
+
import { type Narrowing, type Place } from './context.ts';
|
|
3
|
+
import { ControlFlowChecker } from './control-flow.ts';
|
|
4
|
+
import { type LiteralType, type Member, type Type } from './types.ts';
|
|
5
|
+
type Value = LiteralType['value'];
|
|
6
|
+
export declare abstract class TaggedUnionChecker extends ControlFlowChecker {
|
|
7
|
+
/** The paths among the checked member expressions, with the types of their fields. */
|
|
8
|
+
private readonly paths;
|
|
9
|
+
/** The types of the fields of the paths, before narrowing: what their tags are found in. */
|
|
10
|
+
private readonly declared;
|
|
11
|
+
protected isTagOf(place: Place, field: string): boolean;
|
|
12
|
+
protected narrowable(node: ast.Expression): {
|
|
13
|
+
place: Place;
|
|
14
|
+
type: Type;
|
|
15
|
+
} | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* The type of `object.field` read by `node`, which is `type` unless the expression is a path
|
|
18
|
+
* narrowed here. Getters and methods are not paths: they may give another value each time.
|
|
19
|
+
*/
|
|
20
|
+
protected pathType(node: ast.MemberExpression, object: Type, type: Type): Type;
|
|
21
|
+
/** The field `name` of an object or a class instance, unless it is a method or an accessor. */
|
|
22
|
+
protected dataField(type: Type, name: string): Member | undefined;
|
|
23
|
+
/** The values of the tag `name` in each member of a union; `null` when `name` is not a tag. */
|
|
24
|
+
protected tagValues(members: readonly Type[], name: string): Value[][] | null;
|
|
25
|
+
/** `d.kind == "courier"` narrows `d` too: the members with this tag, or the others. */
|
|
26
|
+
protected narrow(node: ast.Expression, assumeTrue: boolean): Narrowing;
|
|
27
|
+
/** `switch d.kind`: in a case the members with its tags, in `default` those of no case. */
|
|
28
|
+
protected narrowCase(node: ast.SwitchStatement, switchCase: ast.SwitchCase): Narrowing;
|
|
29
|
+
private narrowTag;
|
|
30
|
+
/**
|
|
31
|
+
* `{ kind: "pickup", shop: "Main st." }` where one of several objects is expected: the member
|
|
32
|
+
* that a tag of the literal chooses, or `null` when no tag chooses one. A tag of no member is
|
|
33
|
+
* reported, and the literal then gets `UNKNOWN`, so that it is not reported again.
|
|
34
|
+
*/
|
|
35
|
+
protected taggedMember(node: ast.ObjectLiteral, objects: readonly Type[], union: Type): Type | null;
|
|
36
|
+
}
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { placeOf } from "./context.js";
|
|
2
|
+
import { ControlFlowChecker } from "./control-flow.js";
|
|
3
|
+
import { literalValue, narrowByTag } from "./flow.js";
|
|
4
|
+
import { findClassMember, memberTypeOf, nonNull, typeToString, unionMembers, UNKNOWN, } from "./types.js";
|
|
5
|
+
export class TaggedUnionChecker extends ControlFlowChecker {
|
|
6
|
+
/** The paths among the checked member expressions, with the types of their fields. */
|
|
7
|
+
paths = new WeakMap();
|
|
8
|
+
/** The types of the fields of the paths, before narrowing: what their tags are found in. */
|
|
9
|
+
declared = new WeakMap();
|
|
10
|
+
isTagOf(place, field) {
|
|
11
|
+
const type = place.path === '' ? place.binding.type : this.declared.get(place);
|
|
12
|
+
return type != null && this.tagValues(unionMembers(nonNull(type)), field) !== null;
|
|
13
|
+
}
|
|
14
|
+
narrowable(node) {
|
|
15
|
+
if (node.kind === 'Identifier') {
|
|
16
|
+
const binding = this.narrowableBinding(node);
|
|
17
|
+
if (!binding?.type)
|
|
18
|
+
return undefined;
|
|
19
|
+
const place = placeOf(binding);
|
|
20
|
+
return { place, type: this.flow.get(place) ?? binding.type };
|
|
21
|
+
}
|
|
22
|
+
const path = node.kind === 'MemberExpression' ? this.paths.get(node) : undefined;
|
|
23
|
+
return path && { place: path.place, type: this.flow.get(path.place) ?? path.type };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The type of `object.field` read by `node`, which is `type` unless the expression is a path
|
|
27
|
+
* narrowed here. Getters and methods are not paths: they may give another value each time.
|
|
28
|
+
*/
|
|
29
|
+
pathType(node, object, type) {
|
|
30
|
+
const parent = node.optional ? undefined : this.narrowable(node.object)?.place;
|
|
31
|
+
const { name } = node.property;
|
|
32
|
+
if (!parent || !unionMembers(object).every((member) => this.dataField(member, name))) {
|
|
33
|
+
this.paths.delete(node);
|
|
34
|
+
return type;
|
|
35
|
+
}
|
|
36
|
+
const place = placeOf(parent.binding, parent.path === '' ? name : `${parent.path}.${name}`);
|
|
37
|
+
this.paths.set(node, { place, type });
|
|
38
|
+
this.declared.set(place, type);
|
|
39
|
+
return this.flow.get(place) ?? type;
|
|
40
|
+
}
|
|
41
|
+
/** The field `name` of an object or a class instance, unless it is a method or an accessor. */
|
|
42
|
+
dataField(type, name) {
|
|
43
|
+
let member;
|
|
44
|
+
if (type.kind === 'object') {
|
|
45
|
+
member = type.members.get(name);
|
|
46
|
+
}
|
|
47
|
+
else if (type.kind === 'class') {
|
|
48
|
+
this.ensureClassResolved(type.info);
|
|
49
|
+
member = findClassMember(type.info, name, false);
|
|
50
|
+
if (member && type.args)
|
|
51
|
+
member = { ...member, type: memberTypeOf(type, member) };
|
|
52
|
+
}
|
|
53
|
+
return member && !member.method && member.accessor === undefined ? member : undefined;
|
|
54
|
+
}
|
|
55
|
+
/** The values of the tag `name` in each member of a union; `null` when `name` is not a tag. */
|
|
56
|
+
tagValues(members, name) {
|
|
57
|
+
if (members.length < 2)
|
|
58
|
+
return null;
|
|
59
|
+
const tags = [];
|
|
60
|
+
for (const member of members) {
|
|
61
|
+
const field = this.dataField(member, name);
|
|
62
|
+
const types = field ? unionMembers(field.type) : [];
|
|
63
|
+
if (!types.every((type) => type.kind === 'literal'))
|
|
64
|
+
return null;
|
|
65
|
+
if (types.length === 0)
|
|
66
|
+
return null;
|
|
67
|
+
tags.push(types.map((type) => type.value));
|
|
68
|
+
}
|
|
69
|
+
return tags;
|
|
70
|
+
}
|
|
71
|
+
/** `d.kind == "courier"` narrows `d` too: the members with this tag, or the others. */
|
|
72
|
+
narrow(node, assumeTrue) {
|
|
73
|
+
const narrowing = super.narrow(node, assumeTrue);
|
|
74
|
+
if (node.kind !== 'BinaryExpression')
|
|
75
|
+
return narrowing;
|
|
76
|
+
if (node.operator !== '==' && node.operator !== '!=')
|
|
77
|
+
return narrowing;
|
|
78
|
+
const right = literalValue(node.right);
|
|
79
|
+
const value = right ?? literalValue(node.left);
|
|
80
|
+
if (value === undefined)
|
|
81
|
+
return narrowing;
|
|
82
|
+
const target = right !== undefined ? node.left : node.right;
|
|
83
|
+
const equal = (node.operator === '==') === assumeTrue;
|
|
84
|
+
return [...narrowing, ...this.narrowTag(target, [value], equal)];
|
|
85
|
+
}
|
|
86
|
+
/** `switch d.kind`: in a case the members with its tags, in `default` those of no case. */
|
|
87
|
+
narrowCase(node, switchCase) {
|
|
88
|
+
const narrowing = super.narrowCase(node, switchCase);
|
|
89
|
+
if (!node.discriminant)
|
|
90
|
+
return narrowing;
|
|
91
|
+
const isDefault = switchCase.tests.length === 0;
|
|
92
|
+
const tests = isDefault ? node.cases.flatMap((each) => each.tests) : switchCase.tests;
|
|
93
|
+
const values = tests.map(literalValue);
|
|
94
|
+
if (!isDefault && values.includes(undefined))
|
|
95
|
+
return narrowing;
|
|
96
|
+
const known = values.filter((value) => value !== undefined);
|
|
97
|
+
return [...narrowing, ...this.narrowTag(node.discriminant, known, !isDefault)];
|
|
98
|
+
}
|
|
99
|
+
narrowTag(target, values, equal) {
|
|
100
|
+
if (target.kind !== 'MemberExpression' || target.optional)
|
|
101
|
+
return [];
|
|
102
|
+
const owner = this.narrowable(target.object);
|
|
103
|
+
const tags = owner && this.tagValues(unionMembers(nonNull(owner.type)), target.property.name);
|
|
104
|
+
return owner && tags ? [[owner.place, narrowByTag(owner.type, tags, values, equal)]] : [];
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* `{ kind: "pickup", shop: "Main st." }` where one of several objects is expected: the member
|
|
108
|
+
* that a tag of the literal chooses, or `null` when no tag chooses one. A tag of no member is
|
|
109
|
+
* reported, and the literal then gets `UNKNOWN`, so that it is not reported again.
|
|
110
|
+
*/
|
|
111
|
+
taggedMember(node, objects, union) {
|
|
112
|
+
for (const property of node.properties) {
|
|
113
|
+
if (property.kind === 'SpreadElement')
|
|
114
|
+
continue;
|
|
115
|
+
const name = property.key.kind === 'Identifier' ? property.key.name : property.key.value;
|
|
116
|
+
const value = literalValue(property.value);
|
|
117
|
+
const tags = value === undefined ? null : this.tagValues(objects, name);
|
|
118
|
+
if (value === undefined || !tags)
|
|
119
|
+
continue;
|
|
120
|
+
const chosen = objects.filter((_, i) => tags[i].includes(value));
|
|
121
|
+
if (chosen.length === 1)
|
|
122
|
+
return chosen[0];
|
|
123
|
+
if (chosen.length > 1)
|
|
124
|
+
continue;
|
|
125
|
+
const known = [...new Set(tags.flat())].map((each) => JSON.stringify(each));
|
|
126
|
+
const choices = known.length > 1 ? `${known.slice(0, -1).join(', ')} or ${known.at(-1)}` : known[0];
|
|
127
|
+
this.error(`${JSON.stringify(value)} is not a kind of ${typeToString(union)}: write ${choices}`, property.value);
|
|
128
|
+
return UNKNOWN;
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -32,7 +32,6 @@ export const UNTYPED_GLOBALS = [
|
|
|
32
32
|
'Response',
|
|
33
33
|
'Request',
|
|
34
34
|
'Headers',
|
|
35
|
-
'AbortController',
|
|
36
35
|
'fetch',
|
|
37
36
|
'structuredClone',
|
|
38
37
|
'queueMicrotask',
|
|
@@ -45,3 +44,6 @@ export const UNTYPED_GLOBALS = [
|
|
|
45
44
|
'crypto',
|
|
46
45
|
'performance',
|
|
47
46
|
];
|
|
47
|
+
// Classes that are names of types too, `any` in both: `new AbortController()` and
|
|
48
|
+
// `signal AbortSignal`. TypeScript's lib files replace them as they replace the DOM.
|
|
49
|
+
export const UNTYPED_CLASSES = ['AbortController', 'AbortSignal', 'ImageData'];
|
|
@@ -20,6 +20,10 @@ export declare abstract class AttributeEmitter extends UpdateEmitter {
|
|
|
20
20
|
protected attribute(element: string, tag: string | null, attribute: ast.JsxAttribute | ast.JsxSpreadAttribute, live: Live | null, svg?: boolean, scope?: string | null): void;
|
|
21
21
|
/** The class of an element with the class of its style: `"card rook-card-1x9k2az"`. */
|
|
22
22
|
private scopedClass;
|
|
23
|
+
/** Whether `class={...}` is a list of classes, `[styles.tab, active ? styles.active : null]`. */
|
|
24
|
+
protected isClassList(value: ast.Expression): boolean;
|
|
25
|
+
/** The classes of `class={...}` as a string, before the class of the style is added to them. */
|
|
26
|
+
protected classText(value: ast.Expression, text: string): string;
|
|
23
27
|
/** Sets the class of an element: `className`, or the attribute of an SVG element. */
|
|
24
28
|
protected scopeClass(element: string, classes: string, svg: boolean): void;
|
|
25
29
|
/** `<app-card item-count={1} />`: the property of a web component of this module, if it has one. */
|
|
@@ -3,6 +3,7 @@ import { attributeName } from "../html-tag.js";
|
|
|
3
3
|
import { SVG_NAMESPACE } from "../svg.js";
|
|
4
4
|
import { eventName, mentions } from "./element-helpers.js";
|
|
5
5
|
import { ARROW } from "./syntax.js";
|
|
6
|
+
import { rootsOf } from "./shared.js";
|
|
6
7
|
import { UpdateEmitter } from "./updates.js";
|
|
7
8
|
// Attributes of elements: properties, setAttribute, styles, `bind:` and event handlers.
|
|
8
9
|
export class AttributeEmitter extends UpdateEmitter {
|
|
@@ -46,6 +47,15 @@ export class AttributeEmitter extends UpdateEmitter {
|
|
|
46
47
|
this.scopedClass(element, value, live, svg, scope);
|
|
47
48
|
return;
|
|
48
49
|
}
|
|
50
|
+
if (name === 'class' &&
|
|
51
|
+
value !== null &&
|
|
52
|
+
value.kind !== 'EventHandler' &&
|
|
53
|
+
this.isClassList(value)) {
|
|
54
|
+
this.setLive(live, value, (text) => svg
|
|
55
|
+
? `${element}.setAttribute("class", $$classes(${text}));`
|
|
56
|
+
: `${element}.className = $$classes(${text});`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
49
59
|
const webProperty = tag === null || svg ? null : this.webComponentProperty(tag, name);
|
|
50
60
|
if (webProperty) {
|
|
51
61
|
this.webComponentAttribute(element, webProperty, value, live);
|
|
@@ -102,15 +112,24 @@ export class AttributeEmitter extends UpdateEmitter {
|
|
|
102
112
|
this.scopeClass(element, `${value?.value ?? ''} ${scope}`.trim(), svg);
|
|
103
113
|
}
|
|
104
114
|
else if (value.kind !== 'EventHandler') {
|
|
105
|
-
const nullable = this.typeOf(value)?.kind !== 'string';
|
|
106
115
|
this.setLive(live, value, (text) => {
|
|
107
|
-
const scoped = `\`\${${
|
|
116
|
+
const scoped = `\`\${${this.classText(value, text)}} ${scope}\``;
|
|
108
117
|
return svg
|
|
109
118
|
? `${element}.setAttribute("class", ${scoped});`
|
|
110
119
|
: `${element}.className = ${scoped};`;
|
|
111
120
|
});
|
|
112
121
|
}
|
|
113
122
|
}
|
|
123
|
+
/** Whether `class={...}` is a list of classes, `[styles.tab, active ? styles.active : null]`. */
|
|
124
|
+
isClassList(value) {
|
|
125
|
+
return value.kind === 'ArrayLiteral' || this.typeOf(value)?.kind === 'array';
|
|
126
|
+
}
|
|
127
|
+
/** The classes of `class={...}` as a string, before the class of the style is added to them. */
|
|
128
|
+
classText(value, text) {
|
|
129
|
+
if (this.isClassList(value))
|
|
130
|
+
return `$$classes(${text})`;
|
|
131
|
+
return this.typeOf(value)?.kind === 'string' ? text : `${text} ?? ""`;
|
|
132
|
+
}
|
|
114
133
|
/** Sets the class of an element: `className`, or the attribute of an SVG element. */
|
|
115
134
|
scopeClass(element, classes, svg) {
|
|
116
135
|
const text = JSON.stringify(classes);
|
|
@@ -178,6 +197,9 @@ export class AttributeEmitter extends UpdateEmitter {
|
|
|
178
197
|
try {
|
|
179
198
|
const body = this.block(() => {
|
|
180
199
|
this.line(`${target} = ${current};`);
|
|
200
|
+
for (const name of rootsOf(value, this.shared)) {
|
|
201
|
+
this.line(`$$changed(${this.shared.listeners.get(name)});`);
|
|
202
|
+
}
|
|
181
203
|
for (const source of written) {
|
|
182
204
|
// The element already shows what was entered: its own update is skipped.
|
|
183
205
|
const own = live && dependsOn.has(source) ? live.ownIndex(source) : null;
|
|
@@ -25,9 +25,14 @@ export interface EarlyReturns {
|
|
|
25
25
|
}
|
|
26
26
|
/** Marks where the setters of an inlined component with early returns go: before any return. */
|
|
27
27
|
export declare const SETTERS: string;
|
|
28
|
+
/**
|
|
29
|
+
* Marks the `$$watch(...)` of a component that reads state of the module, around the id of the
|
|
30
|
+
* source: it stays only if something depends on the state (resolveMarkers).
|
|
31
|
+
*/
|
|
32
|
+
export declare const LISTEN: string;
|
|
28
33
|
/** `f(a, undefined, undefined)` → `f(a)`: missing arguments are undefined anyway. */
|
|
29
34
|
export declare function withoutTrailingUndefined(args: string[]): string[];
|
|
30
|
-
/** Whether a component has `mount
|
|
35
|
+
/** Whether a component has `mount { ... }`. */
|
|
31
36
|
export declare function hasMount(component: ast.ComponentDeclaration): boolean;
|
|
32
37
|
/** Whether a component returns before the end of its body. */
|
|
33
38
|
export declare function hasEarlyReturn(component: ast.ComponentDeclaration): boolean;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { containsReturn } from "../recursion.js";
|
|
2
2
|
/** Marks where the setters of an inlined component with early returns go: before any return. */
|
|
3
3
|
export const SETTERS = String.fromCharCode(0xe002);
|
|
4
|
+
/**
|
|
5
|
+
* Marks the `$$watch(...)` of a component that reads state of the module, around the id of the
|
|
6
|
+
* source: it stays only if something depends on the state (resolveMarkers).
|
|
7
|
+
*/
|
|
8
|
+
export const LISTEN = String.fromCharCode(0xe004);
|
|
4
9
|
/** `f(a, undefined, undefined)` → `f(a)`: missing arguments are undefined anyway. */
|
|
5
10
|
export function withoutTrailingUndefined(args) {
|
|
6
11
|
let end = args.length;
|
|
@@ -8,7 +13,7 @@ export function withoutTrailingUndefined(args) {
|
|
|
8
13
|
end--;
|
|
9
14
|
return args.slice(0, end);
|
|
10
15
|
}
|
|
11
|
-
/** Whether a component has `mount
|
|
16
|
+
/** Whether a component has `mount { ... }`. */
|
|
12
17
|
export function hasMount(component) {
|
|
13
18
|
return component.body.body.some((statement) => statement.kind === 'MountStatement');
|
|
14
19
|
}
|
|
@@ -9,7 +9,7 @@ export declare abstract class ComponentPropsEmitter extends ControlFlowEmitter {
|
|
|
9
9
|
/** Setters waiting for their place at the top of an inlined component, by its result. */
|
|
10
10
|
protected readonly pendingSetters: Map<string, string[]>;
|
|
11
11
|
/**
|
|
12
|
-
* In a component with `mount
|
|
12
|
+
* In a component with `mount`: the variable that gets its first node once the markup is
|
|
13
13
|
* created, so that `$$mount` can wait until that node is in the document.
|
|
14
14
|
*/
|
|
15
15
|
protected mountNode: string | null;
|
|
@@ -17,7 +17,14 @@ export declare abstract class ComponentPropsEmitter extends ControlFlowEmitter {
|
|
|
17
17
|
* A use of a recursive component: a call of its function. The parent's updates call the
|
|
18
18
|
* returned function with the new values of the properties that depend on its state.
|
|
19
19
|
*/
|
|
20
|
-
protected callComponent(component: ast.ComponentDeclaration, values: ReadonlyMap<string, string>, reactiveProps: ReadonlyMap<string, [string, Set<Source>]>, live: Live | null, root?: string): string;
|
|
20
|
+
protected callComponent(component: ast.ComponentDeclaration, values: ReadonlyMap<string, string>, reactiveProps: ReadonlyMap<string, [string, Set<Source>]>, live: Live | null, root?: string, binders?: ReadonlyMap<string, string>): string;
|
|
21
|
+
/**
|
|
22
|
+
* `bind:active={tab}`: the function that the component calls with the new value of `active`. It
|
|
23
|
+
* is written in the caller, so it writes the caller's `tab` and updates what reads it there.
|
|
24
|
+
*/
|
|
25
|
+
protected binder(target: ast.Expression): string;
|
|
26
|
+
/** `$$bindActive?.(active);` in a function component, which gets the binders as arguments. */
|
|
27
|
+
protected bindersOf(component: ast.ComponentDeclaration, reactive: Reactive): string[];
|
|
21
28
|
protected reactiveOf(component: ast.ComponentDeclaration, props: Iterable<string>): Reactive;
|
|
22
29
|
/**
|
|
23
30
|
* `title, kind = "info"`: parameters with their default values, each name with a prefix. A tag
|
|
@@ -40,3 +47,5 @@ export declare abstract class ComponentPropsEmitter extends ControlFlowEmitter {
|
|
|
40
47
|
*/
|
|
41
48
|
protected spreadProps(argument: ast.Expression, component: ast.ComponentDeclaration, live: Live | null, values: Map<string, string>, reactiveProps: Map<string, [string, Set<Source>]>): void;
|
|
42
49
|
}
|
|
50
|
+
/** The bound properties of a component, `bind active string`, in their order. */
|
|
51
|
+
export declare function boundParams(component: ast.ComponentDeclaration): ast.Parameter[];
|
|
@@ -3,6 +3,7 @@ import { declaredNames } from "../names.js";
|
|
|
3
3
|
import { isConstant, isSimple, rootName } from "./analysis.js";
|
|
4
4
|
import { ControlFlowEmitter } from "./control-flow.js";
|
|
5
5
|
import { Reactive, updatesOf } from "./reactive.js";
|
|
6
|
+
import { rootsOf } from "./shared.js";
|
|
6
7
|
import { ARROW, indentMore, lowerFirst, POSTFIX, upperFirst } from "./syntax.js";
|
|
7
8
|
import { withoutTrailingUndefined } from "./component-helpers.js";
|
|
8
9
|
/** The properties of components: their values, spread objects, and calls of function components. */
|
|
@@ -12,7 +13,7 @@ export class ComponentPropsEmitter extends ControlFlowEmitter {
|
|
|
12
13
|
/** Setters waiting for their place at the top of an inlined component, by its result. */
|
|
13
14
|
pendingSetters = new Map();
|
|
14
15
|
/**
|
|
15
|
-
* In a component with `mount
|
|
16
|
+
* In a component with `mount`: the variable that gets its first node once the markup is
|
|
16
17
|
* created, so that `$$mount` can wait until that node is in the document.
|
|
17
18
|
*/
|
|
18
19
|
mountNode = null;
|
|
@@ -20,15 +21,17 @@ export class ComponentPropsEmitter extends ControlFlowEmitter {
|
|
|
20
21
|
* A use of a recursive component: a call of its function. The parent's updates call the
|
|
21
22
|
* returned function with the new values of the properties that depend on its state.
|
|
22
23
|
*/
|
|
23
|
-
callComponent(component, values, reactiveProps, live, root) {
|
|
24
|
+
callComponent(component, values, reactiveProps, live, root, binders = new Map()) {
|
|
24
25
|
const name = component.name.name;
|
|
25
26
|
const id = this.nextId();
|
|
26
27
|
const result = `$$${lowerFirst(name)}${id}`;
|
|
27
28
|
// A missing property with a default value is left undefined, so that the default applies.
|
|
28
29
|
const argument = (param) => values.get(param.name.name) ?? (param.defaultValue ? 'undefined' : 'null');
|
|
29
|
-
// The
|
|
30
|
+
// The functions that write bound properties into the caller come next, then the element a ref
|
|
31
|
+
// created for it, as `$$root`.
|
|
30
32
|
const args = withoutTrailingUndefined([
|
|
31
33
|
...component.params.map(argument),
|
|
34
|
+
...boundParams(component).map((param) => binders.get(param.name.name) ?? 'undefined'),
|
|
32
35
|
...(root === undefined ? [] : [root]),
|
|
33
36
|
]);
|
|
34
37
|
const fn = this.functionNames.get(component) ?? `$$${name}`;
|
|
@@ -49,13 +52,51 @@ export class ComponentPropsEmitter extends ControlFlowEmitter {
|
|
|
49
52
|
live.depend(sources, `${setter}(${setterArgs.join(', ')});`);
|
|
50
53
|
return result;
|
|
51
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* `bind:active={tab}`: the function that the component calls with the new value of `active`. It
|
|
57
|
+
* is written in the caller, so it writes the caller's `tab` and updates what reads it there.
|
|
58
|
+
*/
|
|
59
|
+
binder(target) {
|
|
60
|
+
const name = `$$bind${this.nextId()}`;
|
|
61
|
+
const text = this.liveExpression(target);
|
|
62
|
+
// `todos` of `bind:todos={todos}`, or the state that `bind:open={menu.open}` writes into.
|
|
63
|
+
const written = [...(this.reactive?.rootSources(target) ?? [])].filter((source) => source.kind === 'state');
|
|
64
|
+
const body = this.block(() => {
|
|
65
|
+
this.line(`${text} = $$value;`);
|
|
66
|
+
for (const name of rootsOf(target, this.shared)) {
|
|
67
|
+
this.line(`$$changed(${this.shared.listeners.get(name)});`);
|
|
68
|
+
}
|
|
69
|
+
for (const source of written)
|
|
70
|
+
this.line(`${this.write(source)};`);
|
|
71
|
+
});
|
|
72
|
+
this.line(`const ${name} = ($$value) => ${body};`);
|
|
73
|
+
return name;
|
|
74
|
+
}
|
|
75
|
+
/** `$$bindActive?.(active);` in a function component, which gets the binders as arguments. */
|
|
76
|
+
bindersOf(component, reactive) {
|
|
77
|
+
return boundParams(component).map((param) => {
|
|
78
|
+
const binder = `$$bind${upperFirst(param.name.name)}`;
|
|
79
|
+
reactive.sources.get(param.name.name).bound = `${binder}?.(${this.name(param.name.name)});`;
|
|
80
|
+
return binder;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
52
83
|
reactiveOf(component, props) {
|
|
84
|
+
const shared = new Set(this.shared.listeners.keys());
|
|
53
85
|
return new Reactive(component, this.fn, props, (sourceName, kind) => {
|
|
54
86
|
const id = this.nextId();
|
|
55
87
|
// `$$card$price`, an argument of a decorator, is updated by `$$updateCard$price3`.
|
|
56
88
|
const update = `$$update${upperFirst(sourceName.replace(/^\$\$/, ''))}${id}`;
|
|
57
|
-
return {
|
|
58
|
-
|
|
89
|
+
return {
|
|
90
|
+
id,
|
|
91
|
+
name: sourceName,
|
|
92
|
+
kind,
|
|
93
|
+
update,
|
|
94
|
+
computations: [],
|
|
95
|
+
dependents: [],
|
|
96
|
+
writes: [],
|
|
97
|
+
bound: null,
|
|
98
|
+
};
|
|
99
|
+
}, shared);
|
|
59
100
|
}
|
|
60
101
|
/**
|
|
61
102
|
* `title, kind = "info"`: parameters with their default values, each name with a prefix. A tag
|
|
@@ -171,3 +212,7 @@ export class ComponentPropsEmitter extends ControlFlowEmitter {
|
|
|
171
212
|
}
|
|
172
213
|
}
|
|
173
214
|
}
|
|
215
|
+
/** The bound properties of a component, `bind active string`, in their order. */
|
|
216
|
+
export function boundParams(component) {
|
|
217
|
+
return component.params.filter((param) => param.bind);
|
|
218
|
+
}
|
|
@@ -19,16 +19,17 @@ export declare abstract class ComponentEmitter extends StyleEmitter {
|
|
|
19
19
|
* reads; one that reads no state is a constant.
|
|
20
20
|
*/
|
|
21
21
|
private derivedValue;
|
|
22
|
+
/**
|
|
23
|
+
* `derived summary { ... }`: the block becomes a function that computes the value, called first
|
|
24
|
+
* and in the updates of what it reads; one that reads no state runs once. The block computes
|
|
25
|
+
* while the markup is created or updated, so it calls no updates, as the expressions there.
|
|
26
|
+
*/
|
|
27
|
+
private derivedBlock;
|
|
22
28
|
/**
|
|
23
29
|
* An argument of a decorator that reads properties of the component: when they change, it is
|
|
24
30
|
* computed again, and what reads it is updated.
|
|
25
31
|
*/
|
|
26
32
|
private decoratorArgument;
|
|
27
|
-
/**
|
|
28
|
-
* `mount() { ... }` becomes `$$mount(() => { ... })`. Its body runs later, like a handler: what
|
|
29
|
-
* it changes updates the markup.
|
|
30
|
-
*/
|
|
31
|
-
protected mountStatement(node: ast.MountStatement): void;
|
|
32
33
|
/**
|
|
33
34
|
* Inlines a component: its code goes into a block where its properties are constants. Attribute
|
|
34
35
|
* values are computed before the block, so that the component's own names cannot hide the
|