@excom/quark 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/.rush/temp/chunked-rush-logs/quark.apply-exports.chunks.jsonl +1 -0
  2. package/.rush/temp/chunked-rush-logs/quark.build_package-metas.chunks.jsonl +1 -0
  3. package/.rush/temp/operation/apply-exports/all.log +1 -0
  4. package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
  5. package/.rush/temp/operation/apply-exports/state.json +3 -0
  6. package/.rush/temp/operation/build_package-metas/all.log +1 -0
  7. package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
  8. package/.rush/temp/operation/build_package-metas/state.json +3 -0
  9. package/.rush/temp/shrinkwrap-deps.json +3 -0
  10. package/config/rig.json +6 -0
  11. package/index.ts +4 -0
  12. package/language.ts +7 -0
  13. package/package.json +45 -0
  14. package/rush-logs/quark.apply-exports.cache.log +1 -0
  15. package/rush-logs/quark.apply-exports.log +1 -0
  16. package/rush-logs/quark.build_package-metas.cache.log +1 -0
  17. package/rush-logs/quark.build_package-metas.log +1 -0
  18. package/src/actions.ts +445 -0
  19. package/src/ast.ts +187 -0
  20. package/src/bindings.ts +115 -0
  21. package/src/builtin-modules.ts +401 -0
  22. package/src/constants.ts +47 -0
  23. package/src/cycle-check.ts +121 -0
  24. package/src/devtools-hook.ts +303 -0
  25. package/src/element-api.ts +109 -0
  26. package/src/evaluator.ts +317 -0
  27. package/src/form-controls.ts +69 -0
  28. package/src/language-tables.ts +137 -0
  29. package/src/language.ts +1225 -0
  30. package/src/observer.ts +110 -0
  31. package/src/paint.ts +305 -0
  32. package/src/properties.ts +818 -0
  33. package/src/props.ts +94 -0
  34. package/src/quark-element.ts +7 -0
  35. package/src/quark-internal.ts +365 -0
  36. package/src/quark.ts +817 -0
  37. package/src/resolvers.ts +867 -0
  38. package/src/rule.ts +800 -0
  39. package/src/scope-id.ts +47 -0
  40. package/src/selector-utils.ts +430 -0
  41. package/src/settle.ts +117 -0
  42. package/src/types.ts +155 -0
  43. package/src/utils.ts +79 -0
  44. package/src/variables.ts +359 -0
  45. package/support/demos/css-variables.html +18 -0
  46. package/support/demos/dispatch.html +20 -0
  47. package/support/demos/events.html +9 -0
  48. package/support/demos/iterate.html +13 -0
  49. package/support/demos/js-api.html +17 -0
  50. package/support/demos/provider-list.html +16 -0
  51. package/support/demos/toggle-content.html +14 -0
  52. package/support/demos/unset.html +12 -0
  53. package/support/demos/view-transition.html +36 -0
  54. package/support/docs/ATTRIBUTES.md +37 -0
  55. package/support/docs/AT_RULES.md +45 -0
  56. package/support/docs/BUILTINS.md +57 -0
  57. package/support/docs/CONTENT.md +40 -0
  58. package/support/docs/CSS_VARIABLES.md +16 -0
  59. package/support/docs/DECLARATIONS.md +26 -0
  60. package/support/docs/DELAY.md +37 -0
  61. package/support/docs/DIAGNOSTICS.md +32 -0
  62. package/support/docs/DISPATCH.md +81 -0
  63. package/support/docs/ELEMENT_PROPERTIES.md +34 -0
  64. package/support/docs/EXPRESSIONS.md +31 -0
  65. package/support/docs/INTERNAL.md +8 -0
  66. package/support/docs/JS_API.md +53 -0
  67. package/support/docs/JS_WRITES.md +36 -0
  68. package/support/docs/LIMITATIONS.md +26 -0
  69. package/support/docs/LOOP_GUARD.md +30 -0
  70. package/support/docs/METHODS.md +43 -0
  71. package/support/docs/MODULES.md +111 -0
  72. package/support/docs/NO_REVERSION.md +18 -0
  73. package/support/docs/ON.md +87 -0
  74. package/support/docs/REACTIVITY.md +24 -0
  75. package/support/docs/README.md +143 -0
  76. package/support/docs/SELECTORS.md +87 -0
  77. package/support/docs/SHEETS.md +67 -0
  78. package/support/docs/SYNTAX.md +36 -0
  79. package/support/docs/USE.md +24 -0
  80. package/support/docs/VALUES.md +27 -0
  81. package/support/docs/VARIABLES.md +47 -0
  82. package/support/docs/VIEW_TRANSITION.md +64 -0
  83. package/support/docs-sections.json +65 -0
  84. package/support/package-meta.json +208 -0
  85. package/support/scripts/build-language-docs.mjs +196 -0
  86. package/support/tests/__snapshots__/css-variables.view.test.ts.snap +23 -0
  87. package/support/tests/__snapshots__/dispatch.view.test.ts.snap +23 -0
  88. package/support/tests/__snapshots__/events.view.test.ts.snap +23 -0
  89. package/support/tests/__snapshots__/iterate.view.test.ts.snap +23 -0
  90. package/support/tests/__snapshots__/js-api.view.test.ts.snap +23 -0
  91. package/support/tests/__snapshots__/provider-list.view.test.ts.snap +23 -0
  92. package/support/tests/__snapshots__/quark-features.test.ts.snap +1079 -0
  93. package/support/tests/__snapshots__/quark-with-providers.test.ts.snap +89 -0
  94. package/support/tests/__snapshots__/quark.test.ts.snap +221 -0
  95. package/support/tests/__snapshots__/selectors.test.ts.snap +133 -0
  96. package/support/tests/__snapshots__/toggle-content.view.test.ts.snap +23 -0
  97. package/support/tests/__snapshots__/unset.view.test.ts.snap +23 -0
  98. package/support/tests/__snapshots__/view-transition.view.test.ts.snap +23 -0
  99. package/support/tests/actions.test.ts +437 -0
  100. package/support/tests/builtin-modules.test.ts +337 -0
  101. package/support/tests/builtins.test.ts +189 -0
  102. package/support/tests/css-variables.view.test.ts +35 -0
  103. package/support/tests/delay.test.ts +244 -0
  104. package/support/tests/devtools-hook.test.ts +496 -0
  105. package/support/tests/diagnostics.test.ts +224 -0
  106. package/support/tests/dispatch.view.test.ts +43 -0
  107. package/support/tests/element-api.test.ts +202 -0
  108. package/support/tests/evaluator.test.ts +237 -0
  109. package/support/tests/events.view.test.ts +40 -0
  110. package/support/tests/form-controls.test.ts +452 -0
  111. package/support/tests/helpers.ts +354 -0
  112. package/support/tests/internals.test.ts +568 -0
  113. package/support/tests/iterate.view.test.ts +40 -0
  114. package/support/tests/js-api.view.test.ts +45 -0
  115. package/support/tests/language-docs.test.ts +147 -0
  116. package/support/tests/lifecycle.test.ts +440 -0
  117. package/support/tests/loop-guard.test.ts +323 -0
  118. package/support/tests/provider-list.view.test.ts +47 -0
  119. package/support/tests/quark-features.test.ts +2887 -0
  120. package/support/tests/quark-with-providers.test.ts +169 -0
  121. package/support/tests/quark.test.ts +359 -0
  122. package/support/tests/resolvers.test.ts +346 -0
  123. package/support/tests/selector-analysis.test.ts +281 -0
  124. package/support/tests/selectors.test.ts +670 -0
  125. package/support/tests/toggle-content.view.test.ts +40 -0
  126. package/support/tests/unset.view.test.ts +42 -0
  127. package/support/tests/view-helpers.ts +81 -0
  128. package/support/tests/view-transition.test.ts +1069 -0
  129. package/support/tests/view-transition.view.test.ts +76 -0
  130. package/tsconfig.json +5 -0
