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.
Files changed (123) hide show
  1. package/README.md +2 -0
  2. package/README.ru.md +2 -0
  3. package/dist/ast/declarations.d.ts +19 -3
  4. package/dist/ast/expressions.d.ts +4 -8
  5. package/dist/ast/statements.d.ts +3 -1
  6. package/dist/checker/assignments.d.ts +2 -0
  7. package/dist/checker/assignments.js +9 -3
  8. package/dist/checker/base.d.ts +8 -2
  9. package/dist/checker/base.js +3 -3
  10. package/dist/checker/builtins.js +7 -3
  11. package/dist/checker/channels.js +1 -2
  12. package/dist/checker/classes.js +7 -1
  13. package/dist/checker/closing.js +1 -1
  14. package/dist/checker/completion.d.ts +1 -1
  15. package/dist/checker/completion.js +2 -1
  16. package/dist/checker/component-bindings.d.ts +7 -0
  17. package/dist/checker/component-bindings.js +45 -0
  18. package/dist/checker/component-values.js +10 -7
  19. package/dist/checker/components.d.ts +2 -2
  20. package/dist/checker/components.js +8 -3
  21. package/dist/checker/context.d.ts +28 -5
  22. package/dist/checker/context.js +23 -0
  23. package/dist/checker/control-flow.d.ts +6 -5
  24. package/dist/checker/control-flow.js +48 -40
  25. package/dist/checker/declarations.d.ts +2 -2
  26. package/dist/checker/declarations.js +3 -2
  27. package/dist/checker/decorator-exports.js +4 -0
  28. package/dist/checker/decorator-includes.d.ts +9 -0
  29. package/dist/checker/decorator-includes.js +71 -0
  30. package/dist/checker/decorator-members.d.ts +8 -10
  31. package/dist/checker/decorator-members.js +28 -31
  32. package/dist/checker/decorator-wrappers.d.ts +1 -1
  33. package/dist/checker/decorator-wrappers.js +3 -2
  34. package/dist/checker/decorators.d.ts +14 -5
  35. package/dist/checker/decorators.js +52 -25
  36. package/dist/checker/expressions.d.ts +1 -1
  37. package/dist/checker/expressions.js +7 -6
  38. package/dist/checker/flow.d.ts +25 -3
  39. package/dist/checker/flow.js +48 -8
  40. package/dist/checker/functions.d.ts +2 -2
  41. package/dist/checker/functions.js +9 -5
  42. package/dist/checker/helpers.d.ts +4 -1
  43. package/dist/checker/helpers.js +7 -2
  44. package/dist/checker/imports.js +5 -2
  45. package/dist/checker/loops.d.ts +2 -0
  46. package/dist/checker/loops.js +27 -2
  47. package/dist/checker/markup.js +14 -2
  48. package/dist/checker/members.js +3 -1
  49. package/dist/checker/shared-state.d.ts +11 -0
  50. package/dist/checker/shared-state.js +37 -0
  51. package/dist/checker/statements.d.ts +5 -2
  52. package/dist/checker/statements.js +51 -22
  53. package/dist/checker/tagged-unions.d.ts +37 -0
  54. package/dist/checker/tagged-unions.js +132 -0
  55. package/dist/checker/untyped-globals.d.ts +1 -0
  56. package/dist/checker/untyped-globals.js +3 -1
  57. package/dist/codegen/attributes.d.ts +4 -0
  58. package/dist/codegen/attributes.js +24 -2
  59. package/dist/codegen/component-helpers.d.ts +6 -1
  60. package/dist/codegen/component-helpers.js +6 -1
  61. package/dist/codegen/component-props.d.ts +11 -2
  62. package/dist/codegen/component-props.js +50 -5
  63. package/dist/codegen/components.d.ts +6 -5
  64. package/dist/codegen/components.js +44 -21
  65. package/dist/codegen/control-flow.d.ts +2 -0
  66. package/dist/codegen/control-flow.js +23 -9
  67. package/dist/codegen/creation.js +5 -2
  68. package/dist/codegen/early-returns.d.ts +4 -3
  69. package/dist/codegen/early-returns.js +17 -5
  70. package/dist/codegen/emitter.d.ts +3 -0
  71. package/dist/codegen/emitter.js +18 -4
  72. package/dist/codegen/expressions.js +0 -2
  73. package/dist/codegen/functions.d.ts +11 -1
  74. package/dist/codegen/functions.js +31 -3
  75. package/dist/codegen/live.d.ts +60 -0
  76. package/dist/codegen/live.js +29 -0
  77. package/dist/codegen/mutations.d.ts +7 -0
  78. package/dist/codegen/mutations.js +43 -2
  79. package/dist/codegen/options.d.ts +2 -0
  80. package/dist/codegen/reactive.d.ts +4 -59
  81. package/dist/codegen/reactive.js +34 -72
  82. package/dist/codegen/runtime.js +6 -1
  83. package/dist/codegen/shared.d.ts +22 -0
  84. package/dist/codegen/shared.js +93 -0
  85. package/dist/codegen/simple-statements.d.ts +1 -1
  86. package/dist/codegen/statements.d.ts +5 -0
  87. package/dist/codegen/statements.js +35 -0
  88. package/dist/codegen/updates.d.ts +11 -3
  89. package/dist/codegen/updates.js +52 -4
  90. package/dist/codegen/web-components.d.ts +2 -2
  91. package/dist/codegen/web-components.js +2 -2
  92. package/dist/compile.js +6 -2
  93. package/dist/decorator-copy.d.ts +1 -1
  94. package/dist/decorator-copy.js +1 -0
  95. package/dist/decorator-names.d.ts +14 -0
  96. package/dist/decorator-names.js +56 -0
  97. package/dist/decorators.d.ts +2 -3
  98. package/dist/decorators.js +36 -14
  99. package/dist/inline-packages.d.ts +2 -1
  100. package/dist/inline-packages.js +9 -4
  101. package/dist/names.d.ts +4 -0
  102. package/dist/names.js +29 -0
  103. package/dist/parser/base.d.ts +1 -0
  104. package/dist/parser/base.js +1 -1
  105. package/dist/parser/control-flow.d.ts +5 -0
  106. package/dist/parser/control-flow.js +28 -2
  107. package/dist/parser/declarations.d.ts +5 -15
  108. package/dist/parser/declarations.js +15 -119
  109. package/dist/parser/decorators.d.ts +6 -2
  110. package/dist/parser/decorators.js +43 -19
  111. package/dist/parser/params.d.ts +21 -0
  112. package/dist/parser/params.js +143 -0
  113. package/dist/parser/simple-statements.d.ts +1 -0
  114. package/dist/parser/simple-statements.js +22 -6
  115. package/dist/parser/statements.d.ts +5 -2
  116. package/dist/parser/statements.js +18 -11
  117. package/dist/runtime/sources.js +3 -2
  118. package/dist/walk.js +3 -3
  119. package/package.json +2 -2
  120. package/src/runtime/decode.js +41 -10
  121. package/src/runtime/index.js +1 -0
  122. package/src/runtime/markup.js +27 -17
  123. 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()`, `effect`. Assignments and `return`
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
- for (const statement of ownStatements(node.body)) {
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() { ... }`: a function that runs once the markup is in the document, like a handler.
104
- * It may return the function that cleans up, or nothing.
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
- const results = this.checkFunction([], func([], []), null, node.body, { closure: true });
108
- if (results.length === 0)
109
- return;
110
- const result = results.length === 1 ? nonNull(results[0]) : null;
111
- const cleans = result !== null &&
112
- (isUntyped(result) ||
113
- result.kind === 'never' ||
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 (values.length === 0) {
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
+ }
@@ -1 +1,2 @@
1
1
  export declare const UNTYPED_GLOBALS: readonly string[];
2
+ export declare const UNTYPED_CLASSES: readonly string[];
@@ -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 = `\`\${${nullable ? `${text} ?? ""` : text}} ${scope}\``;
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()`: the variable that gets its first node once the markup is
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()`: the variable that gets its first node once the markup is
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 element a ref created for it goes last, as `$$root`.
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 { id, name: sourceName, kind, update, computations: [], dependents: [], writes: [] };
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