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.
Files changed (135) hide show
  1. package/README.md +33 -2
  2. package/README.ru.md +33 -2
  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/bun-register.d.ts +1 -0
  7. package/dist/bun-register.js +9 -0
  8. package/dist/bun.d.ts +34 -0
  9. package/dist/bun.js +99 -0
  10. package/dist/checker/assignments.d.ts +2 -0
  11. package/dist/checker/assignments.js +9 -3
  12. package/dist/checker/base.d.ts +8 -2
  13. package/dist/checker/base.js +3 -3
  14. package/dist/checker/builtins.js +7 -3
  15. package/dist/checker/channels.js +1 -2
  16. package/dist/checker/classes.js +7 -1
  17. package/dist/checker/closing.js +1 -1
  18. package/dist/checker/completion.d.ts +1 -1
  19. package/dist/checker/completion.js +2 -1
  20. package/dist/checker/component-bindings.d.ts +7 -0
  21. package/dist/checker/component-bindings.js +45 -0
  22. package/dist/checker/component-values.js +10 -7
  23. package/dist/checker/components.d.ts +2 -2
  24. package/dist/checker/components.js +8 -3
  25. package/dist/checker/context.d.ts +28 -5
  26. package/dist/checker/context.js +23 -0
  27. package/dist/checker/control-flow.d.ts +6 -5
  28. package/dist/checker/control-flow.js +48 -40
  29. package/dist/checker/declarations.d.ts +2 -2
  30. package/dist/checker/declarations.js +3 -2
  31. package/dist/checker/decorator-exports.js +4 -0
  32. package/dist/checker/decorator-includes.d.ts +9 -0
  33. package/dist/checker/decorator-includes.js +71 -0
  34. package/dist/checker/decorator-members.d.ts +8 -10
  35. package/dist/checker/decorator-members.js +28 -31
  36. package/dist/checker/decorator-wrappers.d.ts +1 -1
  37. package/dist/checker/decorator-wrappers.js +3 -2
  38. package/dist/checker/decorators.d.ts +14 -5
  39. package/dist/checker/decorators.js +52 -25
  40. package/dist/checker/expressions.d.ts +1 -1
  41. package/dist/checker/expressions.js +7 -6
  42. package/dist/checker/flow.d.ts +25 -3
  43. package/dist/checker/flow.js +48 -8
  44. package/dist/checker/functions.d.ts +2 -2
  45. package/dist/checker/functions.js +9 -5
  46. package/dist/checker/helpers.d.ts +4 -1
  47. package/dist/checker/helpers.js +7 -2
  48. package/dist/checker/imports.js +5 -2
  49. package/dist/checker/loops.d.ts +2 -0
  50. package/dist/checker/loops.js +27 -2
  51. package/dist/checker/markup.js +14 -2
  52. package/dist/checker/members.js +3 -1
  53. package/dist/checker/shared-state.d.ts +11 -0
  54. package/dist/checker/shared-state.js +37 -0
  55. package/dist/checker/statements.d.ts +5 -2
  56. package/dist/checker/statements.js +51 -22
  57. package/dist/checker/tagged-unions.d.ts +37 -0
  58. package/dist/checker/tagged-unions.js +132 -0
  59. package/dist/checker/untyped-globals.d.ts +1 -0
  60. package/dist/checker/untyped-globals.js +3 -1
  61. package/dist/codegen/attributes.d.ts +4 -0
  62. package/dist/codegen/attributes.js +24 -2
  63. package/dist/codegen/component-helpers.d.ts +6 -1
  64. package/dist/codegen/component-helpers.js +6 -1
  65. package/dist/codegen/component-props.d.ts +11 -2
  66. package/dist/codegen/component-props.js +50 -5
  67. package/dist/codegen/components.d.ts +6 -5
  68. package/dist/codegen/components.js +44 -21
  69. package/dist/codegen/control-flow.d.ts +2 -0
  70. package/dist/codegen/control-flow.js +23 -9
  71. package/dist/codegen/creation.js +5 -2
  72. package/dist/codegen/early-returns.d.ts +4 -3
  73. package/dist/codegen/early-returns.js +17 -5
  74. package/dist/codegen/emitter.d.ts +3 -0
  75. package/dist/codegen/emitter.js +18 -4
  76. package/dist/codegen/expressions.js +0 -2
  77. package/dist/codegen/functions.d.ts +11 -1
  78. package/dist/codegen/functions.js +31 -3
  79. package/dist/codegen/live.d.ts +60 -0
  80. package/dist/codegen/live.js +29 -0
  81. package/dist/codegen/mutations.d.ts +7 -0
  82. package/dist/codegen/mutations.js +43 -2
  83. package/dist/codegen/options.d.ts +2 -0
  84. package/dist/codegen/reactive.d.ts +4 -59
  85. package/dist/codegen/reactive.js +34 -72
  86. package/dist/codegen/runtime.js +24 -7
  87. package/dist/codegen/shared.d.ts +22 -0
  88. package/dist/codegen/shared.js +93 -0
  89. package/dist/codegen/simple-statements.d.ts +1 -1
  90. package/dist/codegen/statements.d.ts +5 -0
  91. package/dist/codegen/statements.js +35 -0
  92. package/dist/codegen/updates.d.ts +11 -3
  93. package/dist/codegen/updates.js +52 -4
  94. package/dist/codegen/web-components.d.ts +2 -2
  95. package/dist/codegen/web-components.js +2 -2
  96. package/dist/compile-file.d.ts +4 -1
  97. package/dist/compile.d.ts +1 -1
  98. package/dist/compile.js +6 -2
  99. package/dist/decorator-copy.d.ts +1 -1
  100. package/dist/decorator-copy.js +1 -0
  101. package/dist/decorator-names.d.ts +14 -0
  102. package/dist/decorator-names.js +56 -0
  103. package/dist/decorators.d.ts +2 -3
  104. package/dist/decorators.js +36 -14
  105. package/dist/index.d.ts +1 -1
  106. package/dist/inline-packages.d.ts +2 -1
  107. package/dist/inline-packages.js +9 -4
  108. package/dist/loader.js +6 -19
  109. package/dist/names.d.ts +4 -0
  110. package/dist/names.js +29 -0
  111. package/dist/parser/base.d.ts +1 -0
  112. package/dist/parser/base.js +1 -1
  113. package/dist/parser/control-flow.d.ts +5 -0
  114. package/dist/parser/control-flow.js +28 -2
  115. package/dist/parser/declarations.d.ts +5 -15
  116. package/dist/parser/declarations.js +15 -119
  117. package/dist/parser/decorators.d.ts +6 -2
  118. package/dist/parser/decorators.js +43 -19
  119. package/dist/parser/params.d.ts +21 -0
  120. package/dist/parser/params.js +143 -0
  121. package/dist/parser/simple-statements.d.ts +1 -0
  122. package/dist/parser/simple-statements.js +22 -6
  123. package/dist/parser/statements.d.ts +5 -2
  124. package/dist/parser/statements.js +18 -11
  125. package/dist/run-cache.d.ts +7 -0
  126. package/dist/run-cache.js +24 -0
  127. package/dist/run-file.js +6 -1
  128. package/dist/runtime/sources.js +4 -3
  129. package/dist/walk.js +3 -3
  130. package/package.json +10 -2
  131. package/src/runtime/decode.js +41 -10
  132. package/src/runtime/go.js +10 -4
  133. package/src/runtime/index.js +1 -0
  134. package/src/runtime/markup.js +27 -17
  135. package/src/runtime/shared.js +17 -0
