@bettercms-ai/convert 0.26.0 → 0.26.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @bettercms-ai/convert
2
2
 
3
+ ## 0.26.1
4
+
5
+ ### Patch Changes
6
+
7
+ - c83b686: The codemod routes an image through `bcmsImage` where a call site already declares its field in `bcmsBindings` and still passes the raw import (the element was stamped, and the page kept rendering the file its code imports), and names an image's asset from an optimizer url (`/_image?href=…`). The components skill gains "Read a published value where the app renders the page".
8
+ - Updated dependencies [1a52ee6]
9
+ - @bettercms-ai/types@1.36.0
10
+
3
11
  ## 0.26.0
4
12
 
5
13
  ### Minor Changes
package/dist/cli.js CHANGED
@@ -4090,6 +4090,13 @@ var IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".avif", ".gif", ".svg
4090
4090
  var ASSET_COMPONENTS = /* @__PURE__ */ new Set(["Image", "Picture"]);
4091
4091
  var ASSET_MODULE = "astro:assets";
4092
4092
  function baseName(url) {
4093
+ const inner = /[?&](?:href|url)=([^&#]+)/.exec(url)?.[1];
4094
+ if (inner) {
4095
+ try {
4096
+ url = decodeURIComponent(inner);
4097
+ } catch {
4098
+ }
4099
+ }
4093
4100
  const file = url.split(/[?#]/)[0].split("/").pop() ?? "";
4094
4101
  const base = file.split(".")[0] ?? "";
4095
4102
  return base === "" ? null : base.toLowerCase();
@@ -4115,6 +4122,29 @@ function passedTo(content, parsed, binding) {
4115
4122
  for (const root of parsed.roots) visit(root);
4116
4123
  return out;
4117
4124
  }
4125
+ function declaredAt(content, parsed, binding, path) {
4126
+ const out = [];
4127
+ const names = [`"${path}"`, `'${path}'`];
4128
+ const visit = (node) => {
4129
+ if (node.type === "expr") {
4130
+ for (const child of node.children) visit(child);
4131
+ return;
4132
+ }
4133
+ if (node.type !== "element" || node.raw) return;
4134
+ const bindings = node.attrs.find((a) => a.name === "bcmsBindings");
4135
+ const declared = bindings?.expressionRange ? content.slice(bindings.expressionRange.start, bindings.expressionRange.end) : "";
4136
+ if (/^[A-Z]/.test(node.tag) && !ASSET_COMPONENTS.has(node.tag) && names.some((name) => declared.includes(name))) {
4137
+ for (const attr of node.attrs) {
4138
+ if (!attr.expressionRange) continue;
4139
+ const inner = content.slice(attr.expressionRange.start, attr.expressionRange.end).replace(/^\{|\}$/g, "").trim();
4140
+ if (inner === binding) out.push({ node, prop: attr.name, attr });
4141
+ }
4142
+ }
4143
+ for (const child of node.children) visit(child);
4144
+ };
4145
+ for (const root of parsed.roots) visit(root);
4146
+ return out;
4147
+ }
4118
4148
  function assetCandidates(parsed, dialect, original) {
4119
4149
  const wanted = baseName(original);
4120
4150
  if (wanted === null || parsed.error) return [];
@@ -4162,6 +4192,18 @@ async function bindImages(ctx, targets) {
4162
4192
  const rendered = binding === null ? [] : renderedBy(ctx.content, ctx.parsed, ctx.dialect, binding);
4163
4193
  const free = rendered.filter((hit2) => !claimed.has(hit2.node.attrInsertAt));
4164
4194
  const args = (b) => [ctx.snapshotName(target.slug), JSON.stringify(target.path), b, ...ctx.slug ? [ctx.slug] : []];
4195
+ if (free.length === 0 && binding !== null) {
4196
+ const declared = declaredAt(ctx.content, ctx.parsed, binding, target.path).filter((c) => !claimed.has(c.attr.range.start));
4197
+ if (declared.length === 1) {
4198
+ const call = declared[0];
4199
+ claimed.add(call.attr.range.start);
4200
+ out.splices.push({ start: call.attr.range.start, end: call.attr.range.end, text: rule.attr(call.prop, `${ctx.image}(${args(binding).join(", ")})`) });
4201
+ out.slugs.push(target.slug);
4202
+ out.bound.push(target.key);
4203
+ continue;
4204
+ }
4205
+ }
4206
+ if (target.declared) continue;
4165
4207
  if (free.length === 0 && binding !== null && ctx.drill) {
4166
4208
  const calls = passedTo(ctx.content, ctx.parsed, binding).filter((c) => !claimed.has(c.node.attrInsertAt));
4167
4209
  const call = calls.length === 1 ? calls[0] : null;
@@ -10184,10 +10226,13 @@ async function convertSources(briefIn, sources, options) {
10184
10226
  async function convertFile(file, content, targets, parsed, declared, llmFallback, drill) {
10185
10227
  const outcomes = /* @__PURE__ */ new Map();
10186
10228
  const pendingTargets = [];
10229
+ let unreadImages = false;
10187
10230
  const dialect = dialectOf(file, content);
10188
10231
  for (const target of targets) {
10189
10232
  if (parsed && dialect && convertedHere(content, parsed, dialect, target, file)) {
10190
10233
  outcomes.set(target.key, "declared");
10234
+ if (target.kind === "image" && !content.includes("bcmsImage(")) unreadImages = true;
10235
+ else if (target.kind === "image" && !new RegExp(`bcmsImage\\([^)]*["']${target.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']`).test(content)) unreadImages = true;
10191
10236
  } else pendingTargets.push(target);
10192
10237
  }
10193
10238
  const empty = {
@@ -10202,7 +10247,7 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
10202
10247
  snapshots: [],
10203
10248
  notes: []
10204
10249
  };
10205
- if (pendingTargets.length === 0) return empty;
10250
+ if (pendingTargets.length === 0 && !unreadImages) return empty;
10206
10251
  if (!dialect) {
10207
10252
  if (isTolerantDialect(file)) {
10208
10253
  const already = declaredByScan(content);
@@ -10403,15 +10448,15 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
10403
10448
  continue;
10404
10449
  }
10405
10450
  const declares = (d) => d.scope === target.scope && (d.path === target.path || d.path === shapeOfPath(target.path));
10406
- const declaredAt = new Set(declarations.filter(declares).map((d) => d.at));
10451
+ const declaredAt2 = new Set(declarations.filter(declares).map((d) => d.at));
10407
10452
  const claimedAt = new Set(declarations.filter((d) => !declares(d) && d.via === "field").map((d) => d.at));
10408
10453
  const writesField = (site) => site.where !== "attr" || target.kind === "image";
10409
10454
  const fresh = eligible.filter(
10410
- (site) => !declaredAt.has(site.attrInsertAt) && !(writesField(site) && claimedAt.has(site.attrInsertAt))
10455
+ (site) => !declaredAt2.has(site.attrInsertAt) && !(writesField(site) && claimedAt.has(site.attrInsertAt))
10411
10456
  );
10412
10457
  located += eligible.length;
10413
10458
  const blocked = eligible.filter(
10414
- (site) => !declaredAt.has(site.attrInsertAt) && writesField(site) && claimedAt.has(site.attrInsertAt)
10459
+ (site) => !declaredAt2.has(site.attrInsertAt) && writesField(site) && claimedAt.has(site.attrInsertAt)
10415
10460
  );
10416
10461
  if (blocked.length > 0) {
10417
10462
  outcomes.set(target.key, "AMBIGUOUS_LITERAL");
@@ -10420,7 +10465,7 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
10420
10465
  if (fresh.length === 0) {
10421
10466
  outcomes.set(
10422
10467
  target.key,
10423
- eligible.some((site) => declaredAt.has(site.attrInsertAt)) ? "declared" : "AMBIGUOUS_LITERAL"
10468
+ eligible.some((site) => declaredAt2.has(site.attrInsertAt)) ? "declared" : "AMBIGUOUS_LITERAL"
10424
10469
  );
10425
10470
  continue;
10426
10471
  }
@@ -10606,8 +10651,8 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
10606
10651
  },
10607
10652
  targets.filter((t) => {
10608
10653
  const outcome = outcomes.get(t.key);
10609
- return t.kind === "image" && t.scope === "page" && (outcome === void 0 || UNPLACED.has(outcome));
10610
- }).map((t) => ({ key: t.key, path: t.path, kind: t.kind, original: t.fallback, slug: t.slug }))
10654
+ return t.kind === "image" && t.scope === "page" && (outcome === void 0 || outcome === "declared" || UNPLACED.has(outcome));
10655
+ }).map((t) => ({ key: t.key, path: t.path, kind: t.kind, original: t.fallback, slug: t.slug, declared: outcomes.get(t.key) === "declared" }))
10611
10656
  );
10612
10657
  for (const key of images.bound) outcomes.set(key, "rewritten");
10613
10658
  for (const refusal of images.refused) outcomes.set(refusal.key, refusal.reason);
@@ -10897,7 +10942,7 @@ function styleEdits(before, after) {
10897
10942
  // package.json
10898
10943
  var package_default = {
10899
10944
  name: "@bettercms-ai/convert",
10900
- version: "0.26.0",
10945
+ version: "0.26.1",
10901
10946
  type: "module",
10902
10947
  description: "Deterministic codemod that turns an imported site's text-matched bindings into declared ones \u2014 parser offsets in, one magic-string pass out, plus a receipt of everything it could not do.",
10903
10948
  bin: {
@@ -10930,7 +10975,7 @@ var package_default = {
10930
10975
  dependencies: {
10931
10976
  "@astrojs/compiler": "^2.13.1",
10932
10977
  "@babel/parser": "^7.29.2",
10933
- "@bettercms-ai/types": "^1.32.0",
10978
+ "@bettercms-ai/types": "^1.36.0",
10934
10979
  "@vue/compiler-sfc": "^3.5.0",
10935
10980
  "magic-string": "^0.30.21",
10936
10981
  parse5: "^7.3.0",