@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.
- package/.rush/temp/chunked-rush-logs/quark.apply-exports.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/quark.build_package-metas.chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/all.log +1 -0
- package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/state.json +3 -0
- package/.rush/temp/operation/build_package-metas/all.log +1 -0
- package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_package-metas/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +3 -0
- package/config/rig.json +6 -0
- package/index.ts +4 -0
- package/language.ts +7 -0
- package/package.json +45 -0
- package/rush-logs/quark.apply-exports.cache.log +1 -0
- package/rush-logs/quark.apply-exports.log +1 -0
- package/rush-logs/quark.build_package-metas.cache.log +1 -0
- package/rush-logs/quark.build_package-metas.log +1 -0
- package/src/actions.ts +445 -0
- package/src/ast.ts +187 -0
- package/src/bindings.ts +115 -0
- package/src/builtin-modules.ts +401 -0
- package/src/constants.ts +47 -0
- package/src/cycle-check.ts +121 -0
- package/src/devtools-hook.ts +303 -0
- package/src/element-api.ts +109 -0
- package/src/evaluator.ts +317 -0
- package/src/form-controls.ts +69 -0
- package/src/language-tables.ts +137 -0
- package/src/language.ts +1225 -0
- package/src/observer.ts +110 -0
- package/src/paint.ts +305 -0
- package/src/properties.ts +818 -0
- package/src/props.ts +94 -0
- package/src/quark-element.ts +7 -0
- package/src/quark-internal.ts +365 -0
- package/src/quark.ts +817 -0
- package/src/resolvers.ts +867 -0
- package/src/rule.ts +800 -0
- package/src/scope-id.ts +47 -0
- package/src/selector-utils.ts +430 -0
- package/src/settle.ts +117 -0
- package/src/types.ts +155 -0
- package/src/utils.ts +79 -0
- package/src/variables.ts +359 -0
- package/support/demos/css-variables.html +18 -0
- package/support/demos/dispatch.html +20 -0
- package/support/demos/events.html +9 -0
- package/support/demos/iterate.html +13 -0
- package/support/demos/js-api.html +17 -0
- package/support/demos/provider-list.html +16 -0
- package/support/demos/toggle-content.html +14 -0
- package/support/demos/unset.html +12 -0
- package/support/demos/view-transition.html +36 -0
- package/support/docs/ATTRIBUTES.md +37 -0
- package/support/docs/AT_RULES.md +45 -0
- package/support/docs/BUILTINS.md +57 -0
- package/support/docs/CONTENT.md +40 -0
- package/support/docs/CSS_VARIABLES.md +16 -0
- package/support/docs/DECLARATIONS.md +26 -0
- package/support/docs/DELAY.md +37 -0
- package/support/docs/DIAGNOSTICS.md +32 -0
- package/support/docs/DISPATCH.md +81 -0
- package/support/docs/ELEMENT_PROPERTIES.md +34 -0
- package/support/docs/EXPRESSIONS.md +31 -0
- package/support/docs/INTERNAL.md +8 -0
- package/support/docs/JS_API.md +53 -0
- package/support/docs/JS_WRITES.md +36 -0
- package/support/docs/LIMITATIONS.md +26 -0
- package/support/docs/LOOP_GUARD.md +30 -0
- package/support/docs/METHODS.md +43 -0
- package/support/docs/MODULES.md +111 -0
- package/support/docs/NO_REVERSION.md +18 -0
- package/support/docs/ON.md +87 -0
- package/support/docs/REACTIVITY.md +24 -0
- package/support/docs/README.md +143 -0
- package/support/docs/SELECTORS.md +87 -0
- package/support/docs/SHEETS.md +67 -0
- package/support/docs/SYNTAX.md +36 -0
- package/support/docs/USE.md +24 -0
- package/support/docs/VALUES.md +27 -0
- package/support/docs/VARIABLES.md +47 -0
- package/support/docs/VIEW_TRANSITION.md +64 -0
- package/support/docs-sections.json +65 -0
- package/support/package-meta.json +208 -0
- package/support/scripts/build-language-docs.mjs +196 -0
- package/support/tests/__snapshots__/css-variables.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/dispatch.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/events.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/iterate.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/js-api.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/provider-list.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/quark-features.test.ts.snap +1079 -0
- package/support/tests/__snapshots__/quark-with-providers.test.ts.snap +89 -0
- package/support/tests/__snapshots__/quark.test.ts.snap +221 -0
- package/support/tests/__snapshots__/selectors.test.ts.snap +133 -0
- package/support/tests/__snapshots__/toggle-content.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/unset.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/view-transition.view.test.ts.snap +23 -0
- package/support/tests/actions.test.ts +437 -0
- package/support/tests/builtin-modules.test.ts +337 -0
- package/support/tests/builtins.test.ts +189 -0
- package/support/tests/css-variables.view.test.ts +35 -0
- package/support/tests/delay.test.ts +244 -0
- package/support/tests/devtools-hook.test.ts +496 -0
- package/support/tests/diagnostics.test.ts +224 -0
- package/support/tests/dispatch.view.test.ts +43 -0
- package/support/tests/element-api.test.ts +202 -0
- package/support/tests/evaluator.test.ts +237 -0
- package/support/tests/events.view.test.ts +40 -0
- package/support/tests/form-controls.test.ts +452 -0
- package/support/tests/helpers.ts +354 -0
- package/support/tests/internals.test.ts +568 -0
- package/support/tests/iterate.view.test.ts +40 -0
- package/support/tests/js-api.view.test.ts +45 -0
- package/support/tests/language-docs.test.ts +147 -0
- package/support/tests/lifecycle.test.ts +440 -0
- package/support/tests/loop-guard.test.ts +323 -0
- package/support/tests/provider-list.view.test.ts +47 -0
- package/support/tests/quark-features.test.ts +2887 -0
- package/support/tests/quark-with-providers.test.ts +169 -0
- package/support/tests/quark.test.ts +359 -0
- package/support/tests/resolvers.test.ts +346 -0
- package/support/tests/selector-analysis.test.ts +281 -0
- package/support/tests/selectors.test.ts +670 -0
- package/support/tests/toggle-content.view.test.ts +40 -0
- package/support/tests/unset.view.test.ts +42 -0
- package/support/tests/view-helpers.ts +81 -0
- package/support/tests/view-transition.test.ts +1069 -0
- package/support/tests/view-transition.view.test.ts +76 -0
- package/tsconfig.json +5 -0
package/src/quark.ts
ADDED
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
export * from "./types";
|
|
2
|
+
import { collectUseRules, type Statement, type TransitionRule } from "./ast";
|
|
3
|
+
import {
|
|
4
|
+
BINDING_CHANGE_EVENT,
|
|
5
|
+
type BindingChangeDetail,
|
|
6
|
+
dispatchBindingChange,
|
|
7
|
+
} from "./bindings";
|
|
8
|
+
import {
|
|
9
|
+
BUILTIN_MODULE_SCHEME,
|
|
10
|
+
builtinModuleName,
|
|
11
|
+
QUARK_MODULES,
|
|
12
|
+
resolveBuiltinModule,
|
|
13
|
+
} from "./builtin-modules";
|
|
14
|
+
import { warnStaticCycles } from "./cycle-check";
|
|
15
|
+
import { attachDevtools, publicize } from "./devtools-hook";
|
|
16
|
+
import { installElementApi } from "./element-api";
|
|
17
|
+
import { listen, observe, unobserve } from "./observer";
|
|
18
|
+
import type { Attribute, Listener, Variable } from "./properties";
|
|
19
|
+
import {
|
|
20
|
+
PROP_CHANGE_EVENT,
|
|
21
|
+
type PropChangeDetail,
|
|
22
|
+
subscribeProp,
|
|
23
|
+
} from "./props";
|
|
24
|
+
import { getQuarkInternal } from "./quark-internal";
|
|
25
|
+
import { Rule, transitionSpec } from "./rule";
|
|
26
|
+
import { acquireScopeId, releaseScopeId } from "./scope-id";
|
|
27
|
+
import { addBusyCheck, whenSettled } from "./settle";
|
|
28
|
+
import type {
|
|
29
|
+
MutationMap,
|
|
30
|
+
QuarkListenerConfig,
|
|
31
|
+
QuarkOptions,
|
|
32
|
+
RunTrace,
|
|
33
|
+
TransitionSpec,
|
|
34
|
+
Vars,
|
|
35
|
+
} from "./types";
|
|
36
|
+
import {
|
|
37
|
+
getQuarkHost,
|
|
38
|
+
getQuarkMin,
|
|
39
|
+
isInfoLogging,
|
|
40
|
+
QuarkLogger,
|
|
41
|
+
stringToHash,
|
|
42
|
+
} from "./utils";
|
|
43
|
+
import { LoopGuard, resolveModuleReference } from "@excom/kit-utils";
|
|
44
|
+
import type { UseRule } from "@excom/quark-parser";
|
|
45
|
+
import { parse } from "@excom/quark-parser";
|
|
46
|
+
|
|
47
|
+
/*
|
|
48
|
+
* Loop-guard trips show in DevTools as orchestration errors. The
|
|
49
|
+
* Orchestrator is where a runaway attr / effect / event cycle is
|
|
50
|
+
* reasoned about, whichever engine dropped the write.
|
|
51
|
+
*/
|
|
52
|
+
// `element.quark` — the JS side of bindings, on every element
|
|
53
|
+
installElementApi();
|
|
54
|
+
|
|
55
|
+
LoopGuard.onTrip((trip) => {
|
|
56
|
+
const element = trip.target instanceof Element ? trip.target : null;
|
|
57
|
+
publicize(["quark", "error"], {
|
|
58
|
+
...(element ? { weakElement: new WeakRef(element) } : {}),
|
|
59
|
+
tag: element?.localName ?? null,
|
|
60
|
+
selector: null,
|
|
61
|
+
ruleId: null,
|
|
62
|
+
sheetId: null,
|
|
63
|
+
runId: null,
|
|
64
|
+
key: trip.name,
|
|
65
|
+
expression: null,
|
|
66
|
+
errorMessage: trip.message,
|
|
67
|
+
errorName: trip.kind === "batch" ? "LoopGuardBatch" : "LoopGuardDepth",
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/** Deepest loop-guard stamp among the changes a run reacts to. */
|
|
72
|
+
const inheritedDepth = (mutationMap: MutationMap) => {
|
|
73
|
+
let depth = 0;
|
|
74
|
+
mutationMap.forEach((attrs, element) =>
|
|
75
|
+
attrs.forEach((attr) => {
|
|
76
|
+
depth = Math.max(depth, LoopGuard.depthOf(element, attr));
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
return depth;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
interface TQuarkRegistry {
|
|
83
|
+
sheets: WeakRef<Quark>[];
|
|
84
|
+
add: (sheet: Quark) => void;
|
|
85
|
+
remove: (sheet: Quark) => void;
|
|
86
|
+
findRules: (selectorOrId: string | number) => Rule[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const QuarkRegistry: TQuarkRegistry = {
|
|
90
|
+
sheets: [],
|
|
91
|
+
add: (sheet: Quark) => {
|
|
92
|
+
QuarkRegistry.sheets.push(new WeakRef(sheet));
|
|
93
|
+
},
|
|
94
|
+
remove: (sheet: Quark) => {
|
|
95
|
+
QuarkRegistry.sheets = QuarkRegistry.sheets.filter(
|
|
96
|
+
(q: WeakRef<Quark>) => q.deref() !== sheet
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
findRules: (selectorOrId: string | number) => {
|
|
100
|
+
return QuarkRegistry.sheets
|
|
101
|
+
.map((q: WeakRef<Quark>) => q.deref())
|
|
102
|
+
.filter((q: Quark | undefined) => q)
|
|
103
|
+
.flatMap((q: Quark) =>
|
|
104
|
+
q.rules.filter((r: Rule) => {
|
|
105
|
+
const isNumber = typeof selectorOrId === "number";
|
|
106
|
+
return isNumber
|
|
107
|
+
? r.id === selectorOrId
|
|
108
|
+
: String(r.selector)
|
|
109
|
+
.split(" ")
|
|
110
|
+
.slice(-1)[0]
|
|
111
|
+
.includes(String(selectorOrId));
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if ((import.meta as any).env?.DEV) {
|
|
118
|
+
(window as any).QuarkRegistry = QuarkRegistry;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// settle (see settle.ts): a registered sheet is busy while a rule pass is
|
|
122
|
+
// queued or running, or while its `@use` modules load before the first run
|
|
123
|
+
addBusyCheck(() =>
|
|
124
|
+
QuarkRegistry.sheets.some((ref) => {
|
|
125
|
+
const sheet = ref.deref();
|
|
126
|
+
return (
|
|
127
|
+
!!sheet &&
|
|
128
|
+
(sheet.isRunningRules ||
|
|
129
|
+
sheet.isLoadingModules ||
|
|
130
|
+
sheet.ELEMENTS_TO_MATCH.size > 0)
|
|
131
|
+
);
|
|
132
|
+
})
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
export const DEFAULT_OPTIONS = {
|
|
136
|
+
observe: true,
|
|
137
|
+
autoRegister: true,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
let quarkIdTotal = 0;
|
|
141
|
+
export class Quark {
|
|
142
|
+
/**
|
|
143
|
+
* Loads a `@use "url"` JS module. Resolves against the document origin.
|
|
144
|
+
* Overridable (stubbed in tests, or a bundler-aware loader).
|
|
145
|
+
*/
|
|
146
|
+
static moduleLoader: (url: string) => Promise<Vars> = resolveModuleReference;
|
|
147
|
+
/**
|
|
148
|
+
* Install the Nucleus DevTools hook (shared with
|
|
149
|
+
* `Neutron.attachDevtools`). Chrome extensions install the global at
|
|
150
|
+
* `document_start`; this is for tests and late attach.
|
|
151
|
+
*/
|
|
152
|
+
static attachDevtools = attachDevtools;
|
|
153
|
+
/**
|
|
154
|
+
* Resolve once Quark is idle — no queued or running rule pass, no paint
|
|
155
|
+
* waiting to commit, no async `content` or `@use` load pending — or
|
|
156
|
+
* after `timeout` ms (default 1000): `"settled"`, `"until"` or
|
|
157
|
+
* `"timeout"`. For tests and tools; sheets have no after-render hook.
|
|
158
|
+
*/
|
|
159
|
+
static whenSettled = whenSettled;
|
|
160
|
+
id: number;
|
|
161
|
+
rules: Rule[] = [];
|
|
162
|
+
host: WeakRef<HTMLElement>;
|
|
163
|
+
listenerConfig: QuarkListenerConfig;
|
|
164
|
+
src: string;
|
|
165
|
+
hash: string;
|
|
166
|
+
options: QuarkOptions;
|
|
167
|
+
isRegistered: boolean = false;
|
|
168
|
+
/** The host's `q-scope` marker id; set while registered (see scope-id.ts). */
|
|
169
|
+
scopeId: string | null = null;
|
|
170
|
+
observer: MutationObserver | null = null;
|
|
171
|
+
/** `prop()` subscriptions this sheet holds, released on unregister. */
|
|
172
|
+
private propSubscriptions: Map<Element, Map<string, () => void>> = new Map();
|
|
173
|
+
ELEMENTS_TO_MATCH: MutationMap = new Map();
|
|
174
|
+
allAttrs: string[] = [];
|
|
175
|
+
/**
|
|
176
|
+
* `$name` → properties in this sheet that reference it. Bindings
|
|
177
|
+
* cascade across sheets (CSS custom-property semantics), so writers
|
|
178
|
+
* are not known statically, but a sheet always knows its readers.
|
|
179
|
+
* Cheap dismiss of `quark-binding-change` for unreferenced names.
|
|
180
|
+
*/
|
|
181
|
+
bindingIndex: Map<string, Array<Variable | Attribute | Listener>> = new Map();
|
|
182
|
+
/** property name → properties in this sheet that read it via `prop()`. */
|
|
183
|
+
propIndex: Map<string, Array<Variable | Attribute | Listener>> = new Map();
|
|
184
|
+
/** Namespaced module buckets from `@use` rules, once all imports settle. */
|
|
185
|
+
pendingModules: Promise<{ [key: string]: Vars }> | null = null;
|
|
186
|
+
/** Timers of pending `@delay` blocks (cleared on unregister). */
|
|
187
|
+
delayTimers = new Set<ReturnType<typeof setTimeout>>();
|
|
188
|
+
/**
|
|
189
|
+
* Set while this sheet is inside the outermost `run()`: what ran, in
|
|
190
|
+
* order. `$binding` writes meanwhile are queued (`queueBindingChange`)
|
|
191
|
+
* and announced when the run completes, with the trace so consumers
|
|
192
|
+
* that already ran after a write are skipped. A def under `tbody td`
|
|
193
|
+
* no longer nests a run per cell for readers this pass already hits.
|
|
194
|
+
*/
|
|
195
|
+
runTrace: RunTrace | null = null;
|
|
196
|
+
private runDepth = 0;
|
|
197
|
+
private pendingBindingChanges: Map<
|
|
198
|
+
Element,
|
|
199
|
+
Map<string, Omit<BindingChangeDetail, "name">>
|
|
200
|
+
> = new Map();
|
|
201
|
+
/** `writeBinding` notifier used mid-run: remember, dispatch after. */
|
|
202
|
+
queueBindingChange = (owner: Element, detail: BindingChangeDetail) => {
|
|
203
|
+
const { name, ...rest } = detail;
|
|
204
|
+
let names = this.pendingBindingChanges.get(owner);
|
|
205
|
+
if (!names) this.pendingBindingChanges.set(owner, (names = new Map()));
|
|
206
|
+
// the latest write wins: a consumer is stale unless it ran after it
|
|
207
|
+
names.set(name, { ...rest, sinceSeq: this.runTrace?.seq ?? 0 });
|
|
208
|
+
this.runTrace?.written.add(name);
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* Announce the run's deferred writes. A write is `selfCovered` (this
|
|
212
|
+
* sheet ignores its own event) when every rule of this sheet reading
|
|
213
|
+
* the name fanned out over the owner's subtree with its full property
|
|
214
|
+
* set (each reader below the owner was visited) and no reader below
|
|
215
|
+
* the owner was visited before the write. Readers visited after it
|
|
216
|
+
* already evaluated with the new value (`written` gating in
|
|
217
|
+
* `Property.run`).
|
|
218
|
+
*/
|
|
219
|
+
private flushBindingChanges(trace: RunTrace) {
|
|
220
|
+
const pending = this.pendingBindingChanges;
|
|
221
|
+
if (!pending.size) return;
|
|
222
|
+
this.pendingBindingChanges = new Map();
|
|
223
|
+
const stale = new Set<Omit<BindingChangeDetail, "name">>();
|
|
224
|
+
// readers visited before the write they depend on
|
|
225
|
+
trace.visited.forEach((byProperty, element) =>
|
|
226
|
+
byProperty.forEach((seq, property) =>
|
|
227
|
+
property.referencedVarNames.forEach((name) => {
|
|
228
|
+
if (!trace.written.has(name)) return;
|
|
229
|
+
const entry = nearestPending(pending, element, name);
|
|
230
|
+
if (entry && seq < (entry.sinceSeq ?? 0)) stale.add(entry);
|
|
231
|
+
})
|
|
232
|
+
)
|
|
233
|
+
);
|
|
234
|
+
const host = this.host.deref();
|
|
235
|
+
pending.forEach((names, owner) =>
|
|
236
|
+
names.forEach((detail, name) => {
|
|
237
|
+
const readers = this.bindingIndex.get(name);
|
|
238
|
+
const selfCovered =
|
|
239
|
+
!!host &&
|
|
240
|
+
!!readers?.length &&
|
|
241
|
+
!stale.has(detail) &&
|
|
242
|
+
readers.every((reader) => isCovered(trace, reader, owner, host));
|
|
243
|
+
dispatchBindingChange(owner, { name, ...detail, trace, selfCovered });
|
|
244
|
+
})
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
// `isRunningRules` is for debouncing
|
|
248
|
+
isRunningRules = false;
|
|
249
|
+
/** Registered, first run waiting on `@use` modules (settle busy source). */
|
|
250
|
+
isLoadingModules = false;
|
|
251
|
+
/** Some rule sits inside a `@view-transition` block. */
|
|
252
|
+
hasTransitions = false;
|
|
253
|
+
/**
|
|
254
|
+
* From `register()` until that registration's first run settles. Paints
|
|
255
|
+
* resolved meanwhile are the first render and never start a view
|
|
256
|
+
* transition, unless their block says `first-render`.
|
|
257
|
+
*/
|
|
258
|
+
isFirstRender = true;
|
|
259
|
+
private registrations = 0;
|
|
260
|
+
runRules() {
|
|
261
|
+
const mapToRun = new Map(this.ELEMENTS_TO_MATCH);
|
|
262
|
+
this.ELEMENTS_TO_MATCH.clear();
|
|
263
|
+
mapToRun.forEach((_, element) => {
|
|
264
|
+
if (!element.isConnected) {
|
|
265
|
+
mapToRun.delete(element);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
// the causal depth the queued changes inherited (see queueRunRules)
|
|
269
|
+
const depth = this.pendingDepth;
|
|
270
|
+
this.pendingDepth = 0;
|
|
271
|
+
if (mapToRun.size) {
|
|
272
|
+
LoopGuard.run(depth, () => this.run(mapToRun));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Loop-guard depth for the next `runRules()`: deepest chain among the
|
|
277
|
+
* queued changes. Read when a change is queued (MutationObserver is
|
|
278
|
+
* still in the write's task, so the `(element, attribute)` stamp is
|
|
279
|
+
* live) and carried across the debounce.
|
|
280
|
+
*/
|
|
281
|
+
private pendingDepth = 0;
|
|
282
|
+
queueRunRules = ({
|
|
283
|
+
element,
|
|
284
|
+
attribute,
|
|
285
|
+
}: {
|
|
286
|
+
element: HTMLElement;
|
|
287
|
+
attribute: string;
|
|
288
|
+
}) => {
|
|
289
|
+
this.pendingDepth = Math.max(
|
|
290
|
+
this.pendingDepth,
|
|
291
|
+
LoopGuard.current(),
|
|
292
|
+
LoopGuard.depthOf(element, attribute)
|
|
293
|
+
);
|
|
294
|
+
if (this.ELEMENTS_TO_MATCH.has(element)) {
|
|
295
|
+
this.ELEMENTS_TO_MATCH.get(element)?.add(attribute);
|
|
296
|
+
} else {
|
|
297
|
+
this.ELEMENTS_TO_MATCH.set(element, new Set([attribute]));
|
|
298
|
+
}
|
|
299
|
+
if (!this.isRunningRules) {
|
|
300
|
+
this.isRunningRules = true;
|
|
301
|
+
// todo: clean up - double setTimeout(0) ensures that a new quark run is triggered after the current paint is complete.
|
|
302
|
+
setTimeout(() => {
|
|
303
|
+
setTimeout(() => {
|
|
304
|
+
this.runRules();
|
|
305
|
+
this.isRunningRules = false;
|
|
306
|
+
}, 0);
|
|
307
|
+
}, 0);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
constructor({
|
|
311
|
+
src,
|
|
312
|
+
options = {},
|
|
313
|
+
}: {
|
|
314
|
+
src: string;
|
|
315
|
+
modules?: { [key: string]: Vars };
|
|
316
|
+
options?: QuarkOptions;
|
|
317
|
+
}) {
|
|
318
|
+
this.id = quarkIdTotal;
|
|
319
|
+
quarkIdTotal++;
|
|
320
|
+
const min = getQuarkMin(src);
|
|
321
|
+
const hash = stringToHash(min);
|
|
322
|
+
this.options = {
|
|
323
|
+
...DEFAULT_OPTIONS,
|
|
324
|
+
...options,
|
|
325
|
+
};
|
|
326
|
+
/*
|
|
327
|
+
* `isScoped` wraps the sheet in `@scope`, anchoring every rule to
|
|
328
|
+
* the host (CSS inline-<style> scoping; quark-sheet passes it unless
|
|
329
|
+
* `is-global` is set). Otherwise top-level rules run in the root
|
|
330
|
+
* context and authors write `@scope { }` themselves.
|
|
331
|
+
*/
|
|
332
|
+
this.src = this.options.isScoped ? wrapInScope(min) : min;
|
|
333
|
+
this.hash = hash;
|
|
334
|
+
const ast = parse(this.src);
|
|
335
|
+
// Kick off @use imports immediately so the fetch overlaps rule
|
|
336
|
+
// construction, registration, and the initial DOM matching pass.
|
|
337
|
+
const useRules = collectUseRules(ast.body);
|
|
338
|
+
if (useRules.length) {
|
|
339
|
+
this.pendingModules = loadUseModules(useRules);
|
|
340
|
+
}
|
|
341
|
+
const buildRules = (
|
|
342
|
+
statements: Statement[],
|
|
343
|
+
scoped: boolean,
|
|
344
|
+
transition: TransitionSpec | null = null
|
|
345
|
+
) =>
|
|
346
|
+
statements.forEach((statement) => {
|
|
347
|
+
if (statement.type === "rule") {
|
|
348
|
+
new Rule({
|
|
349
|
+
quarkInstance: this,
|
|
350
|
+
statement,
|
|
351
|
+
source: this.src,
|
|
352
|
+
scoped,
|
|
353
|
+
transition,
|
|
354
|
+
});
|
|
355
|
+
} else if (
|
|
356
|
+
statement.type === "atrule" &&
|
|
357
|
+
statement.name === "scope" &&
|
|
358
|
+
statement.block
|
|
359
|
+
) {
|
|
360
|
+
buildRules(statement.block.body, true, transition);
|
|
361
|
+
} else if (
|
|
362
|
+
statement.type === "atrule" &&
|
|
363
|
+
statement.name === "view-transition"
|
|
364
|
+
) {
|
|
365
|
+
// sheet-level block: its rules transition, owned by the host
|
|
366
|
+
buildRules(
|
|
367
|
+
(statement as TransitionRule).block.body,
|
|
368
|
+
scoped,
|
|
369
|
+
transitionSpec(statement as TransitionRule, this.src, null)
|
|
370
|
+
);
|
|
371
|
+
} else if (
|
|
372
|
+
statement.type === "atrule" &&
|
|
373
|
+
["warn", "debug", "error", "delay"].includes(statement.name)
|
|
374
|
+
) {
|
|
375
|
+
// no element to evaluate against outside a rule
|
|
376
|
+
QuarkLogger.error({
|
|
377
|
+
method: "rule",
|
|
378
|
+
message: `Quark: @${statement.name} must be written inside a rule`,
|
|
379
|
+
});
|
|
380
|
+
} else if (
|
|
381
|
+
statement.type === "atrule" &&
|
|
382
|
+
(statement.name === "dispatch" || statement.name === "command")
|
|
383
|
+
) {
|
|
384
|
+
QuarkLogger.error({
|
|
385
|
+
method: "rule",
|
|
386
|
+
message: `Quark: @${statement.name} must be written inside an @on block`,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
buildRules(ast.body, false);
|
|
391
|
+
this.rules.forEach((rule) =>
|
|
392
|
+
[
|
|
393
|
+
...rule.variables,
|
|
394
|
+
...rule.attributes,
|
|
395
|
+
...rule.listeners,
|
|
396
|
+
...rule.diagnostics,
|
|
397
|
+
...rule.delays,
|
|
398
|
+
].forEach((prop) => {
|
|
399
|
+
prop.referencedVarNames.forEach((name) =>
|
|
400
|
+
this.bindingIndex.set(name, [
|
|
401
|
+
...(this.bindingIndex.get(name) ?? []),
|
|
402
|
+
prop,
|
|
403
|
+
])
|
|
404
|
+
);
|
|
405
|
+
prop.referencedPropNames.forEach((name) =>
|
|
406
|
+
this.propIndex.set(name, [...(this.propIndex.get(name) ?? []), prop])
|
|
407
|
+
);
|
|
408
|
+
})
|
|
409
|
+
);
|
|
410
|
+
// rules that write each other's selector attributes: warn at build
|
|
411
|
+
warnStaticCycles(this);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* A binding changed somewhere in the DOM (any sheet, or JS via
|
|
416
|
+
* `element.quark.setProperty()`). If this sheet references the name and the owner is in
|
|
417
|
+
* scope, re-run those properties for the owner's subtree.
|
|
418
|
+
*/
|
|
419
|
+
handleBindingChange(e: CustomEvent<BindingChangeDetail>) {
|
|
420
|
+
this.applyBindingChange(e.target as HTMLElement, e.detail);
|
|
421
|
+
}
|
|
422
|
+
applyBindingChange(owner: HTMLElement, detail?: BindingChangeDetail) {
|
|
423
|
+
const { name, sheetId, runId, sinceSeq, trace, selfCovered } =
|
|
424
|
+
detail ?? ({} as BindingChangeDetail);
|
|
425
|
+
// this sheet's own run already refreshed every reader of that write
|
|
426
|
+
if (selfCovered && sheetId === this.id) return;
|
|
427
|
+
const properties = this.bindingIndex.get(name);
|
|
428
|
+
if (!properties?.length) return;
|
|
429
|
+
const host = this.host.deref();
|
|
430
|
+
if (!host) return;
|
|
431
|
+
// owner must be an ancestor-or-self of this sheet's scope, or within it
|
|
432
|
+
if (!(host.contains(owner) || owner.contains(host))) return;
|
|
433
|
+
this.runElement(owner, [name], {
|
|
434
|
+
runId,
|
|
435
|
+
properties,
|
|
436
|
+
isAsyncRun: true,
|
|
437
|
+
changedBinding: { name, origin: new WeakRef(owner), sinceSeq, trace },
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
run(mutationMap: MutationMap, options: QuarkOptions = {}) {
|
|
442
|
+
const host = this.host.deref();
|
|
443
|
+
if (!host) return QuarkLogger.error("Quark: Host not found");
|
|
444
|
+
const runId = options?.runId || generateID();
|
|
445
|
+
if (isInfoLogging()) {
|
|
446
|
+
QuarkLogger.info({
|
|
447
|
+
method: "run",
|
|
448
|
+
sheetId: this.id,
|
|
449
|
+
runId: runId,
|
|
450
|
+
changes: new Map(
|
|
451
|
+
[...mutationMap].map(([key, set]) => [key, [...set].join(",")])
|
|
452
|
+
),
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
const trace = (this.runTrace ??= {
|
|
456
|
+
seq: 0,
|
|
457
|
+
ran: new Map(),
|
|
458
|
+
visited: new Map(),
|
|
459
|
+
written: new Set(),
|
|
460
|
+
coverage: new Map(),
|
|
461
|
+
});
|
|
462
|
+
this.runDepth++;
|
|
463
|
+
try {
|
|
464
|
+
/*
|
|
465
|
+
* A sync trigger (binding / prop change) carries the chain in the
|
|
466
|
+
* stamps of the changed names. Observer-driven runs already opened
|
|
467
|
+
* their context in runRules().
|
|
468
|
+
*/
|
|
469
|
+
LoopGuard.run(inheritedDepth(mutationMap), () =>
|
|
470
|
+
this.rules?.forEach((rule) => {
|
|
471
|
+
if (
|
|
472
|
+
!options.properties ||
|
|
473
|
+
options.properties.find((p) => p.parent === rule)
|
|
474
|
+
) {
|
|
475
|
+
rule.run(mutationMap, {
|
|
476
|
+
host,
|
|
477
|
+
options: {
|
|
478
|
+
runId,
|
|
479
|
+
...this.options,
|
|
480
|
+
...options,
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
})
|
|
485
|
+
);
|
|
486
|
+
} finally {
|
|
487
|
+
this.runDepth--;
|
|
488
|
+
if (this.runDepth === 0) {
|
|
489
|
+
this.runTrace = null;
|
|
490
|
+
this.flushBindingChanges(trace);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
runElement(scope: Element, attrs: string[], options: QuarkOptions = {}) {
|
|
496
|
+
const mutationMap = new Map();
|
|
497
|
+
mutationMap.set(scope, new Set(attrs));
|
|
498
|
+
this.run(mutationMap, options);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
runAll(options: QuarkOptions = {}) {
|
|
502
|
+
const host = this.host.deref();
|
|
503
|
+
if (!host) return QuarkLogger.error("Quark: Host not found");
|
|
504
|
+
this.runElement(host, ["RUN_ALL"], {
|
|
505
|
+
...options,
|
|
506
|
+
isFirstRun: true,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
register({
|
|
511
|
+
sheetElement,
|
|
512
|
+
modules,
|
|
513
|
+
}: {
|
|
514
|
+
sheetElement: HTMLElement;
|
|
515
|
+
modules?: { [key: string]: Vars };
|
|
516
|
+
}) {
|
|
517
|
+
if (this.isRegistered) {
|
|
518
|
+
throw new Error("Quark: Quark sheet already registered");
|
|
519
|
+
}
|
|
520
|
+
{
|
|
521
|
+
// CAREFUL using this variable. Needs to be garbage collectable.
|
|
522
|
+
const __h = getQuarkHost(sheetElement.parentElement);
|
|
523
|
+
this.host = new WeakRef(__h);
|
|
524
|
+
// scoped selector matching rides on the host marker; only mark when a
|
|
525
|
+
// rule actually queries descendants
|
|
526
|
+
if (this.rules.some((r) => r.isScoped && r.matchSelector)) {
|
|
527
|
+
this.scopeId = acquireScopeId(__h);
|
|
528
|
+
}
|
|
529
|
+
// new children are seen by the MutationObserver (childList), whoever
|
|
530
|
+
// inserted them, no render event contract with elements
|
|
531
|
+
this.listenerConfig = [
|
|
532
|
+
[
|
|
533
|
+
// property changes bubble from the element that owns them
|
|
534
|
+
new WeakRef(__h.getRootNode()),
|
|
535
|
+
PROP_CHANGE_EVENT,
|
|
536
|
+
(e) => this.handlePropChange(e as CustomEvent<PropChangeDetail>),
|
|
537
|
+
],
|
|
538
|
+
[
|
|
539
|
+
// bindings cascade across sheets, so listen at the root, an
|
|
540
|
+
// owner above this sheet's host is still a valid writer
|
|
541
|
+
new WeakRef(__h.getRootNode()),
|
|
542
|
+
BINDING_CHANGE_EVENT,
|
|
543
|
+
(e) =>
|
|
544
|
+
this.handleBindingChange(e as CustomEvent<BindingChangeDetail>),
|
|
545
|
+
],
|
|
546
|
+
];
|
|
547
|
+
const elementInternal = getQuarkInternal(this.host);
|
|
548
|
+
elementInternal.addModules(this.hash, modules);
|
|
549
|
+
QuarkRegistry.add(this);
|
|
550
|
+
this.isRegistered = true;
|
|
551
|
+
this.isFirstRender = true;
|
|
552
|
+
const registration = ++this.registrations;
|
|
553
|
+
publicize(["quark", "sheet", "registered"], {
|
|
554
|
+
weakElement: this.host,
|
|
555
|
+
tag: __h.localName,
|
|
556
|
+
sheetId: this.id,
|
|
557
|
+
hash: this.hash,
|
|
558
|
+
ruleCount: this.rules.length,
|
|
559
|
+
isScoped: !!this.options.isScoped,
|
|
560
|
+
});
|
|
561
|
+
const finish = () => {
|
|
562
|
+
this.isLoadingModules = false;
|
|
563
|
+
// the sheet may have been unregistered while @use modules loaded
|
|
564
|
+
if (!this.isRegistered) return;
|
|
565
|
+
if (this.options.observe) {
|
|
566
|
+
/*
|
|
567
|
+
* Listeners first: binding changes announced during the first
|
|
568
|
+
* run (this run's deferred defs) must reach
|
|
569
|
+
* readers that already ran, whatever the rule order.
|
|
570
|
+
*/
|
|
571
|
+
listen(this.listenerConfig);
|
|
572
|
+
}
|
|
573
|
+
this.runAll();
|
|
574
|
+
if (this.hasTransitions) {
|
|
575
|
+
// the first render ends once this run's cascade settles
|
|
576
|
+
whenSettled().then(() => {
|
|
577
|
+
if (this.registrations === registration) {
|
|
578
|
+
this.isFirstRender = false;
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
} else {
|
|
582
|
+
this.isFirstRender = false;
|
|
583
|
+
}
|
|
584
|
+
if (this.options.observe) {
|
|
585
|
+
this.allAttrs = this.reduceAttrs();
|
|
586
|
+
const o = observe(this.host, this.allAttrs, this.queueRunRules, {
|
|
587
|
+
childRemovals: this.rules.some((rule) => rule.reactsToRemovals),
|
|
588
|
+
classNames: this.allAttrs.includes("class")
|
|
589
|
+
? this.reduceClassNames()
|
|
590
|
+
: null,
|
|
591
|
+
});
|
|
592
|
+
if (!o) return QuarkLogger.error("Quark: Observer not created");
|
|
593
|
+
this.observer = o;
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
if (this.pendingModules) {
|
|
597
|
+
// First run (and observation) is gated on @use imports so every
|
|
598
|
+
// rule sees its modules; the fetches started at parse time.
|
|
599
|
+
this.isLoadingModules = true;
|
|
600
|
+
this.pendingModules.then((useModules) => {
|
|
601
|
+
const current = elementInternal.getModules(this.hash);
|
|
602
|
+
elementInternal.addModules(this.hash, {
|
|
603
|
+
...useModules,
|
|
604
|
+
// `as *` exports merge into the bare (dfault) bucket rather
|
|
605
|
+
// than replacing modules passed to register()
|
|
606
|
+
dfault: { ...current?.dfault, ...useModules.dfault },
|
|
607
|
+
});
|
|
608
|
+
finish();
|
|
609
|
+
});
|
|
610
|
+
} else {
|
|
611
|
+
finish();
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
unregister() {
|
|
617
|
+
const host = this.host?.deref();
|
|
618
|
+
if (this.isRegistered && host) {
|
|
619
|
+
publicize(["quark", "sheet", "unregistered"], {
|
|
620
|
+
weakElement: this.host,
|
|
621
|
+
tag: host.localName,
|
|
622
|
+
sheetId: this.id,
|
|
623
|
+
hash: this.hash,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
QuarkRegistry.remove(this);
|
|
627
|
+
this.isLoadingModules = false;
|
|
628
|
+
// a delayed block must not fire into a sheet that let go
|
|
629
|
+
this.delayTimers.forEach((timer) => clearTimeout(timer));
|
|
630
|
+
this.delayTimers.clear();
|
|
631
|
+
unobserve(this.observer, this.listenerConfig);
|
|
632
|
+
this.observer = null;
|
|
633
|
+
this.propSubscriptions.forEach((byName) =>
|
|
634
|
+
byName.forEach((release) => release())
|
|
635
|
+
);
|
|
636
|
+
this.propSubscriptions = new Map();
|
|
637
|
+
this.isRegistered = false;
|
|
638
|
+
if (this.scopeId) {
|
|
639
|
+
const host = this.host?.deref();
|
|
640
|
+
if (host) releaseScopeId(host);
|
|
641
|
+
this.scopeId = null;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Observe `element[name]` for this sheet (once per element + name).
|
|
647
|
+
* Called from `prop()` on the first read by a property that statically
|
|
648
|
+
* references the name.
|
|
649
|
+
*/
|
|
650
|
+
subscribeProp(element: Element, name: string) {
|
|
651
|
+
if (!this.isRegistered) return;
|
|
652
|
+
let byName = this.propSubscriptions.get(element);
|
|
653
|
+
if (!byName) this.propSubscriptions.set(element, (byName = new Map()));
|
|
654
|
+
if (!byName.has(name)) byName.set(name, subscribeProp(element, name));
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* A property changed on an element (coalesced per microtask, see
|
|
659
|
+
* props.ts). Re-run this sheet's readers of those names on that
|
|
660
|
+
* element.
|
|
661
|
+
*/
|
|
662
|
+
handlePropChange(e: CustomEvent<PropChangeDetail>) {
|
|
663
|
+
const names = e.detail?.names ?? [];
|
|
664
|
+
const properties = names.flatMap((name) => this.propIndex.get(name) ?? []);
|
|
665
|
+
if (!properties.length) return;
|
|
666
|
+
const host = this.host.deref();
|
|
667
|
+
if (!host) return;
|
|
668
|
+
const element = e.target as HTMLElement;
|
|
669
|
+
if (this.options.isScoped && element !== host && !host.contains(element)) {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
// the assignment that fired this event stamped `(element, name)`
|
|
673
|
+
LoopGuard.run(
|
|
674
|
+
Math.max(0, ...names.map((name) => LoopGuard.depthOf(element, name))),
|
|
675
|
+
() => this.runElement(element, ["PROP"], { properties, isAsyncRun: true })
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
private reduceAttrs() {
|
|
680
|
+
const attrs = new Set<string>();
|
|
681
|
+
this.rules.forEach((rule) => {
|
|
682
|
+
rule?.observedAttrs?.forEach((attr) => attrs.add(attr));
|
|
683
|
+
});
|
|
684
|
+
return Array.from(attrs);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Class tokens the rules select on (`.x`), so the observer can drop
|
|
689
|
+
* `class` records that touch none of them. `null` when any class
|
|
690
|
+
* change matters: `[class~="x"]`, an escaped name, `attr("class")`.
|
|
691
|
+
*/
|
|
692
|
+
private reduceClassNames(): Set<string> | null {
|
|
693
|
+
const names = new Set<string>();
|
|
694
|
+
for (const rule of this.rules) {
|
|
695
|
+
if (rule.readsAttr("class")) return null;
|
|
696
|
+
for (const deps of [rule.deps, rule.hostDeps]) {
|
|
697
|
+
if (!deps) continue;
|
|
698
|
+
if (!deps.classNames) return null;
|
|
699
|
+
deps.classNames.forEach((name) => names.add(name));
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return names;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/** Nearest ancestor-or-self of `element` with a pending write of `name`. */
|
|
707
|
+
const nearestPending = (
|
|
708
|
+
pending: Map<Element, Map<string, Omit<BindingChangeDetail, "name">>>,
|
|
709
|
+
element: Element,
|
|
710
|
+
name: string
|
|
711
|
+
) => {
|
|
712
|
+
for (let el: Element | null = element; el; el = el.parentElement) {
|
|
713
|
+
const entry = pending.get(el)?.get(name);
|
|
714
|
+
if (entry) return entry;
|
|
715
|
+
}
|
|
716
|
+
return undefined;
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Did the run reach every element below `owner` that `reader`'s rule
|
|
721
|
+
* matches? True when the rule fanned out (full property set) from an
|
|
722
|
+
* ancestor of `owner`, or from `owner` itself if the reader was
|
|
723
|
+
* visited there too or the rule cannot match `owner` (a fan-out
|
|
724
|
+
* root's own properties are not part of a children fan-out).
|
|
725
|
+
*/
|
|
726
|
+
const isCovered = (
|
|
727
|
+
trace: RunTrace,
|
|
728
|
+
reader: Variable | Attribute | Listener,
|
|
729
|
+
owner: Element,
|
|
730
|
+
host: HTMLElement
|
|
731
|
+
): boolean => {
|
|
732
|
+
const roots = trace.coverage.get(reader.parent);
|
|
733
|
+
if (!roots?.size) return false;
|
|
734
|
+
for (let el = owner.parentElement; el; el = el.parentElement) {
|
|
735
|
+
if (roots.has(el)) return true;
|
|
736
|
+
}
|
|
737
|
+
if (!roots.has(owner)) return false;
|
|
738
|
+
return (
|
|
739
|
+
trace.visited.get(owner)?.has(reader) ||
|
|
740
|
+
!reader.parent.matchesElement(owner, host)
|
|
741
|
+
);
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
function generateID() {
|
|
745
|
+
return Math.random().toString(36).substring(2, 10);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Wrap sheet source in the implicit `@scope{}`, after any leading `@use`
|
|
750
|
+
* statements. Imports are sheet-level, not scope-level, so they must
|
|
751
|
+
* stay outside the block.
|
|
752
|
+
*/
|
|
753
|
+
function wrapInScope(min: string): string {
|
|
754
|
+
const insertAt =
|
|
755
|
+
parse(min).body.find(
|
|
756
|
+
(statement) => !(statement.type === "atrule" && statement.name === "use")
|
|
757
|
+
)?.start ?? min.length;
|
|
758
|
+
return `${min.slice(0, insertAt)}@scope{${min.slice(insertAt)}}`;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* `"/mods/string-utils.js"` -> `"string-utils"` (SCSS-style default ns);
|
|
763
|
+
* `"quark:math"` -> `"math"`.
|
|
764
|
+
*/
|
|
765
|
+
function deriveUseNamespace(url: string): string {
|
|
766
|
+
const path = url.split(/[?#]/)[0].replace(BUILTIN_MODULE_SCHEME, "");
|
|
767
|
+
const segment = path.split("/").filter(Boolean).pop() ?? "";
|
|
768
|
+
return segment.replace(/\.[a-zA-Z]+$/, "");
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Import all `@use` modules in parallel. `as *` exports land in the bare
|
|
773
|
+
* `dfault` bucket; others under their given or derived namespace. Failed
|
|
774
|
+
* imports are logged and skipped so one bad module does not block the
|
|
775
|
+
* sheet.
|
|
776
|
+
*/
|
|
777
|
+
function loadUseModules(useRules: UseRule[]): Promise<{ [key: string]: Vars }> {
|
|
778
|
+
return Promise.all(
|
|
779
|
+
useRules.map(async (use) => {
|
|
780
|
+
try {
|
|
781
|
+
// `quark:` urls are the built-in modules (no fetch, no loader)
|
|
782
|
+
if (builtinModuleName(use.url) !== null) {
|
|
783
|
+
const mod = resolveBuiltinModule(use.url);
|
|
784
|
+
if (!mod) {
|
|
785
|
+
throw new Error(
|
|
786
|
+
`unknown built-in module; available: ${Object.keys(QUARK_MODULES)
|
|
787
|
+
.map((name) => `"${BUILTIN_MODULE_SCHEME}${name}"`)
|
|
788
|
+
.join(", ")}`
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
return { use, mod };
|
|
792
|
+
}
|
|
793
|
+
return { use, mod: await Quark.moduleLoader(use.url) };
|
|
794
|
+
} catch (error) {
|
|
795
|
+
QuarkLogger.error({
|
|
796
|
+
method: "use",
|
|
797
|
+
message: `Quark: Failed to load @use module "${use.url}"`,
|
|
798
|
+
error: [error],
|
|
799
|
+
});
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
})
|
|
803
|
+
).then((loaded) => {
|
|
804
|
+
const modules: { [key: string]: Vars } = {};
|
|
805
|
+
for (const entry of loaded) {
|
|
806
|
+
if (!entry) continue;
|
|
807
|
+
const namespace =
|
|
808
|
+
entry.use.namespace ?? deriveUseNamespace(entry.use.url);
|
|
809
|
+
if (namespace === "*") {
|
|
810
|
+
modules.dfault = { ...modules.dfault, ...entry.mod };
|
|
811
|
+
} else {
|
|
812
|
+
modules[namespace] = { ...entry.mod };
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return modules;
|
|
816
|
+
});
|
|
817
|
+
}
|