tutuca 0.9.97 → 0.9.98

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.
@@ -9799,37 +9799,47 @@ class Renderer {
9799
9799
  renderEach(stack, iterInfo, node, viewName) {
9800
9800
  const { seq, filter, loopWith } = iterInfo.eval(stack);
9801
9801
  const r = [];
9802
- const { iterData, start, end } = unpackLoopResult(loopWith.call(stack.it, seq), seq);
9803
- getSeqInfo(seq)(seq, (key, value, attrName) => {
9804
- if (filter.call(stack.it, key, value, iterData)) {
9805
- const dom = this.renderIt(stack.enter(value, { key }, true), node, key, viewName);
9806
- this.pushEachEntry(r, node.nodeId, attrName, key, dom);
9807
- }
9808
- }, start, end);
9802
+ const { iterData, start, end, keys } = unpackLoopResult(loopWith.call(stack.it, seq, makeLoopCtx(stack, filter)), seq);
9803
+ const renderOne = (key, value, attrName) => {
9804
+ const dom = this.renderIt(stack.enter(value, { key }, true), node, key, viewName);
9805
+ this.pushEachEntry(r, node.nodeId, attrName, key, dom);
9806
+ };
9807
+ if (keys)
9808
+ imKeysIter(seq, renderOne, keys);
9809
+ else
9810
+ getSeqInfo(seq)(seq, (key, value, attrName) => {
9811
+ if (filter.call(stack.it, key, value, iterData))
9812
+ renderOne(key, value, attrName);
9813
+ }, start, end);
9809
9814
  return r;
9810
9815
  }
9811
9816
  renderEachWhen(stack, iterInfo, view, nid) {
9812
9817
  const { seq, filter, loopWith, enricher } = iterInfo.eval(stack);
9813
9818
  const r = [];
9814
9819
  const it = stack.it;
9815
- const { iterData, start, end } = unpackLoopResult(loopWith.call(it, seq), seq);
9816
- getSeqInfo(seq)(seq, (key, value, attrName) => {
9817
- if (filter.call(it, key, value, iterData)) {
9818
- const cachePath = enricher ? [view, it, value] : [view, value];
9819
- const binds = { key, value };
9820
- const cacheKey = `${nid}-${key}`;
9821
- if (enricher)
9822
- enricher.call(it, binds, key, value, iterData);
9823
- const cachedNode = this.cache.get(cachePath, cacheKey);
9824
- if (cachedNode)
9825
- this.pushEachEntry(r, nid, attrName, key, cachedNode);
9826
- else {
9827
- const dom = this.renderView(view, stack.enter(value, binds, false));
9828
- this.pushEachEntry(r, nid, attrName, key, dom);
9829
- this.cache.set(cachePath, cacheKey, dom);
9830
- }
9820
+ const { iterData, start, end, keys } = unpackLoopResult(loopWith.call(it, seq, makeLoopCtx(stack, filter)), seq);
9821
+ const renderOne = (key, value, attrName) => {
9822
+ const cachePath = enricher ? [view, it, value] : [view, value];
9823
+ const binds = { key, value };
9824
+ const cacheKey = `${nid}-${key}`;
9825
+ if (enricher)
9826
+ enricher.call(it, binds, key, value, iterData);
9827
+ const cachedNode = this.cache.get(cachePath, cacheKey);
9828
+ if (cachedNode)
9829
+ this.pushEachEntry(r, nid, attrName, key, cachedNode);
9830
+ else {
9831
+ const dom = this.renderView(view, stack.enter(value, binds, false));
9832
+ this.pushEachEntry(r, nid, attrName, key, dom);
9833
+ this.cache.set(cachePath, cacheKey, dom);
9831
9834
  }
9832
- }, start, end);
9835
+ };
9836
+ if (keys)
9837
+ imKeysIter(seq, renderOne, keys);
9838
+ else
9839
+ getSeqInfo(seq)(seq, (key, value, attrName) => {
9840
+ if (filter.call(it, key, value, iterData))
9841
+ renderOne(key, value, attrName);
9842
+ }, start, end);
9833
9843
  return r;
9834
9844
  }
9835
9845
  renderView(view, stack) {
@@ -9861,8 +9871,15 @@ var DATASET_ATTRS, getSeqInfo = (seq) => isIndexed(seq) ? imIndexedIter : isKeye
9861
9871
  return [s, e < s ? s : e];
9862
9872
  }, filterAlwaysTrue = (_v, _k, _seq) => true, nullLoopWith = (seq) => ({ iterData: { seq } }), unpackLoopResult = (result, seq) => {
9863
9873
  const r = result ?? {};
9864
- return { iterData: r.iterData ?? { seq }, start: r.start, end: r.end };
9865
- }, imIndexedIter = (seq, visit, start, end) => {
9874
+ return { iterData: r.iterData ?? { seq }, start: r.start, end: r.end, keys: r.keys };
9875
+ }, imKeysIter = (seq, visit, keys) => {
9876
+ const attrName = isIndexed(seq) ? "si" : "sk";
9877
+ for (const key of keys)
9878
+ visit(key, seq.get(key), attrName);
9879
+ }, makeLoopCtx = (stack, filter) => ({
9880
+ lookup: (name) => stack.lookupBind(name),
9881
+ filter: (key, value, iterData) => filter.call(stack.it, key, value, iterData)
9882
+ }), imIndexedIter = (seq, visit, start, end) => {
9866
9883
  const [s, e] = normalizeRange(start, end, seq.size);
9867
9884
  for (let i = s;i < e; i++)
9868
9885
  visit(i, seq.get(i), "si");
@@ -10049,11 +10066,11 @@ class IterInfo {
10049
10066
  return { seq, filter, loopWith, enricher };
10050
10067
  }
10051
10068
  enrichBinds(stack, key) {
10052
- const { seq, loopWith, enricher } = this.eval(stack);
10069
+ const { seq, filter, loopWith, enricher } = this.eval(stack);
10053
10070
  const value = seq?.get ? seq.get(key, null) : null;
10054
10071
  const binds = { key, value };
10055
10072
  if (enricher) {
10056
- const { iterData } = unpackLoopResult(loopWith.call(stack.it, seq), seq);
10073
+ const { iterData } = unpackLoopResult(loopWith.call(stack.it, seq, makeLoopCtx(stack, filter)), seq);
10057
10074
  enricher.call(stack.it, binds, key, value, iterData);
10058
10075
  }
10059
10076
  return binds;
@@ -14760,6 +14777,13 @@ function phaseOps(phase) {
14760
14777
  function resolveArgs(args, self) {
14761
14778
  return typeof args === "function" ? args(self) ?? [] : args ?? [];
14762
14779
  }
14780
+ function phaseHasBubble(phase) {
14781
+ if (!phase)
14782
+ return false;
14783
+ if (phase.bubble?.length)
14784
+ return true;
14785
+ return (phase.do ?? []).some((op) => op.type === "bubble");
14786
+ }
14763
14787
  function dispatchPhase(dispatcher, targetPath, phase, self) {
14764
14788
  if (!phase)
14765
14789
  return;
@@ -14938,6 +14962,8 @@ async function driveStack(stack, value, phase, opts = {}) {
14938
14962
  const t = info?.transaction;
14939
14963
  opts.onMessage({ kind: t?.handlerProp ?? "input", name: t?.name, args: t?.args, path: t?.path }, old, val);
14940
14964
  });
14965
+ if (phaseHasBubble(phase))
14966
+ console.warn("drive(): a `bubble` action is a no-op here — drive originates at the root and bubbles travel child→parent, so there is no ancestor to receive it (and the root's own bubble handler is skipped). To exercise a bubble handler, call it directly.");
14941
14967
  dispatchPhase(rootDispatcher(transactor), new Path([]), phase, value);
14942
14968
  await transactor.settle();
14943
14969
  return transactor.state.val;
@@ -15113,13 +15139,17 @@ var init__registry = __esm(() => {
15113
15139
  exitOn: (result) => result.hasErrors ? 3 : 0
15114
15140
  },
15115
15141
  test: {
15116
- describe: "Run tests defined by getTests() (optional <name> to filter by component).",
15142
+ describe: "Run tests defined by getTests() (optional <name> to filter by component). Pass a directory to run every *.test.js / *.dev.js under it.",
15117
15143
  defaultFormat: "cli",
15118
15144
  needsEnv: true,
15119
15145
  parseOptions: {
15120
15146
  grep: { type: "string" },
15121
15147
  bail: { type: "boolean" }
15122
15148
  },
15149
+ acceptsDir: true,
15150
+ dirMatch: (name) => name.endsWith(".test.js") || name.endsWith(".dev.js"),
15151
+ dirFilter: (normalized) => typeof normalized.mod.getTests === "function",
15152
+ mergeResults: (reports) => new TestReport({ modules: reports.flatMap((r) => r.modules) }),
15123
15153
  run: (normalized, { values, positionals }) => runTests({
15124
15154
  getTests: normalized.mod.getTests,
15125
15155
  components: normalized.components,
@@ -16011,6 +16041,23 @@ var init_env2 = __esm(() => {
16011
16041
  init_lint_check();
16012
16042
  });
16013
16043
 
16044
+ // tools/cli/walk.js
16045
+ import { readdirSync as readdirSync2 } from "node:fs";
16046
+ import { resolve as resolve4 } from "node:path";
16047
+ function walkFiles(dir, { match }, acc = []) {
16048
+ for (const e of readdirSync2(dir, { withFileTypes: true })) {
16049
+ if (e.name.startsWith(".") || e.name === "node_modules")
16050
+ continue;
16051
+ const full = resolve4(dir, e.name);
16052
+ if (e.isDirectory())
16053
+ walkFiles(full, { match }, acc);
16054
+ else if (e.isFile() && match(e.name))
16055
+ acc.push(full);
16056
+ }
16057
+ return acc;
16058
+ }
16059
+ var init_walk = () => {};
16060
+
16014
16061
  // tools/cli/commands/storybook.js
16015
16062
  var exports_storybook = {};
16016
16063
  __export(exports_storybook, {
@@ -16021,45 +16068,34 @@ import {
16021
16068
  createReadStream,
16022
16069
  existsSync as existsSync4,
16023
16070
  mkdirSync as mkdirSync3,
16024
- readdirSync as readdirSync2,
16025
16071
  readFileSync as readFileSync3,
16026
16072
  statSync,
16027
16073
  writeFileSync
16028
16074
  } from "node:fs";
16029
16075
  import { createServer } from "node:http";
16030
- import { dirname as dirname4, join, normalize, relative as relative2, resolve as resolve4, sep } from "node:path";
16076
+ import { dirname as dirname4, join, normalize, relative as relative2, resolve as resolve5, sep } from "node:path";
16031
16077
  import { fileURLToPath as fileURLToPath4 } from "node:url";
16032
16078
  import { parseArgs as parseArgs3 } from "node:util";
16033
- function findDevModules(root, dir, acc) {
16034
- for (const e of readdirSync2(dir, { withFileTypes: true })) {
16035
- if (e.name.startsWith(".") || e.name === "node_modules")
16036
- continue;
16037
- const full = resolve4(dir, e.name);
16038
- if (e.isDirectory()) {
16039
- findDevModules(root, full, acc);
16040
- } else if (e.isFile() && e.name.endsWith(".dev.js")) {
16041
- acc.push(`/${relative2(root, full).split(sep).join("/")}`);
16042
- }
16043
- }
16044
- return acc;
16079
+ function findDevModules(root) {
16080
+ return walkFiles(root, { match: (name) => name.endsWith(".dev.js") }).map((full) => `/${relative2(root, full).split(sep).join("/")}`);
16045
16081
  }
16046
16082
  function findSelf() {
16047
16083
  const here = dirname4(fileURLToPath4(import.meta.url));
16048
16084
  const pkgCandidates = [
16049
- resolve4(here, "..", "..", "..", "package.json"),
16050
- resolve4(here, "..", "package.json")
16085
+ resolve5(here, "..", "..", "..", "package.json"),
16086
+ resolve5(here, "..", "package.json")
16051
16087
  ];
16052
16088
  const pkgPath = pkgCandidates.find(existsSync4);
16053
16089
  const version = pkgPath ? JSON.parse(readFileSync3(pkgPath, "utf8")).version : "latest";
16054
- const distCandidates = [resolve4(here, "..", "..", "..", "dist"), resolve4(here, ".")];
16055
- const distRoot = distCandidates.find((d) => existsSync4(resolve4(d, "tutuca-storybook.js"))) ?? null;
16090
+ const distCandidates = [resolve5(here, "..", "..", "..", "dist"), resolve5(here, ".")];
16091
+ const distRoot = distCandidates.find((d) => existsSync4(resolve5(d, "tutuca-storybook.js"))) ?? null;
16056
16092
  return { version, distRoot };
16057
16093
  }
16058
16094
  function resolveTutucaBase(projectDir, self, forCdn) {
16059
16095
  if (forCdn)
16060
16096
  return { base: `https://cdn.jsdelivr.net/npm/tutuca@${self.version}/dist`, serveDist: null };
16061
- const nm = resolve4(projectDir, "node_modules", "tutuca", "dist");
16062
- if (existsSync4(resolve4(nm, "tutuca-dev.js"))) {
16097
+ const nm = resolve5(projectDir, "node_modules", "tutuca", "dist");
16098
+ if (existsSync4(resolve5(nm, "tutuca-dev.js"))) {
16063
16099
  return { base: "/node_modules/tutuca/dist", serveDist: null };
16064
16100
  }
16065
16101
  if (self.distRoot)
@@ -16134,7 +16170,7 @@ async function runDevTests(projectDir, devModuleUrls) {
16134
16170
  const failures = [];
16135
16171
  const importErrors = [];
16136
16172
  for (const url of devModuleUrls) {
16137
- const abs = resolve4(projectDir, url.slice(1));
16173
+ const abs = resolve5(projectDir, url.slice(1));
16138
16174
  let mod;
16139
16175
  try {
16140
16176
  mod = await import(abs);
@@ -16166,7 +16202,7 @@ async function discoverModules(projectDir, devModuleUrls) {
16166
16202
  await createNodeEnv();
16167
16203
  const modules = [];
16168
16204
  for (const url of devModuleUrls) {
16169
- const abs = resolve4(projectDir, url.slice(1));
16205
+ const abs = resolve5(projectDir, url.slice(1));
16170
16206
  try {
16171
16207
  const mod = await import(abs);
16172
16208
  const { normalized, present } = normalizeModule(mod, { path: abs });
@@ -16258,7 +16294,7 @@ async function run4(argv, opts = {}) {
16258
16294
  `);
16259
16295
  return;
16260
16296
  }
16261
- const projectDir = resolve4(parsed.positionals[0] ?? process.cwd());
16297
+ const projectDir = resolve5(parsed.positionals[0] ?? process.cwd());
16262
16298
  if (!existsSync4(projectDir) || !statSync(projectDir).isDirectory()) {
16263
16299
  emitError(opts, {
16264
16300
  code: CODES.USAGE_MISSING_ARGUMENT,
@@ -16266,7 +16302,7 @@ async function run4(argv, opts = {}) {
16266
16302
  hint: "Pass a project directory to scan, or omit it to use the current directory."
16267
16303
  });
16268
16304
  }
16269
- const devModuleUrls = findDevModules(projectDir, projectDir, []);
16305
+ const devModuleUrls = findDevModules(projectDir);
16270
16306
  if (devModuleUrls.length === 0) {
16271
16307
  emitError(opts, {
16272
16308
  code: CODES.USAGE_MISSING_ARGUMENT,
@@ -16278,13 +16314,13 @@ async function run4(argv, opts = {}) {
16278
16314
  const check = !parsed.values["no-check"];
16279
16315
  const self = findSelf();
16280
16316
  if (parsed.values.out) {
16281
- const outDir = resolve4(parsed.values.out);
16317
+ const outDir = resolve5(parsed.values.out);
16282
16318
  mkdirSync3(outDir, { recursive: true });
16283
16319
  const { base: base2 } = resolveTutucaBase(projectDir, self, true);
16284
16320
  const imports2 = buildImports(base2, { margaui });
16285
16321
  const bootstrapName = "tutuca-storybook.bootstrap.js";
16286
- writeFileSync(resolve4(outDir, "index.html"), renderIndexHtml(imports2, { margaui, bootstrapUrl: `./${bootstrapName}` }));
16287
- writeFileSync(resolve4(outDir, bootstrapName), renderBootstrap(devModuleUrls, { margaui, check }));
16322
+ writeFileSync(resolve5(outDir, "index.html"), renderIndexHtml(imports2, { margaui, bootstrapUrl: `./${bootstrapName}` }));
16323
+ writeFileSync(resolve5(outDir, bootstrapName), renderBootstrap(devModuleUrls, { margaui, check }));
16288
16324
  process.stdout.write(`wrote static storybook → ${relative2(process.cwd(), outDir) || "."}/
16289
16325
  index.html + ${bootstrapName} (${devModuleUrls.length} dev modules, CDN import map)
16290
16326
  Host it from the project root so /*.dev.js paths resolve.
@@ -16410,6 +16446,7 @@ var init_storybook = __esm(() => {
16410
16446
  init_test();
16411
16447
  init_env2();
16412
16448
  init_errors();
16449
+ init_walk();
16413
16450
  MIME = {
16414
16451
  ".js": "text/javascript",
16415
16452
  ".mjs": "text/javascript",
@@ -16943,17 +16980,60 @@ ${group}
16943
16980
  // tools/tutuca.js
16944
16981
  init_install_skill();
16945
16982
  init_storybook();
16983
+
16984
+ // tools/cli/dev-build-hook.js
16985
+ import { register } from "node:module";
16986
+ function installDevBuildResolveHook() {
16987
+ let devUrl;
16988
+ try {
16989
+ devUrl = import.meta.resolve("tutuca/dev");
16990
+ } catch {
16991
+ return false;
16992
+ }
16993
+ const hookSource = `
16994
+ let devUrl;
16995
+ export async function initialize(data) { devUrl = data.devUrl; }
16996
+ export async function resolve(specifier, context, nextResolve) {
16997
+ return specifier === "tutuca"
16998
+ ? { url: devUrl, shortCircuit: true }
16999
+ : nextResolve(specifier, context);
17000
+ }`;
17001
+ try {
17002
+ register(`data:text/javascript,${encodeURIComponent(hookSource)}`, import.meta.url, {
17003
+ data: { devUrl }
17004
+ });
17005
+ return true;
17006
+ } catch {
17007
+ return false;
17008
+ }
17009
+ }
17010
+
17011
+ // tools/tutuca.js
16946
17012
  init_errors();
16947
17013
 
16948
17014
  // tools/cli/with-module.js
16949
17015
  init_env2();
17016
+ init_errors();
17017
+ import { statSync as statSync2 } from "node:fs";
16950
17018
  import { parseArgs as parseArgs4 } from "node:util";
16951
17019
 
16952
17020
  // tools/cli/load.js
16953
- import { resolve as resolve5 } from "node:path";
17021
+ import { existsSync as existsSync5 } from "node:fs";
17022
+ import { resolve as resolve6 } from "node:path";
16954
17023
  async function loadAndNormalize(modulePath) {
16955
- const abs = resolve5(modulePath);
16956
- const mod = await import(abs);
17024
+ const abs = resolve6(modulePath);
17025
+ let mod;
17026
+ try {
17027
+ mod = await import(abs);
17028
+ } catch (e) {
17029
+ const entryProblem = e?.code === "ERR_UNSUPPORTED_DIR_IMPORT" || e?.code === "ERR_MODULE_NOT_FOUND" && !existsSync5(abs);
17030
+ if (entryProblem) {
17031
+ const err = new Error(e.code === "ERR_UNSUPPORTED_DIR_IMPORT" ? `expected a module file, got a directory: ${modulePath}` : `module not found: ${modulePath}`);
17032
+ err.code = "ERR_MODULE_LOAD_FAILED";
17033
+ throw err;
17034
+ }
17035
+ throw e;
17036
+ }
16957
17037
  const { normalized } = normalizeModule(mod, { path: abs });
16958
17038
  return normalized;
16959
17039
  }
@@ -17456,15 +17536,57 @@ async function emit(result, { format: format5, pretty, output }) {
17456
17536
  }
17457
17537
 
17458
17538
  // tools/cli/with-module.js
17539
+ init_walk();
17459
17540
  async function runCommand(cmd, argv, globalOpts) {
17460
17541
  const parsed = parseArgs4({
17461
17542
  args: argv,
17462
17543
  options: cmd.parseOptions ?? {},
17463
17544
  allowPositionals: true
17464
17545
  });
17546
+ let stat = null;
17547
+ try {
17548
+ stat = statSync2(globalOpts.module);
17549
+ } catch {}
17550
+ if (stat?.isDirectory()) {
17551
+ await runOnDir(cmd, parsed, globalOpts);
17552
+ return;
17553
+ }
17465
17554
  const normalized = await loadAndNormalize(globalOpts.module);
17466
17555
  const env = cmd.needsEnv ? await createNodeEnv() : null;
17467
17556
  const result = await cmd.run(normalized, parsed, env);
17557
+ await emitResult(cmd, result, globalOpts);
17558
+ }
17559
+ async function runOnDir(cmd, parsed, globalOpts) {
17560
+ if (!cmd.acceptsDir) {
17561
+ emitError(globalOpts, {
17562
+ code: CODES.MODULE_LOAD_FAILED,
17563
+ message: `expected a module file, got a directory: ${globalOpts.module}`,
17564
+ hint: "This command takes a single module file (.js). Pass a file path."
17565
+ });
17566
+ return;
17567
+ }
17568
+ const files = walkFiles(globalOpts.module, { match: cmd.dirMatch });
17569
+ const normalizedAll = [];
17570
+ for (const file of files) {
17571
+ const normalized = await loadAndNormalize(file);
17572
+ if (!cmd.dirFilter || cmd.dirFilter(normalized))
17573
+ normalizedAll.push(normalized);
17574
+ }
17575
+ if (normalizedAll.length === 0) {
17576
+ emitError(globalOpts, {
17577
+ code: CODES.MODULE_LOAD_FAILED,
17578
+ message: `no test modules found under ${globalOpts.module}`,
17579
+ hint: "Test modules are *.test.js / *.dev.js files that export getTests()."
17580
+ });
17581
+ return;
17582
+ }
17583
+ const env = cmd.needsEnv ? await createNodeEnv() : null;
17584
+ const results = [];
17585
+ for (const normalized of normalizedAll)
17586
+ results.push(await cmd.run(normalized, parsed, env));
17587
+ await emitResult(cmd, cmd.mergeResults(results), globalOpts);
17588
+ }
17589
+ async function emitResult(cmd, result, globalOpts) {
17468
17590
  await emit(result, {
17469
17591
  format: globalOpts.format ?? cmd.defaultFormat,
17470
17592
  pretty: globalOpts.pretty,
@@ -17573,6 +17695,8 @@ async function main() {
17573
17695
  opts.module = rest[1];
17574
17696
  commandArgs = rest.slice(2);
17575
17697
  }
17698
+ if (command === "test")
17699
+ installDevBuildResolveHook();
17576
17700
  try {
17577
17701
  await runCommand(cmd, commandArgs, opts);
17578
17702
  } catch (e) {
@@ -17610,6 +17734,13 @@ async function main() {
17610
17734
  hint: shape.hint ?? `Run \`tutuca help ${command}\` for valid flags.`
17611
17735
  });
17612
17736
  }
17737
+ if (e?.code === "ERR_MODULE_LOAD_FAILED") {
17738
+ emitError(opts, {
17739
+ code: CODES.MODULE_LOAD_FAILED,
17740
+ message: e.message,
17741
+ hint: "Pass a module file path. `tutuca test` also accepts a directory to run every test module under it."
17742
+ });
17743
+ }
17613
17744
  throw e;
17614
17745
  }
17615
17746
  }
@@ -1842,37 +1842,47 @@ class Renderer {
1842
1842
  renderEach(stack, iterInfo, node, viewName) {
1843
1843
  const { seq, filter, loopWith } = iterInfo.eval(stack);
1844
1844
  const r = [];
1845
- const { iterData, start, end } = unpackLoopResult(loopWith.call(stack.it, seq), seq);
1846
- getSeqInfo(seq)(seq, (key, value, attrName) => {
1847
- if (filter.call(stack.it, key, value, iterData)) {
1848
- const dom = this.renderIt(stack.enter(value, { key }, true), node, key, viewName);
1849
- this.pushEachEntry(r, node.nodeId, attrName, key, dom);
1850
- }
1851
- }, start, end);
1845
+ const { iterData, start, end, keys } = unpackLoopResult(loopWith.call(stack.it, seq, makeLoopCtx(stack, filter)), seq);
1846
+ const renderOne = (key, value, attrName) => {
1847
+ const dom = this.renderIt(stack.enter(value, { key }, true), node, key, viewName);
1848
+ this.pushEachEntry(r, node.nodeId, attrName, key, dom);
1849
+ };
1850
+ if (keys)
1851
+ imKeysIter(seq, renderOne, keys);
1852
+ else
1853
+ getSeqInfo(seq)(seq, (key, value, attrName) => {
1854
+ if (filter.call(stack.it, key, value, iterData))
1855
+ renderOne(key, value, attrName);
1856
+ }, start, end);
1852
1857
  return r;
1853
1858
  }
1854
1859
  renderEachWhen(stack, iterInfo, view, nid) {
1855
1860
  const { seq, filter, loopWith, enricher } = iterInfo.eval(stack);
1856
1861
  const r = [];
1857
1862
  const it = stack.it;
1858
- const { iterData, start, end } = unpackLoopResult(loopWith.call(it, seq), seq);
1859
- getSeqInfo(seq)(seq, (key, value, attrName) => {
1860
- if (filter.call(it, key, value, iterData)) {
1861
- const cachePath = enricher ? [view, it, value] : [view, value];
1862
- const binds = { key, value };
1863
- const cacheKey = `${nid}-${key}`;
1864
- if (enricher)
1865
- enricher.call(it, binds, key, value, iterData);
1866
- const cachedNode = this.cache.get(cachePath, cacheKey);
1867
- if (cachedNode)
1868
- this.pushEachEntry(r, nid, attrName, key, cachedNode);
1869
- else {
1870
- const dom = this.renderView(view, stack.enter(value, binds, false));
1871
- this.pushEachEntry(r, nid, attrName, key, dom);
1872
- this.cache.set(cachePath, cacheKey, dom);
1873
- }
1863
+ const { iterData, start, end, keys } = unpackLoopResult(loopWith.call(it, seq, makeLoopCtx(stack, filter)), seq);
1864
+ const renderOne = (key, value, attrName) => {
1865
+ const cachePath = enricher ? [view, it, value] : [view, value];
1866
+ const binds = { key, value };
1867
+ const cacheKey = `${nid}-${key}`;
1868
+ if (enricher)
1869
+ enricher.call(it, binds, key, value, iterData);
1870
+ const cachedNode = this.cache.get(cachePath, cacheKey);
1871
+ if (cachedNode)
1872
+ this.pushEachEntry(r, nid, attrName, key, cachedNode);
1873
+ else {
1874
+ const dom = this.renderView(view, stack.enter(value, binds, false));
1875
+ this.pushEachEntry(r, nid, attrName, key, dom);
1876
+ this.cache.set(cachePath, cacheKey, dom);
1874
1877
  }
1875
- }, start, end);
1878
+ };
1879
+ if (keys)
1880
+ imKeysIter(seq, renderOne, keys);
1881
+ else
1882
+ getSeqInfo(seq)(seq, (key, value, attrName) => {
1883
+ if (filter.call(it, key, value, iterData))
1884
+ renderOne(key, value, attrName);
1885
+ }, start, end);
1876
1886
  return r;
1877
1887
  }
1878
1888
  renderView(view, stack) {
@@ -1908,8 +1918,17 @@ var filterAlwaysTrue = (_v, _k, _seq) => true;
1908
1918
  var nullLoopWith = (seq) => ({ iterData: { seq } });
1909
1919
  var unpackLoopResult = (result, seq) => {
1910
1920
  const r = result ?? {};
1911
- return { iterData: r.iterData ?? { seq }, start: r.start, end: r.end };
1921
+ return { iterData: r.iterData ?? { seq }, start: r.start, end: r.end, keys: r.keys };
1912
1922
  };
1923
+ var imKeysIter = (seq, visit, keys) => {
1924
+ const attrName = isIndexed(seq) ? "si" : "sk";
1925
+ for (const key of keys)
1926
+ visit(key, seq.get(key), attrName);
1927
+ };
1928
+ var makeLoopCtx = (stack, filter) => ({
1929
+ lookup: (name) => stack.lookupBind(name),
1930
+ filter: (key, value, iterData) => filter.call(stack.it, key, value, iterData)
1931
+ });
1913
1932
  var imIndexedIter = (seq, visit, start, end) => {
1914
1933
  const [s, e] = normalizeRange(start, end, seq.size);
1915
1934
  for (let i = s;i < e; i++)
@@ -2457,11 +2476,11 @@ class IterInfo {
2457
2476
  return { seq, filter, loopWith, enricher };
2458
2477
  }
2459
2478
  enrichBinds(stack, key) {
2460
- const { seq, loopWith, enricher } = this.eval(stack);
2479
+ const { seq, filter, loopWith, enricher } = this.eval(stack);
2461
2480
  const value = seq?.get ? seq.get(key, null) : null;
2462
2481
  const binds = { key, value };
2463
2482
  if (enricher) {
2464
- const { iterData } = unpackLoopResult(loopWith.call(stack.it, seq), seq);
2483
+ const { iterData } = unpackLoopResult(loopWith.call(stack.it, seq, makeLoopCtx(stack, filter)), seq);
2465
2484
  enricher.call(stack.it, binds, key, value, iterData);
2466
2485
  }
2467
2486
  return binds;
@@ -6040,6 +6059,13 @@ function phaseOps(phase) {
6040
6059
  function resolveArgs(args, self) {
6041
6060
  return typeof args === "function" ? args(self) ?? [] : args ?? [];
6042
6061
  }
6062
+ function phaseHasBubble(phase) {
6063
+ if (!phase)
6064
+ return false;
6065
+ if (phase.bubble?.length)
6066
+ return true;
6067
+ return (phase.do ?? []).some((op) => op.type === "bubble");
6068
+ }
6043
6069
  function dispatchPhase(dispatcher, targetPath, phase, self) {
6044
6070
  if (!phase)
6045
6071
  return;
@@ -6976,6 +7002,8 @@ async function driveStack(stack, value, phase, opts = {}) {
6976
7002
  const t = info?.transaction;
6977
7003
  opts.onMessage({ kind: t?.handlerProp ?? "input", name: t?.name, args: t?.args, path: t?.path }, old, val);
6978
7004
  });
7005
+ if (phaseHasBubble(phase))
7006
+ console.warn("drive(): a `bubble` action is a no-op here — drive originates at the root and bubbles travel child→parent, so there is no ancestor to receive it (and the root's own bubble handler is skipped). To exercise a bubble handler, call it directly.");
6979
7007
  dispatchPhase(rootDispatcher(transactor), new Path([]), phase, value);
6980
7008
  await transactor.settle();
6981
7009
  return transactor.state.val;
@@ -7964,7 +7992,8 @@ class FieldSet extends Field {
7964
7992
  }
7965
7993
  function mkCompField(field, scope, args) {
7966
7994
  const Comp = scope?.lookupComponent(field.type) ?? null;
7967
- console.assert(!scope || Comp !== null, "component not found", { field });
7995
+ if (Comp === null)
7996
+ console.warn(scope ? `component field "${field.name}": component "${field.type}" not found in scope` : `component field "${field.name}": cannot resolve component "${field.type}" — built without a registered scope (use ${field.type}.make({}) as the default, or build via a registered component)`);
7968
7997
  return Comp?.make({ ...field.args, ...args }, { scope }) ?? null;
7969
7998
  }
7970
7999
 
@@ -8138,13 +8167,30 @@ function resolveAlter(Comp, name) {
8138
8167
  }
8139
8168
  return fn;
8140
8169
  }
8170
+ var seqGet = (seq, key) => Array.isArray(seq) ? seq[key] : seq.get ? seq.get(key) : seq[key];
8141
8171
  function collectIterBindings(Comp, compInstance, seq, opts = {}) {
8142
8172
  const whenFn = resolveAlter(Comp, opts.when) ?? filterAlwaysTrue;
8143
8173
  const loopWithFn = resolveAlter(Comp, opts.loopWith) ?? nullLoopWith;
8144
8174
  const enrichFn = resolveAlter(Comp, opts.enrichWith);
8175
+ const scopeEnrichFn = resolveAlter(Comp, opts.scopeEnrich);
8145
8176
  const it = compInstance;
8146
- const { iterData, start, end } = unpackLoopResult(loopWithFn.call(it, seq), seq);
8177
+ const scope = scopeEnrichFn ? scopeEnrichFn.call(it) ?? {} : opts.scope ?? {};
8178
+ const ctx = {
8179
+ lookup: (name) => scope[name],
8180
+ filter: (key, value, iterData2) => whenFn.call(it, key, value, iterData2)
8181
+ };
8182
+ const { iterData, start, end, keys } = unpackLoopResult(loopWithFn.call(it, seq, ctx), seq);
8147
8183
  const out = [];
8184
+ if (keys) {
8185
+ for (const key of keys) {
8186
+ const value = seqGet(seq, key);
8187
+ const binds = { key, value };
8188
+ if (enrichFn)
8189
+ enrichFn.call(it, binds, key, value, iterData);
8190
+ out.push(binds);
8191
+ }
8192
+ return out;
8193
+ }
8148
8194
  const iter = pickIter(seq);
8149
8195
  iter(seq, (key, value) => {
8150
8196
  if (!whenFn.call(it, key, value, iterData))
@@ -8408,6 +8454,7 @@ export {
8408
8454
  removeIn,
8409
8455
  remove,
8410
8456
  phaseOps,
8457
+ phaseHasBubble,
8411
8458
  mergeWith,
8412
8459
  mergeDeepWith,
8413
8460
  mergeDeep,