tutuca 0.9.96 → 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.
@@ -9334,7 +9334,7 @@ class WeakMapDomCache {
9334
9334
  const key = keys[i];
9335
9335
  let next = cur.get(key);
9336
9336
  if (!next) {
9337
- if (typeof key !== "object") {
9337
+ if (!isWeakKey(key)) {
9338
9338
  this.badKey += 1;
9339
9339
  return;
9340
9340
  }
@@ -9347,7 +9347,7 @@ class WeakMapDomCache {
9347
9347
  const leaf = cur.get(lastKey);
9348
9348
  if (leaf)
9349
9349
  leaf[cacheKey] = v;
9350
- else if (typeof lastKey === "object")
9350
+ else if (isWeakKey(lastKey))
9351
9351
  cur.set(lastKey, { [cacheKey]: v });
9352
9352
  else
9353
9353
  this.badKey += 1;
@@ -9359,6 +9359,7 @@ class WeakMapDomCache {
9359
9359
  return { hit, miss, badKey };
9360
9360
  }
9361
9361
  }
9362
+ var isWeakKey = (k) => k !== null && (typeof k === "object" || typeof k === "function");
9362
9363
 
9363
9364
  // src/vdom.js
9364
9365
  function childOpts(vnode, ns, opts) {
@@ -9798,37 +9799,47 @@ class Renderer {
9798
9799
  renderEach(stack, iterInfo, node, viewName) {
9799
9800
  const { seq, filter, loopWith } = iterInfo.eval(stack);
9800
9801
  const r = [];
9801
- const { iterData, start, end } = unpackLoopResult(loopWith.call(stack.it, seq), seq);
9802
- getSeqInfo(seq)(seq, (key, value, attrName) => {
9803
- if (filter.call(stack.it, key, value, iterData)) {
9804
- const dom = this.renderIt(stack.enter(value, { key }, true), node, key, viewName);
9805
- this.pushEachEntry(r, node.nodeId, attrName, key, dom);
9806
- }
9807
- }, 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);
9808
9814
  return r;
9809
9815
  }
9810
9816
  renderEachWhen(stack, iterInfo, view, nid) {
9811
9817
  const { seq, filter, loopWith, enricher } = iterInfo.eval(stack);
9812
9818
  const r = [];
9813
9819
  const it = stack.it;
9814
- const { iterData, start, end } = unpackLoopResult(loopWith.call(it, seq), seq);
9815
- getSeqInfo(seq)(seq, (key, value, attrName) => {
9816
- if (filter.call(it, key, value, iterData)) {
9817
- const cachePath = enricher ? [view, it, value] : [view, value];
9818
- const binds = { key, value };
9819
- const cacheKey = `${nid}-${key}`;
9820
- if (enricher)
9821
- enricher.call(it, binds, key, value, iterData);
9822
- const cachedNode = this.cache.get(cachePath, cacheKey);
9823
- if (cachedNode)
9824
- this.pushEachEntry(r, nid, attrName, key, cachedNode);
9825
- else {
9826
- const dom = this.renderView(view, stack.enter(value, binds, false));
9827
- this.pushEachEntry(r, nid, attrName, key, dom);
9828
- this.cache.set(cachePath, cacheKey, dom);
9829
- }
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);
9830
9834
  }
9831
- }, 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);
9832
9843
  return r;
9833
9844
  }
9834
9845
  renderView(view, stack) {
@@ -9860,8 +9871,15 @@ var DATASET_ATTRS, getSeqInfo = (seq) => isIndexed(seq) ? imIndexedIter : isKeye
9860
9871
  return [s, e < s ? s : e];
9861
9872
  }, filterAlwaysTrue = (_v, _k, _seq) => true, nullLoopWith = (seq) => ({ iterData: { seq } }), unpackLoopResult = (result, seq) => {
9862
9873
  const r = result ?? {};
9863
- return { iterData: r.iterData ?? { seq }, start: r.start, end: r.end };
9864
- }, 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) => {
9865
9883
  const [s, e] = normalizeRange(start, end, seq.size);
9866
9884
  for (let i = s;i < e; i++)
9867
9885
  visit(i, seq.get(i), "si");
@@ -10048,11 +10066,11 @@ class IterInfo {
10048
10066
  return { seq, filter, loopWith, enricher };
10049
10067
  }
10050
10068
  enrichBinds(stack, key) {
10051
- const { seq, loopWith, enricher } = this.eval(stack);
10069
+ const { seq, filter, loopWith, enricher } = this.eval(stack);
10052
10070
  const value = seq?.get ? seq.get(key, null) : null;
10053
10071
  const binds = { key, value };
10054
10072
  if (enricher) {
10055
- const { iterData } = unpackLoopResult(loopWith.call(stack.it, seq), seq);
10073
+ const { iterData } = unpackLoopResult(loopWith.call(stack.it, seq, makeLoopCtx(stack, filter)), seq);
10056
10074
  enricher.call(stack.it, binds, key, value, iterData);
10057
10075
  }
10058
10076
  return binds;
@@ -14759,6 +14777,13 @@ function phaseOps(phase) {
14759
14777
  function resolveArgs(args, self) {
14760
14778
  return typeof args === "function" ? args(self) ?? [] : args ?? [];
14761
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
+ }
14762
14787
  function dispatchPhase(dispatcher, targetPath, phase, self) {
14763
14788
  if (!phase)
14764
14789
  return;
@@ -14937,6 +14962,8 @@ async function driveStack(stack, value, phase, opts = {}) {
14937
14962
  const t = info?.transaction;
14938
14963
  opts.onMessage({ kind: t?.handlerProp ?? "input", name: t?.name, args: t?.args, path: t?.path }, old, val);
14939
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.");
14940
14967
  dispatchPhase(rootDispatcher(transactor), new Path([]), phase, value);
14941
14968
  await transactor.settle();
14942
14969
  return transactor.state.val;
@@ -15112,13 +15139,17 @@ var init__registry = __esm(() => {
15112
15139
  exitOn: (result) => result.hasErrors ? 3 : 0
15113
15140
  },
15114
15141
  test: {
15115
- 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.",
15116
15143
  defaultFormat: "cli",
15117
15144
  needsEnv: true,
15118
15145
  parseOptions: {
15119
15146
  grep: { type: "string" },
15120
15147
  bail: { type: "boolean" }
15121
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) }),
15122
15153
  run: (normalized, { values, positionals }) => runTests({
15123
15154
  getTests: normalized.mod.getTests,
15124
15155
  components: normalized.components,
@@ -16010,6 +16041,23 @@ var init_env2 = __esm(() => {
16010
16041
  init_lint_check();
16011
16042
  });
16012
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
+
16013
16061
  // tools/cli/commands/storybook.js
16014
16062
  var exports_storybook = {};
16015
16063
  __export(exports_storybook, {
@@ -16020,45 +16068,34 @@ import {
16020
16068
  createReadStream,
16021
16069
  existsSync as existsSync4,
16022
16070
  mkdirSync as mkdirSync3,
16023
- readdirSync as readdirSync2,
16024
16071
  readFileSync as readFileSync3,
16025
16072
  statSync,
16026
16073
  writeFileSync
16027
16074
  } from "node:fs";
16028
16075
  import { createServer } from "node:http";
16029
- 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";
16030
16077
  import { fileURLToPath as fileURLToPath4 } from "node:url";
16031
16078
  import { parseArgs as parseArgs3 } from "node:util";
16032
- function findDevModules(root, dir, acc) {
16033
- for (const e of readdirSync2(dir, { withFileTypes: true })) {
16034
- if (e.name.startsWith(".") || e.name === "node_modules")
16035
- continue;
16036
- const full = resolve4(dir, e.name);
16037
- if (e.isDirectory()) {
16038
- findDevModules(root, full, acc);
16039
- } else if (e.isFile() && e.name.endsWith(".dev.js")) {
16040
- acc.push(`/${relative2(root, full).split(sep).join("/")}`);
16041
- }
16042
- }
16043
- return acc;
16079
+ function findDevModules(root) {
16080
+ return walkFiles(root, { match: (name) => name.endsWith(".dev.js") }).map((full) => `/${relative2(root, full).split(sep).join("/")}`);
16044
16081
  }
16045
16082
  function findSelf() {
16046
16083
  const here = dirname4(fileURLToPath4(import.meta.url));
16047
16084
  const pkgCandidates = [
16048
- resolve4(here, "..", "..", "..", "package.json"),
16049
- resolve4(here, "..", "package.json")
16085
+ resolve5(here, "..", "..", "..", "package.json"),
16086
+ resolve5(here, "..", "package.json")
16050
16087
  ];
16051
16088
  const pkgPath = pkgCandidates.find(existsSync4);
16052
16089
  const version = pkgPath ? JSON.parse(readFileSync3(pkgPath, "utf8")).version : "latest";
16053
- const distCandidates = [resolve4(here, "..", "..", "..", "dist"), resolve4(here, ".")];
16054
- 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;
16055
16092
  return { version, distRoot };
16056
16093
  }
16057
16094
  function resolveTutucaBase(projectDir, self, forCdn) {
16058
16095
  if (forCdn)
16059
16096
  return { base: `https://cdn.jsdelivr.net/npm/tutuca@${self.version}/dist`, serveDist: null };
16060
- const nm = resolve4(projectDir, "node_modules", "tutuca", "dist");
16061
- if (existsSync4(resolve4(nm, "tutuca-dev.js"))) {
16097
+ const nm = resolve5(projectDir, "node_modules", "tutuca", "dist");
16098
+ if (existsSync4(resolve5(nm, "tutuca-dev.js"))) {
16062
16099
  return { base: "/node_modules/tutuca/dist", serveDist: null };
16063
16100
  }
16064
16101
  if (self.distRoot)
@@ -16133,7 +16170,7 @@ async function runDevTests(projectDir, devModuleUrls) {
16133
16170
  const failures = [];
16134
16171
  const importErrors = [];
16135
16172
  for (const url of devModuleUrls) {
16136
- const abs = resolve4(projectDir, url.slice(1));
16173
+ const abs = resolve5(projectDir, url.slice(1));
16137
16174
  let mod;
16138
16175
  try {
16139
16176
  mod = await import(abs);
@@ -16165,7 +16202,7 @@ async function discoverModules(projectDir, devModuleUrls) {
16165
16202
  await createNodeEnv();
16166
16203
  const modules = [];
16167
16204
  for (const url of devModuleUrls) {
16168
- const abs = resolve4(projectDir, url.slice(1));
16205
+ const abs = resolve5(projectDir, url.slice(1));
16169
16206
  try {
16170
16207
  const mod = await import(abs);
16171
16208
  const { normalized, present } = normalizeModule(mod, { path: abs });
@@ -16257,7 +16294,7 @@ async function run4(argv, opts = {}) {
16257
16294
  `);
16258
16295
  return;
16259
16296
  }
16260
- const projectDir = resolve4(parsed.positionals[0] ?? process.cwd());
16297
+ const projectDir = resolve5(parsed.positionals[0] ?? process.cwd());
16261
16298
  if (!existsSync4(projectDir) || !statSync(projectDir).isDirectory()) {
16262
16299
  emitError(opts, {
16263
16300
  code: CODES.USAGE_MISSING_ARGUMENT,
@@ -16265,7 +16302,7 @@ async function run4(argv, opts = {}) {
16265
16302
  hint: "Pass a project directory to scan, or omit it to use the current directory."
16266
16303
  });
16267
16304
  }
16268
- const devModuleUrls = findDevModules(projectDir, projectDir, []);
16305
+ const devModuleUrls = findDevModules(projectDir);
16269
16306
  if (devModuleUrls.length === 0) {
16270
16307
  emitError(opts, {
16271
16308
  code: CODES.USAGE_MISSING_ARGUMENT,
@@ -16277,13 +16314,13 @@ async function run4(argv, opts = {}) {
16277
16314
  const check = !parsed.values["no-check"];
16278
16315
  const self = findSelf();
16279
16316
  if (parsed.values.out) {
16280
- const outDir = resolve4(parsed.values.out);
16317
+ const outDir = resolve5(parsed.values.out);
16281
16318
  mkdirSync3(outDir, { recursive: true });
16282
16319
  const { base: base2 } = resolveTutucaBase(projectDir, self, true);
16283
16320
  const imports2 = buildImports(base2, { margaui });
16284
16321
  const bootstrapName = "tutuca-storybook.bootstrap.js";
16285
- writeFileSync(resolve4(outDir, "index.html"), renderIndexHtml(imports2, { margaui, bootstrapUrl: `./${bootstrapName}` }));
16286
- 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 }));
16287
16324
  process.stdout.write(`wrote static storybook → ${relative2(process.cwd(), outDir) || "."}/
16288
16325
  index.html + ${bootstrapName} (${devModuleUrls.length} dev modules, CDN import map)
16289
16326
  Host it from the project root so /*.dev.js paths resolve.
@@ -16409,6 +16446,7 @@ var init_storybook = __esm(() => {
16409
16446
  init_test();
16410
16447
  init_env2();
16411
16448
  init_errors();
16449
+ init_walk();
16412
16450
  MIME = {
16413
16451
  ".js": "text/javascript",
16414
16452
  ".mjs": "text/javascript",
@@ -16942,17 +16980,60 @@ ${group}
16942
16980
  // tools/tutuca.js
16943
16981
  init_install_skill();
16944
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
16945
17012
  init_errors();
16946
17013
 
16947
17014
  // tools/cli/with-module.js
16948
17015
  init_env2();
17016
+ init_errors();
17017
+ import { statSync as statSync2 } from "node:fs";
16949
17018
  import { parseArgs as parseArgs4 } from "node:util";
16950
17019
 
16951
17020
  // tools/cli/load.js
16952
- import { resolve as resolve5 } from "node:path";
17021
+ import { existsSync as existsSync5 } from "node:fs";
17022
+ import { resolve as resolve6 } from "node:path";
16953
17023
  async function loadAndNormalize(modulePath) {
16954
- const abs = resolve5(modulePath);
16955
- 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
+ }
16956
17037
  const { normalized } = normalizeModule(mod, { path: abs });
16957
17038
  return normalized;
16958
17039
  }
@@ -17455,15 +17536,57 @@ async function emit(result, { format: format5, pretty, output }) {
17455
17536
  }
17456
17537
 
17457
17538
  // tools/cli/with-module.js
17539
+ init_walk();
17458
17540
  async function runCommand(cmd, argv, globalOpts) {
17459
17541
  const parsed = parseArgs4({
17460
17542
  args: argv,
17461
17543
  options: cmd.parseOptions ?? {},
17462
17544
  allowPositionals: true
17463
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
+ }
17464
17554
  const normalized = await loadAndNormalize(globalOpts.module);
17465
17555
  const env = cmd.needsEnv ? await createNodeEnv() : null;
17466
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) {
17467
17590
  await emit(result, {
17468
17591
  format: globalOpts.format ?? cmd.defaultFormat,
17469
17592
  pretty: globalOpts.pretty,
@@ -17572,6 +17695,8 @@ async function main() {
17572
17695
  opts.module = rest[1];
17573
17696
  commandArgs = rest.slice(2);
17574
17697
  }
17698
+ if (command === "test")
17699
+ installDevBuildResolveHook();
17575
17700
  try {
17576
17701
  await runCommand(cmd, commandArgs, opts);
17577
17702
  } catch (e) {
@@ -17609,6 +17734,13 @@ async function main() {
17609
17734
  hint: shape.hint ?? `Run \`tutuca help ${command}\` for valid flags.`
17610
17735
  });
17611
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
+ }
17612
17744
  throw e;
17613
17745
  }
17614
17746
  }
@@ -1322,6 +1322,8 @@ class RequestHandler {
1322
1322
  import { isIndexed, isKeyed } from "immutable";
1323
1323
 
1324
1324
  // src/cache.js
1325
+ var isWeakKey = (k) => k !== null && (typeof k === "object" || typeof k === "function");
1326
+
1325
1327
  class NullDomCache {
1326
1328
  get(_keys, _cacheKey) {}
1327
1329
  set(_keys, _cacheKey, _v) {}
@@ -1365,7 +1367,7 @@ class WeakMapDomCache {
1365
1367
  const key = keys[i];
1366
1368
  let next = cur.get(key);
1367
1369
  if (!next) {
1368
- if (typeof key !== "object") {
1370
+ if (!isWeakKey(key)) {
1369
1371
  this.badKey += 1;
1370
1372
  return;
1371
1373
  }
@@ -1378,7 +1380,7 @@ class WeakMapDomCache {
1378
1380
  const leaf = cur.get(lastKey);
1379
1381
  if (leaf)
1380
1382
  leaf[cacheKey] = v;
1381
- else if (typeof lastKey === "object")
1383
+ else if (isWeakKey(lastKey))
1382
1384
  cur.set(lastKey, { [cacheKey]: v });
1383
1385
  else
1384
1386
  this.badKey += 1;
@@ -1840,37 +1842,47 @@ class Renderer {
1840
1842
  renderEach(stack, iterInfo, node, viewName) {
1841
1843
  const { seq, filter, loopWith } = iterInfo.eval(stack);
1842
1844
  const r = [];
1843
- const { iterData, start, end } = unpackLoopResult(loopWith.call(stack.it, seq), seq);
1844
- getSeqInfo(seq)(seq, (key, value, attrName) => {
1845
- if (filter.call(stack.it, key, value, iterData)) {
1846
- const dom = this.renderIt(stack.enter(value, { key }, true), node, key, viewName);
1847
- this.pushEachEntry(r, node.nodeId, attrName, key, dom);
1848
- }
1849
- }, 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);
1850
1857
  return r;
1851
1858
  }
1852
1859
  renderEachWhen(stack, iterInfo, view, nid) {
1853
1860
  const { seq, filter, loopWith, enricher } = iterInfo.eval(stack);
1854
1861
  const r = [];
1855
1862
  const it = stack.it;
1856
- const { iterData, start, end } = unpackLoopResult(loopWith.call(it, seq), seq);
1857
- getSeqInfo(seq)(seq, (key, value, attrName) => {
1858
- if (filter.call(it, key, value, iterData)) {
1859
- const cachePath = enricher ? [view, it, value] : [view, value];
1860
- const binds = { key, value };
1861
- const cacheKey = `${nid}-${key}`;
1862
- if (enricher)
1863
- enricher.call(it, binds, key, value, iterData);
1864
- const cachedNode = this.cache.get(cachePath, cacheKey);
1865
- if (cachedNode)
1866
- this.pushEachEntry(r, nid, attrName, key, cachedNode);
1867
- else {
1868
- const dom = this.renderView(view, stack.enter(value, binds, false));
1869
- this.pushEachEntry(r, nid, attrName, key, dom);
1870
- this.cache.set(cachePath, cacheKey, dom);
1871
- }
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);
1872
1877
  }
1873
- }, 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);
1874
1886
  return r;
1875
1887
  }
1876
1888
  renderView(view, stack) {
@@ -1906,8 +1918,17 @@ var filterAlwaysTrue = (_v, _k, _seq) => true;
1906
1918
  var nullLoopWith = (seq) => ({ iterData: { seq } });
1907
1919
  var unpackLoopResult = (result, seq) => {
1908
1920
  const r = result ?? {};
1909
- 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 };
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);
1910
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
+ });
1911
1932
  var imIndexedIter = (seq, visit, start, end) => {
1912
1933
  const [s, e] = normalizeRange(start, end, seq.size);
1913
1934
  for (let i = s;i < e; i++)
@@ -2455,11 +2476,11 @@ class IterInfo {
2455
2476
  return { seq, filter, loopWith, enricher };
2456
2477
  }
2457
2478
  enrichBinds(stack, key) {
2458
- const { seq, loopWith, enricher } = this.eval(stack);
2479
+ const { seq, filter, loopWith, enricher } = this.eval(stack);
2459
2480
  const value = seq?.get ? seq.get(key, null) : null;
2460
2481
  const binds = { key, value };
2461
2482
  if (enricher) {
2462
- const { iterData } = unpackLoopResult(loopWith.call(stack.it, seq), seq);
2483
+ const { iterData } = unpackLoopResult(loopWith.call(stack.it, seq, makeLoopCtx(stack, filter)), seq);
2463
2484
  enricher.call(stack.it, binds, key, value, iterData);
2464
2485
  }
2465
2486
  return binds;
@@ -6038,6 +6059,13 @@ function phaseOps(phase) {
6038
6059
  function resolveArgs(args, self) {
6039
6060
  return typeof args === "function" ? args(self) ?? [] : args ?? [];
6040
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
+ }
6041
6069
  function dispatchPhase(dispatcher, targetPath, phase, self) {
6042
6070
  if (!phase)
6043
6071
  return;
@@ -6974,6 +7002,8 @@ async function driveStack(stack, value, phase, opts = {}) {
6974
7002
  const t = info?.transaction;
6975
7003
  opts.onMessage({ kind: t?.handlerProp ?? "input", name: t?.name, args: t?.args, path: t?.path }, old, val);
6976
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.");
6977
7007
  dispatchPhase(rootDispatcher(transactor), new Path([]), phase, value);
6978
7008
  await transactor.settle();
6979
7009
  return transactor.state.val;
@@ -7962,7 +7992,8 @@ class FieldSet extends Field {
7962
7992
  }
7963
7993
  function mkCompField(field, scope, args) {
7964
7994
  const Comp = scope?.lookupComponent(field.type) ?? null;
7965
- 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)`);
7966
7997
  return Comp?.make({ ...field.args, ...args }, { scope }) ?? null;
7967
7998
  }
7968
7999
 
@@ -8136,13 +8167,30 @@ function resolveAlter(Comp, name) {
8136
8167
  }
8137
8168
  return fn;
8138
8169
  }
8170
+ var seqGet = (seq, key) => Array.isArray(seq) ? seq[key] : seq.get ? seq.get(key) : seq[key];
8139
8171
  function collectIterBindings(Comp, compInstance, seq, opts = {}) {
8140
8172
  const whenFn = resolveAlter(Comp, opts.when) ?? filterAlwaysTrue;
8141
8173
  const loopWithFn = resolveAlter(Comp, opts.loopWith) ?? nullLoopWith;
8142
8174
  const enrichFn = resolveAlter(Comp, opts.enrichWith);
8175
+ const scopeEnrichFn = resolveAlter(Comp, opts.scopeEnrich);
8143
8176
  const it = compInstance;
8144
- 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);
8145
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
+ }
8146
8194
  const iter = pickIter(seq);
8147
8195
  iter(seq, (key, value) => {
8148
8196
  if (!whenFn.call(it, key, value, iterData))
@@ -8406,6 +8454,7 @@ export {
8406
8454
  removeIn,
8407
8455
  remove,
8408
8456
  phaseOps,
8457
+ phaseHasBubble,
8409
8458
  mergeWith,
8410
8459
  mergeDeepWith,
8411
8460
  mergeDeep,