tutuca 0.15.0 → 1.0.0-beta.2

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 (170) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/LICENSE +1 -1
  3. package/README.md +64 -170
  4. package/{dist → deps}/immer.js +674 -249
  5. package/dist/tutuca.js +2342 -4057
  6. package/dist/tutuca.min.js +3 -3
  7. package/package.json +78 -96
  8. package/src/component.js +66 -0
  9. package/src/css/classes.js +77 -0
  10. package/src/dev/formatters.js +393 -0
  11. package/src/dev/jsonml.js +35 -0
  12. package/src/dev/panel.js +400 -0
  13. package/src/dev/summary.js +239 -0
  14. package/src/dev/vdom-formatters.js +102 -0
  15. package/src/drag.js +23 -0
  16. package/src/embed/element.js +111 -0
  17. package/src/env.js +30 -0
  18. package/src/events.js +85 -0
  19. package/src/history/bar.js +198 -0
  20. package/src/history/index.js +149 -0
  21. package/src/host.js +360 -0
  22. package/src/lint/index.js +470 -0
  23. package/src/meta.js +12 -0
  24. package/src/optimize.js +58 -0
  25. package/src/paths.js +205 -0
  26. package/src/playground/element.js +360 -0
  27. package/src/playground/index.js +5 -0
  28. package/src/playground/sources.js +107 -0
  29. package/src/playground/ui/playground.js +287 -0
  30. package/src/schema.js +460 -0
  31. package/src/scope.js +92 -0
  32. package/src/storybook/index.js +80 -0
  33. package/src/storybook/runner.js +452 -0
  34. package/src/storybook/stories.js +208 -0
  35. package/src/storybook/stress.js +76 -0
  36. package/src/storybook/ui/common.js +37 -0
  37. package/src/storybook/ui/link.js +80 -0
  38. package/src/storybook/ui/panels.js +32 -0
  39. package/src/storybook/ui/story-panel.js +432 -0
  40. package/src/storybook/ui/storybook.js +191 -0
  41. package/src/storybook/ui/stress-panel.js +231 -0
  42. package/src/sync/automerge.js +159 -0
  43. package/src/sync/codec.js +206 -0
  44. package/src/sync/diff.js +153 -0
  45. package/src/sync/index.js +97 -0
  46. package/src/sync/migrate.js +184 -0
  47. package/src/sync/ops.js +160 -0
  48. package/src/sync/remote.js +206 -0
  49. package/src/tag.js +24 -0
  50. package/src/tasks.js +244 -0
  51. package/src/test/driver.js +141 -0
  52. package/src/test/pbt.js +589 -0
  53. package/src/tutuca.js +66 -0
  54. package/src/vdom.js +349 -0
  55. package/src/view.js +354 -0
  56. package/src/walk.js +79 -0
  57. package/dist/chai.js +0 -4210
  58. package/dist/tutuca-cli.js +0 -16315
  59. package/dist/tutuca-components.js +0 -2232
  60. package/dist/tutuca-dev.ext.js +0 -8476
  61. package/dist/tutuca-dev.js +0 -13220
  62. package/dist/tutuca-dev.min.js +0 -5
  63. package/dist/tutuca-extra.ext.js +0 -4261
  64. package/dist/tutuca-extra.js +0 -4964
  65. package/dist/tutuca-extra.min.js +0 -3
  66. package/dist/tutuca-storybook.js +0 -970
  67. package/dist/tutuca.ext.js +0 -4183
  68. package/skill/margaui/SKILL.md +0 -105
  69. package/skill/margaui/components/accordion.md +0 -127
  70. package/skill/margaui/components/alert.md +0 -174
  71. package/skill/margaui/components/aura.md +0 -97
  72. package/skill/margaui/components/avatar.md +0 -220
  73. package/skill/margaui/components/badge.md +0 -193
  74. package/skill/margaui/components/breadcrumbs.md +0 -103
  75. package/skill/margaui/components/button.md +0 -322
  76. package/skill/margaui/components/calendar.md +0 -67
  77. package/skill/margaui/components/card.md +0 -373
  78. package/skill/margaui/components/carousel.md +0 -387
  79. package/skill/margaui/components/chat.md +0 -171
  80. package/skill/margaui/components/checkbox.md +0 -101
  81. package/skill/margaui/components/collapse.md +0 -172
  82. package/skill/margaui/components/countdown.md +0 -165
  83. package/skill/margaui/components/diff.md +0 -53
  84. package/skill/margaui/components/divider.md +0 -107
  85. package/skill/margaui/components/dock.md +0 -173
  86. package/skill/margaui/components/drawer.md +0 -184
  87. package/skill/margaui/components/dropdown.md +0 -388
  88. package/skill/margaui/components/fab.md +0 -346
  89. package/skill/margaui/components/fieldset.md +0 -88
  90. package/skill/margaui/components/file-input.md +0 -84
  91. package/skill/margaui/components/filter.md +0 -52
  92. package/skill/margaui/components/footer.md +0 -583
  93. package/skill/margaui/components/hero.md +0 -135
  94. package/skill/margaui/components/hover-3d.md +0 -129
  95. package/skill/margaui/components/hover-gallery.md +0 -49
  96. package/skill/margaui/components/indicator.md +0 -265
  97. package/skill/margaui/components/input.md +0 -389
  98. package/skill/margaui/components/join.md +0 -100
  99. package/skill/margaui/components/kbd.md +0 -127
  100. package/skill/margaui/components/label.md +0 -102
  101. package/skill/margaui/components/link.md +0 -96
  102. package/skill/margaui/components/list.md +0 -182
  103. package/skill/margaui/components/loading.md +0 -105
  104. package/skill/margaui/components/mask.md +0 -168
  105. package/skill/margaui/components/megamenu.md +0 -131
  106. package/skill/margaui/components/menu.md +0 -887
  107. package/skill/margaui/components/mockup-browser.md +0 -39
  108. package/skill/margaui/components/mockup-code.md +0 -81
  109. package/skill/margaui/components/mockup-phone.md +0 -39
  110. package/skill/margaui/components/mockup-window.md +0 -33
  111. package/skill/margaui/components/modal.md +0 -196
  112. package/skill/margaui/components/navbar.md +0 -282
  113. package/skill/margaui/components/otp.md +0 -171
  114. package/skill/margaui/components/pagination.md +0 -122
  115. package/skill/margaui/components/progress.md +0 -135
  116. package/skill/margaui/components/radial-progress.md +0 -67
  117. package/skill/margaui/components/radio.md +0 -133
  118. package/skill/margaui/components/range.md +0 -134
  119. package/skill/margaui/components/rating.md +0 -170
  120. package/skill/margaui/components/select.md +0 -225
  121. package/skill/margaui/components/skeleton.md +0 -64
  122. package/skill/margaui/components/stack.md +0 -142
  123. package/skill/margaui/components/stat.md +0 -254
  124. package/skill/margaui/components/status.md +0 -73
  125. package/skill/margaui/components/steps.md +0 -138
  126. package/skill/margaui/components/swap.md +0 -152
  127. package/skill/margaui/components/tab.md +0 -248
  128. package/skill/margaui/components/table.md +0 -1018
  129. package/skill/margaui/components/text-rotate.md +0 -91
  130. package/skill/margaui/components/textarea.md +0 -85
  131. package/skill/margaui/components/theme-controller.md +0 -266
  132. package/skill/margaui/components/timeline.md +0 -1356
  133. package/skill/margaui/components/toast.md +0 -165
  134. package/skill/margaui/components/toggle.md +0 -135
  135. package/skill/margaui/components/tooltip.md +0 -181
  136. package/skill/margaui/components/validator.md +0 -163
  137. package/skill/tutuca/SKILL.md +0 -56
  138. package/skill/tutuca/advanced.md +0 -292
  139. package/skill/tutuca/cli.md +0 -239
  140. package/skill/tutuca/component-design.md +0 -168
  141. package/skill/tutuca/core.md +0 -935
  142. package/skill/tutuca/iteration.md +0 -207
  143. package/skill/tutuca/macros.md +0 -86
  144. package/skill/tutuca/margaui.md +0 -175
  145. package/skill/tutuca/messages-and-intents.md +0 -414
  146. package/skill/tutuca/patterns/README.md +0 -48
  147. package/skill/tutuca/patterns/add-a-story.md +0 -26
  148. package/skill/tutuca/patterns/bind-text-and-attributes.md +0 -30
  149. package/skill/tutuca/patterns/conditional-attribute-value.md +0 -29
  150. package/skill/tutuca/patterns/coordinate-components.md +0 -54
  151. package/skill/tutuca/patterns/edit-through-a-dynamic-target.md +0 -32
  152. package/skill/tutuca/patterns/enrich-each-item.md +0 -25
  153. package/skill/tutuca/patterns/file-input.md +0 -39
  154. package/skill/tutuca/patterns/filter-a-list.md +0 -25
  155. package/skill/tutuca/patterns/filter-and-paginate.md +0 -60
  156. package/skill/tutuca/patterns/handle-events.md +0 -45
  157. package/skill/tutuca/patterns/iterate-a-list.md +0 -18
  158. package/skill/tutuca/patterns/paginate-a-list.md +0 -29
  159. package/skill/tutuca/patterns/render-a-child-component.md +0 -21
  160. package/skill/tutuca/patterns/reuse-markup-with-macros.md +0 -36
  161. package/skill/tutuca/patterns/share-state-across-the-tree.md +0 -53
  162. package/skill/tutuca/patterns/show-or-hide-content.md +0 -23
  163. package/skill/tutuca/patterns/switch-between-views.md +0 -30
  164. package/skill/tutuca/patterns/tabbed-interface.md +0 -43
  165. package/skill/tutuca/semantics.md +0 -219
  166. package/skill/tutuca/storybook.md +0 -270
  167. package/skill/tutuca/styles.md +0 -48
  168. package/skill/tutuca/testing.md +0 -345
  169. package/skill/tutuca-source/SKILL.md +0 -33
  170. package/skill/tutuca-source/tutuca.ext.js +0 -4183
