rooklang 0.3.4 → 0.4.1
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 +33 -2
- package/README.ru.md +33 -2
- 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/bun-register.d.ts +1 -0
- package/dist/bun-register.js +9 -0
- package/dist/bun.d.ts +34 -0
- package/dist/bun.js +99 -0
- 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 +24 -7
- 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-file.d.ts +4 -1
- package/dist/compile.d.ts +1 -1
- 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/index.d.ts +1 -1
- package/dist/inline-packages.d.ts +2 -1
- package/dist/inline-packages.js +9 -4
- package/dist/loader.js +6 -19
- 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/run-cache.d.ts +7 -0
- package/dist/run-cache.js +24 -0
- package/dist/run-file.js +6 -1
- package/dist/runtime/sources.js +4 -3
- package/dist/walk.js +3 -3
- package/package.json +10 -2
- package/src/runtime/decode.js +41 -10
- package/src/runtime/go.js +10 -4
- package/src/runtime/index.js +1 -0
- package/src/runtime/markup.js +27 -17
- package/src/runtime/shared.js +17 -0
|
@@ -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
|
|
@@ -50,6 +50,10 @@ export class ComponentEmitter extends StyleEmitter {
|
|
|
50
50
|
const { reactive } = this;
|
|
51
51
|
const [name] = node.names;
|
|
52
52
|
const [value] = node.values;
|
|
53
|
+
if (node.body && name) {
|
|
54
|
+
this.derivedBlock(name, node.body.body);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
53
57
|
const sources = reactive && value ? reactive.dependencies(value) : new Set();
|
|
54
58
|
super.statement(sources.size > 0 ? node : { ...node, keyword: 'const' });
|
|
55
59
|
if (!reactive || !name || !value || sources.size === 0)
|
|
@@ -57,6 +61,25 @@ export class ComponentEmitter extends StyleEmitter {
|
|
|
57
61
|
const expression = this.withHoisting(false, () => this.expression(value, ARROW));
|
|
58
62
|
reactive.compute(sources, `${this.name(name.name)} = ${expression};`);
|
|
59
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* `derived summary { ... }`: the block becomes a function that computes the value, called first
|
|
66
|
+
* and in the updates of what it reads; one that reads no state runs once. The block computes
|
|
67
|
+
* while the markup is created or updated, so it calls no updates, as the expressions there.
|
|
68
|
+
*/
|
|
69
|
+
derivedBlock(name, block) {
|
|
70
|
+
const { reactive } = this;
|
|
71
|
+
const sources = reactive ? reactive.dependencies(block) : new Set();
|
|
72
|
+
const [, body] = this.withRendering(this.rendering + 1, () => this.func([], block));
|
|
73
|
+
const variable = this.name(name.name);
|
|
74
|
+
if (!reactive || sources.size === 0) {
|
|
75
|
+
this.line(`const ${variable} = (() => ${body})();`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const compute = `$$compute${upperFirst(name.name)}${this.nextId()}`;
|
|
79
|
+
this.line(`const ${compute} = () => ${body};`);
|
|
80
|
+
this.line(`let ${variable} = ${compute}();`);
|
|
81
|
+
reactive.compute(sources, `${variable} = ${compute}();`);
|
|
82
|
+
}
|
|
60
83
|
/**
|
|
61
84
|
* An argument of a decorator that reads properties of the component: when they change, it is
|
|
62
85
|
* computed again, and what reads it is updated.
|
|
@@ -67,26 +90,13 @@ export class ComponentEmitter extends StyleEmitter {
|
|
|
67
90
|
const source = reactive?.sources.get(node.names[0].name);
|
|
68
91
|
if (!reactive || !value || !source)
|
|
69
92
|
return;
|
|
70
|
-
|
|
93
|
+
// Properties, or an argument of a decorator that includes this one, which changes with them.
|
|
94
|
+
const props = new Set([...reactive.dependencies(value)].filter((each) => each !== source));
|
|
71
95
|
if (props.size === 0)
|
|
72
96
|
return;
|
|
73
97
|
const assignment = `${this.name(source.name)} = ${this.expression(value, ARROW)};`;
|
|
74
98
|
reactive.depend(props, `${assignment}\n${this.indent()}${this.write(source)};`);
|
|
75
99
|
}
|
|
76
|
-
/**
|
|
77
|
-
* `mount() { ... }` becomes `$$mount(() => { ... })`. Its body runs later, like a handler: what
|
|
78
|
-
* it changes updates the markup.
|
|
79
|
-
*/
|
|
80
|
-
mountStatement(node) {
|
|
81
|
-
this.inHandler++;
|
|
82
|
-
try {
|
|
83
|
-
const [, body] = this.withRendering(0, () => this.func([], node.body));
|
|
84
|
-
this.line(`$$mount(() => ${this.mountNode ?? 'null'}, () => ${body});`);
|
|
85
|
-
}
|
|
86
|
-
finally {
|
|
87
|
-
this.inHandler--;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
100
|
/**
|
|
91
101
|
* Inlines a component: its code goes into a block where its properties are constants. Attribute
|
|
92
102
|
* values are computed before the block, so that the component's own names cannot hide the
|
|
@@ -101,6 +111,7 @@ export class ComponentEmitter extends StyleEmitter {
|
|
|
101
111
|
this.line(`// <${name}>`);
|
|
102
112
|
const values = new Map();
|
|
103
113
|
const reactiveProps = new Map();
|
|
114
|
+
const binders = new Map();
|
|
104
115
|
const rest = attrsParam(component);
|
|
105
116
|
const forwarded = [];
|
|
106
117
|
for (const attribute of node.attributes) {
|
|
@@ -108,9 +119,12 @@ export class ComponentEmitter extends StyleEmitter {
|
|
|
108
119
|
this.spreadProps(attribute.argument, component, live, values, reactiveProps);
|
|
109
120
|
continue;
|
|
110
121
|
}
|
|
111
|
-
const
|
|
122
|
+
const bound = attribute.name.name.startsWith('bind:');
|
|
123
|
+
const prop = bound ? attribute.name.name.slice('bind:'.length) : attribute.name.name;
|
|
112
124
|
if (prop === 'ref')
|
|
113
125
|
continue;
|
|
126
|
+
if (bound)
|
|
127
|
+
binders.set(prop, this.binder(attribute.value));
|
|
114
128
|
if (rest && !component.params.some((param) => param.name.name === prop)) {
|
|
115
129
|
forwarded.push(attribute);
|
|
116
130
|
continue;
|
|
@@ -133,8 +147,9 @@ export class ComponentEmitter extends StyleEmitter {
|
|
|
133
147
|
if (rest)
|
|
134
148
|
values.set(rest.name.name, this.attrsApplier(component, forwarded, live));
|
|
135
149
|
const root = this.rootFor(node);
|
|
136
|
-
if (isFunction)
|
|
137
|
-
return this.callComponent(component, values, reactiveProps, live, root?.name);
|
|
150
|
+
if (isFunction) {
|
|
151
|
+
return this.callComponent(component, values, reactiveProps, live, root?.name, binders);
|
|
152
|
+
}
|
|
138
153
|
const result = `$$${lowerFirst(name)}${this.nextId()}`;
|
|
139
154
|
const label = hasEarlyReturn(component) ? `$$body${this.nextId()}` : null;
|
|
140
155
|
const mountNode = hasMount(component) ? `$$mountNode${this.nextId()}` : null;
|
|
@@ -153,8 +168,12 @@ export class ComponentEmitter extends StyleEmitter {
|
|
|
153
168
|
for (const param of component.params) {
|
|
154
169
|
const value = values.get(param.name.name) ??
|
|
155
170
|
(param.defaultValue ? this.expression(param.defaultValue, ARROW) : 'null');
|
|
156
|
-
const
|
|
157
|
-
|
|
171
|
+
const name = this.name(param.name.name);
|
|
172
|
+
const binder = binders.get(param.name.name);
|
|
173
|
+
const keyword = setters.has(param.name.name) || param.bind ? 'let' : 'const';
|
|
174
|
+
this.line(`${keyword} ${name} = ${value};`);
|
|
175
|
+
if (binder)
|
|
176
|
+
reactive.sources.get(param.name.name).bound = `${binder}(${name});`;
|
|
158
177
|
}
|
|
159
178
|
this.body(component, result, reactive, { kind: 'inline', setters }, label);
|
|
160
179
|
})))));
|
|
@@ -182,7 +201,11 @@ export class ComponentEmitter extends StyleEmitter {
|
|
|
182
201
|
// A ref to the component created the element at its top: `$$root`, undefined without one.
|
|
183
202
|
const own = this.takesRoot(component) ? ownRoot(component) : null;
|
|
184
203
|
const preset = own && typeof own !== 'string' ? { node: own, name: '$$root', created: false } : null;
|
|
185
|
-
const params = [
|
|
204
|
+
const params = [
|
|
205
|
+
this.parameters(component.params, ''),
|
|
206
|
+
...this.bindersOf(component, reactive),
|
|
207
|
+
...(preset ? ['$$root'] : []),
|
|
208
|
+
]
|
|
186
209
|
.filter((each) => each !== '')
|
|
187
210
|
.join(', ');
|
|
188
211
|
const body = this.withRefs(component, preset, () => this.withStyles(component, result, () => this.inComponent(reactive, mountNode, () => this.block(() => {
|
|
@@ -20,6 +20,8 @@ export declare abstract class ControlFlowEmitter extends ElementEmitter {
|
|
|
20
20
|
* state go into its update. The block of `<Page />` gets the component, `($$component3) => ...`.
|
|
21
21
|
*/
|
|
22
22
|
protected blockCreator(statements: readonly ast.Statement[], parent: Live, loop?: ast.ForInStatement, component?: ast.Identifier): string;
|
|
23
|
+
/** The key of a list, `with key todo.id`: `(todo) => todo.id`, which its blocks are found by. */
|
|
24
|
+
private listKey;
|
|
23
25
|
/** A classic `for` loop in live markup: a function that runs it and returns the content. */
|
|
24
26
|
private loopContent;
|
|
25
27
|
}
|
|
@@ -31,7 +31,9 @@ export class ControlFlowEmitter extends ElementEmitter {
|
|
|
31
31
|
}
|
|
32
32
|
case 'ForInStatement': {
|
|
33
33
|
const items = this.liveFunction(statement.iterable);
|
|
34
|
-
|
|
34
|
+
const create = this.blockCreator(statement.body.body, live, statement);
|
|
35
|
+
const key = statement.withKey ? `, ${this.listKey(statement, statement.withKey)}` : '';
|
|
36
|
+
call = `$$list(${items}, ${create}${key})`;
|
|
35
37
|
break;
|
|
36
38
|
}
|
|
37
39
|
case 'ForStatement':
|
|
@@ -85,20 +87,22 @@ export class ControlFlowEmitter extends ElementEmitter {
|
|
|
85
87
|
*/
|
|
86
88
|
blockCreator(statements, parent, loop, component) {
|
|
87
89
|
const block = new Block(parent);
|
|
88
|
-
const params = loop
|
|
89
|
-
? [loop.value, ...(loop.key ? [loop.key] : [])].map(parameter)
|
|
90
|
-
: component
|
|
91
|
-
? [parameter(component)]
|
|
92
|
-
: [];
|
|
90
|
+
const params = loop ? loopParameters(loop) : component ? [parameter(component)] : [];
|
|
93
91
|
return this.withFunction('none', params, () => this.withRendering(this.rendering + 1, () => {
|
|
94
92
|
const paramList = this.params(params);
|
|
95
93
|
const fragment = `$$block${this.nextId()}`;
|
|
96
94
|
const body = this.block(() => this.withScope(declarationsOf(statements), () => {
|
|
97
95
|
this.line(`const ${fragment} = document.createDocumentFragment();`);
|
|
98
96
|
this.children(fragment, statements, block);
|
|
99
|
-
// The block of an item that stays gets the item's new position
|
|
97
|
+
// The block of an item that stays gets the item's new position, and with a key the
|
|
98
|
+
// item itself, which may be another object with the same key.
|
|
100
99
|
const key = loop?.key && loop.key.name !== '_' ? this.name(loop.key.name) : null;
|
|
101
|
-
const
|
|
100
|
+
const item = loop?.withKey && loop.value.name !== '_' ? this.name(loop.value.name) : null;
|
|
101
|
+
const updates = [
|
|
102
|
+
...(key ? [`${key} = $$index;`] : []),
|
|
103
|
+
...(item ? [`${item} = $$item;`] : []),
|
|
104
|
+
...block.statements,
|
|
105
|
+
];
|
|
102
106
|
if (updates.length === 0) {
|
|
103
107
|
this.line(`return [${fragment}];`);
|
|
104
108
|
return;
|
|
@@ -107,11 +111,17 @@ export class ControlFlowEmitter extends ElementEmitter {
|
|
|
107
111
|
for (const statement of updates)
|
|
108
112
|
this.line(indentMore(statement));
|
|
109
113
|
});
|
|
110
|
-
|
|
114
|
+
const args = item ? '$$index, $$item' : key ? '$$index' : '';
|
|
115
|
+
this.line(`return [${fragment}, (${args}) => ${update}];`);
|
|
111
116
|
}));
|
|
112
117
|
return `(${paramList}) => ${body}`;
|
|
113
118
|
}));
|
|
114
119
|
}
|
|
120
|
+
/** The key of a list, `with key todo.id`: `(todo) => todo.id`, which its blocks are found by. */
|
|
121
|
+
listKey(loop, key) {
|
|
122
|
+
const params = loopParameters(loop);
|
|
123
|
+
return this.withFunction('none', params, () => `(${this.params(params)}) => ${this.expression(key, CONDITIONAL)}`);
|
|
124
|
+
}
|
|
115
125
|
/** A classic `for` loop in live markup: a function that runs it and returns the content. */
|
|
116
126
|
loopContent(statement) {
|
|
117
127
|
return this.withFunction('none', [], () => this.withRendering(this.rendering + 1, () => {
|
|
@@ -136,6 +146,10 @@ function returnNumber(value, at) {
|
|
|
136
146
|
};
|
|
137
147
|
return { kind: 'ReturnStatement', values: [number], start: at.start, end: at.start };
|
|
138
148
|
}
|
|
149
|
+
/** The variables of a loop, `(todo, i)`, as the parameters of its blocks and of its key. */
|
|
150
|
+
function loopParameters(loop) {
|
|
151
|
+
return [loop.value, ...(loop.key ? [loop.key] : [])].map(parameter);
|
|
152
|
+
}
|
|
139
153
|
/** A loop variable as a parameter of the function that creates the loop's blocks. */
|
|
140
154
|
function parameter(name) {
|
|
141
155
|
return {
|
package/dist/codegen/creation.js
CHANGED
|
@@ -70,11 +70,14 @@ export class CreationEmitter extends AttributeEmitter {
|
|
|
70
70
|
const classes = `${value?.value ?? ''} ${scope}`.trim();
|
|
71
71
|
return { attribute: svg, key, value: JSON.stringify(classes) };
|
|
72
72
|
}
|
|
73
|
-
const nullable = this.typeOf(value)?.kind !== 'string';
|
|
74
73
|
return this.valueEntry(element, svg, key, value, live, (text) => {
|
|
75
|
-
return `\`\${${
|
|
74
|
+
return `\`\${${this.classText(value, text)}} ${scope}\``;
|
|
76
75
|
});
|
|
77
76
|
}
|
|
77
|
+
if (name === 'class' && value !== null && this.isClassList(value)) {
|
|
78
|
+
const key = svg ? 'class' : 'className';
|
|
79
|
+
return this.valueEntry(element, svg, key, value, live, (text) => `$$classes(${text})`);
|
|
80
|
+
}
|
|
78
81
|
// SVG attributes are set with setAttribute: their properties are objects.
|
|
79
82
|
const property = svg ? null : domProperty(tag, name);
|
|
80
83
|
if (value === null) {
|
|
@@ -38,9 +38,10 @@ export declare abstract class EarlyReturnEmitter extends ComponentValueEmitter {
|
|
|
38
38
|
protected declaredLive(node: ast.ElementExpression): Live | null;
|
|
39
39
|
/**
|
|
40
40
|
* After the markup of a component is created, before anything inserts it: the first node for
|
|
41
|
-
* `mount
|
|
42
|
-
* variables of its style,
|
|
43
|
-
* component is removed.
|
|
41
|
+
* `mount` (a fragment is empty once inserted, so its first child is taken now), the CSS
|
|
42
|
+
* variables of its style, the first run of the effects, whose cleanups also run when the
|
|
43
|
+
* component is removed, and the updates that listen to the state of the module it reads. Those
|
|
44
|
+
* are kept only if something depends on the state, which is known at the end (resolveMarkers).
|
|
44
45
|
*/
|
|
45
46
|
protected markupCreated(result: string): void;
|
|
46
47
|
/** Declarations that every return needs, written before the statements of the body. */
|
|
@@ -4,7 +4,7 @@ import { effectReads } from "./mutations.js";
|
|
|
4
4
|
import { Block, updatesOf } from "./reactive.js";
|
|
5
5
|
import { ARROW, indentMore } from "./syntax.js";
|
|
6
6
|
import { inlinesUpdate, needed } from "./updates.js";
|
|
7
|
-
import { SETTERS } from "./component-helpers.js";
|
|
7
|
+
import { LISTEN, SETTERS } from "./component-helpers.js";
|
|
8
8
|
export class EarlyReturnEmitter extends ComponentValueEmitter {
|
|
9
9
|
/**
|
|
10
10
|
* The effects of the component being written. One after an early return may not be created, so
|
|
@@ -81,9 +81,10 @@ export class EarlyReturnEmitter extends ComponentValueEmitter {
|
|
|
81
81
|
}
|
|
82
82
|
/**
|
|
83
83
|
* After the markup of a component is created, before anything inserts it: the first node for
|
|
84
|
-
* `mount
|
|
85
|
-
* variables of its style,
|
|
86
|
-
* component is removed.
|
|
84
|
+
* `mount` (a fragment is empty once inserted, so its first child is taken now), the CSS
|
|
85
|
+
* variables of its style, the first run of the effects, whose cleanups also run when the
|
|
86
|
+
* component is removed, and the updates that listen to the state of the module it reads. Those
|
|
87
|
+
* are kept only if something depends on the state, which is known at the end (resolveMarkers).
|
|
87
88
|
*/
|
|
88
89
|
markupCreated(result) {
|
|
89
90
|
if (this.mountNode) {
|
|
@@ -96,6 +97,12 @@ export class EarlyReturnEmitter extends ComponentValueEmitter {
|
|
|
96
97
|
continue;
|
|
97
98
|
this.line(`$$onRemove(() => $$runDeferred(${effect.cleanups}.splice(0)));`);
|
|
98
99
|
}
|
|
100
|
+
for (const source of this.reactive?.sources.values() ?? []) {
|
|
101
|
+
if (source.kind !== 'shared')
|
|
102
|
+
continue;
|
|
103
|
+
const listeners = this.shared.listeners.get(source.name);
|
|
104
|
+
this.line(`${LISTEN}${source.id}${LISTEN}$$watch(${listeners}, ${source.update});`);
|
|
105
|
+
}
|
|
99
106
|
}
|
|
100
107
|
/** Declarations that every return needs, written before the statements of the body. */
|
|
101
108
|
prepareEarlyReturns(result, reactive, ending, label, component) {
|
|
@@ -182,13 +189,18 @@ export class EarlyReturnEmitter extends ComponentValueEmitter {
|
|
|
182
189
|
let functions = 0;
|
|
183
190
|
for (const source of reactive.sources.values()) {
|
|
184
191
|
const called = source.writes.some((write) => needed(source, write).length > 0);
|
|
185
|
-
|
|
192
|
+
// State of the module: the changes are elsewhere, and they run the update it listens with.
|
|
193
|
+
const listened = source.kind === 'shared' && updatesOf(source).length > 0;
|
|
194
|
+
if (!listened && (source.kind !== 'state' || !called || inlinesUpdate(source)))
|
|
186
195
|
continue;
|
|
187
196
|
const body = this.block(() => {
|
|
188
197
|
// Derived values are computed again even before the markup exists, which a function of
|
|
189
198
|
// the body may change the state before.
|
|
190
199
|
for (const computation of source.computations)
|
|
191
200
|
this.line(indentMore(computation));
|
|
201
|
+
// The parent gets the value of a bound property even before the markup exists.
|
|
202
|
+
if (source.bound)
|
|
203
|
+
this.line(source.bound);
|
|
192
204
|
if (source.writes.some((write) => write.early))
|
|
193
205
|
this.line(`if (!${result}) return;`);
|
|
194
206
|
for (const dependent of source.dependents)
|
|
@@ -3,6 +3,7 @@ import { type LocalizedPackage } from './imported.ts';
|
|
|
3
3
|
import type { Root } from '../refs.ts';
|
|
4
4
|
import type { BindingKind } from './analysis.ts';
|
|
5
5
|
import type { JsOptions } from './options.ts';
|
|
6
|
+
import { type SharedState } from './shared.ts';
|
|
6
7
|
/** How a function implements its `defer` statements. */
|
|
7
8
|
export type DeferMode = 'none' | 'try' | 'stack' | 'effect';
|
|
8
9
|
interface FunctionContext {
|
|
@@ -30,6 +31,8 @@ interface FunctionContext {
|
|
|
30
31
|
export declare abstract class Emitter {
|
|
31
32
|
protected readonly program: ast.Program;
|
|
32
33
|
protected readonly options: JsOptions;
|
|
34
|
+
/** The state of the module that its components share, and that of other modules it reads. */
|
|
35
|
+
protected readonly shared: SharedState;
|
|
33
36
|
protected lines: string[];
|
|
34
37
|
private level;
|
|
35
38
|
private readonly scopes;
|
package/dist/codegen/emitter.js
CHANGED
|
@@ -4,6 +4,7 @@ import { localize } from "./imported.js";
|
|
|
4
4
|
import { assignedNames, collectValueNames } from "../names.js";
|
|
5
5
|
import { recursiveComponents } from "../recursion.js";
|
|
6
6
|
import { forEachChild } from "../walk.js";
|
|
7
|
+
import { sharedState } from "./shared.js";
|
|
7
8
|
import { JS_RESERVED, JS_UNDECLARABLE } from "./syntax.js";
|
|
8
9
|
/**
|
|
9
10
|
* The base of the JS generator: the output buffer, scopes of declared names and the state of the
|
|
@@ -19,6 +20,8 @@ import { JS_RESERVED, JS_UNDECLARABLE } from "./syntax.js";
|
|
|
19
20
|
export class Emitter {
|
|
20
21
|
program;
|
|
21
22
|
options;
|
|
23
|
+
/** The state of the module that its components share, and that of other modules it reads. */
|
|
24
|
+
shared;
|
|
22
25
|
lines = [];
|
|
23
26
|
level = 0;
|
|
24
27
|
scopes = [];
|
|
@@ -68,16 +71,27 @@ export class Emitter {
|
|
|
68
71
|
constructor(program, options) {
|
|
69
72
|
this.program = program;
|
|
70
73
|
this.options = options;
|
|
74
|
+
this.shared = sharedState(program, options.shared);
|
|
71
75
|
const decorators = new Map(options.decorators);
|
|
72
76
|
const imported = [...(options.decorators?.values() ?? [])];
|
|
73
77
|
for (const statement of program.body) {
|
|
74
78
|
if (statement.kind === 'DecoratorDeclaration') {
|
|
75
79
|
decorators.set(statement.name.name, { node: statement, moduleNames: [] });
|
|
76
80
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
}
|
|
82
|
+
// Those that the applied ones include are applied too.
|
|
83
|
+
const apply = (name) => {
|
|
84
|
+
if (this.appliedDecorators.has(name))
|
|
85
|
+
return;
|
|
86
|
+
this.appliedDecorators.add(name);
|
|
87
|
+
for (const use of decorators.get(name)?.node.includes ?? [])
|
|
88
|
+
apply(use.name.name);
|
|
89
|
+
};
|
|
90
|
+
for (const statement of program.body) {
|
|
91
|
+
if (statement.kind !== 'ComponentDeclaration')
|
|
92
|
+
continue;
|
|
93
|
+
for (const use of statement.decorators)
|
|
94
|
+
apply(use.name.name);
|
|
81
95
|
}
|
|
82
96
|
const originals = new WeakSet();
|
|
83
97
|
const collect = (node) => {
|
|
@@ -80,8 +80,6 @@ export class ExpressionEmitter extends Emitter {
|
|
|
80
80
|
// The member is a variable of the component's code: see decorate().
|
|
81
81
|
case 'DecoratorMember':
|
|
82
82
|
return [decoratedName(node.decorator.name, node.member.name), PRIMARY];
|
|
83
|
-
case 'DecoratorGet':
|
|
84
|
-
return [decoratedName(node.target.decorator.name, node.target.member.name), PRIMARY];
|
|
85
83
|
case 'ThisExpression':
|
|
86
84
|
return ['this', PRIMARY];
|
|
87
85
|
// `as` is only for the checker.
|
|
@@ -10,7 +10,17 @@ export declare abstract class FunctionEmitter extends WebComponentEmitter {
|
|
|
10
10
|
* it runs again, and when `{if}`, `{switch}` or `{for}` removes the component.
|
|
11
11
|
*/
|
|
12
12
|
protected effectStatement(node: ast.EffectStatement): void;
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* `mount { ... }` becomes `$$mount(() => node, () => { ... })`. Its body runs later, like a
|
|
15
|
+
* handler: what it changes updates the markup. What it defers goes into a list that the runtime
|
|
16
|
+
* runs when `{if}`, `{switch}` or `{for}` removes the component.
|
|
17
|
+
*/
|
|
18
|
+
protected mountStatement(node: ast.MountStatement): void;
|
|
19
|
+
/**
|
|
20
|
+
* The body of an effect or of `mount`, whose `defer`s put their calls into `cleanups`. An effect
|
|
21
|
+
* that runs again first runs what it deferred before.
|
|
22
|
+
*/
|
|
23
|
+
private deferringBody;
|
|
14
24
|
/** A `defer` in 'stack' and 'effect' modes; in 'try' mode statementsWithDefers handles them. */
|
|
15
25
|
protected deferStatement(node: ast.DeferStatement): void;
|
|
16
26
|
/** Statements of a body whose `defer`s are all at its top level: one try/finally per `defer`. */
|
|
@@ -46,7 +46,7 @@ export class FunctionEmitter extends WebComponentEmitter {
|
|
|
46
46
|
const cleanups = findDefers(node.body).length > 0 ? `$$cleanups${id}` : null;
|
|
47
47
|
this.inHandler++;
|
|
48
48
|
try {
|
|
49
|
-
const body = this.withRendering(0, () => cleanups === null ? this.func([], node.body)[1] : this.
|
|
49
|
+
const body = this.withRendering(0, () => cleanups === null ? this.func([], node.body)[1] : this.deferringBody(node.body, cleanups));
|
|
50
50
|
if (cleanups !== null)
|
|
51
51
|
this.line(`const ${cleanups} = [];`);
|
|
52
52
|
this.line(`const ${name} = () => ${body};`);
|
|
@@ -56,11 +56,39 @@ export class FunctionEmitter extends WebComponentEmitter {
|
|
|
56
56
|
}
|
|
57
57
|
this.addEffect({ name, node, cleanups });
|
|
58
58
|
}
|
|
59
|
-
|
|
59
|
+
/**
|
|
60
|
+
* `mount { ... }` becomes `$$mount(() => node, () => { ... })`. Its body runs later, like a
|
|
61
|
+
* handler: what it changes updates the markup. What it defers goes into a list that the runtime
|
|
62
|
+
* runs when `{if}`, `{switch}` or `{for}` removes the component.
|
|
63
|
+
*/
|
|
64
|
+
mountStatement(node) {
|
|
65
|
+
const cleanups = findDefers(node.body).length > 0 ? `$$cleanups${this.nextId()}` : null;
|
|
66
|
+
this.inHandler++;
|
|
67
|
+
try {
|
|
68
|
+
const body = this.withRendering(0, () => cleanups === null
|
|
69
|
+
? this.func([], node.body)[1]
|
|
70
|
+
: this.deferringBody(node.body, cleanups, false));
|
|
71
|
+
const args = [`() => ${this.mountNode ?? 'null'}`, `() => ${body}`];
|
|
72
|
+
if (cleanups !== null) {
|
|
73
|
+
this.line(`const ${cleanups} = [];`);
|
|
74
|
+
args.push(cleanups);
|
|
75
|
+
}
|
|
76
|
+
this.line(`$$mount(${args.join(', ')});`);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
this.inHandler--;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The body of an effect or of `mount`, whose `defer`s put their calls into `cleanups`. An effect
|
|
84
|
+
* that runs again first runs what it deferred before.
|
|
85
|
+
*/
|
|
86
|
+
deferringBody(body, cleanups, again = true) {
|
|
60
87
|
return this.withFunction('effect', [], () => {
|
|
61
88
|
this.fn.deferList = cleanups;
|
|
62
89
|
return this.block(() => this.withScope(declarationsOf(body.body), () => {
|
|
63
|
-
|
|
90
|
+
if (again)
|
|
91
|
+
this.line(`$$runDeferred(${cleanups}.splice(0));`);
|
|
64
92
|
this.statements(body.body);
|
|
65
93
|
}));
|
|
66
94
|
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type * as ast from '../ast.ts';
|
|
2
|
+
/** What markup can depend on: a `state` variable, or a property that gets the parent's state. */
|
|
3
|
+
export interface Source {
|
|
4
|
+
/** Unique in the module; marks the places that change the source in the generated code. */
|
|
5
|
+
readonly id: number;
|
|
6
|
+
readonly name: string;
|
|
7
|
+
/** `shared` is state of the module, which the component reads and listens to. */
|
|
8
|
+
readonly kind: 'state' | 'prop' | 'shared';
|
|
9
|
+
/** The function that brings dependent nodes up to date, e.g. `$$updateCount3`. */
|
|
10
|
+
readonly update: string;
|
|
11
|
+
/**
|
|
12
|
+
* Statements that compute again the `derived` values that read it: `double = count * 2`. They
|
|
13
|
+
* run first, also before the markup exists, so that the values are right whenever they are read.
|
|
14
|
+
*/
|
|
15
|
+
readonly computations: string[];
|
|
16
|
+
/** Statements that bring the nodes depending on it up to date. */
|
|
17
|
+
readonly dependents: string[];
|
|
18
|
+
/** Places that change it. */
|
|
19
|
+
readonly writes: Write[];
|
|
20
|
+
/**
|
|
21
|
+
* A bound property, `bind active string`: the call that gives the parent its new value,
|
|
22
|
+
* `$$bindActive?.(active);`. Places that change it make the call; the parent setting it does not.
|
|
23
|
+
*/
|
|
24
|
+
bound: string | null;
|
|
25
|
+
}
|
|
26
|
+
export interface Write {
|
|
27
|
+
/** Whether it may run before the markup exists: in a function of the body, not in a handler. */
|
|
28
|
+
early: boolean;
|
|
29
|
+
/** A dependent it does not need: `bind:` does not update its own element. */
|
|
30
|
+
skip: number | null;
|
|
31
|
+
}
|
|
32
|
+
/** Everything a change of the source runs: the computations, then the updates of the nodes. */
|
|
33
|
+
export declare function updatesOf(source: Source): string[];
|
|
34
|
+
/**
|
|
35
|
+
* Where the updates of live markup go: the update functions of a component's state, or the update
|
|
36
|
+
* of a block of `{if ...}` / `{for ...}`.
|
|
37
|
+
*/
|
|
38
|
+
export interface Live {
|
|
39
|
+
/** Sources whose values an expression reads. */
|
|
40
|
+
dependencies(node: ast.Node): Set<Source>;
|
|
41
|
+
/** Adds a statement that brings a part of the markup up to date when the sources change. */
|
|
42
|
+
depend(sources: ReadonlySet<Source>, statement: string): void;
|
|
43
|
+
/** The index that the next statement for a source gets, for `bind:` to skip its own update. */
|
|
44
|
+
ownIndex(source: Source): number | null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A block of markup control flow: a branch of `{if ...}` or the content for one item of
|
|
48
|
+
* `{for ...}`. It is created again when needed, so its updates go into a function of its own,
|
|
49
|
+
* which runs when any source that the block reads changes.
|
|
50
|
+
*/
|
|
51
|
+
export declare class Block implements Live {
|
|
52
|
+
readonly statements: string[];
|
|
53
|
+
/** Sources that any statement depends on. */
|
|
54
|
+
readonly sources: Set<Source>;
|
|
55
|
+
private readonly parent;
|
|
56
|
+
constructor(parent: Live);
|
|
57
|
+
dependencies(node: ast.Node): Set<Source>;
|
|
58
|
+
depend(sources: ReadonlySet<Source>, statement: string): void;
|
|
59
|
+
ownIndex(): null;
|
|
60
|
+
}
|