@@ -0,0 +1,354 @@
1
+ import { Quark } from "../../index";
2
+ import * as paint from "../../src/paint";
3
+ import { QuarkRegistry } from "../../src/quark";
4
+ import { isQuarkBusy } from "../../src/settle";
5
+ import { Attribute, Listener, Variable } from "../../src/properties";
6
+ import { QuarkInternal } from "../../src/quark-internal";
7
+ import type { QuarkOptions } from "../../src/types";
8
+ import {
9
+ expect,
10
+ fixture,
11
+ vi,
12
+ wait,
13
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
14
+
15
+ /**
16
+ * Sheet + targets must share a parent (Quark host = sheetElement.parent).
17
+ * Sheets are host-scoped by default (quark-sheet's default). Pass
18
+ * `{ isScoped: false }` for global (root-context) sheets.
19
+ */
20
+ export const createSheet = (
21
+ bodyHtml: string,
22
+ src: string,
23
+ modules?: Record<string, unknown>,
24
+ options?: QuarkOptions
25
+ ) => {
26
+ const root = fixture<HTMLElement>(
27
+ `<section><div id="sheet"></div>${bodyHtml}</section>`
28
+ );
29
+ const sheetElement = root.querySelector("#sheet") as HTMLElement;
30
+ const quark = new Quark({ src, options: { isScoped: true, ...options } });
31
+ const register = () =>
32
+ quark.register({
33
+ sheetElement,
34
+ ...(modules ? { modules: { dfault: modules } } : {}),
35
+ });
36
+ return { root, quark, sheetElement, register };
37
+ };
38
+
39
+ export const mount = (
40
+ bodyHtml: string,
41
+ src: string,
42
+ modules?: Record<string, unknown>,
43
+ options?: QuarkOptions
44
+ ) => {
45
+ const sheet = createSheet(bodyHtml, src, modules, options);
46
+ sheet.register();
47
+ return sheet;
48
+ };
49
+
50
+ /**
51
+ * Unregister every sheet still registered. Sheets listen for provision /
52
+ * binding events on the document root, so one left registered by an
53
+ * earlier test keeps matching (until its host is GC'd, which makes
54
+ * complexity counts depend on GC timing). Call from `afterEach`.
55
+ */
56
+ export const unregisterAll = () => {
57
+ QuarkRegistry.sheets.forEach((ref) => {
58
+ const sheet = ref.deref();
59
+ if (sheet?.isRegistered) sheet.unregister();
60
+ });
61
+ };
62
+
63
+ /**
64
+ * Quark settles across MutationObserver + double setTimeout(0) rule
65
+ * runs + paint commit. Four macrotasks cover most observer-driven
66
+ * updates. A paint queued at the last tick (first-run `iterate()` whose
67
+ * rows then bind their content) needs one more, so keep waiting while
68
+ * the engine reports work (`isQuarkBusy`: queued elements, a run in
69
+ * flight, paints waiting to commit, async content or `@use` modules
70
+ * pending).
71
+ */
72
+ export const flush = async () => {
73
+ await wait(0);
74
+ await wait(0);
75
+ await wait(0);
76
+ await wait(0);
77
+ for (let i = 0; i < 16 && isQuarkBusy(); i++) await wait(0);
78
+ };
79
+
80
+ export type ComplexityBudget = {
81
+ quarkRuns: number;
82
+ ruleRuns: number;
83
+ variableRuns: number;
84
+ attributeRuns: number;
85
+ listenerRuns: number;
86
+ setVar: number;
87
+ getVar: number;
88
+ schedulePaint: number;
89
+ querySelectorAll: number;
90
+ /**
91
+ * Weighted cost of rule fan-out queries
92
+ * (`ancestor.querySelectorAll(sel)` in Rule._run): each call adds the
93
+ * number of elements in the scanned subtree. Lower means the
94
+ * nearest-common-ancestor logic rooted queries deeper.
95
+ */
96
+ queryScopeCost: number;
97
+ matches: number;
98
+ closest: number;
99
+ parentElement: number;
100
+ setAttribute: number;
101
+ removeAttribute: number;
102
+ textContent: number;
103
+ importNode: number;
104
+ };
105
+
106
+ /**
107
+ * Count Quark work units (not wall-clock). Snapshots are the regression
108
+ * baseline.
109
+ *
110
+ * Start measuring before the work under test; call `take()` after it
111
+ * settles. For observer-driven cases, apply triggering DOM writes
112
+ * *before* measuring so harness mutations are not counted.
113
+ *
114
+ * Do not spy `queueRunRules` after `register()`: MutationObserver
115
+ * closes over the original function at observe time.
116
+ */
117
+ /** Elements in `el`'s subtree, the scan breadth of a query rooted there. */
118
+ const countDescendants = (el: Element): number => {
119
+ let count = 0;
120
+ for (const child of el.children) count += 1 + countDescendants(child);
121
+ return count;
122
+ };
123
+
124
+ export const measureComplexity = (quark: Quark) => {
125
+ const ruleRunsBefore = quark.rules.reduce((n, r) => n + r.numberOfRuns, 0);
126
+ /*
127
+ * Fan-out queries are identifiable by selector: scoped rules query
128
+ * with their `[q-scope="<id>"]`-prefixed selector (id exists only
129
+ * after register, so collect lazily in take()); unscoped with the
130
+ * bare one.
131
+ */
132
+ const ruleSelectors = () =>
133
+ new Set(quark.rules.flatMap((r) => [r.matchSelector, r.scopedSelector()]));
134
+
135
+ const runSpy = vi.spyOn(quark, "run");
136
+ // Property executions by kind, the primary "work done" regression signal.
137
+ const variableRunSpy = vi.spyOn(Variable.prototype, "_run");
138
+ const attributeRunSpy = vi.spyOn(Attribute.prototype, "_run");
139
+ const listenerRunSpy = vi.spyOn(Listener.prototype, "_run");
140
+ // Element-state traffic (_q_): variable binding writes and reads.
141
+ const setVarSpy = vi.spyOn(QuarkInternal.prototype, "setVar");
142
+ const getVarSpy = vi.spyOn(QuarkInternal.prototype, "getVar");
143
+ const paintSpy = vi.spyOn(paint, "schedulePaint");
144
+ const qsaSpy = vi.spyOn(Element.prototype, "querySelectorAll");
145
+ const matchesSpy = vi.spyOn(Element.prototype, "matches");
146
+ const closestSpy = vi.spyOn(Element.prototype, "closest");
147
+ // upward traversal cost, binding resolution walks ancestors via
148
+ // `el.parentElement` (findBindingOwner in src/bindings.ts)
149
+ const parentElementSpy = vi.spyOn(Node.prototype, "parentElement", "get");
150
+ const setAttrSpy = vi.spyOn(Element.prototype, "setAttribute");
151
+ const removeAttrSpy = vi.spyOn(Element.prototype, "removeAttribute");
152
+ const textContentSpy = vi.spyOn(Element.prototype, "textContent", "set");
153
+ const importNodeSpy = vi.spyOn(document, "importNode");
154
+
155
+ return {
156
+ take: (): ComplexityBudget => ({
157
+ quarkRuns: runSpy.mock.calls.length,
158
+ ruleRuns:
159
+ quark.rules.reduce((n, r) => n + r.numberOfRuns, 0) - ruleRunsBefore,
160
+ variableRuns: variableRunSpy.mock.calls.length,
161
+ attributeRuns: attributeRunSpy.mock.calls.length,
162
+ listenerRuns: listenerRunSpy.mock.calls.length,
163
+ setVar: setVarSpy.mock.calls.length,
164
+ getVar: getVarSpy.mock.calls.length,
165
+ schedulePaint: paintSpy.mock.calls.length,
166
+ querySelectorAll: qsaSpy.mock.calls.length,
167
+ // subtree sizes are sampled at take() time (post-settle), so rendered
168
+ // rows count toward the scope they were rendered into
169
+ queryScopeCost: (() => {
170
+ const selectors = ruleSelectors();
171
+ return qsaSpy.mock.calls.reduce(
172
+ (total, [selector], i) =>
173
+ selectors.has(selector as string)
174
+ ? total + countDescendants(qsaSpy.mock.contexts[i] as Element)
175
+ : total,
176
+ 0
177
+ );
178
+ })(),
179
+ matches: matchesSpy.mock.calls.length,
180
+ closest: closestSpy.mock.calls.length,
181
+ parentElement: parentElementSpy.mock.calls.length,
182
+ setAttribute: setAttrSpy.mock.calls.length,
183
+ removeAttribute: removeAttrSpy.mock.calls.length,
184
+ textContent: textContentSpy.mock.calls.length,
185
+ importNode: importNodeSpy.mock.calls.length,
186
+ }),
187
+ stop: () => {
188
+ runSpy.mockRestore();
189
+ variableRunSpy.mockRestore();
190
+ attributeRunSpy.mockRestore();
191
+ listenerRunSpy.mockRestore();
192
+ setVarSpy.mockRestore();
193
+ getVarSpy.mockRestore();
194
+ paintSpy.mockRestore();
195
+ qsaSpy.mockRestore();
196
+ matchesSpy.mockRestore();
197
+ closestSpy.mockRestore();
198
+ parentElementSpy.mockRestore();
199
+ setAttrSpy.mockRestore();
200
+ removeAttrSpy.mockRestore();
201
+ textContentSpy.mockRestore();
202
+ importNodeSpy.mockRestore();
203
+ },
204
+ };
205
+ };
206
+
207
+ export const expectComplexity = (budget: ComplexityBudget) => {
208
+ expect(budget).toMatchSnapshot("complexity");
209
+ };
210
+
211
+ /**
212
+ * happy-dom caches `matches()` / `querySelectorAll()` per node and only
213
+ * drops the cache when that node, or a node the match walked through,
214
+ * changes. A `:has()` or sibling-combinator result goes stale after a
215
+ * descendant / sibling changes on its own. Browsers do not cache. Call
216
+ * from `beforeAll` in suites that rely on those selectors; returns the
217
+ * restore function for `afterAll`.
218
+ */
219
+ export const bypassSelectorCache = () => {
220
+ const cacheOf = (node: Node) => {
221
+ const sym = Object.getOwnPropertySymbols(node).find(
222
+ (s) => s.description === "cache"
223
+ );
224
+ return sym
225
+ ? (
226
+ node as unknown as Record<
227
+ symbol,
228
+ Record<string, Map<string, unknown>>
229
+ >
230
+ )[sym]
231
+ : undefined;
232
+ };
233
+ const patch = <T extends Node>(
234
+ proto: T,
235
+ name: "matches" | "querySelectorAll"
236
+ ) => {
237
+ const original = (proto as unknown as Record<string, Function>)[name];
238
+ (proto as unknown as Record<string, Function>)[name] = function (
239
+ this: Node,
240
+ selector: string
241
+ ) {
242
+ cacheOf(this)?.[name]?.delete(selector);
243
+ return original.call(this, selector);
244
+ };
245
+ return () => {
246
+ (proto as unknown as Record<string, Function>)[name] = original;
247
+ };
248
+ };
249
+ const restores = [
250
+ patch(Element.prototype, "matches"),
251
+ patch(Element.prototype, "querySelectorAll"),
252
+ patch(Document.prototype, "querySelectorAll"),
253
+ ];
254
+ return () => restores.forEach((restore) => restore());
255
+ };
256
+
257
+ /** One `document.startViewTransition()` call seen by the stub. */
258
+ export type StubViewTransition = {
259
+ types: Set<string>;
260
+ ready: Promise<void>;
261
+ updateCallbackDone: Promise<void>;
262
+ finished: Promise<void>;
263
+ /** `update` was invoked / its promise settled. */
264
+ isUpdating: boolean;
265
+ isUpdated: boolean;
266
+ isSkipped: boolean;
267
+ skipTransition: () => void;
268
+ /**
269
+ * Invoke `update` (once) and resolve when it settled. Automatic a task
270
+ * after the call unless the stub was installed with `autoUpdate: false`.
271
+ */
272
+ runUpdate: () => Promise<void>;
273
+ };
274
+
275
+ /**
276
+ * Browser-like `document.startViewTransition` for happy-dom (which has
277
+ * none). Like Chromium: `update` runs a task later (after the old-state
278
+ * capture), `updateCallbackDone` / `ready` settle with it, `finished`
279
+ * after `animationMs`; `document.activeViewTransition` is set meanwhile,
280
+ * and starting a transition while one is active skips the active one.
281
+ * Pass `withTypes: false` for an engine that rejects the options object
282
+ * (no transition types), and
283
+ * `autoUpdate: false` to call each transition's `runUpdate()` yourself
284
+ * (to assert the pending phase). `restore()` removes it again.
285
+ */
286
+ export const installViewTransitionStub = ({
287
+ animationMs = 0,
288
+ withTypes = true,
289
+ autoUpdate = true,
290
+ }: { animationMs?: number; withTypes?: boolean; autoUpdate?: boolean } = {}) => {
291
+ const doc = document as Document & Record<string, any>;
292
+ const calls: StubViewTransition[] = [];
293
+ doc.startViewTransition = (
294
+ arg?: (() => unknown) | { update?: () => unknown; types?: string[] }
295
+ ) => {
296
+ if (typeof arg !== "function" && !withTypes) {
297
+ throw new TypeError("parameter 1 is not of type 'Function'");
298
+ }
299
+ const update = typeof arg === "function" ? arg : arg?.update;
300
+ const types = new Set(typeof arg === "function" ? [] : (arg?.types ?? []));
301
+ let settleUpdate!: (error?: unknown) => void;
302
+ let finish!: () => void;
303
+ const updateCallbackDone = new Promise<void>((resolve, reject) => {
304
+ settleUpdate = (error) => (error ? reject(error) : resolve());
305
+ });
306
+ const finished = new Promise<void>((resolve) => (finish = resolve));
307
+ let updating: Promise<void> | undefined;
308
+ const transition: StubViewTransition = {
309
+ types,
310
+ ready: updateCallbackDone,
311
+ updateCallbackDone,
312
+ finished,
313
+ isUpdating: false,
314
+ isUpdated: false,
315
+ isSkipped: false,
316
+ skipTransition: () => {
317
+ transition.isSkipped = true;
318
+ finish();
319
+ },
320
+ runUpdate: () =>
321
+ (updating ??= (async () => {
322
+ transition.isUpdating = true;
323
+ try {
324
+ await update?.();
325
+ transition.isUpdated = true;
326
+ settleUpdate();
327
+ } catch (error) {
328
+ settleUpdate(error);
329
+ }
330
+ setTimeout(finish, animationMs);
331
+ })()),
332
+ };
333
+ const active = doc.activeViewTransition as StubViewTransition | undefined;
334
+ active?.skipTransition();
335
+ doc.activeViewTransition = transition;
336
+ finished.then(() => {
337
+ if (doc.activeViewTransition === transition) {
338
+ doc.activeViewTransition = null;
339
+ }
340
+ });
341
+ calls.push(transition);
342
+ // the stub never lets a rejected `updateCallbackDone` go unhandled
343
+ updateCallbackDone.catch(() => {});
344
+ if (autoUpdate) setTimeout(transition.runUpdate, 0);
345
+ return transition;
346
+ };
347
+ return {
348
+ calls,
349
+ restore: () => {
350
+ delete doc.startViewTransition;
351
+ delete doc.activeViewTransition;
352
+ },
353
+ };
354
+ };