package/package.json CHANGED
@@ -1,98 +1,80 @@
1
1
  {
2
- "name": "tutuca",
3
- "version": "0.15.0",
4
- "type": "module",
5
- "description": "SPA framework with Immer-powered immutable state and virtual DOM",
6
- "main": "./dist/tutuca.js",
7
- "module": "./dist/tutuca.js",
8
- "exports": {
9
- ".": "./dist/tutuca.js",
10
- "./dev": "./dist/tutuca-dev.js",
11
- "./extra": "./dist/tutuca-extra.js",
12
- "./cli": "./dist/tutuca-cli.js",
13
- "./ext": "./dist/tutuca.ext.js",
14
- "./extra-ext": "./dist/tutuca-extra.ext.js",
15
- "./dev-ext": "./dist/tutuca-dev.ext.js",
16
- "./storybook": "./dist/tutuca-storybook.js",
17
- "./components": "./dist/tutuca-components.js",
18
- "./immer": "./dist/immer.js",
19
- "./chai": "./dist/chai.js",
20
- "./package.json": "./package.json"
21
- },
22
- "bin": {
23
- "tutuca": "./dist/tutuca-cli.js"
24
- },
25
- "scripts": {
26
- "clean": "rm -rf dist skill",
27
- "dist": "node scripts/dist.js",
28
- "dist-ext": "node scripts/dist-ext.js",
29
- "dist-all": "npm run dist && npm run dist-ext && npm run smoke",
30
- "smoke": "node scripts/smoke.js",
31
- "release": "npm run dist-all && npm run sync-docs-storybook && npm run pin-docs-version && npm run build-skill && (npm publish --access public; npm run clean-skill)",
32
- "release-dry": "npm run dist-all && npm run sync-docs-storybook && npm run pin-docs-version && npm run build-skill && (npm publish --dry-run; npm run clean-skill)",
33
- "sync-docs-storybook": "node scripts/sync-docs-storybook.js",
34
- "pin-docs-version": "node scripts/pin-docs-version.js",
35
- "build-skill": "node scripts/build-skill.js",
36
- "clean-skill": "rm -rf skill",
37
- "pretest": "npm run dist && npm run dist-ext",
38
- "test": "vitest run",
39
- "test-watch": "vitest",
40
- "format": "biome format --write",
41
- "lint": "biome lint",
42
- "lint-fix": "biome lint --write",
43
- "fix": "biome check --write",
44
- "tutuca": "node tools/tutuca.js",
45
- "storybook": "node scripts/serve-storybook.js",
46
- "storybook:examples": "node scripts/storybook-examples.js",
47
- "stresstest": "node scripts/stresstest.js",
48
- "smoke-test": "node tools/tutuca.js lint ./test/todo.js && node tools/tutuca.js render ./test/todo.js && node tools/tutuca.js test ./test/todo.js && node tools/tutuca.js lint ./test/json.js && node tools/tutuca.js render ./test/json.js"
49
- },
50
- "sideEffects": [
51
- "./deps/chai.js"
52
- ],
53
- "files": [
54
- "dist/",
55
- "!dist/*.br",
56
- "skill"
57
- ],
58
- "dependencies": {
59
- "immer": "^10.2.0",
60
- "jsdom": "^28.0.0 || ^29.0.0",
61
- "prettier": "^3.0.0"
62
- },
63
- "peerDependencies": {
64
- "chai": "^6.2.2"
65
- },
66
- "peerDependenciesMeta": {
67
- "chai": {
68
- "optional": true
69
- }
70
- },
71
- "keywords": [
72
- "frontend",
73
- "framework",
74
- "immer",
75
- "virtual-dom"
76
- ],
77
- "repository": {
78
- "type": "git",
79
- "url": "git+https://github.com/marianoguerra/tutuca.git"
80
- },
81
- "engines": {
82
- "node": ">=18"
83
- },
84
- "author": "Mariano Guerra",
85
- "license": "MIT",
86
- "bugs": {
87
- "url": "https://github.com/marianoguerra/tutuca/issues"
88
- },
89
- "homepage": "https://github.com/marianoguerra/tutuca#readme",
90
- "devDependencies": {
91
- "@biomejs/biome": "^2.5.1",
92
- "chai": "^6.2.2",
93
- "esbuild": "^0.28.1",
94
- "fast-check": "^4.8.0",
95
- "htmlparser2": "^12.0.0",
96
- "vitest": "^4.1.10"
97
- }
2
+ "name": "tutuca",
3
+ "version": "1.0.0-beta.2",
4
+ "description": "A small framework for web apps in the browser, with typed state, tests, a storybook and pluggable sync",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Mariano Guerra",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/marianoguerra/tutuca.git"
11
+ },
12
+ "homepage": "https://marianoguerra.github.io/tutuca/",
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
16
+ "sideEffects": false,
17
+ "exports": {
18
+ ".": "./src/tutuca.js",
19
+ "./vdom": "./src/vdom.js",
20
+ "./dev": "./src/dev/formatters.js",
21
+ "./lint": "./src/lint/index.js",
22
+ "./classes": "./src/css/classes.js",
23
+ "./pbt": "./src/test/pbt.js",
24
+ "./storybook": "./src/storybook/index.js",
25
+ "./element": "./src/embed/element.js",
26
+ "./playground": "./src/playground/index.js",
27
+ "./history": "./src/history/index.js",
28
+ "./history/bar": "./src/history/bar.js",
29
+ "./sync": "./src/sync/index.js",
30
+ "./sync/remote": "./src/sync/remote.js",
31
+ "./automerge": "./src/sync/automerge.js",
32
+ "./package.json": "./package.json"
33
+ },
34
+ "files": [
35
+ "src/",
36
+ "deps/",
37
+ "dist/",
38
+ "README.md",
39
+ "CHANGELOG.md",
40
+ "LICENSE"
41
+ ],
42
+ "scripts": {
43
+ "test": "node --test test/*/*.test.js",
44
+ "lint": "biome check .",
45
+ "fmt": "biome check --write .",
46
+ "dist": "node scripts/dist.js",
47
+ "smoke": "node scripts/smoke.js",
48
+ "prepublishOnly": "npm run lint && npm test && npm run dist && npm run smoke"
49
+ },
50
+ "peerDependencies": {
51
+ "@automerge/automerge": "^3",
52
+ "fast-check": "^4"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "fast-check": {
56
+ "optional": true
57
+ },
58
+ "@automerge/automerge": {
59
+ "optional": true
60
+ }
61
+ },
62
+ "devDependencies": {
63
+ "@automerge/automerge": "^3.5.0",
64
+ "@automerge/automerge-repo": "^2.6.0-alpha.3",
65
+ "@automerge/automerge-repo-network-broadcastchannel": "^2.6.0-alpha.3",
66
+ "@automerge/automerge-repo-network-websocket": "^2.6.0-alpha.3",
67
+ "@automerge/automerge-repo-storage-indexeddb": "^2.6.0-alpha.3",
68
+ "@biomejs/biome": "^2.5.15",
69
+ "@codemirror/lang-javascript": "^6.2.5",
70
+ "@codemirror/theme-one-dark": "^6.1.3",
71
+ "acorn": "^8.19.0",
72
+ "acorn-walk": "^8.3.5",
73
+ "codemirror": "^6.0.2",
74
+ "esbuild": "^0.28.2",
75
+ "fast-check": "^4.10.2",
76
+ "immer": "11.1.21",
77
+ "jsdom": "^30.1.3",
78
+ "margaui": "^0.5750.1"
79
+ }
98
80
  }
