uidex 0.5.0 → 0.5.2

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/dist/cli/cli.cjs CHANGED
@@ -739,17 +739,38 @@ function audit(opts) {
739
739
  }
740
740
  }
741
741
  if (lint) {
742
+ const dynamicAttrRe = /\bdata-uidex(?:-(region|widget|primitive))?\s*=\s*\{/g;
742
743
  for (const f of files) {
743
- const lines = f.content.split("\n");
744
- const candidateRe = /<(button|a|input|select|textarea)(?=[\s/>])([^>]*)>/g;
745
744
  let m;
746
- while ((m = candidateRe.exec(f.content)) !== null) {
747
- const attrs = m[2];
748
- if (attrs && attrs.includes("data-uidex")) continue;
749
- const idx = m.index;
745
+ dynamicAttrRe.lastIndex = 0;
746
+ while ((m = dynamicAttrRe.exec(f.content)) !== null) {
747
+ const kind = m[1] ?? "element";
750
748
  let line = 1;
751
- for (let i = 0; i < idx; i++) if (f.content[i] === "\n") line++;
752
- void lines;
749
+ for (let i = 0; i < m.index; i++) if (f.content[i] === "\n") line++;
750
+ const attrName = m[1] ? `data-uidex-${m[1]}` : "data-uidex";
751
+ diagnostics.push({
752
+ code: "dynamic-attr",
753
+ severity: "warning",
754
+ message: `\`${attrName}={\u2026}\` uses a dynamic expression; the scanner cannot resolve the ${kind} id statically`,
755
+ file: f.displayPath,
756
+ line,
757
+ hint: dynamicAttrHint(kind)
758
+ });
759
+ }
760
+ }
761
+ }
762
+ if (lint) {
763
+ for (const f of files) {
764
+ const tagRe = /<(button|a|input|select|textarea)(?=[\s/>])/g;
765
+ let m;
766
+ while ((m = tagRe.exec(f.content)) !== null) {
767
+ const afterTag = m.index + m[0].length;
768
+ const closeIdx = findJsxOpeningEnd(f.content, afterTag);
769
+ if (closeIdx === -1) continue;
770
+ const attrs = f.content.slice(afterTag, closeIdx);
771
+ if (attrs.includes("data-uidex")) continue;
772
+ let line = 1;
773
+ for (let i = 0; i < m.index; i++) if (f.content[i] === "\n") line++;
753
774
  diagnostics.push({
754
775
  code: "missing-element-annotation",
755
776
  severity: "info",
@@ -959,6 +980,63 @@ function extractEntitiesArray(source) {
959
980
  }
960
981
  return null;
961
982
  }
983
+ function findJsxOpeningEnd(src, start) {
984
+ let i = start;
985
+ while (i < src.length) {
986
+ const ch = src[i];
987
+ if (ch === ">" || ch === "/" && src[i + 1] === ">") return i;
988
+ if (ch === '"' || ch === "'" || ch === "`") {
989
+ i = skipString(src, i);
990
+ } else if (ch === "{") {
991
+ i = skipBraces(src, i);
992
+ } else {
993
+ i++;
994
+ }
995
+ }
996
+ return -1;
997
+ }
998
+ function skipString(src, start) {
999
+ const quote = src[start];
1000
+ let i = start + 1;
1001
+ while (i < src.length) {
1002
+ if (src[i] === "\\" && quote !== "`") {
1003
+ i += 2;
1004
+ continue;
1005
+ }
1006
+ if (quote === "`" && src[i] === "$" && src[i + 1] === "{") {
1007
+ i = skipBraces(src, i + 1);
1008
+ continue;
1009
+ }
1010
+ if (src[i] === quote) return i + 1;
1011
+ i++;
1012
+ }
1013
+ return i;
1014
+ }
1015
+ function skipBraces(src, start) {
1016
+ let depth = 1;
1017
+ let i = start + 1;
1018
+ while (i < src.length && depth > 0) {
1019
+ const ch = src[i];
1020
+ if (ch === "{") {
1021
+ depth++;
1022
+ i++;
1023
+ } else if (ch === "}") {
1024
+ depth--;
1025
+ i++;
1026
+ } else if (ch === '"' || ch === "'" || ch === "`") {
1027
+ i = skipString(src, i);
1028
+ } else {
1029
+ i++;
1030
+ }
1031
+ }
1032
+ return i;
1033
+ }
1034
+ function dynamicAttrHint(kind) {
1035
+ if (kind === "region") {
1036
+ return `Use a string literal: \`data-uidex-region="id"\`, or declare the region via \`export const uidex = { region: "id" } as const satisfies Uidex.Region\` on the file that passes the region value`;
1037
+ }
1038
+ return `The scanner requires string-literal attribute values. If this component forwards the annotation via a prop, restructure so the caller provides the annotated element directly (e.g. via a slot or render prop) with a string-literal \`data-uidex\` attribute`;
1039
+ }
962
1040
  function stableStringify(value) {
963
1041
  return JSON.stringify(value, stableReplacer);
964
1042
  }
@@ -1112,6 +1190,11 @@ function emit(opts) {
1112
1190
  lines.push(" acceptance?: readonly string[]");
1113
1191
  lines.push(" description?: string");
1114
1192
  lines.push(" }");
1193
+ lines.push(" export interface Region {");
1194
+ lines.push(" region: RegionId | false");
1195
+ lines.push(" name?: string");
1196
+ lines.push(" description?: string");
1197
+ lines.push(" }");
1115
1198
  lines.push(" export interface Flow {");
1116
1199
  lines.push(" flow: FlowId");
1117
1200
  lines.push(" name?: string");
@@ -1160,6 +1243,7 @@ var KIND_DISCRIMINATORS = [
1160
1243
  "feature",
1161
1244
  "primitive",
1162
1245
  "widget",
1246
+ "region",
1163
1247
  "flow",
1164
1248
  "notFlow"
1165
1249
  ];
@@ -1181,12 +1265,14 @@ var ALLOWED_FIELDS = {
1181
1265
  ]),
1182
1266
  primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
1183
1267
  widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "description"]),
1268
+ region: /* @__PURE__ */ new Set(["region", "name", "description"]),
1184
1269
  flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
1185
1270
  };
1186
1271
  var FALSEABLE = /* @__PURE__ */ new Set([
1187
1272
  "page",
1188
1273
  "feature",
1189
- "primitive"
1274
+ "primitive",
1275
+ "region"
1190
1276
  ]);
1191
1277
  var ExtractError = class extends Error {
1192
1278
  code;
@@ -2053,7 +2139,7 @@ function collectJSXAncestry(content) {
2053
2139
  continue;
2054
2140
  }
2055
2141
  if (c === '"' || c === "'") {
2056
- const next = skipString(content, i, c);
2142
+ const next = skipString2(content, i, c);
2057
2143
  advanceLines(i, next);
2058
2144
  i = next;
2059
2145
  continue;
@@ -2113,7 +2199,7 @@ function collectJSXAncestry(content) {
2113
2199
  }
2114
2200
  return out2;
2115
2201
  }
2116
- function skipString(content, start, quote) {
2202
+ function skipString2(content, start, quote) {
2117
2203
  const N = content.length;
2118
2204
  let i = start + 1;
2119
2205
  while (i < N) {
@@ -2143,7 +2229,7 @@ function skipTemplate(content, start) {
2143
2229
  while (i < N && depth > 0) {
2144
2230
  const cj = content[i];
2145
2231
  if (cj === '"' || cj === "'") {
2146
- i = skipString(content, i, cj);
2232
+ i = skipString2(content, i, cj);
2147
2233
  continue;
2148
2234
  }
2149
2235
  if (cj === "`") {
@@ -2166,7 +2252,7 @@ function findTagEnd(content, start) {
2166
2252
  while (i < N) {
2167
2253
  const c = content[i];
2168
2254
  if (c === '"' || c === "'") {
2169
- i = skipString(content, i, c);
2255
+ i = skipString2(content, i, c);
2170
2256
  continue;
2171
2257
  }
2172
2258
  if (c === "`") {
@@ -2179,7 +2265,7 @@ function findTagEnd(content, start) {
2179
2265
  while (i < N && depth > 0) {
2180
2266
  const cj = content[i];
2181
2267
  if (cj === '"' || cj === "'") {
2182
- i = skipString(content, i, cj);
2268
+ i = skipString2(content, i, cj);
2183
2269
  continue;
2184
2270
  }
2185
2271
  if (cj === "`") {
@@ -2834,6 +2920,21 @@ function resolve3(ctx) {
2834
2920
  };
2835
2921
  registry.add(region);
2836
2922
  }
2923
+ for (const ef of ctx.extracted) {
2924
+ const exp = exportFor(ef.file.displayPath, "region");
2925
+ if (!exp) continue;
2926
+ if (exp.id === false) continue;
2927
+ if (typeof exp.id === "string" && !registry.get("region", exp.id)) {
2928
+ const meta = buildMetaFromExport(exp);
2929
+ const region = {
2930
+ kind: "region",
2931
+ id: exp.id,
2932
+ loc: { file: ef.file.displayPath, line: exp.loc.line },
2933
+ ...meta ? { meta } : {}
2934
+ };
2935
+ registry.add(region);
2936
+ }
2937
+ }
2837
2938
  const primitiveConventions = conventions.primitives;
2838
2939
  for (const ef of ctx.extracted) {
2839
2940
  const file = ef.file.displayPath;