package/README.md CHANGED
@@ -58,10 +58,12 @@ close(results)
58
58
  The guide with examples and a playground: <https://qwern-9876r.github.io/RookLang/>.
59
59
  Specification: [docs/spec.md](docs/spec.md), design of components:
60
60
  [docs/components.md](docs/components.md), an example: [examples/hello.rk](examples/hello.rk).
61
+ Where Rook stands and the road to 1.0: [docs/roadmap.md](docs/roadmap.md); what changed in each
62
+ version: [CHANGELOG.md](CHANGELOG.md).
61
63
 
62
64
  ## Installation
63
65
 
64
- Rook needs Node.js ≥ 24.
66
+ Rook needs Node.js ≥ 24 or [Bun](#bun).
65
67
 
66
68
  ```sh
67
69
  npm install rooklang
@@ -75,7 +77,8 @@ The compiled code imports what channels, goroutines and markup need from `rookla
75
77
  the package is a dependency of the project, not only of its build.
76
78
 
77
79
  From code, the compiler is `import { compile } from 'rooklang'`. The package also has the
78
- [Vite](#vite) plugin, `rooklang/vite`, and `rooklang/browser`, the compiler without the
80
+ [Vite](#vite) plugin, `rooklang/vite`, the [Bun](#bun) plugin, `rooklang/bun`, and
81
+ `rooklang/browser`, the compiler without the
79
82
  TypeScript compiler for running in a browser, as in the site's playground: there imports are not
80
83
  type checked.
81
84
 
@@ -208,6 +211,34 @@ import { add } from './math.rk';
208
211
  it('adds', () => expect(add(1, 2)).toBe(3));
209
212
  ```
210
213
 
214
+ ## Bun
215
+
216
+ Rook works in Bun too: `bun add rooklang`, then `bunx rook build app.rk` or `bunx rook run
217
+ app.rk`. Under the runtime of Bun, `bun --bun rook run app.rk`, the program runs in Bun.
218
+
219
+ To let Bun load `.rk` modules itself, preload the plugin `rooklang/bun` in `bunfig.toml`:
220
+
221
+ ```toml
222
+ preload = ["rooklang/bun/register"]
223
+
224
+ [test]
225
+ preload = ["rooklang/bun/register"]
226
+ ```
227
+
228
+ Then `bun app.rk` runs a module, TypeScript imports `.rk` modules, `bun --watch` and `bun --hot`
229
+ follow them, and `bun test` runs tests of them; goroutines work there too. The plugin itself is
230
+ `import { rook } from 'rooklang/bun'`, for `Bun.plugin(rook())` or `Bun.build({ plugins: [rook()] })`;
231
+ a bundle of `Bun.build` cannot have the workers of `go` yet, so build those with `rook build` or
232
+ Vite.
233
+
234
+ ```ts
235
+ // math.test.ts
236
+ import { expect, test } from 'bun:test';
237
+ import { add } from './math.rk';
238
+
239
+ test('adds', () => expect(add(1, 2)).toBe(3));
240
+ ```
241
+
211
242
  ## Conventions
212
243
 
213
244
  - Imports are written with the `.ts` extension (`import { x } from './lexer.ts'`).
package/README.ru.md CHANGED
@@ -58,10 +58,12 @@ close(results)
58
58
  Руководство с примерами и песочницей: <https://qwern-9876r.github.io/RookLang/ru/>.
59
59
  Спецификация: [docs/spec.ru.md](docs/spec.ru.md), концепция компонентов:
60
60
  [docs/components.ru.md](docs/components.ru.md), пример: [examples/hello.rk](examples/hello.rk).
61
+ Где Rook сейчас и путь к 1.0: [docs/roadmap.ru.md](docs/roadmap.ru.md); что изменилось в каждой
62
+ версии: [CHANGELOG.ru.md](CHANGELOG.ru.md).
61
63
 
62
64
  ## Установка
63
65
 
64
- Нужен Node.js ≥ 24.
66
+ Нужен Node.js ≥ 24 или [Bun](#bun).
65
67
 
66
68
  ```sh
67
69
  npm install rooklang
@@ -75,7 +77,8 @@ npx rook run --watch app.rk # и запускать снова, ког
75
77
  `rooklang/runtime`, поэтому пакет — зависимость проекта, а не только его сборки.
76
78
 
77
79
  Из кода компилятор подключается как `import { compile } from 'rooklang'`. Ещё в пакете есть
78
- плагин для [Vite](#vite) — `rooklang/vite` — и `rooklang/browser`: компилятор без
80
+ плагин для [Vite](#vite) — `rooklang/vite`, — плагин для [Bun](#bun) — `rooklang/bun` — и
81
+ `rooklang/browser`: компилятор без
79
82
  компилятора TypeScript, для работы в браузере, как в песочнице сайта; импорты там не проверяются
80
83
  по типам.
81
84
 
@@ -208,6 +211,34 @@ import { add } from './math.rk';
208
211
  it('adds', () => expect(add(1, 2)).toBe(3));
209
212
  ```
210
213
 
214
+ ## Bun
215
+
216
+ Rook работает и в Bun: `bun add rooklang`, потом `bunx rook build app.rk` или `bunx rook run
217
+ app.rk`. Под рантаймом Bun, `bun --bun rook run app.rk`, программа выполняется в Bun.
218
+
219
+ Чтобы Bun сам загружал `.rk`-модули, плагин `rooklang/bun` подключают заранее в `bunfig.toml`:
220
+
221
+ ```toml
222
+ preload = ["rooklang/bun/register"]
223
+
224
+ [test]
225
+ preload = ["rooklang/bun/register"]
226
+ ```
227
+
228
+ Тогда `bun app.rk` запускает модуль, TypeScript импортирует `.rk`-модули, `bun --watch` и
229
+ `bun --hot` следят за ними, а `bun test` запускает их тесты; горутины там тоже работают. Сам плагин —
230
+ `import { rook } from 'rooklang/bun'`, для `Bun.plugin(rook())` или
231
+ `Bun.build({ plugins: [rook()] })`; в бандле `Bun.build` воркеров `go` пока быть не может, поэтому
232
+ такой код собирают `rook build` или Vite.
233
+
234
+ ```ts
235
+ // math.test.ts
236
+ import { expect, test } from 'bun:test';
237
+ import { add } from './math.rk';
238
+
239
+ test('adds', () => expect(add(1, 2)).toBe(3));
240
+ ```
241
+
211
242
  ## Соглашения
212
243
 
213
244
  - Импорты пишутся с расширением `.ts` (`import { x } from './lexer.ts'`).
@@ -1,4 +1,4 @@
1
- import type { Expression, Identifier, StringLiteral } from './expressions.ts';
1
+ import type { Expression, FuncExpression, Identifier, StringLiteral } from './expressions.ts';
2
2
  import type { TypeMember, TypeNode, TypeReference } from './markup.ts';
3
3
  import type { BlockStatement, Statement } from './statements.ts';
4
4
  export interface NodeBase {
@@ -57,6 +57,11 @@ export interface Parameter extends NodeBase {
57
57
  * properties, which `{...attrs}` puts on one of its elements.
58
58
  */
59
59
  rest?: true;
60
+ /**
61
+ * `bind active string`: a property of a component that it may change. `bind:active={tab}`
62
+ * writes the changes into the parent's `tab`; without it the property is the component's state.
63
+ */
64
+ bind?: true;
60
65
  /**
61
66
  * `constructor(private name string)`: the parameter is a property too, which the constructor
62
67
  * assigns, as in TypeScript.
@@ -97,6 +102,11 @@ export interface DecoratorDeclaration extends NodeBase {
97
102
  params: Parameter[];
98
103
  /** `needs cart, auth`: the decorators whose public members it uses; they are applied above it. */
99
104
  needs: Identifier[];
105
+ /**
106
+ * `includes visible(margin), cart`: decorators applied with it, above its own body, whose
107
+ * arguments see its parameters. Their public members are its own for the component, `@shop.count`.
108
+ */
109
+ includes: DecoratorUse[];
100
110
  body: BlockStatement;
101
111
  }
102
112
  /** `@visible("300px")` or `@visible` before `comp`; the arguments are positional. */
@@ -118,14 +128,20 @@ export interface VariableDeclaration extends NodeBase {
118
128
  exported: boolean;
119
129
  keyword: 'let' | 'const' | 'state' | 'derived' | 'ref';
120
130
  /**
121
- * `public state`, `public derived` or `public const` in a decorator: the component reads it with
122
- * `get(@name.x)`.
131
+ * `public state`, `public derived` or `public const` in a decorator: the component reads it as
132
+ * `@name.x`.
123
133
  */
124
134
  isPublic: boolean;
125
135
  /** `_` is an ordinary identifier here; the checker treats it as "skip this value". */
126
136
  names: Identifier[];
127
137
  type: TypeNode | null;
128
138
  values: Expression[];
139
+ /**
140
+ * `derived summary string { ...; return text }`: the block that computes a derived value, a
141
+ * function without parameters, so that what looks for the `return`s, `defer`s and `break`s of a
142
+ * component stops at it, as at other functions. Its result type is `type`; `values` is empty.
143
+ */
144
+ body: FuncExpression | null;
129
145
  }
130
146
  /** `class Admin extends User implements Shape { ... }` */
131
147
  export interface ClassDeclaration extends NodeBase {
@@ -1,7 +1,7 @@
1
1
  import type { NodeBase, Parameter } from './declarations.ts';
2
2
  import type { ElementExpression, TypeNode } from './markup.ts';
3
3
  import type { BlockStatement } from './statements.ts';
4
- export type Expression = Identifier | NumberLiteral | BigIntLiteral | StringLiteral | RegExpLiteral | TemplateLiteral | BooleanLiteral | NullLiteral | ThisExpression | SuperExpression | ArrayLiteral | ObjectLiteral | FuncExpression | ArrowFunction | UnaryExpression | BinaryExpression | ConditionalExpression | CallExpression | NewExpression | MemberExpression | IndexExpression | ElementExpression | DecoratorMember | DecoratorGet | AsExpression | GoExpression | ChanExpression | StreamChanExpression | QueueExpression | TopicExpression | DecodeExpression | TryExpression | ConversionExpression | MakeExpression;
4
+ export type Expression = Identifier | NumberLiteral | BigIntLiteral | StringLiteral | RegExpLiteral | TemplateLiteral | BooleanLiteral | NullLiteral | ThisExpression | SuperExpression | ArrayLiteral | ObjectLiteral | FuncExpression | ArrowFunction | UnaryExpression | BinaryExpression | ConditionalExpression | CallExpression | NewExpression | MemberExpression | IndexExpression | ElementExpression | DecoratorMember | AsExpression | GoExpression | ChanExpression | StreamChanExpression | QueueExpression | TopicExpression | DecodeExpression | TryExpression | ConversionExpression | MakeExpression;
5
5
  /**
6
6
  * `[]byte("rook")`: the UTF-8 bytes of a string. `string(data)`, the way back, is a call, since
7
7
  * `string` may be the name of a value.
@@ -231,16 +231,12 @@ export interface IndexExpression extends NodeBase {
231
231
  optional: boolean;
232
232
  }
233
233
  /**
234
- * `@visible.shown`: a public member of a decorator applied to the component. The names belong to
235
- * the decorator, not to any scope, so walkers do not visit them as identifiers.
234
+ * `@visible.shown` or `@visible.show()`: a public member of a decorator applied to the component,
235
+ * read or called. The names belong to the decorator, not to any scope, so walkers do not visit them
236
+ * as identifiers.
236
237
  */
237
238
  export interface DecoratorMember extends NodeBase {
238
239
  kind: 'DecoratorMember';
239
240
  decorator: Identifier;
240
241
  member: Identifier;
241
242
  }
242
- /** `get(@visible.shown)`: reads public state or a constant of a decorator. */
243
- export interface DecoratorGet extends NodeBase {
244
- kind: 'DecoratorGet';
245
- target: DecoratorMember;
246
- }
@@ -86,6 +86,8 @@ export interface ForInStatement extends NodeBase {
86
86
  key: Identifier | null;
87
87
  value: Identifier;
88
88
  iterable: Expression;
89
+ /** `with key todo.id` of a `{for}` in markup: what its blocks are found again by. */
90
+ withKey?: Expression;
89
91
  body: BlockStatement;
90
92
  go?: boolean;
91
93
  }
@@ -156,7 +158,7 @@ export interface DeferStatement extends NodeBase {
156
158
  body: CallExpression | BlockStatement;
157
159
  }
158
160
  /**
159
- * `mount() { ... }` at the top level of a component: runs once the component's markup is in the
161
+ * `mount { ... }` at the top level of a component: runs once the component's markup is in the
160
162
  * document. It may return a function, which runs when `{if}`, `{switch}` or `{for}` removes the
161
163
  * markup.
162
164
  */
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import { rook } from "./bun.js";
2
+ // `preload = ["rooklang/bun/register"]` in bunfig.toml: Bun then compiles `.rk` modules when they
3
+ // load, in `bun app.rk`, in imports from TypeScript and in `bun test` (bun.ts). `rook run` under
4
+ // Bun preloads it too, and the workers of `go` import it before their module.
5
+ const { Bun } = globalThis;
6
+ if (Bun === undefined) {
7
+ throw new Error('rooklang/bun/register is for Bun; in Node, rook run compiles .rk files');
8
+ }
9
+ Bun.plugin(rook());
package/dist/bun.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ /** What a plugin of Bun is given, of what Rook uses: the package needs no types of Bun. */
2
+ interface PluginBuilder {
3
+ onResolve(constraints: {
4
+ filter: RegExp;
5
+ namespace?: string;
6
+ }, callback: (args: {
7
+ path: string;
8
+ importer: string;
9
+ }) => {
10
+ path: string;
11
+ namespace?: string;
12
+ }): void;
13
+ onLoad(constraints: {
14
+ filter: RegExp;
15
+ namespace?: string;
16
+ }, callback: (args: {
17
+ path: string;
18
+ }) => {
19
+ contents: string;
20
+ loader: 'js';
21
+ }): void;
22
+ }
23
+ export interface BunPlugin {
24
+ name: string;
25
+ setup(build: PluginBuilder): void;
26
+ }
27
+ export interface RookBunOptions {
28
+ /** Check types before generating code. Defaults to `true`. */
29
+ typeCheck?: boolean;
30
+ /** Markup and components; by default as the package.json of the file says. */
31
+ markup?: boolean;
32
+ }
33
+ export declare function rook(options?: RookBunOptions): BunPlugin;
34
+ export {};
package/dist/bun.js ADDED
@@ -0,0 +1,99 @@
1
+ import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { basename, dirname, extname, join, resolve } from 'node:path';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
+ import { isMainThread } from 'node:worker_threads';
6
+ import { displayPath } from "./build.js";
7
+ import { compileFile } from "./compile-file.js";
8
+ import { cachedFile, listFiles, withMap } from "./run-cache.js";
9
+ import { RUNTIME_MODULE } from "./runtime-module.js";
10
+ // The plugin of Bun: `bun app.rk`, `import "./cart.rk"` from TypeScript and `bun test` compile the
11
+ // `.rk` modules when they load, as `rook run` does in Node (loader.ts). `preload =
12
+ // ["rooklang/bun/register"]` in bunfig.toml registers it (bun-register.ts), and so does
13
+ // `rook run` under Bun.
14
+ //
15
+ // `app.rk.goroutines.js` is the worker module of `go` in app.rk: Bun drops the query of
16
+ // `app.rk?goroutines` before a plugin sees it, as Vite does (vite.ts). A Web Worker of Bun has no
17
+ // preload and loads its module without the plugins, so it starts with a module that registers the
18
+ // plugin first (startWorker in codegen/runtime.ts). The main thread compiles a module once and
19
+ // leaves its code and that of its worker module in ROOK_RUN_CACHE, a directory of the run, which
20
+ // the workers take instead of compiling again; without a run of `rook run`, the plugin makes one
21
+ // and removes it when the process ends.
22
+ /** The worker module of `go` in a module, `app.rk.goroutines.js`, next to it. */
23
+ const GOROUTINES = '.goroutines.js';
24
+ // The files next to this one: .ts in src, .js in the dist of the package.
25
+ const extension = extname(fileURLToPath(import.meta.url));
26
+ // The worker of `go` registers the plugin with this module before it imports its module.
27
+ const register = new URL(`./bun-register${extension}`, import.meta.url).href;
28
+ // The compiled code imports the runtime of this compiler, wherever the program is, by its URL: a
29
+ // worker of Bun does not give a plugin the bare `rooklang/runtime`, and looks for the package.
30
+ const runtimeImport = `from "${RUNTIME_MODULE}";`;
31
+ const runtime = `from ${JSON.stringify(import.meta.resolve(RUNTIME_MODULE))};`;
32
+ export function rook(options = {}) {
33
+ return {
34
+ name: 'rook',
35
+ setup(build) {
36
+ build.onResolve({ filter: /\.rk\.goroutines\.js$/ }, (args) => ({
37
+ path: args.path.startsWith('file:')
38
+ ? fileURLToPath(args.path)
39
+ : resolve(dirname(args.importer), args.path),
40
+ namespace: 'rook-goroutines',
41
+ }));
42
+ build.onLoad({ filter: /.*/, namespace: 'rook-goroutines' }, (args) => ({
43
+ contents: load(args.path.slice(0, -GOROUTINES.length), true, options),
44
+ loader: 'js',
45
+ }));
46
+ build.onLoad({ filter: /\.rk$/ }, (args) => ({
47
+ contents: load(args.path, false, options),
48
+ loader: 'js',
49
+ }));
50
+ },
51
+ };
52
+ }
53
+ /**
54
+ * The code of a module or of its worker module, from the cache of the run when it is there. The
55
+ * key has the time the file was changed, since `bun --hot` loads a changed module again.
56
+ */
57
+ function load(path, worker, options) {
58
+ const url = `${pathToFileURL(path).href}#${statSync(path).mtimeMs}`;
59
+ const cache = runCache();
60
+ const file = cachedFile(cache, worker ? `${url}${GOROUTINES}` : url);
61
+ if (existsSync(file))
62
+ return readFileSync(file, 'utf8');
63
+ const result = compile(path, options);
64
+ const code = withMap(result.code.replaceAll(runtimeImport, runtime), result.map);
65
+ const workerCode = withMap((result.worker ?? '').replaceAll(runtimeImport, runtime), result.workerMap);
66
+ if (isMainThread) {
67
+ // The CSS modules it imports; the .rk modules are listed when they are loaded.
68
+ const read = result.dependencies.map((each) => resolve(dirname(path), each));
69
+ listFiles(cache, [path, ...read.filter((each) => existsSync(each))]);
70
+ writeFileSync(cachedFile(cache, url), code);
71
+ writeFileSync(cachedFile(cache, `${url}${GOROUTINES}`), workerCode);
72
+ }
73
+ return worker ? workerCode : code;
74
+ }
75
+ function compile(path, options) {
76
+ return compileFile(displayPath(path), {
77
+ rewriteImports: false,
78
+ typeCheck: options.typeCheck ?? process.env['ROOK_NO_CHECK'] === undefined,
79
+ markup: options.markup ?? markupSetting(),
80
+ quiet: !isMainThread,
81
+ workerUrl: `./${basename(path)}${GOROUTINES}`,
82
+ nodeLoader: register,
83
+ });
84
+ }
85
+ /** ROOK_MARKUP of `rook run`, 1 or 0, turns markup on or off for every file. */
86
+ function markupSetting() {
87
+ const setting = process.env['ROOK_MARKUP'];
88
+ return setting === undefined ? undefined : setting === '1';
89
+ }
90
+ /** The directory of the run: that of `rook run`, or one of this process, which its workers share. */
91
+ function runCache() {
92
+ const existing = process.env['ROOK_RUN_CACHE'];
93
+ if (existing !== undefined)
94
+ return existing;
95
+ const dir = mkdtempSync(join(tmpdir(), 'rook-bun-'));
96
+ process.env['ROOK_RUN_CACHE'] = dir;
97
+ process.on('exit', () => rmSync(dir, { recursive: true, force: true }));
98
+ return dir;
99
+ }
@@ -2,6 +2,8 @@ import type * as ast from '../ast.ts';
2
2
  import { InitializerChecker } from './initializers.ts';
3
3
  import { type Type } from './types.ts';
4
4
  export declare abstract class AssignmentChecker extends InitializerChecker {
5
+ /** The name of the `derived` whose block is being checked: it computes, so it changes no state. */
6
+ protected derivedBlock: string | null;
5
7
  /**
6
8
  * `a, b = f()` or `return f()`: the call must return exactly `count` values. Returns their
7
9
  * types.
@@ -1,3 +1,4 @@
1
+ import { placeOf } from "./context.js";
1
2
  import { guardedByError, unpackedResults } from "./error-results.js";
2
3
  import { isUntyped, countValues, callName, isCall } from "./helpers.js";
3
4
  import { InitializerChecker } from "./initializers.js";
@@ -5,6 +6,8 @@ import { BOOL, ANY, isAssignable, isNullable, nonNull, nullable, typeToString, U
5
6
  // Where values go: assignments to variables, fields and elements, and `return`. Both unpack a call
6
7
  // that returns several values.
7
8
  export class AssignmentChecker extends InitializerChecker {
9
+ /** The name of the `derived` whose block is being checked: it computes, so it changes no state. */
10
+ derivedBlock = null;
8
11
  /**
9
12
  * `a, b = f()` or `return f()`: the call must return exactly `count` values. Returns their
10
13
  * types.
@@ -66,13 +69,13 @@ export class AssignmentChecker extends InitializerChecker {
66
69
  const binding = this.narrowableBinding(target);
67
70
  const declared = binding?.type;
68
71
  if (binding && declared && isNullable(declared) && !isNullable(valueTypes[i] ?? UNKNOWN)) {
69
- this.flow.set(binding, nonNull(declared));
72
+ this.flow.set(placeOf(binding), nonNull(declared));
70
73
  }
71
74
  });
72
75
  }
73
76
  /** The type a value assigned to `target` must have. */
74
77
  checkTarget(target) {
75
- if (this.checkDecoratorWrite(target))
78
+ if (this.checkDecoratorWrite(target) || this.checkSharedWrite(target))
76
79
  return UNKNOWN;
77
80
  switch (target.kind) {
78
81
  case 'Identifier': {
@@ -92,13 +95,16 @@ export class AssignmentChecker extends InitializerChecker {
92
95
  else if (binding.kind === 'ref') {
93
96
  this.error(`cannot assign to "${target.name}": a ref always points to its element`, target);
94
97
  }
98
+ else if (binding.kind === 'state' && this.derivedBlock !== null) {
99
+ this.error(`derived "${this.derivedBlock}" cannot change the state "${target.name}": change it in an event handler or an effect`, target);
100
+ }
95
101
  else if (binding.kind === 'loop') {
96
102
  this.error(`cannot assign to loop variable "${target.name}"`, target);
97
103
  }
98
104
  else if (binding.kind === 'prop') {
99
105
  this.error(this.decorator
100
106
  ? `cannot assign to "${target.name}": parameters of a decorator are read-only`
101
- : `cannot assign to "${target.name}": component properties are read-only; to change the parent's state, pass a function`, target);
107
+ : `cannot assign to "${target.name}": component properties are read-only; to change it, declare it with bind, e.g. comp ${this.component?.node.name.name ?? 'Tabs'}(bind ${target.name} ...)`, target);
102
108
  }
103
109
  else if (binding.kind !== 'let' &&
104
110
  binding.kind !== 'state' &&
@@ -1,6 +1,6 @@
1
1
  import type * as ast from '../ast.ts';
2
2
  import type { Diagnostic } from '../diagnostics.ts';
3
- import { type CheckOptions, type BindingKind, type Binding, type ComponentInfo, type DecoratorInfo, type AliasEntry, Scope, type FunctionContext, type ClassContext, type Flow } from './context.ts';
3
+ import { type CheckOptions, type BindingKind, type Binding, type ComponentInfo, type DecoratorInfo, type AliasEntry, Scope, type FunctionContext, type ClassContext, type Flow, type Place } from './context.ts';
4
4
  import { type ClassInfo, type FunctionType, type Member, type Type, type TypeParam } from './types.ts';
5
5
  export declare abstract class CheckerBase {
6
6
  protected abstract checkClassBodies(node: ast.ClassDeclaration, info: ClassInfo): void;
@@ -18,6 +18,13 @@ export declare abstract class CheckerBase {
18
18
  protected abstract checkTry(node: ast.TryStatement): void;
19
19
  protected abstract forget(target: ast.Expression): void;
20
20
  protected abstract narrowableBinding(node: ast.Expression): Binding | undefined;
21
+ /** A variable or a path of data fields that conditions narrow, with its type at this point. */
22
+ protected abstract narrowable(node: ast.Expression): {
23
+ place: Place;
24
+ type: Type;
25
+ } | undefined;
26
+ /** Whether an assignment to `field` of `place` may change the member of a union it is. */
27
+ protected abstract isTagOf(place: Place, field: string): boolean;
21
28
  /** The error for a value that may be null where it must not be. */
22
29
  protected abstract nullError(node: ast.Expression): void;
23
30
  protected abstract checkFunction(params: readonly ast.Parameter[], type: FunctionType, results: Type[] | null, body: ast.BlockStatement | ast.Expression, options?: {
@@ -44,7 +51,6 @@ export declare abstract class CheckerBase {
44
51
  protected abstract orderByNeeds(decorators: readonly DecoratorInfo[]): DecoratorInfo[];
45
52
  protected abstract wrappedType(info: ComponentInfo, own: Type, seen: Set<ComponentInfo>): Type;
46
53
  protected abstract checkDecoratorMember(node: ast.DecoratorMember): Type;
47
- protected abstract checkDecoratorGet(node: ast.DecoratorGet): Type;
48
54
  protected abstract checkDecoratorWrite(target: ast.Expression): boolean;
49
55
  protected abstract checkComponentUse(node: ast.ElementExpression, tag: ast.Identifier): Type;
50
56
  protected abstract checkRefDeclaration(node: ast.VariableDeclaration): void;
@@ -4,9 +4,9 @@ import { Scope, globalScope, } from "./context.js";
4
4
  import { DOCUMENT_FRAGMENT, eventType, HTML_ELEMENT, NODE } from "./dom.js";
5
5
  import { explainMismatch, isAssignable, LazyMap, memberNames, nonNull, property, typeToString, UNKNOWN, } from "./types.js";
6
6
  // The type checker is a chain of layers, one per area, like the code generator: base → imports →
7
- // declarations → class members → classes → resolution → initializers → assignments → statements → control flow →
8
- // functions → expressions → array literals → operators → markup → forwarding → components → component values →
9
- // refs → styles → decorator wrappers → decorator exports → decorators → decorator members →
7
+ // shared state → declarations → class members → classes → resolution → initializers → assignments → statements → control flow →
8
+ // tagged unions → functions → expressions → array literals → operators → markup → forwarding → component bindings → components → component values →
9
+ // refs → styles → decorator wrappers → decorator exports → decorators → decorator includes → decorator members →
10
10
  // members → calls → goroutines → channels → signals → queues → topics → decode → try → bytes → loops → go for →
11
11
  // iterators → closing → completion → Checker (checker.ts). Statements, expressions and markup
12
12
  // check each other recursively, so methods of later layers that earlier ones call are declared
@@ -6,7 +6,7 @@ import { chanOf } from "./channel.js";
6
6
  import { CHANNEL_OBJECT, TOPIC_OBJECT } from "./channel-objects.js";
7
7
  import { PROMISE } from "./promise.js";
8
8
  import { BUFFERS } from "./transfer.js";
9
- import { UNTYPED_GLOBALS } from "./untyped-globals.js";
9
+ import { UNTYPED_CLASSES, UNTYPED_GLOBALS } from "./untyped-globals.js";
10
10
  import { ANY, arrayOf, BIGINT, BOOL, ERROR, ERROR_CLASS, func, method, NUMBER, nullable, property, STRING, topicOf, union, } from "./types.js";
11
11
  // Types of the JS standard library that Rook programs use most. Anything not listed here
12
12
  // can still be reached through the untyped globals below.
@@ -249,7 +249,10 @@ export const GLOBAL_VALUES = new Map([
249
249
  ['cancel', func([ANY], [])],
250
250
  ['Channel', CHANNEL_OBJECT],
251
251
  ['Topic', TOPIC_OBJECT],
252
- ...[...UNTYPED_GLOBALS, ...BUFFERS].map((name) => [name, ANY]),
252
+ ...[...UNTYPED_GLOBALS, ...UNTYPED_CLASSES, ...BUFFERS].map((name) => [
253
+ name,
254
+ ANY,
255
+ ]),
253
256
  ]);
254
257
  /** Built-in types that a library from TypeScript's lib files replaces: untyped ones and the DOM. */
255
258
  export const LIBRARY_TYPES = new Set([
@@ -260,6 +263,7 @@ export const LIBRARY_TYPES = new Set([
260
263
  'Promise',
261
264
  'URL',
262
265
  'Event',
266
+ ...UNTYPED_CLASSES,
263
267
  ...BUFFERS,
264
268
  ...[...DOM_TYPES.keys()].filter((name) => name !== 'Content'),
265
269
  ...SVG_TYPES.keys(),
@@ -278,7 +282,7 @@ export const GLOBAL_TYPES = new Map([
278
282
  ['any', ANY],
279
283
  ['error', ERROR],
280
284
  ['Error', ERROR_CLASS.instance],
281
- ...['Date', 'RegExp', 'Map', 'Set', 'URL', ...BUFFERS].map((name) => [name, ANY]),
285
+ ...['Date', 'RegExp', 'Map', 'Set', 'URL', ...UNTYPED_CLASSES, ...BUFFERS].map((name) => [name, ANY]),
282
286
  ['Promise', PROMISE],
283
287
  ...DOM_TYPES,
284
288
  ...SVG_TYPES,
@@ -1,4 +1,3 @@
1
- import { assignedNames } from "../names.js";
2
1
  import { cannotWait } from "../parser/syntax.js";
3
2
  import { chanDirection, chanOf, elementOf, isChan } from "./channel.js";
4
3
  import { GoroutineChecker } from "./goroutines.js";
@@ -115,7 +114,7 @@ export class ChannelChecker extends GoroutineChecker {
115
114
  : ANY;
116
115
  }
117
116
  checkSelect(node) {
118
- this.dropNarrowing(assignedNames(node));
117
+ this.dropNarrowing(node);
119
118
  const entry = this.flow;
120
119
  for (const { names, operation, body } of node.cases) {
121
120
  this.flow = new Map(entry);
@@ -195,6 +195,7 @@ export class ClassChecker extends ClassMemberChecker {
195
195
  types: new Map(),
196
196
  decorators: new Map(),
197
197
  components: new Map(),
198
+ shared: new Set(),
198
199
  };
199
200
  const declarations = { values: new Map(), types: new Map() };
200
201
  const valueOf = (name) => this.moduleScope.values.get(name)?.type ?? UNKNOWN;
@@ -217,6 +218,8 @@ export class ClassChecker extends ClassMemberChecker {
217
218
  case 'VariableDeclaration':
218
219
  for (const name of statement.names) {
219
220
  add(statement.exported, 'values', name.name, valueOf(name.name));
221
+ if (statement.exported && statement.keyword === 'state')
222
+ exports.shared.add(name.name);
220
223
  }
221
224
  break;
222
225
  case 'ClassDeclaration':
@@ -230,7 +233,10 @@ export class ClassChecker extends ClassMemberChecker {
230
233
  case 'ComponentDeclaration': {
231
234
  const info = this.moduleScope.values.get(statement.name.name)?.component;
232
235
  if (statement.exported && info?.node === statement) {
233
- info.inline = this.modulePackages().components.get(info.node.name.name) ?? null;
236
+ const found = this.modulePackages().components.get(info.node.name.name) ?? null;
237
+ // Code that reads state another module shares listens to it here, so it stays here.
238
+ const shared = found?.moduleNames.some((name) => this.moduleScope.values.get(name)?.shared !== undefined);
239
+ info.inline = shared ? null : found;
234
240
  info.expressionTypes = this.checkedTypes;
235
241
  exports.components.set(statement.name.name, info);
236
242
  }
@@ -4,7 +4,7 @@ import { IteratorChecker } from "./iterators.js";
4
4
  const OPERATIONS = new Set(['send', 'receive', 'range', 'stream', 'publish', 'listen', 'try']);
5
5
  const MESSAGES = {
6
6
  chan: (name) => `"${name}" is never closed: close it when nothing more is sent, e.g. close(${name})`,
7
- topic: (name) => `"${name}" is never closed, and a topic listens until it is closed: close it when it is no longer needed, e.g. close(${name}), in a component in the function that mount() returns`,
7
+ topic: (name) => `"${name}" is never closed, and a topic listens until it is closed: close it when it is no longer needed, e.g. close(${name}); a component closes it with defer close(${name}) in mount`,
8
8
  tick: (name) => `"${name}" is never closed, and a tick runs until it is closed: e.g. close(${name})`,
9
9
  signal: (name) => `"${name}" is never closed, and it keeps the program running until it is: close it once the signal has come, e.g. close(${name})`,
10
10
  };
@@ -1,6 +1,6 @@
1
1
  import type * as ast from '../ast.ts';
2
2
  import { ClosingChecker } from './closing.ts';
3
- import type { CompletionResult } from './context.ts';
3
+ import { type CompletionResult } from './context.ts';
4
4
  import { type Type } from './types.ts';
5
5
  export declare abstract class CompletionChecker extends ClosingChecker {
6
6
  protected completion: CompletionResult | null;
@@ -1,5 +1,6 @@
1
1
  import { ClosingChecker } from "./closing.js";
2
2
  import { ARRAY_MEMBERS, ATTRIBUTE_NAMES, BIGINT_MEMBERS, BOOL_MEMBERS, EVENT_ATTRIBUTES, HTML_TAGS, NUMBER_MEMBERS, STRING_MEMBERS, } from "./completion-names.js";
3
+ import { placeOf, } from "./context.js";
3
4
  import { isSubclass, literalBase, memberNames, memberTypeOf, nonNull, typeToString, union, UNKNOWN, } from "./types.js";
4
5
  // Completion in editors (src/completion.ts): the editor writes the name `options.completion` where
5
6
  // the cursor is, and this layer lists what can be written there when the check reaches that name:
@@ -79,7 +80,7 @@ export class CompletionChecker extends ClosingChecker {
79
80
  if (component) {
80
81
  return { name, kind: 'component', detail: typeToString(this.componentValue(component)) };
81
82
  }
82
- const type = binding.type && (this.flow.get(binding) ?? binding.type);
83
+ const type = binding.type && (this.flow.get(placeOf(binding)) ?? binding.type);
83
84
  const detail = type ? { detail: typeToString(type) } : {};
84
85
  if (kind === 'function' || type?.kind === 'function')
85
86
  return { name, kind: 'function', ...detail };
@@ -0,0 +1,7 @@
1
+ import type * as ast from '../ast.ts';
2
+ import type { ComponentInfo } from './context.ts';
3
+ import { ForwardingChecker } from './forwarding.ts';
4
+ export declare abstract class ComponentBindingChecker extends ForwardingChecker {
5
+ /** `bind:active={tab}` on a component; adds the name of the property to `given`. */
6
+ protected checkPropBinding(attribute: ast.JsxAttribute, info: ComponentInfo, component: string, given: Set<string>): void;
7
+ }
@@ -0,0 +1,45 @@
1
+ import { attrsParam } from "../attrs.js";
2
+ import { ForwardingChecker } from "./forwarding.js";
3
+ import { isUntyped } from "./helpers.js";
4
+ import { isAssignable, typeToString } from "./types.js";
5
+ // Bound properties: `comp Tabs(bind active string)` may change `active`, and
6
+ // `<Tabs bind:active={tab} />` writes those changes into the parent's `tab`, as `bind:value` of an
7
+ // input does. The value goes both ways, so the variable and the property have the same type.
8
+ export class ComponentBindingChecker extends ForwardingChecker {
9
+ /** `bind:active={tab}` on a component; adds the name of the property to `given`. */
10
+ checkPropBinding(attribute, info, component, given) {
11
+ const name = attribute.name.name.slice('bind:'.length);
12
+ const prop = info.props.get(name);
13
+ const { value } = attribute;
14
+ if (given.has(name))
15
+ this.error(`duplicate property "${name}"`, attribute.name);
16
+ given.add(name);
17
+ if (!prop) {
18
+ this.error(attrsParam(info.node)
19
+ ? `${attribute.name.name} does not go through the attributes of <${component}>`
20
+ : `<${component}> has no property "${name}" to bind`, attribute.name);
21
+ }
22
+ else if (!prop.bind) {
23
+ this.error(`"${name}" of <${component}> is not bound: the component declares it with bind to change it, e.g. comp ${component}(bind ${name} ...)`, attribute.name);
24
+ }
25
+ if (value === null || value.kind === 'StringLiteral' || value.kind === 'EventHandler') {
26
+ this.error(`"bind:${name}" needs a variable in braces, e.g. bind:${name}={${name}}`, attribute);
27
+ return;
28
+ }
29
+ const bindable = (value.kind === 'Identifier' && value.name !== '_') ||
30
+ ((value.kind === 'MemberExpression' || value.kind === 'IndexExpression') && !value.optional);
31
+ if (!bindable) {
32
+ this.checkValue(value);
33
+ this.error(`"bind:${name}" needs a variable or a field to write to`, value);
34
+ return;
35
+ }
36
+ const type = this.checkTarget(value);
37
+ this.checkedTypes.set(value, type);
38
+ if (!prop?.bind || isUntyped(type) || isUntyped(prop.type))
39
+ return;
40
+ // The parent gives its value, and the component writes its own back.
41
+ if (!isAssignable(type, prop.type) || !isAssignable(prop.type, type)) {
42
+ this.error(`cannot bind ${typeToString(type)} to "${name}" of <${component}>, which is ${typeToString(prop.type)}`, value);
43
+ }
44
+ }
45
+ }