@@ -0,0 +1,66 @@
1
+ // Component specs: component() turns options into a ComponentSpec that makes instances.
2
+ import { immerable } from "../deps/immer.js";
3
+ import { DRAG_EVENTS } from "./drag.js";
4
+ import { idSym, NoDefaultError, scopeSym, specSym, toAnswers, toMessages } from "./schema.js";
5
+ import { toExpr } from "./view.js";
6
+ import { Collect } from "./walk.js";
7
+
8
+ let nextId = 1;
9
+ export class ComponentSpec {
10
+ constructor({ name, fields, methods, views, on, answers, loops, scopes, provides, invariants }) {
11
+ this.name = name;
12
+ this.fields = fields;
13
+ this.methods = methods;
14
+ // Copy descriptors, not values, so getters in methods stay getters.
15
+ this.proto = Object.create(null, Object.getOwnPropertyDescriptors(methods));
16
+ Object.assign(this.proto, { [immerable]: true, [specSym]: this });
17
+ this.views = views;
18
+ this.on = on;
19
+ this.answers = answers; // ask name -> outcome (ok, error, unhandled) -> its handler.
20
+ this.loops = loops;
21
+ this.scopes = scopes;
22
+ this.provides = provides;
23
+ this.invariants = invariants; // name → predicate on the instance, checked in dev and tests.
24
+ const collect = new Collect();
25
+ for (const key in views) collect.visit(views[key]);
26
+ this.eventHandlers = collect.eventHandlers;
27
+ this.eventNames = new Set([...this.eventHandlers.values()].flat().map((h) => h.name));
28
+ // A drop target needs the drag from its start: the host tracks it.
29
+ if (this.eventNames.has("drop")) for (const name of DRAG_EVENTS) this.eventNames.add(name);
30
+ }
31
+ make(args = {}, scope = null) {
32
+ const state = Object.create(this.proto);
33
+ state[idSym] = nextId++;
34
+ if (scope !== null) state[scopeSym] = scope;
35
+ for (const key in this.fields) {
36
+ try {
37
+ state[key] = this.fields[key].make(args[key], scope, state);
38
+ } catch (error) {
39
+ // Prefix the field, so nested failures read as a path: Root.child: Child.item: ...
40
+ const Wrapped = error instanceof NoDefaultError ? NoDefaultError : Error;
41
+ throw new Wrapped(`${this.name}.${key}: ${error.message}`, { cause: error });
42
+ }
43
+ }
44
+ return state;
45
+ }
46
+ }
47
+ const ownEntries = (record) => Object.assign(Object.create(null), record); // no toString &c in entries
48
+ export const component = (opts) => {
49
+ const { name, fields = {}, methods = {}, view, views = {} } = opts;
50
+ const sources = view === undefined ? views : { ...views, main: view };
51
+ const exprs = ownEntries({});
52
+ for (const key in sources) exprs[key] = toExpr(sources[key]);
53
+ if (!("main" in exprs)) throw new Error(`Component ${name} has no main view`);
54
+ return new ComponentSpec({
55
+ name,
56
+ fields,
57
+ methods,
58
+ views: exprs,
59
+ on: toMessages(opts.on),
60
+ answers: toAnswers(opts.answers),
61
+ loops: ownEntries(opts.loops),
62
+ scopes: ownEntries(opts.scopes),
63
+ provides: ownEntries(opts.provides),
64
+ invariants: ownEntries(opts.invariants),
65
+ });
66
+ };
@@ -0,0 +1,77 @@
1
+ // Class names for CSS compilers that need them listed: the constant ones a view tree holds, and
2
+ // the computed ones as they show up in the DOM.
3
+ import { Walker } from "../tutuca.js";
4
+
5
+ // Constants are class names only while inside a class attribute.
6
+ class ClassNames extends Walker {
7
+ inClass = false;
8
+ constructor(classes) {
9
+ super();
10
+ this.classes = classes;
11
+ }
12
+ attr(node, name, value) {
13
+ const outer = this.inClass;
14
+ this.inClass = name === "class";
15
+ super.attr(node, name, value);
16
+ this.inClass = outer;
17
+ }
18
+ constant(node) {
19
+ if (!this.inClass || typeof node.value !== "string") return;
20
+ for (const c of node.value.split(/\s+/)) if (c) this.classes.add(c);
21
+ }
22
+ }
23
+
24
+ // The constant class names in a tree, including every $if branch; classes
25
+ // read from the state are only known at render time.
26
+ export const collectClasses = (tree, classes = new Set()) => {
27
+ new ClassNames(classes).visit(tree);
28
+ return classes;
29
+ };
30
+
31
+ // The class names the components use: the constant ones in views and those loops bind.
32
+ export const classNamesOf = (specs, extra = []) => {
33
+ const classes = new Set(extra.flatMap((c) => c.split(/\s+/)));
34
+ for (const spec of specs)
35
+ for (const name in spec.views) collectClasses(spec.views[name], classes);
36
+ return [...classes].filter(Boolean);
37
+ };
38
+
39
+ // Compiles the class names stories render that the static css lacks: views can compute
40
+ // classes in methods and loops, which collectClasses can't see. compile(classes) → css.
41
+ export class ClassCompiler {
42
+ constructor(compile, known = []) {
43
+ this.compile = compile;
44
+ this.seen = new Set(known);
45
+ this.styles = new Set();
46
+ this.css = "";
47
+ this.queued = false;
48
+ }
49
+ watch(rootNode, style) {
50
+ this.styles.add(style);
51
+ style.textContent = this.css;
52
+ this.scan(rootNode);
53
+ }
54
+ scan(rootNode) {
55
+ let added = false;
56
+ for (const el of rootNode.querySelectorAll("[class]")) {
57
+ for (const c of el.getAttribute("class").split(/\s+/)) {
58
+ if (c !== "" && !this.seen.has(c)) {
59
+ this.seen.add(c);
60
+ added = true;
61
+ }
62
+ }
63
+ }
64
+ if (added && !this.queued) {
65
+ this.queued = true;
66
+ queueMicrotask(() => this.update());
67
+ }
68
+ }
69
+ async update() {
70
+ this.queued = false;
71
+ this.css = await this.compile([...this.seen]);
72
+ for (const style of this.styles) {
73
+ if (style.isConnected) style.textContent = this.css;
74
+ else this.styles.delete(style);
75
+ }
76
+ }
77
+ }