uidex 0.3.0 → 0.5.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/dist/cli/cli.cjs +1116 -112
- package/dist/cli/cli.cjs.map +1 -1
- package/dist/cloud/index.cjs +395 -72
- package/dist/cloud/index.cjs.map +1 -1
- package/dist/cloud/index.d.cts +60 -86
- package/dist/cloud/index.d.ts +60 -86
- package/dist/cloud/index.js +396 -71
- package/dist/cloud/index.js.map +1 -1
- package/dist/headless/index.cjs +1505 -791
- package/dist/headless/index.cjs.map +1 -1
- package/dist/headless/index.d.cts +83 -75
- package/dist/headless/index.d.ts +83 -75
- package/dist/headless/index.js +1514 -791
- package/dist/headless/index.js.map +1 -1
- package/dist/index.cjs +6281 -3190
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +337 -229
- package/dist/index.d.ts +337 -229
- package/dist/index.js +6362 -3231
- package/dist/index.js.map +1 -1
- package/dist/playwright/index.cjs +4 -4
- package/dist/playwright/index.cjs.map +1 -1
- package/dist/playwright/index.js +3 -3
- package/dist/playwright/index.js.map +1 -1
- package/dist/playwright/reporter.cjs +3 -3
- package/dist/playwright/reporter.cjs.map +1 -1
- package/dist/playwright/reporter.js +3 -3
- package/dist/playwright/reporter.js.map +1 -1
- package/dist/react/index.cjs +6291 -3206
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +239 -186
- package/dist/react/index.d.ts +239 -186
- package/dist/react/index.js +6338 -3208
- package/dist/react/index.js.map +1 -1
- package/dist/scan/index.cjs +212 -82
- package/dist/scan/index.cjs.map +1 -1
- package/dist/scan/index.d.cts +31 -0
- package/dist/scan/index.d.ts +31 -0
- package/dist/scan/index.js +211 -81
- package/dist/scan/index.js.map +1 -1
- package/package.json +10 -8
- package/templates/claude/api.md +110 -0
- package/templates/claude/audit.md +8 -2
- package/templates/claude/rules.md +15 -0
package/dist/scan/index.cjs
CHANGED
|
@@ -27,7 +27,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
27
|
));
|
|
28
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
29
|
|
|
30
|
-
// src/scan/index.ts
|
|
30
|
+
// src/scanner/scan/index.ts
|
|
31
31
|
var scan_exports = {};
|
|
32
32
|
__export(scan_exports, {
|
|
33
33
|
CONFIG_FILENAME: () => CONFIG_FILENAME,
|
|
@@ -54,12 +54,16 @@ __export(scan_exports, {
|
|
|
54
54
|
});
|
|
55
55
|
module.exports = __toCommonJS(scan_exports);
|
|
56
56
|
|
|
57
|
-
// src/scan/discover.ts
|
|
57
|
+
// src/scanner/scan/discover.ts
|
|
58
58
|
var fs = __toESM(require("fs"), 1);
|
|
59
59
|
var path = __toESM(require("path"), 1);
|
|
60
60
|
|
|
61
|
-
// src/scan/config.ts
|
|
61
|
+
// src/scanner/scan/config.ts
|
|
62
62
|
var DEFAULT_TYPE_MODE = "strict";
|
|
63
|
+
var WELL_KNOWN_FILES = {
|
|
64
|
+
page: "uidex.page.ts",
|
|
65
|
+
feature: "uidex.feature.ts"
|
|
66
|
+
};
|
|
63
67
|
var ConfigError = class extends Error {
|
|
64
68
|
constructor(message) {
|
|
65
69
|
super(message);
|
|
@@ -97,14 +101,14 @@ var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set(["scopeLeak", "coverage", "acce
|
|
|
97
101
|
function fail(msg) {
|
|
98
102
|
throw new ConfigError(`Invalid .uidex.json: ${msg}`);
|
|
99
103
|
}
|
|
100
|
-
function assertObject(value,
|
|
104
|
+
function assertObject(value, path10) {
|
|
101
105
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
102
|
-
fail(`${
|
|
106
|
+
fail(`${path10} must be an object`);
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
|
-
function assertStringArray(value,
|
|
109
|
+
function assertStringArray(value, path10) {
|
|
106
110
|
if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) {
|
|
107
|
-
fail(`${
|
|
111
|
+
fail(`${path10} must be a string[]`);
|
|
108
112
|
}
|
|
109
113
|
}
|
|
110
114
|
function validateConfig(raw) {
|
|
@@ -218,7 +222,7 @@ var DEFAULT_CONVENTIONS = {
|
|
|
218
222
|
regions: "landmarks"
|
|
219
223
|
};
|
|
220
224
|
|
|
221
|
-
// src/scan/discover.ts
|
|
225
|
+
// src/scanner/scan/discover.ts
|
|
222
226
|
var CONFIG_FILENAME = ".uidex.json";
|
|
223
227
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
224
228
|
"node_modules",
|
|
@@ -277,7 +281,7 @@ function discover(options = {}) {
|
|
|
277
281
|
return results.sort((a, b) => a.configPath.localeCompare(b.configPath));
|
|
278
282
|
}
|
|
279
283
|
|
|
280
|
-
// src/scan/walk.ts
|
|
284
|
+
// src/scanner/scan/walk.ts
|
|
281
285
|
var fs2 = __toESM(require("fs"), 1);
|
|
282
286
|
var path2 = __toESM(require("path"), 1);
|
|
283
287
|
var DEFAULT_INCLUDES = ["**/*.{ts,tsx,js,jsx,mjs,cjs}"];
|
|
@@ -402,7 +406,7 @@ function* walkDir(root, dir) {
|
|
|
402
406
|
}
|
|
403
407
|
}
|
|
404
408
|
|
|
405
|
-
// src/scan/extract-uidex-export.ts
|
|
409
|
+
// src/scanner/scan/extract-uidex-export.ts
|
|
406
410
|
var KIND_DISCRIMINATORS = [
|
|
407
411
|
"page",
|
|
408
412
|
"feature",
|
|
@@ -420,7 +424,13 @@ var ALLOWED_FIELDS = {
|
|
|
420
424
|
"acceptance",
|
|
421
425
|
"description"
|
|
422
426
|
]),
|
|
423
|
-
feature: /* @__PURE__ */ new Set([
|
|
427
|
+
feature: /* @__PURE__ */ new Set([
|
|
428
|
+
"feature",
|
|
429
|
+
"name",
|
|
430
|
+
"features",
|
|
431
|
+
"acceptance",
|
|
432
|
+
"description"
|
|
433
|
+
]),
|
|
424
434
|
primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
|
|
425
435
|
widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "description"]),
|
|
426
436
|
flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
|
|
@@ -1156,7 +1166,7 @@ function buildMetadata(value, file, headerPos, diagnostics) {
|
|
|
1156
1166
|
line: pos.line
|
|
1157
1167
|
});
|
|
1158
1168
|
}
|
|
1159
|
-
const features = kind === "page" ? readStringArrayField(byKey, "features") : void 0;
|
|
1169
|
+
const features = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
|
|
1160
1170
|
const widgets = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
|
|
1161
1171
|
const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
|
|
1162
1172
|
const metadata = {
|
|
@@ -1251,7 +1261,7 @@ function posAt(content, offset) {
|
|
|
1251
1261
|
return { offset, line, column: offset - lineStart + 1 };
|
|
1252
1262
|
}
|
|
1253
1263
|
|
|
1254
|
-
// src/scan/jsx-ancestry.ts
|
|
1264
|
+
// src/scanner/scan/jsx-ancestry.ts
|
|
1255
1265
|
var DATA_ATTR_RE = /\bdata-uidex(?:-(region|widget|primitive))?\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
|
|
1256
1266
|
function parseDataAttrs(tagSource) {
|
|
1257
1267
|
if (!tagSource.includes("data-uidex")) return [];
|
|
@@ -1440,7 +1450,7 @@ function findTagEnd(content, start) {
|
|
|
1440
1450
|
return -1;
|
|
1441
1451
|
}
|
|
1442
1452
|
|
|
1443
|
-
// src/scan/extract.ts
|
|
1453
|
+
// src/scanner/scan/extract.ts
|
|
1444
1454
|
var JSDOC_BLOCK = /\/\*\*([\s\S]*?)\*\//g;
|
|
1445
1455
|
function lineAt(content, index) {
|
|
1446
1456
|
let line = 1;
|
|
@@ -1543,10 +1553,10 @@ function extractOne(file) {
|
|
|
1543
1553
|
return annotations;
|
|
1544
1554
|
}
|
|
1545
1555
|
|
|
1546
|
-
// src/scan/resolve.ts
|
|
1556
|
+
// src/scanner/scan/resolve.ts
|
|
1547
1557
|
var path3 = __toESM(require("path"), 1);
|
|
1548
1558
|
|
|
1549
|
-
// src/entities/types.ts
|
|
1559
|
+
// src/shared/entities/types.ts
|
|
1550
1560
|
var ENTITY_KINDS = [
|
|
1551
1561
|
"route",
|
|
1552
1562
|
"page",
|
|
@@ -1579,7 +1589,7 @@ function assertEntityKind(kind) {
|
|
|
1579
1589
|
if (!KIND_SET.has(kind)) throw new UnknownEntityKindError(kind);
|
|
1580
1590
|
}
|
|
1581
1591
|
|
|
1582
|
-
// src/entities/registry.ts
|
|
1592
|
+
// src/shared/entities/registry.ts
|
|
1583
1593
|
function emptyStore() {
|
|
1584
1594
|
return {
|
|
1585
1595
|
route: /* @__PURE__ */ new Map(),
|
|
@@ -1661,10 +1671,33 @@ function createRegistry() {
|
|
|
1661
1671
|
return ids.has(entity.id);
|
|
1662
1672
|
});
|
|
1663
1673
|
};
|
|
1664
|
-
|
|
1674
|
+
const reports = /* @__PURE__ */ new Map();
|
|
1675
|
+
const reportsCbs = /* @__PURE__ */ new Set();
|
|
1676
|
+
const setReports = (kind, id, records) => {
|
|
1677
|
+
reports.set(`${kind}:${id}`, records);
|
|
1678
|
+
for (const cb of reportsCbs) cb();
|
|
1679
|
+
};
|
|
1680
|
+
const getReports = (kind, id) => reports.get(`${kind}:${id}`) ?? [];
|
|
1681
|
+
const listReportKeys = () => Array.from(reports.keys());
|
|
1682
|
+
const onReportsChange = (cb) => {
|
|
1683
|
+
reportsCbs.add(cb);
|
|
1684
|
+
return () => reportsCbs.delete(cb);
|
|
1685
|
+
};
|
|
1686
|
+
return {
|
|
1687
|
+
add,
|
|
1688
|
+
get,
|
|
1689
|
+
list,
|
|
1690
|
+
query,
|
|
1691
|
+
byScope,
|
|
1692
|
+
touchedBy,
|
|
1693
|
+
setReports,
|
|
1694
|
+
getReports,
|
|
1695
|
+
listReportKeys,
|
|
1696
|
+
onReportsChange
|
|
1697
|
+
};
|
|
1665
1698
|
}
|
|
1666
1699
|
|
|
1667
|
-
// src/scan/routes.ts
|
|
1700
|
+
// src/scanner/scan/routes.ts
|
|
1668
1701
|
var PAGE_BASENAME = /^page\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
1669
1702
|
var PAGES_ROUTER_BASENAME = /\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
1670
1703
|
var ROUTE_BASENAME = /^route\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
@@ -1730,7 +1763,7 @@ function pathToId(routePath) {
|
|
|
1730
1763
|
return routePath.replace(/^\/+/, "").replace(/\[\.{3}([^\]]+)\]/g, "$1").replace(/\[([^\]]+)\]/g, "$1").replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1731
1764
|
}
|
|
1732
1765
|
|
|
1733
|
-
// src/scan/resolve.ts
|
|
1766
|
+
// src/scanner/scan/resolve.ts
|
|
1734
1767
|
var DOM_ATTR_KINDS = /* @__PURE__ */ new Set([
|
|
1735
1768
|
"element",
|
|
1736
1769
|
"region",
|
|
@@ -1825,7 +1858,24 @@ function resolve2(ctx) {
|
|
|
1825
1858
|
const routes = conventions.pages === "auto" ? detectRoutes(ctx.extracted.map((e) => e.file)) : [];
|
|
1826
1859
|
const handledPageFiles = /* @__PURE__ */ new Set();
|
|
1827
1860
|
for (const route of routes) {
|
|
1828
|
-
const
|
|
1861
|
+
const routeDir = path3.posix.dirname(route.file);
|
|
1862
|
+
const wellKnownPath = `${routeDir}/${WELL_KNOWN_FILES.page}`;
|
|
1863
|
+
const wellKnownExp = exportFor(wellKnownPath, "page");
|
|
1864
|
+
const routeExp = exportFor(route.file, "page");
|
|
1865
|
+
const exp = wellKnownExp ?? routeExp;
|
|
1866
|
+
const locFile = wellKnownExp ? wellKnownPath : route.file;
|
|
1867
|
+
if (wellKnownExp) handledPageFiles.add(wellKnownPath);
|
|
1868
|
+
handledPageFiles.add(route.file);
|
|
1869
|
+
if (wellKnownExp && routeExp) {
|
|
1870
|
+
diagnostics.push({
|
|
1871
|
+
code: "competing-uidex-export",
|
|
1872
|
+
severity: "warning",
|
|
1873
|
+
message: `Page metadata declared in both ${wellKnownPath} and ${route.file}; ${wellKnownPath} takes precedence.`,
|
|
1874
|
+
file: route.file,
|
|
1875
|
+
line: routeExp.loc.line,
|
|
1876
|
+
hint: `Remove the export from ${route.file} or delete ${wellKnownPath}.`
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1829
1879
|
if (exp && exp.id === false) continue;
|
|
1830
1880
|
const effectiveId = exp && typeof exp.id === "string" ? exp.id : route.id;
|
|
1831
1881
|
const meta = exp ? buildMetaFromExport(exp) : void 0;
|
|
@@ -1833,11 +1883,10 @@ function resolve2(ctx) {
|
|
|
1833
1883
|
const page = {
|
|
1834
1884
|
kind: "page",
|
|
1835
1885
|
id: effectiveId,
|
|
1836
|
-
loc: { file:
|
|
1886
|
+
loc: { file: locFile, line: exp?.loc.line },
|
|
1837
1887
|
...meta ? { meta } : {}
|
|
1838
1888
|
};
|
|
1839
1889
|
registry.add(page);
|
|
1840
|
-
handledPageFiles.add(route.file);
|
|
1841
1890
|
}
|
|
1842
1891
|
for (const ef of ctx.extracted) {
|
|
1843
1892
|
const exp = exportFor(ef.file.displayPath, "page");
|
|
@@ -1853,7 +1902,8 @@ function resolve2(ctx) {
|
|
|
1853
1902
|
}
|
|
1854
1903
|
const featureGlob = typeof conventions.features === "string" ? conventions.features : null;
|
|
1855
1904
|
const conventionalFeatureDirs = /* @__PURE__ */ new Set();
|
|
1856
|
-
const
|
|
1905
|
+
const featureExportFilesByDir = /* @__PURE__ */ new Map();
|
|
1906
|
+
const wellKnownFeatureFileByDir = /* @__PURE__ */ new Map();
|
|
1857
1907
|
const suppressedFeatureDirs = /* @__PURE__ */ new Set();
|
|
1858
1908
|
if (featureGlob) {
|
|
1859
1909
|
const re = globToRegExp(featureGlob + "/**");
|
|
@@ -1862,16 +1912,43 @@ function resolve2(ctx) {
|
|
|
1862
1912
|
const dir = extractFeatureDir(ef.file.displayPath, featureGlob);
|
|
1863
1913
|
if (!dir) continue;
|
|
1864
1914
|
conventionalFeatureDirs.add(dir);
|
|
1915
|
+
const isWellKnown = path3.posix.basename(ef.file.displayPath) === WELL_KNOWN_FILES.feature;
|
|
1916
|
+
if (isWellKnown) wellKnownFeatureFileByDir.set(dir, ef.file.displayPath);
|
|
1865
1917
|
const exp = exportFor(ef.file.displayPath, "feature");
|
|
1866
1918
|
if (exp) {
|
|
1867
1919
|
if (exp.id === false) suppressedFeatureDirs.add(dir);
|
|
1868
|
-
else
|
|
1869
|
-
|
|
1920
|
+
else {
|
|
1921
|
+
let arr = featureExportFilesByDir.get(dir);
|
|
1922
|
+
if (!arr) {
|
|
1923
|
+
arr = [];
|
|
1924
|
+
featureExportFilesByDir.set(dir, arr);
|
|
1925
|
+
}
|
|
1926
|
+
arr.push({ file: ef.file.displayPath, exp });
|
|
1927
|
+
}
|
|
1870
1928
|
}
|
|
1871
1929
|
}
|
|
1872
1930
|
for (const dir of conventionalFeatureDirs) {
|
|
1873
1931
|
if (suppressedFeatureDirs.has(dir)) continue;
|
|
1874
|
-
const
|
|
1932
|
+
const allExports = featureExportFilesByDir.get(dir) ?? [];
|
|
1933
|
+
const wellKnownPath = wellKnownFeatureFileByDir.get(dir);
|
|
1934
|
+
const wellKnownEntry = wellKnownPath ? allExports.find((e) => e.file === wellKnownPath) : void 0;
|
|
1935
|
+
let exp;
|
|
1936
|
+
if (wellKnownEntry) {
|
|
1937
|
+
exp = wellKnownEntry.exp;
|
|
1938
|
+
for (const other of allExports) {
|
|
1939
|
+
if (other.file === wellKnownEntry.file) continue;
|
|
1940
|
+
diagnostics.push({
|
|
1941
|
+
code: "competing-uidex-export",
|
|
1942
|
+
severity: "warning",
|
|
1943
|
+
message: `Feature metadata declared in both ${wellKnownEntry.file} and ${other.file}; ${wellKnownEntry.file} takes precedence.`,
|
|
1944
|
+
file: other.file,
|
|
1945
|
+
line: other.exp.loc.line,
|
|
1946
|
+
hint: `Remove the export from ${other.file} or delete ${wellKnownEntry.file}.`
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
} else if (allExports.length > 0) {
|
|
1950
|
+
exp = allExports[0].exp;
|
|
1951
|
+
}
|
|
1875
1952
|
const id = exp && typeof exp.id === "string" ? exp.id : path3.posix.basename(dir);
|
|
1876
1953
|
const meta = exp ? buildMetaFromExport(exp) : void 0;
|
|
1877
1954
|
const feature = {
|
|
@@ -2004,11 +2081,12 @@ function resolve2(ctx) {
|
|
|
2004
2081
|
exp.id,
|
|
2005
2082
|
buildMetaFromExport(exp)
|
|
2006
2083
|
);
|
|
2084
|
+
const scope = computeScope(file);
|
|
2007
2085
|
const primitive = {
|
|
2008
2086
|
kind: "primitive",
|
|
2009
2087
|
id: exp.id,
|
|
2010
2088
|
loc: { file, line: exp.loc.line },
|
|
2011
|
-
scopes: [
|
|
2089
|
+
...scope ? { scopes: [scope] } : {},
|
|
2012
2090
|
...meta ? { meta } : {}
|
|
2013
2091
|
};
|
|
2014
2092
|
registry.add(primitive);
|
|
@@ -2020,11 +2098,12 @@ function resolve2(ctx) {
|
|
|
2020
2098
|
if (domPrimitives.length > 0) {
|
|
2021
2099
|
for (const p2 of domPrimitives) {
|
|
2022
2100
|
const meta = metaWithComposes("primitive", p2.id);
|
|
2101
|
+
const domScope = computeScope(p2.file);
|
|
2023
2102
|
const primitive = {
|
|
2024
2103
|
kind: "primitive",
|
|
2025
2104
|
id: p2.id,
|
|
2026
2105
|
loc: { file: p2.file, line: p2.line },
|
|
2027
|
-
scopes: [
|
|
2106
|
+
...domScope ? { scopes: [domScope] } : {},
|
|
2028
2107
|
...meta ? { meta } : {}
|
|
2029
2108
|
};
|
|
2030
2109
|
registry.add(primitive);
|
|
@@ -2034,13 +2113,13 @@ function resolve2(ctx) {
|
|
|
2034
2113
|
if (primitiveConventions && fileMatchesAny(file, primitiveConventions)) {
|
|
2035
2114
|
const name = kebab(baseName(file));
|
|
2036
2115
|
if (!name) continue;
|
|
2037
|
-
const
|
|
2116
|
+
const convScope = computeScope(file);
|
|
2038
2117
|
const meta = metaWithComposes("primitive", name);
|
|
2039
2118
|
const primitive = {
|
|
2040
2119
|
kind: "primitive",
|
|
2041
2120
|
id: name,
|
|
2042
2121
|
loc: { file },
|
|
2043
|
-
scopes: [
|
|
2122
|
+
...convScope ? { scopes: [convScope] } : {},
|
|
2044
2123
|
...meta ? { meta } : {}
|
|
2045
2124
|
};
|
|
2046
2125
|
registry.add(primitive);
|
|
@@ -2070,7 +2149,8 @@ function resolve2(ctx) {
|
|
|
2070
2149
|
kind: "flow",
|
|
2071
2150
|
id: flowExport.id,
|
|
2072
2151
|
loc: base.loc,
|
|
2073
|
-
touches: base.touches
|
|
2152
|
+
touches: base.touches,
|
|
2153
|
+
steps: base.steps
|
|
2074
2154
|
};
|
|
2075
2155
|
registry.add(flow);
|
|
2076
2156
|
} else {
|
|
@@ -2115,7 +2195,7 @@ function computeScope(displayPath) {
|
|
|
2115
2195
|
if (pagesIdx !== -1 && parts[pagesIdx + 1]) {
|
|
2116
2196
|
return `page:${parts[pagesIdx + 1]}`;
|
|
2117
2197
|
}
|
|
2118
|
-
return
|
|
2198
|
+
return null;
|
|
2119
2199
|
}
|
|
2120
2200
|
function extractFlowsFromSource(file) {
|
|
2121
2201
|
const flows = [];
|
|
@@ -2149,17 +2229,18 @@ function extractFlowsFromSource(file) {
|
|
|
2149
2229
|
kind: "flow",
|
|
2150
2230
|
id,
|
|
2151
2231
|
loc: { file: file.displayPath, line },
|
|
2152
|
-
touches: dedupe(touches.map((t) => t.id))
|
|
2232
|
+
touches: dedupe(touches.map((t) => t.id)),
|
|
2233
|
+
steps: touches.filter((t) => t.action).map((t) => ({ entityId: t.id, action: t.action }))
|
|
2153
2234
|
});
|
|
2154
2235
|
}
|
|
2155
2236
|
return flows;
|
|
2156
2237
|
}
|
|
2157
2238
|
function captureUidexIds(body) {
|
|
2158
2239
|
const out2 = [];
|
|
2159
|
-
const re = /uidex\(\s*(?:'([^']+)'|"([^"]+)"|`([^`$]+)`)\s*\)
|
|
2240
|
+
const re = /uidex\(\s*(?:'([^']+)'|"([^"]+)"|`([^`$]+)`)\s*\)(?:\.(\w+)\s*\()?/g;
|
|
2160
2241
|
let m;
|
|
2161
2242
|
while ((m = re.exec(body)) !== null) {
|
|
2162
|
-
out2.push({ id: m[1] || m[2] || m[3] });
|
|
2243
|
+
out2.push({ id: m[1] || m[2] || m[3], action: m[4] });
|
|
2163
2244
|
}
|
|
2164
2245
|
return out2;
|
|
2165
2246
|
}
|
|
@@ -2167,7 +2248,8 @@ function dedupe(arr) {
|
|
|
2167
2248
|
return Array.from(new Set(arr));
|
|
2168
2249
|
}
|
|
2169
2250
|
|
|
2170
|
-
// src/scan/audit.ts
|
|
2251
|
+
// src/scanner/scan/audit.ts
|
|
2252
|
+
var path4 = __toESM(require("path"), 1);
|
|
2171
2253
|
var MARKER_FILENAMES = ["UIDEX_PAGE.md", "UIDEX_FEATURE.md"];
|
|
2172
2254
|
function audit(opts) {
|
|
2173
2255
|
const diagnostics = [];
|
|
@@ -2269,6 +2351,32 @@ function audit(opts) {
|
|
|
2269
2351
|
}
|
|
2270
2352
|
}
|
|
2271
2353
|
}
|
|
2354
|
+
if (lint) {
|
|
2355
|
+
const scannedPaths = new Set(files.map((f) => f.displayPath));
|
|
2356
|
+
for (const ef of extracted) {
|
|
2357
|
+
if (!ef.metadata) continue;
|
|
2358
|
+
for (const m of ef.metadata) {
|
|
2359
|
+
if (m.kind !== "page" && m.kind !== "feature") continue;
|
|
2360
|
+
if (typeof m.id !== "string") continue;
|
|
2361
|
+
const filePath = ef.file.displayPath;
|
|
2362
|
+
const wellKnownName = WELL_KNOWN_FILES[m.kind];
|
|
2363
|
+
if (path4.posix.basename(filePath) === wellKnownName) continue;
|
|
2364
|
+
const dir = path4.posix.dirname(filePath);
|
|
2365
|
+
const wellKnownPath = dir === "." ? wellKnownName : `${dir}/${wellKnownName}`;
|
|
2366
|
+
if (scannedPaths.has(wellKnownPath)) continue;
|
|
2367
|
+
const kindLabel = m.kind === "page" ? "Page" : "Feature";
|
|
2368
|
+
diagnostics.push({
|
|
2369
|
+
code: "prefer-well-known-file",
|
|
2370
|
+
severity: "info",
|
|
2371
|
+
message: `${kindLabel} "${m.id}" metadata lives on ${filePath}; prefer ${wellKnownPath}`,
|
|
2372
|
+
file: filePath,
|
|
2373
|
+
line: m.loc.line,
|
|
2374
|
+
entity: { kind: m.kind, id: m.id },
|
|
2375
|
+
hint: `Move the \`export const uidex\` block to ${wellKnownPath} and remove it from ${filePath}.`
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2272
2380
|
if (lint) {
|
|
2273
2381
|
for (const f of files) {
|
|
2274
2382
|
const lines = f.content.split("\n");
|
|
@@ -2295,6 +2403,15 @@ function audit(opts) {
|
|
|
2295
2403
|
const primitives = registry.list("primitive");
|
|
2296
2404
|
const byName = /* @__PURE__ */ new Map();
|
|
2297
2405
|
for (const p2 of primitives) byName.set(p2.id, p2);
|
|
2406
|
+
const declaredFeatures = /* @__PURE__ */ new Map();
|
|
2407
|
+
for (const ef of extracted) {
|
|
2408
|
+
if (!ef.metadata) continue;
|
|
2409
|
+
for (const m of ef.metadata) {
|
|
2410
|
+
if (m.features && m.features.length > 0) {
|
|
2411
|
+
declaredFeatures.set(ef.file.displayPath, new Set(m.features));
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2298
2415
|
for (const f of files) {
|
|
2299
2416
|
const importRe = /import\s+(?:[^'"]+)\s+from\s+['"]([^'"]+)['"]/g;
|
|
2300
2417
|
let m;
|
|
@@ -2305,18 +2422,23 @@ function audit(opts) {
|
|
|
2305
2422
|
baseName2.replace(/\.(tsx|ts|jsx|js|mjs|cjs)$/, "").replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()
|
|
2306
2423
|
);
|
|
2307
2424
|
if (!primitive) continue;
|
|
2308
|
-
const scope = primitive.scopes?.[0]
|
|
2309
|
-
if (scope
|
|
2425
|
+
const scope = primitive.scopes?.[0];
|
|
2426
|
+
if (!scope) continue;
|
|
2310
2427
|
const [kind, id] = scope.split(":");
|
|
2311
2428
|
const importerSegments = f.displayPath.split("/");
|
|
2312
|
-
if (
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
});
|
|
2429
|
+
if (importerSegments.includes(id) && importerSegments.includes(kind + "s")) {
|
|
2430
|
+
continue;
|
|
2431
|
+
}
|
|
2432
|
+
if (kind === "feature" && importerSegments.includes(id)) continue;
|
|
2433
|
+
if (kind === "feature" && declaredFeatures.get(f.displayPath)?.has(id)) {
|
|
2434
|
+
continue;
|
|
2319
2435
|
}
|
|
2436
|
+
diagnostics.push({
|
|
2437
|
+
code: "scope-leak",
|
|
2438
|
+
severity: "warning",
|
|
2439
|
+
message: `Primitive "${primitive.id}" is scoped to ${scope} but is imported from ${f.displayPath}`,
|
|
2440
|
+
file: f.displayPath
|
|
2441
|
+
});
|
|
2320
2442
|
}
|
|
2321
2443
|
}
|
|
2322
2444
|
}
|
|
@@ -2490,7 +2612,7 @@ function stableReplacer(_key, value) {
|
|
|
2490
2612
|
return value;
|
|
2491
2613
|
}
|
|
2492
2614
|
|
|
2493
|
-
// src/scan/emit.ts
|
|
2615
|
+
// src/scanner/scan/emit.ts
|
|
2494
2616
|
function sortById(arr) {
|
|
2495
2617
|
return [...arr].sort((a, b) => a.id.localeCompare(b.id));
|
|
2496
2618
|
}
|
|
@@ -2614,6 +2736,7 @@ function emit(opts) {
|
|
|
2614
2736
|
lines.push(" export interface Feature {");
|
|
2615
2737
|
lines.push(" feature: FeatureId | false");
|
|
2616
2738
|
lines.push(" name?: string");
|
|
2739
|
+
lines.push(" features?: readonly FeatureId[]");
|
|
2617
2740
|
lines.push(" acceptance?: readonly string[]");
|
|
2618
2741
|
lines.push(" description?: string");
|
|
2619
2742
|
lines.push(" }");
|
|
@@ -2670,7 +2793,7 @@ function emit(opts) {
|
|
|
2670
2793
|
return lines.join("\n");
|
|
2671
2794
|
}
|
|
2672
2795
|
|
|
2673
|
-
// src/scan/git.ts
|
|
2796
|
+
// src/scanner/scan/git.ts
|
|
2674
2797
|
var import_node_child_process = require("child_process");
|
|
2675
2798
|
function runGit(args, cwd) {
|
|
2676
2799
|
try {
|
|
@@ -2702,9 +2825,9 @@ function parseGitHubRef(ref) {
|
|
|
2702
2825
|
return m ? m[1] : null;
|
|
2703
2826
|
}
|
|
2704
2827
|
|
|
2705
|
-
// src/scan/scaffold.ts
|
|
2828
|
+
// src/scanner/scan/scaffold.ts
|
|
2706
2829
|
var fs3 = __toESM(require("fs"), 1);
|
|
2707
|
-
var
|
|
2830
|
+
var path5 = __toESM(require("path"), 1);
|
|
2708
2831
|
function scaffoldWidgetSpec(opts) {
|
|
2709
2832
|
const {
|
|
2710
2833
|
registry,
|
|
@@ -2719,7 +2842,7 @@ function scaffoldWidgetSpec(opts) {
|
|
|
2719
2842
|
}
|
|
2720
2843
|
const criteria = widget.meta?.acceptance ?? [];
|
|
2721
2844
|
const filename = `widget-${widgetId}.spec.ts`;
|
|
2722
|
-
const outputPath =
|
|
2845
|
+
const outputPath = path5.resolve(outDir, filename);
|
|
2723
2846
|
if (fs3.existsSync(outputPath) && !force) {
|
|
2724
2847
|
return {
|
|
2725
2848
|
outputPath,
|
|
@@ -2733,7 +2856,7 @@ function scaffoldWidgetSpec(opts) {
|
|
|
2733
2856
|
criteria,
|
|
2734
2857
|
fixtureImport
|
|
2735
2858
|
});
|
|
2736
|
-
fs3.mkdirSync(
|
|
2859
|
+
fs3.mkdirSync(path5.dirname(outputPath), { recursive: true });
|
|
2737
2860
|
fs3.writeFileSync(outputPath, content, "utf8");
|
|
2738
2861
|
return { outputPath, written: true, skipped: false };
|
|
2739
2862
|
}
|
|
@@ -2765,9 +2888,9 @@ function renderSpec(args) {
|
|
|
2765
2888
|
return lines.join("\n");
|
|
2766
2889
|
}
|
|
2767
2890
|
|
|
2768
|
-
// src/scan/pipeline.ts
|
|
2891
|
+
// src/scanner/scan/pipeline.ts
|
|
2769
2892
|
var fs4 = __toESM(require("fs"), 1);
|
|
2770
|
-
var
|
|
2893
|
+
var path6 = __toESM(require("path"), 1);
|
|
2771
2894
|
function runScan(opts = {}) {
|
|
2772
2895
|
const cwd = opts.cwd ?? process.cwd();
|
|
2773
2896
|
const configs = opts.configs ?? discover({ cwd });
|
|
@@ -2799,7 +2922,7 @@ function runOne(dc, opts) {
|
|
|
2799
2922
|
gitContext,
|
|
2800
2923
|
typeMode: config.typeMode
|
|
2801
2924
|
});
|
|
2802
|
-
const outputPath =
|
|
2925
|
+
const outputPath = path6.resolve(configDir, config.output);
|
|
2803
2926
|
const outputRel = config.output;
|
|
2804
2927
|
let existingOnDisk = null;
|
|
2805
2928
|
if (opts.check) {
|
|
@@ -2835,29 +2958,29 @@ function runOne(dc, opts) {
|
|
|
2835
2958
|
};
|
|
2836
2959
|
}
|
|
2837
2960
|
function writeScanResult(result) {
|
|
2838
|
-
fs4.mkdirSync(
|
|
2961
|
+
fs4.mkdirSync(path6.dirname(result.outputPath), { recursive: true });
|
|
2839
2962
|
fs4.writeFileSync(result.outputPath, result.generated, "utf8");
|
|
2840
2963
|
}
|
|
2841
2964
|
|
|
2842
|
-
// src/scan/cli.ts
|
|
2965
|
+
// src/scanner/scan/cli.ts
|
|
2843
2966
|
var fs7 = __toESM(require("fs"), 1);
|
|
2844
|
-
var
|
|
2967
|
+
var path9 = __toESM(require("path"), 1);
|
|
2845
2968
|
|
|
2846
|
-
// src/scan/ai/index.ts
|
|
2969
|
+
// src/scanner/scan/ai/index.ts
|
|
2847
2970
|
var p = __toESM(require("@clack/prompts"), 1);
|
|
2848
2971
|
|
|
2849
|
-
// src/scan/ai/providers/claude.ts
|
|
2972
|
+
// src/scanner/scan/ai/providers/claude.ts
|
|
2850
2973
|
var fs6 = __toESM(require("fs"), 1);
|
|
2851
|
-
var
|
|
2974
|
+
var path8 = __toESM(require("path"), 1);
|
|
2852
2975
|
|
|
2853
|
-
// src/scan/ai/templates.ts
|
|
2976
|
+
// src/scanner/scan/ai/templates.ts
|
|
2854
2977
|
var fs5 = __toESM(require("fs"), 1);
|
|
2855
|
-
var
|
|
2978
|
+
var path7 = __toESM(require("path"), 1);
|
|
2856
2979
|
function templatePath(rel) {
|
|
2857
2980
|
const candidates = [
|
|
2858
|
-
|
|
2981
|
+
path7.resolve(__dirname, "../../templates", rel),
|
|
2859
2982
|
// dist/cli/cli.cjs → ../../templates
|
|
2860
|
-
|
|
2983
|
+
path7.resolve(__dirname, "../../../templates", rel)
|
|
2861
2984
|
// src/scan/ai/foo.ts → ../../../templates
|
|
2862
2985
|
];
|
|
2863
2986
|
for (const c of candidates) {
|
|
@@ -2877,19 +3000,20 @@ function readTemplate(rel) {
|
|
|
2877
3000
|
return fs5.readFileSync(templatePath(rel), "utf8");
|
|
2878
3001
|
}
|
|
2879
3002
|
|
|
2880
|
-
// src/scan/ai/providers/claude.ts
|
|
3003
|
+
// src/scanner/scan/ai/providers/claude.ts
|
|
2881
3004
|
var CLAUDE_FILES = [
|
|
2882
3005
|
{ dest: ".claude/rules/uidex.md", template: "claude/rules.md" },
|
|
2883
|
-
{ dest: ".claude/commands/uidex/audit.md", template: "claude/audit.md" }
|
|
3006
|
+
{ dest: ".claude/commands/uidex/audit.md", template: "claude/audit.md" },
|
|
3007
|
+
{ dest: ".claude/commands/uidex/api.md", template: "claude/api.md" }
|
|
2884
3008
|
];
|
|
2885
3009
|
var claudeProvider = {
|
|
2886
3010
|
id: "claude",
|
|
2887
3011
|
label: "Claude Code",
|
|
2888
|
-
description: "Adds .claude/rules/uidex.md and
|
|
3012
|
+
description: "Adds .claude/rules/uidex.md, /uidex:audit, and /uidex:api slash commands.",
|
|
2889
3013
|
async install({ cwd, force }) {
|
|
2890
3014
|
const changes = [];
|
|
2891
3015
|
for (const file of CLAUDE_FILES) {
|
|
2892
|
-
const dest =
|
|
3016
|
+
const dest = path8.join(cwd, file.dest);
|
|
2893
3017
|
const exists = fs6.existsSync(dest);
|
|
2894
3018
|
if (exists && !force) {
|
|
2895
3019
|
changes.push({
|
|
@@ -2899,7 +3023,7 @@ var claudeProvider = {
|
|
|
2899
3023
|
});
|
|
2900
3024
|
continue;
|
|
2901
3025
|
}
|
|
2902
|
-
fs6.mkdirSync(
|
|
3026
|
+
fs6.mkdirSync(path8.dirname(dest), { recursive: true });
|
|
2903
3027
|
fs6.writeFileSync(dest, readTemplate(file.template));
|
|
2904
3028
|
changes.push({
|
|
2905
3029
|
path: file.dest,
|
|
@@ -2911,7 +3035,7 @@ var claudeProvider = {
|
|
|
2911
3035
|
async uninstall({ cwd }) {
|
|
2912
3036
|
const changes = [];
|
|
2913
3037
|
for (const file of CLAUDE_FILES) {
|
|
2914
|
-
const dest =
|
|
3038
|
+
const dest = path8.join(cwd, file.dest);
|
|
2915
3039
|
if (!fs6.existsSync(dest)) {
|
|
2916
3040
|
changes.push({ path: file.dest, action: "skipped", reason: "absent" });
|
|
2917
3041
|
continue;
|
|
@@ -2919,9 +3043,9 @@ var claudeProvider = {
|
|
|
2919
3043
|
fs6.unlinkSync(dest);
|
|
2920
3044
|
changes.push({ path: file.dest, action: "removed" });
|
|
2921
3045
|
}
|
|
2922
|
-
cleanupEmpty(
|
|
2923
|
-
cleanupEmpty(
|
|
2924
|
-
cleanupEmpty(
|
|
3046
|
+
cleanupEmpty(path8.join(cwd, ".claude/commands/uidex"));
|
|
3047
|
+
cleanupEmpty(path8.join(cwd, ".claude/commands"));
|
|
3048
|
+
cleanupEmpty(path8.join(cwd, ".claude/rules"));
|
|
2925
3049
|
return { changes };
|
|
2926
3050
|
}
|
|
2927
3051
|
};
|
|
@@ -2933,13 +3057,13 @@ function cleanupEmpty(dir) {
|
|
|
2933
3057
|
}
|
|
2934
3058
|
}
|
|
2935
3059
|
|
|
2936
|
-
// src/scan/ai/providers/index.ts
|
|
3060
|
+
// src/scanner/scan/ai/providers/index.ts
|
|
2937
3061
|
var PROVIDERS = [claudeProvider];
|
|
2938
3062
|
function getProvider(id) {
|
|
2939
3063
|
return PROVIDERS.find((p2) => p2.id === id);
|
|
2940
3064
|
}
|
|
2941
3065
|
|
|
2942
|
-
// src/scan/ai/index.ts
|
|
3066
|
+
// src/scanner/scan/ai/index.ts
|
|
2943
3067
|
async function runAiCommand(opts) {
|
|
2944
3068
|
const { cwd, argv } = opts;
|
|
2945
3069
|
const sub = argv[0];
|
|
@@ -3050,8 +3174,8 @@ function err(exitCode, stderr) {
|
|
|
3050
3174
|
return { exitCode, stdout: "", stderr };
|
|
3051
3175
|
}
|
|
3052
3176
|
|
|
3053
|
-
// src/
|
|
3054
|
-
function
|
|
3177
|
+
// src/scanner/cli/parse-args.ts
|
|
3178
|
+
function parseArgs(args) {
|
|
3055
3179
|
const positional = [];
|
|
3056
3180
|
const flags = {};
|
|
3057
3181
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -3075,9 +3199,11 @@ function parseFlags2(args) {
|
|
|
3075
3199
|
}
|
|
3076
3200
|
return { positional, flags };
|
|
3077
3201
|
}
|
|
3202
|
+
|
|
3203
|
+
// src/scanner/scan/cli.ts
|
|
3078
3204
|
async function run(opts) {
|
|
3079
3205
|
const cwd = opts.cwd ?? process.cwd();
|
|
3080
|
-
const { positional, flags } =
|
|
3206
|
+
const { positional, flags } = parseArgs(opts.argv);
|
|
3081
3207
|
const command = positional[0] ?? "help";
|
|
3082
3208
|
const writer = createWriter();
|
|
3083
3209
|
try {
|
|
@@ -3121,6 +3247,10 @@ function helpText2() {
|
|
|
3121
3247
|
" scan [flags] Run the scanner pipeline",
|
|
3122
3248
|
" scaffold widget <id> Emit a Playwright spec from a widget's acceptance",
|
|
3123
3249
|
" ai <install|uninstall|providers> Manage AI assistant integrations",
|
|
3250
|
+
" api <METHOD> <PATH> Call the uidex API",
|
|
3251
|
+
" api --list Show available API routes",
|
|
3252
|
+
" api login Authenticate via browser",
|
|
3253
|
+
" api login --token <tok> Store an auth token directly",
|
|
3124
3254
|
"",
|
|
3125
3255
|
"Flags:",
|
|
3126
3256
|
" --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
|
|
@@ -3132,7 +3262,7 @@ function helpText2() {
|
|
|
3132
3262
|
].join("\n");
|
|
3133
3263
|
}
|
|
3134
3264
|
function runInit(cwd, w) {
|
|
3135
|
-
const configPath =
|
|
3265
|
+
const configPath = path9.join(cwd, CONFIG_FILENAME);
|
|
3136
3266
|
if (fs7.existsSync(configPath)) {
|
|
3137
3267
|
w.err(`.uidex.json already exists at ${configPath}`);
|
|
3138
3268
|
return w.result(1);
|
|
@@ -3144,7 +3274,7 @@ function runInit(cwd, w) {
|
|
|
3144
3274
|
};
|
|
3145
3275
|
fs7.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
3146
3276
|
w.out(`Created ${configPath}`);
|
|
3147
|
-
const gitignorePath =
|
|
3277
|
+
const gitignorePath = path9.join(cwd, ".gitignore");
|
|
3148
3278
|
const entry = "*.gen.ts";
|
|
3149
3279
|
if (fs7.existsSync(gitignorePath)) {
|
|
3150
3280
|
const existing = fs7.readFileSync(gitignorePath, "utf8");
|
|
@@ -3222,7 +3352,7 @@ function runScaffold(cwd, args, flags, w) {
|
|
|
3222
3352
|
for (const r of results) {
|
|
3223
3353
|
const widget = r.registry.get("widget", id);
|
|
3224
3354
|
if (!widget) continue;
|
|
3225
|
-
const outDir =
|
|
3355
|
+
const outDir = path9.resolve(r.configDir, "e2e");
|
|
3226
3356
|
const result = scaffoldWidgetSpec({
|
|
3227
3357
|
registry: r.registry,
|
|
3228
3358
|
widgetId: id,
|