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.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
// src/scan/discover.ts
|
|
1
|
+
// src/scanner/scan/discover.ts
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
|
|
5
|
-
// src/scan/config.ts
|
|
5
|
+
// src/scanner/scan/config.ts
|
|
6
6
|
var DEFAULT_TYPE_MODE = "strict";
|
|
7
|
+
var WELL_KNOWN_FILES = {
|
|
8
|
+
page: "uidex.page.ts",
|
|
9
|
+
feature: "uidex.feature.ts"
|
|
10
|
+
};
|
|
7
11
|
var ConfigError = class extends Error {
|
|
8
12
|
constructor(message) {
|
|
9
13
|
super(message);
|
|
@@ -41,14 +45,14 @@ var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set(["scopeLeak", "coverage", "acce
|
|
|
41
45
|
function fail(msg) {
|
|
42
46
|
throw new ConfigError(`Invalid .uidex.json: ${msg}`);
|
|
43
47
|
}
|
|
44
|
-
function assertObject(value,
|
|
48
|
+
function assertObject(value, path10) {
|
|
45
49
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
46
|
-
fail(`${
|
|
50
|
+
fail(`${path10} must be an object`);
|
|
47
51
|
}
|
|
48
52
|
}
|
|
49
|
-
function assertStringArray(value,
|
|
53
|
+
function assertStringArray(value, path10) {
|
|
50
54
|
if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) {
|
|
51
|
-
fail(`${
|
|
55
|
+
fail(`${path10} must be a string[]`);
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
58
|
function validateConfig(raw) {
|
|
@@ -162,7 +166,7 @@ var DEFAULT_CONVENTIONS = {
|
|
|
162
166
|
regions: "landmarks"
|
|
163
167
|
};
|
|
164
168
|
|
|
165
|
-
// src/scan/discover.ts
|
|
169
|
+
// src/scanner/scan/discover.ts
|
|
166
170
|
var CONFIG_FILENAME = ".uidex.json";
|
|
167
171
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
168
172
|
"node_modules",
|
|
@@ -221,7 +225,7 @@ function discover(options = {}) {
|
|
|
221
225
|
return results.sort((a, b) => a.configPath.localeCompare(b.configPath));
|
|
222
226
|
}
|
|
223
227
|
|
|
224
|
-
// src/scan/walk.ts
|
|
228
|
+
// src/scanner/scan/walk.ts
|
|
225
229
|
import * as fs2 from "fs";
|
|
226
230
|
import * as path2 from "path";
|
|
227
231
|
var DEFAULT_INCLUDES = ["**/*.{ts,tsx,js,jsx,mjs,cjs}"];
|
|
@@ -346,7 +350,7 @@ function* walkDir(root, dir) {
|
|
|
346
350
|
}
|
|
347
351
|
}
|
|
348
352
|
|
|
349
|
-
// src/scan/extract-uidex-export.ts
|
|
353
|
+
// src/scanner/scan/extract-uidex-export.ts
|
|
350
354
|
var KIND_DISCRIMINATORS = [
|
|
351
355
|
"page",
|
|
352
356
|
"feature",
|
|
@@ -364,7 +368,13 @@ var ALLOWED_FIELDS = {
|
|
|
364
368
|
"acceptance",
|
|
365
369
|
"description"
|
|
366
370
|
]),
|
|
367
|
-
feature: /* @__PURE__ */ new Set([
|
|
371
|
+
feature: /* @__PURE__ */ new Set([
|
|
372
|
+
"feature",
|
|
373
|
+
"name",
|
|
374
|
+
"features",
|
|
375
|
+
"acceptance",
|
|
376
|
+
"description"
|
|
377
|
+
]),
|
|
368
378
|
primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
|
|
369
379
|
widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "description"]),
|
|
370
380
|
flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
|
|
@@ -1100,7 +1110,7 @@ function buildMetadata(value, file, headerPos, diagnostics) {
|
|
|
1100
1110
|
line: pos.line
|
|
1101
1111
|
});
|
|
1102
1112
|
}
|
|
1103
|
-
const features = kind === "page" ? readStringArrayField(byKey, "features") : void 0;
|
|
1113
|
+
const features = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
|
|
1104
1114
|
const widgets = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
|
|
1105
1115
|
const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
|
|
1106
1116
|
const metadata = {
|
|
@@ -1195,7 +1205,7 @@ function posAt(content, offset) {
|
|
|
1195
1205
|
return { offset, line, column: offset - lineStart + 1 };
|
|
1196
1206
|
}
|
|
1197
1207
|
|
|
1198
|
-
// src/scan/jsx-ancestry.ts
|
|
1208
|
+
// src/scanner/scan/jsx-ancestry.ts
|
|
1199
1209
|
var DATA_ATTR_RE = /\bdata-uidex(?:-(region|widget|primitive))?\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
|
|
1200
1210
|
function parseDataAttrs(tagSource) {
|
|
1201
1211
|
if (!tagSource.includes("data-uidex")) return [];
|
|
@@ -1384,7 +1394,7 @@ function findTagEnd(content, start) {
|
|
|
1384
1394
|
return -1;
|
|
1385
1395
|
}
|
|
1386
1396
|
|
|
1387
|
-
// src/scan/extract.ts
|
|
1397
|
+
// src/scanner/scan/extract.ts
|
|
1388
1398
|
var JSDOC_BLOCK = /\/\*\*([\s\S]*?)\*\//g;
|
|
1389
1399
|
function lineAt(content, index) {
|
|
1390
1400
|
let line = 1;
|
|
@@ -1487,10 +1497,10 @@ function extractOne(file) {
|
|
|
1487
1497
|
return annotations;
|
|
1488
1498
|
}
|
|
1489
1499
|
|
|
1490
|
-
// src/scan/resolve.ts
|
|
1500
|
+
// src/scanner/scan/resolve.ts
|
|
1491
1501
|
import * as path3 from "path";
|
|
1492
1502
|
|
|
1493
|
-
// src/entities/types.ts
|
|
1503
|
+
// src/shared/entities/types.ts
|
|
1494
1504
|
var ENTITY_KINDS = [
|
|
1495
1505
|
"route",
|
|
1496
1506
|
"page",
|
|
@@ -1523,7 +1533,7 @@ function assertEntityKind(kind) {
|
|
|
1523
1533
|
if (!KIND_SET.has(kind)) throw new UnknownEntityKindError(kind);
|
|
1524
1534
|
}
|
|
1525
1535
|
|
|
1526
|
-
// src/entities/registry.ts
|
|
1536
|
+
// src/shared/entities/registry.ts
|
|
1527
1537
|
function emptyStore() {
|
|
1528
1538
|
return {
|
|
1529
1539
|
route: /* @__PURE__ */ new Map(),
|
|
@@ -1605,10 +1615,33 @@ function createRegistry() {
|
|
|
1605
1615
|
return ids.has(entity.id);
|
|
1606
1616
|
});
|
|
1607
1617
|
};
|
|
1608
|
-
|
|
1618
|
+
const reports = /* @__PURE__ */ new Map();
|
|
1619
|
+
const reportsCbs = /* @__PURE__ */ new Set();
|
|
1620
|
+
const setReports = (kind, id, records) => {
|
|
1621
|
+
reports.set(`${kind}:${id}`, records);
|
|
1622
|
+
for (const cb of reportsCbs) cb();
|
|
1623
|
+
};
|
|
1624
|
+
const getReports = (kind, id) => reports.get(`${kind}:${id}`) ?? [];
|
|
1625
|
+
const listReportKeys = () => Array.from(reports.keys());
|
|
1626
|
+
const onReportsChange = (cb) => {
|
|
1627
|
+
reportsCbs.add(cb);
|
|
1628
|
+
return () => reportsCbs.delete(cb);
|
|
1629
|
+
};
|
|
1630
|
+
return {
|
|
1631
|
+
add,
|
|
1632
|
+
get,
|
|
1633
|
+
list,
|
|
1634
|
+
query,
|
|
1635
|
+
byScope,
|
|
1636
|
+
touchedBy,
|
|
1637
|
+
setReports,
|
|
1638
|
+
getReports,
|
|
1639
|
+
listReportKeys,
|
|
1640
|
+
onReportsChange
|
|
1641
|
+
};
|
|
1609
1642
|
}
|
|
1610
1643
|
|
|
1611
|
-
// src/scan/routes.ts
|
|
1644
|
+
// src/scanner/scan/routes.ts
|
|
1612
1645
|
var PAGE_BASENAME = /^page\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
1613
1646
|
var PAGES_ROUTER_BASENAME = /\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
1614
1647
|
var ROUTE_BASENAME = /^route\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
@@ -1674,7 +1707,7 @@ function pathToId(routePath) {
|
|
|
1674
1707
|
return routePath.replace(/^\/+/, "").replace(/\[\.{3}([^\]]+)\]/g, "$1").replace(/\[([^\]]+)\]/g, "$1").replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1675
1708
|
}
|
|
1676
1709
|
|
|
1677
|
-
// src/scan/resolve.ts
|
|
1710
|
+
// src/scanner/scan/resolve.ts
|
|
1678
1711
|
var DOM_ATTR_KINDS = /* @__PURE__ */ new Set([
|
|
1679
1712
|
"element",
|
|
1680
1713
|
"region",
|
|
@@ -1769,7 +1802,24 @@ function resolve2(ctx) {
|
|
|
1769
1802
|
const routes = conventions.pages === "auto" ? detectRoutes(ctx.extracted.map((e) => e.file)) : [];
|
|
1770
1803
|
const handledPageFiles = /* @__PURE__ */ new Set();
|
|
1771
1804
|
for (const route of routes) {
|
|
1772
|
-
const
|
|
1805
|
+
const routeDir = path3.posix.dirname(route.file);
|
|
1806
|
+
const wellKnownPath = `${routeDir}/${WELL_KNOWN_FILES.page}`;
|
|
1807
|
+
const wellKnownExp = exportFor(wellKnownPath, "page");
|
|
1808
|
+
const routeExp = exportFor(route.file, "page");
|
|
1809
|
+
const exp = wellKnownExp ?? routeExp;
|
|
1810
|
+
const locFile = wellKnownExp ? wellKnownPath : route.file;
|
|
1811
|
+
if (wellKnownExp) handledPageFiles.add(wellKnownPath);
|
|
1812
|
+
handledPageFiles.add(route.file);
|
|
1813
|
+
if (wellKnownExp && routeExp) {
|
|
1814
|
+
diagnostics.push({
|
|
1815
|
+
code: "competing-uidex-export",
|
|
1816
|
+
severity: "warning",
|
|
1817
|
+
message: `Page metadata declared in both ${wellKnownPath} and ${route.file}; ${wellKnownPath} takes precedence.`,
|
|
1818
|
+
file: route.file,
|
|
1819
|
+
line: routeExp.loc.line,
|
|
1820
|
+
hint: `Remove the export from ${route.file} or delete ${wellKnownPath}.`
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1773
1823
|
if (exp && exp.id === false) continue;
|
|
1774
1824
|
const effectiveId = exp && typeof exp.id === "string" ? exp.id : route.id;
|
|
1775
1825
|
const meta = exp ? buildMetaFromExport(exp) : void 0;
|
|
@@ -1777,11 +1827,10 @@ function resolve2(ctx) {
|
|
|
1777
1827
|
const page = {
|
|
1778
1828
|
kind: "page",
|
|
1779
1829
|
id: effectiveId,
|
|
1780
|
-
loc: { file:
|
|
1830
|
+
loc: { file: locFile, line: exp?.loc.line },
|
|
1781
1831
|
...meta ? { meta } : {}
|
|
1782
1832
|
};
|
|
1783
1833
|
registry.add(page);
|
|
1784
|
-
handledPageFiles.add(route.file);
|
|
1785
1834
|
}
|
|
1786
1835
|
for (const ef of ctx.extracted) {
|
|
1787
1836
|
const exp = exportFor(ef.file.displayPath, "page");
|
|
@@ -1797,7 +1846,8 @@ function resolve2(ctx) {
|
|
|
1797
1846
|
}
|
|
1798
1847
|
const featureGlob = typeof conventions.features === "string" ? conventions.features : null;
|
|
1799
1848
|
const conventionalFeatureDirs = /* @__PURE__ */ new Set();
|
|
1800
|
-
const
|
|
1849
|
+
const featureExportFilesByDir = /* @__PURE__ */ new Map();
|
|
1850
|
+
const wellKnownFeatureFileByDir = /* @__PURE__ */ new Map();
|
|
1801
1851
|
const suppressedFeatureDirs = /* @__PURE__ */ new Set();
|
|
1802
1852
|
if (featureGlob) {
|
|
1803
1853
|
const re = globToRegExp(featureGlob + "/**");
|
|
@@ -1806,16 +1856,43 @@ function resolve2(ctx) {
|
|
|
1806
1856
|
const dir = extractFeatureDir(ef.file.displayPath, featureGlob);
|
|
1807
1857
|
if (!dir) continue;
|
|
1808
1858
|
conventionalFeatureDirs.add(dir);
|
|
1859
|
+
const isWellKnown = path3.posix.basename(ef.file.displayPath) === WELL_KNOWN_FILES.feature;
|
|
1860
|
+
if (isWellKnown) wellKnownFeatureFileByDir.set(dir, ef.file.displayPath);
|
|
1809
1861
|
const exp = exportFor(ef.file.displayPath, "feature");
|
|
1810
1862
|
if (exp) {
|
|
1811
1863
|
if (exp.id === false) suppressedFeatureDirs.add(dir);
|
|
1812
|
-
else
|
|
1813
|
-
|
|
1864
|
+
else {
|
|
1865
|
+
let arr = featureExportFilesByDir.get(dir);
|
|
1866
|
+
if (!arr) {
|
|
1867
|
+
arr = [];
|
|
1868
|
+
featureExportFilesByDir.set(dir, arr);
|
|
1869
|
+
}
|
|
1870
|
+
arr.push({ file: ef.file.displayPath, exp });
|
|
1871
|
+
}
|
|
1814
1872
|
}
|
|
1815
1873
|
}
|
|
1816
1874
|
for (const dir of conventionalFeatureDirs) {
|
|
1817
1875
|
if (suppressedFeatureDirs.has(dir)) continue;
|
|
1818
|
-
const
|
|
1876
|
+
const allExports = featureExportFilesByDir.get(dir) ?? [];
|
|
1877
|
+
const wellKnownPath = wellKnownFeatureFileByDir.get(dir);
|
|
1878
|
+
const wellKnownEntry = wellKnownPath ? allExports.find((e) => e.file === wellKnownPath) : void 0;
|
|
1879
|
+
let exp;
|
|
1880
|
+
if (wellKnownEntry) {
|
|
1881
|
+
exp = wellKnownEntry.exp;
|
|
1882
|
+
for (const other of allExports) {
|
|
1883
|
+
if (other.file === wellKnownEntry.file) continue;
|
|
1884
|
+
diagnostics.push({
|
|
1885
|
+
code: "competing-uidex-export",
|
|
1886
|
+
severity: "warning",
|
|
1887
|
+
message: `Feature metadata declared in both ${wellKnownEntry.file} and ${other.file}; ${wellKnownEntry.file} takes precedence.`,
|
|
1888
|
+
file: other.file,
|
|
1889
|
+
line: other.exp.loc.line,
|
|
1890
|
+
hint: `Remove the export from ${other.file} or delete ${wellKnownEntry.file}.`
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
} else if (allExports.length > 0) {
|
|
1894
|
+
exp = allExports[0].exp;
|
|
1895
|
+
}
|
|
1819
1896
|
const id = exp && typeof exp.id === "string" ? exp.id : path3.posix.basename(dir);
|
|
1820
1897
|
const meta = exp ? buildMetaFromExport(exp) : void 0;
|
|
1821
1898
|
const feature = {
|
|
@@ -1948,11 +2025,12 @@ function resolve2(ctx) {
|
|
|
1948
2025
|
exp.id,
|
|
1949
2026
|
buildMetaFromExport(exp)
|
|
1950
2027
|
);
|
|
2028
|
+
const scope = computeScope(file);
|
|
1951
2029
|
const primitive = {
|
|
1952
2030
|
kind: "primitive",
|
|
1953
2031
|
id: exp.id,
|
|
1954
2032
|
loc: { file, line: exp.loc.line },
|
|
1955
|
-
scopes: [
|
|
2033
|
+
...scope ? { scopes: [scope] } : {},
|
|
1956
2034
|
...meta ? { meta } : {}
|
|
1957
2035
|
};
|
|
1958
2036
|
registry.add(primitive);
|
|
@@ -1964,11 +2042,12 @@ function resolve2(ctx) {
|
|
|
1964
2042
|
if (domPrimitives.length > 0) {
|
|
1965
2043
|
for (const p2 of domPrimitives) {
|
|
1966
2044
|
const meta = metaWithComposes("primitive", p2.id);
|
|
2045
|
+
const domScope = computeScope(p2.file);
|
|
1967
2046
|
const primitive = {
|
|
1968
2047
|
kind: "primitive",
|
|
1969
2048
|
id: p2.id,
|
|
1970
2049
|
loc: { file: p2.file, line: p2.line },
|
|
1971
|
-
scopes: [
|
|
2050
|
+
...domScope ? { scopes: [domScope] } : {},
|
|
1972
2051
|
...meta ? { meta } : {}
|
|
1973
2052
|
};
|
|
1974
2053
|
registry.add(primitive);
|
|
@@ -1978,13 +2057,13 @@ function resolve2(ctx) {
|
|
|
1978
2057
|
if (primitiveConventions && fileMatchesAny(file, primitiveConventions)) {
|
|
1979
2058
|
const name = kebab(baseName(file));
|
|
1980
2059
|
if (!name) continue;
|
|
1981
|
-
const
|
|
2060
|
+
const convScope = computeScope(file);
|
|
1982
2061
|
const meta = metaWithComposes("primitive", name);
|
|
1983
2062
|
const primitive = {
|
|
1984
2063
|
kind: "primitive",
|
|
1985
2064
|
id: name,
|
|
1986
2065
|
loc: { file },
|
|
1987
|
-
scopes: [
|
|
2066
|
+
...convScope ? { scopes: [convScope] } : {},
|
|
1988
2067
|
...meta ? { meta } : {}
|
|
1989
2068
|
};
|
|
1990
2069
|
registry.add(primitive);
|
|
@@ -2014,7 +2093,8 @@ function resolve2(ctx) {
|
|
|
2014
2093
|
kind: "flow",
|
|
2015
2094
|
id: flowExport.id,
|
|
2016
2095
|
loc: base.loc,
|
|
2017
|
-
touches: base.touches
|
|
2096
|
+
touches: base.touches,
|
|
2097
|
+
steps: base.steps
|
|
2018
2098
|
};
|
|
2019
2099
|
registry.add(flow);
|
|
2020
2100
|
} else {
|
|
@@ -2059,7 +2139,7 @@ function computeScope(displayPath) {
|
|
|
2059
2139
|
if (pagesIdx !== -1 && parts[pagesIdx + 1]) {
|
|
2060
2140
|
return `page:${parts[pagesIdx + 1]}`;
|
|
2061
2141
|
}
|
|
2062
|
-
return
|
|
2142
|
+
return null;
|
|
2063
2143
|
}
|
|
2064
2144
|
function extractFlowsFromSource(file) {
|
|
2065
2145
|
const flows = [];
|
|
@@ -2093,17 +2173,18 @@ function extractFlowsFromSource(file) {
|
|
|
2093
2173
|
kind: "flow",
|
|
2094
2174
|
id,
|
|
2095
2175
|
loc: { file: file.displayPath, line },
|
|
2096
|
-
touches: dedupe(touches.map((t) => t.id))
|
|
2176
|
+
touches: dedupe(touches.map((t) => t.id)),
|
|
2177
|
+
steps: touches.filter((t) => t.action).map((t) => ({ entityId: t.id, action: t.action }))
|
|
2097
2178
|
});
|
|
2098
2179
|
}
|
|
2099
2180
|
return flows;
|
|
2100
2181
|
}
|
|
2101
2182
|
function captureUidexIds(body) {
|
|
2102
2183
|
const out2 = [];
|
|
2103
|
-
const re = /uidex\(\s*(?:'([^']+)'|"([^"]+)"|`([^`$]+)`)\s*\)
|
|
2184
|
+
const re = /uidex\(\s*(?:'([^']+)'|"([^"]+)"|`([^`$]+)`)\s*\)(?:\.(\w+)\s*\()?/g;
|
|
2104
2185
|
let m;
|
|
2105
2186
|
while ((m = re.exec(body)) !== null) {
|
|
2106
|
-
out2.push({ id: m[1] || m[2] || m[3] });
|
|
2187
|
+
out2.push({ id: m[1] || m[2] || m[3], action: m[4] });
|
|
2107
2188
|
}
|
|
2108
2189
|
return out2;
|
|
2109
2190
|
}
|
|
@@ -2111,7 +2192,8 @@ function dedupe(arr) {
|
|
|
2111
2192
|
return Array.from(new Set(arr));
|
|
2112
2193
|
}
|
|
2113
2194
|
|
|
2114
|
-
// src/scan/audit.ts
|
|
2195
|
+
// src/scanner/scan/audit.ts
|
|
2196
|
+
import * as path4 from "path";
|
|
2115
2197
|
var MARKER_FILENAMES = ["UIDEX_PAGE.md", "UIDEX_FEATURE.md"];
|
|
2116
2198
|
function audit(opts) {
|
|
2117
2199
|
const diagnostics = [];
|
|
@@ -2213,6 +2295,32 @@ function audit(opts) {
|
|
|
2213
2295
|
}
|
|
2214
2296
|
}
|
|
2215
2297
|
}
|
|
2298
|
+
if (lint) {
|
|
2299
|
+
const scannedPaths = new Set(files.map((f) => f.displayPath));
|
|
2300
|
+
for (const ef of extracted) {
|
|
2301
|
+
if (!ef.metadata) continue;
|
|
2302
|
+
for (const m of ef.metadata) {
|
|
2303
|
+
if (m.kind !== "page" && m.kind !== "feature") continue;
|
|
2304
|
+
if (typeof m.id !== "string") continue;
|
|
2305
|
+
const filePath = ef.file.displayPath;
|
|
2306
|
+
const wellKnownName = WELL_KNOWN_FILES[m.kind];
|
|
2307
|
+
if (path4.posix.basename(filePath) === wellKnownName) continue;
|
|
2308
|
+
const dir = path4.posix.dirname(filePath);
|
|
2309
|
+
const wellKnownPath = dir === "." ? wellKnownName : `${dir}/${wellKnownName}`;
|
|
2310
|
+
if (scannedPaths.has(wellKnownPath)) continue;
|
|
2311
|
+
const kindLabel = m.kind === "page" ? "Page" : "Feature";
|
|
2312
|
+
diagnostics.push({
|
|
2313
|
+
code: "prefer-well-known-file",
|
|
2314
|
+
severity: "info",
|
|
2315
|
+
message: `${kindLabel} "${m.id}" metadata lives on ${filePath}; prefer ${wellKnownPath}`,
|
|
2316
|
+
file: filePath,
|
|
2317
|
+
line: m.loc.line,
|
|
2318
|
+
entity: { kind: m.kind, id: m.id },
|
|
2319
|
+
hint: `Move the \`export const uidex\` block to ${wellKnownPath} and remove it from ${filePath}.`
|
|
2320
|
+
});
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2216
2324
|
if (lint) {
|
|
2217
2325
|
for (const f of files) {
|
|
2218
2326
|
const lines = f.content.split("\n");
|
|
@@ -2239,6 +2347,15 @@ function audit(opts) {
|
|
|
2239
2347
|
const primitives = registry.list("primitive");
|
|
2240
2348
|
const byName = /* @__PURE__ */ new Map();
|
|
2241
2349
|
for (const p2 of primitives) byName.set(p2.id, p2);
|
|
2350
|
+
const declaredFeatures = /* @__PURE__ */ new Map();
|
|
2351
|
+
for (const ef of extracted) {
|
|
2352
|
+
if (!ef.metadata) continue;
|
|
2353
|
+
for (const m of ef.metadata) {
|
|
2354
|
+
if (m.features && m.features.length > 0) {
|
|
2355
|
+
declaredFeatures.set(ef.file.displayPath, new Set(m.features));
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2242
2359
|
for (const f of files) {
|
|
2243
2360
|
const importRe = /import\s+(?:[^'"]+)\s+from\s+['"]([^'"]+)['"]/g;
|
|
2244
2361
|
let m;
|
|
@@ -2249,18 +2366,23 @@ function audit(opts) {
|
|
|
2249
2366
|
baseName2.replace(/\.(tsx|ts|jsx|js|mjs|cjs)$/, "").replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()
|
|
2250
2367
|
);
|
|
2251
2368
|
if (!primitive) continue;
|
|
2252
|
-
const scope = primitive.scopes?.[0]
|
|
2253
|
-
if (scope
|
|
2369
|
+
const scope = primitive.scopes?.[0];
|
|
2370
|
+
if (!scope) continue;
|
|
2254
2371
|
const [kind, id] = scope.split(":");
|
|
2255
2372
|
const importerSegments = f.displayPath.split("/");
|
|
2256
|
-
if (
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
});
|
|
2373
|
+
if (importerSegments.includes(id) && importerSegments.includes(kind + "s")) {
|
|
2374
|
+
continue;
|
|
2375
|
+
}
|
|
2376
|
+
if (kind === "feature" && importerSegments.includes(id)) continue;
|
|
2377
|
+
if (kind === "feature" && declaredFeatures.get(f.displayPath)?.has(id)) {
|
|
2378
|
+
continue;
|
|
2263
2379
|
}
|
|
2380
|
+
diagnostics.push({
|
|
2381
|
+
code: "scope-leak",
|
|
2382
|
+
severity: "warning",
|
|
2383
|
+
message: `Primitive "${primitive.id}" is scoped to ${scope} but is imported from ${f.displayPath}`,
|
|
2384
|
+
file: f.displayPath
|
|
2385
|
+
});
|
|
2264
2386
|
}
|
|
2265
2387
|
}
|
|
2266
2388
|
}
|
|
@@ -2434,7 +2556,7 @@ function stableReplacer(_key, value) {
|
|
|
2434
2556
|
return value;
|
|
2435
2557
|
}
|
|
2436
2558
|
|
|
2437
|
-
// src/scan/emit.ts
|
|
2559
|
+
// src/scanner/scan/emit.ts
|
|
2438
2560
|
function sortById(arr) {
|
|
2439
2561
|
return [...arr].sort((a, b) => a.id.localeCompare(b.id));
|
|
2440
2562
|
}
|
|
@@ -2558,6 +2680,7 @@ function emit(opts) {
|
|
|
2558
2680
|
lines.push(" export interface Feature {");
|
|
2559
2681
|
lines.push(" feature: FeatureId | false");
|
|
2560
2682
|
lines.push(" name?: string");
|
|
2683
|
+
lines.push(" features?: readonly FeatureId[]");
|
|
2561
2684
|
lines.push(" acceptance?: readonly string[]");
|
|
2562
2685
|
lines.push(" description?: string");
|
|
2563
2686
|
lines.push(" }");
|
|
@@ -2614,7 +2737,7 @@ function emit(opts) {
|
|
|
2614
2737
|
return lines.join("\n");
|
|
2615
2738
|
}
|
|
2616
2739
|
|
|
2617
|
-
// src/scan/git.ts
|
|
2740
|
+
// src/scanner/scan/git.ts
|
|
2618
2741
|
import { execSync } from "child_process";
|
|
2619
2742
|
function runGit(args, cwd) {
|
|
2620
2743
|
try {
|
|
@@ -2646,9 +2769,9 @@ function parseGitHubRef(ref) {
|
|
|
2646
2769
|
return m ? m[1] : null;
|
|
2647
2770
|
}
|
|
2648
2771
|
|
|
2649
|
-
// src/scan/scaffold.ts
|
|
2772
|
+
// src/scanner/scan/scaffold.ts
|
|
2650
2773
|
import * as fs3 from "fs";
|
|
2651
|
-
import * as
|
|
2774
|
+
import * as path5 from "path";
|
|
2652
2775
|
function scaffoldWidgetSpec(opts) {
|
|
2653
2776
|
const {
|
|
2654
2777
|
registry,
|
|
@@ -2663,7 +2786,7 @@ function scaffoldWidgetSpec(opts) {
|
|
|
2663
2786
|
}
|
|
2664
2787
|
const criteria = widget.meta?.acceptance ?? [];
|
|
2665
2788
|
const filename = `widget-${widgetId}.spec.ts`;
|
|
2666
|
-
const outputPath =
|
|
2789
|
+
const outputPath = path5.resolve(outDir, filename);
|
|
2667
2790
|
if (fs3.existsSync(outputPath) && !force) {
|
|
2668
2791
|
return {
|
|
2669
2792
|
outputPath,
|
|
@@ -2677,7 +2800,7 @@ function scaffoldWidgetSpec(opts) {
|
|
|
2677
2800
|
criteria,
|
|
2678
2801
|
fixtureImport
|
|
2679
2802
|
});
|
|
2680
|
-
fs3.mkdirSync(
|
|
2803
|
+
fs3.mkdirSync(path5.dirname(outputPath), { recursive: true });
|
|
2681
2804
|
fs3.writeFileSync(outputPath, content, "utf8");
|
|
2682
2805
|
return { outputPath, written: true, skipped: false };
|
|
2683
2806
|
}
|
|
@@ -2709,9 +2832,9 @@ function renderSpec(args) {
|
|
|
2709
2832
|
return lines.join("\n");
|
|
2710
2833
|
}
|
|
2711
2834
|
|
|
2712
|
-
// src/scan/pipeline.ts
|
|
2835
|
+
// src/scanner/scan/pipeline.ts
|
|
2713
2836
|
import * as fs4 from "fs";
|
|
2714
|
-
import * as
|
|
2837
|
+
import * as path6 from "path";
|
|
2715
2838
|
function runScan(opts = {}) {
|
|
2716
2839
|
const cwd = opts.cwd ?? process.cwd();
|
|
2717
2840
|
const configs = opts.configs ?? discover({ cwd });
|
|
@@ -2743,7 +2866,7 @@ function runOne(dc, opts) {
|
|
|
2743
2866
|
gitContext,
|
|
2744
2867
|
typeMode: config.typeMode
|
|
2745
2868
|
});
|
|
2746
|
-
const outputPath =
|
|
2869
|
+
const outputPath = path6.resolve(configDir, config.output);
|
|
2747
2870
|
const outputRel = config.output;
|
|
2748
2871
|
let existingOnDisk = null;
|
|
2749
2872
|
if (opts.check) {
|
|
@@ -2779,29 +2902,29 @@ function runOne(dc, opts) {
|
|
|
2779
2902
|
};
|
|
2780
2903
|
}
|
|
2781
2904
|
function writeScanResult(result) {
|
|
2782
|
-
fs4.mkdirSync(
|
|
2905
|
+
fs4.mkdirSync(path6.dirname(result.outputPath), { recursive: true });
|
|
2783
2906
|
fs4.writeFileSync(result.outputPath, result.generated, "utf8");
|
|
2784
2907
|
}
|
|
2785
2908
|
|
|
2786
|
-
// src/scan/cli.ts
|
|
2909
|
+
// src/scanner/scan/cli.ts
|
|
2787
2910
|
import * as fs7 from "fs";
|
|
2788
|
-
import * as
|
|
2911
|
+
import * as path9 from "path";
|
|
2789
2912
|
|
|
2790
|
-
// src/scan/ai/index.ts
|
|
2913
|
+
// src/scanner/scan/ai/index.ts
|
|
2791
2914
|
import * as p from "@clack/prompts";
|
|
2792
2915
|
|
|
2793
|
-
// src/scan/ai/providers/claude.ts
|
|
2916
|
+
// src/scanner/scan/ai/providers/claude.ts
|
|
2794
2917
|
import * as fs6 from "fs";
|
|
2795
|
-
import * as
|
|
2918
|
+
import * as path8 from "path";
|
|
2796
2919
|
|
|
2797
|
-
// src/scan/ai/templates.ts
|
|
2920
|
+
// src/scanner/scan/ai/templates.ts
|
|
2798
2921
|
import * as fs5 from "fs";
|
|
2799
|
-
import * as
|
|
2922
|
+
import * as path7 from "path";
|
|
2800
2923
|
function templatePath(rel) {
|
|
2801
2924
|
const candidates = [
|
|
2802
|
-
|
|
2925
|
+
path7.resolve(__dirname, "../../templates", rel),
|
|
2803
2926
|
// dist/cli/cli.cjs → ../../templates
|
|
2804
|
-
|
|
2927
|
+
path7.resolve(__dirname, "../../../templates", rel)
|
|
2805
2928
|
// src/scan/ai/foo.ts → ../../../templates
|
|
2806
2929
|
];
|
|
2807
2930
|
for (const c of candidates) {
|
|
@@ -2821,19 +2944,20 @@ function readTemplate(rel) {
|
|
|
2821
2944
|
return fs5.readFileSync(templatePath(rel), "utf8");
|
|
2822
2945
|
}
|
|
2823
2946
|
|
|
2824
|
-
// src/scan/ai/providers/claude.ts
|
|
2947
|
+
// src/scanner/scan/ai/providers/claude.ts
|
|
2825
2948
|
var CLAUDE_FILES = [
|
|
2826
2949
|
{ dest: ".claude/rules/uidex.md", template: "claude/rules.md" },
|
|
2827
|
-
{ dest: ".claude/commands/uidex/audit.md", template: "claude/audit.md" }
|
|
2950
|
+
{ dest: ".claude/commands/uidex/audit.md", template: "claude/audit.md" },
|
|
2951
|
+
{ dest: ".claude/commands/uidex/api.md", template: "claude/api.md" }
|
|
2828
2952
|
];
|
|
2829
2953
|
var claudeProvider = {
|
|
2830
2954
|
id: "claude",
|
|
2831
2955
|
label: "Claude Code",
|
|
2832
|
-
description: "Adds .claude/rules/uidex.md and
|
|
2956
|
+
description: "Adds .claude/rules/uidex.md, /uidex:audit, and /uidex:api slash commands.",
|
|
2833
2957
|
async install({ cwd, force }) {
|
|
2834
2958
|
const changes = [];
|
|
2835
2959
|
for (const file of CLAUDE_FILES) {
|
|
2836
|
-
const dest =
|
|
2960
|
+
const dest = path8.join(cwd, file.dest);
|
|
2837
2961
|
const exists = fs6.existsSync(dest);
|
|
2838
2962
|
if (exists && !force) {
|
|
2839
2963
|
changes.push({
|
|
@@ -2843,7 +2967,7 @@ var claudeProvider = {
|
|
|
2843
2967
|
});
|
|
2844
2968
|
continue;
|
|
2845
2969
|
}
|
|
2846
|
-
fs6.mkdirSync(
|
|
2970
|
+
fs6.mkdirSync(path8.dirname(dest), { recursive: true });
|
|
2847
2971
|
fs6.writeFileSync(dest, readTemplate(file.template));
|
|
2848
2972
|
changes.push({
|
|
2849
2973
|
path: file.dest,
|
|
@@ -2855,7 +2979,7 @@ var claudeProvider = {
|
|
|
2855
2979
|
async uninstall({ cwd }) {
|
|
2856
2980
|
const changes = [];
|
|
2857
2981
|
for (const file of CLAUDE_FILES) {
|
|
2858
|
-
const dest =
|
|
2982
|
+
const dest = path8.join(cwd, file.dest);
|
|
2859
2983
|
if (!fs6.existsSync(dest)) {
|
|
2860
2984
|
changes.push({ path: file.dest, action: "skipped", reason: "absent" });
|
|
2861
2985
|
continue;
|
|
@@ -2863,9 +2987,9 @@ var claudeProvider = {
|
|
|
2863
2987
|
fs6.unlinkSync(dest);
|
|
2864
2988
|
changes.push({ path: file.dest, action: "removed" });
|
|
2865
2989
|
}
|
|
2866
|
-
cleanupEmpty(
|
|
2867
|
-
cleanupEmpty(
|
|
2868
|
-
cleanupEmpty(
|
|
2990
|
+
cleanupEmpty(path8.join(cwd, ".claude/commands/uidex"));
|
|
2991
|
+
cleanupEmpty(path8.join(cwd, ".claude/commands"));
|
|
2992
|
+
cleanupEmpty(path8.join(cwd, ".claude/rules"));
|
|
2869
2993
|
return { changes };
|
|
2870
2994
|
}
|
|
2871
2995
|
};
|
|
@@ -2877,13 +3001,13 @@ function cleanupEmpty(dir) {
|
|
|
2877
3001
|
}
|
|
2878
3002
|
}
|
|
2879
3003
|
|
|
2880
|
-
// src/scan/ai/providers/index.ts
|
|
3004
|
+
// src/scanner/scan/ai/providers/index.ts
|
|
2881
3005
|
var PROVIDERS = [claudeProvider];
|
|
2882
3006
|
function getProvider(id) {
|
|
2883
3007
|
return PROVIDERS.find((p2) => p2.id === id);
|
|
2884
3008
|
}
|
|
2885
3009
|
|
|
2886
|
-
// src/scan/ai/index.ts
|
|
3010
|
+
// src/scanner/scan/ai/index.ts
|
|
2887
3011
|
async function runAiCommand(opts) {
|
|
2888
3012
|
const { cwd, argv } = opts;
|
|
2889
3013
|
const sub = argv[0];
|
|
@@ -2994,8 +3118,8 @@ function err(exitCode, stderr) {
|
|
|
2994
3118
|
return { exitCode, stdout: "", stderr };
|
|
2995
3119
|
}
|
|
2996
3120
|
|
|
2997
|
-
// src/
|
|
2998
|
-
function
|
|
3121
|
+
// src/scanner/cli/parse-args.ts
|
|
3122
|
+
function parseArgs(args) {
|
|
2999
3123
|
const positional = [];
|
|
3000
3124
|
const flags = {};
|
|
3001
3125
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -3019,9 +3143,11 @@ function parseFlags2(args) {
|
|
|
3019
3143
|
}
|
|
3020
3144
|
return { positional, flags };
|
|
3021
3145
|
}
|
|
3146
|
+
|
|
3147
|
+
// src/scanner/scan/cli.ts
|
|
3022
3148
|
async function run(opts) {
|
|
3023
3149
|
const cwd = opts.cwd ?? process.cwd();
|
|
3024
|
-
const { positional, flags } =
|
|
3150
|
+
const { positional, flags } = parseArgs(opts.argv);
|
|
3025
3151
|
const command = positional[0] ?? "help";
|
|
3026
3152
|
const writer = createWriter();
|
|
3027
3153
|
try {
|
|
@@ -3065,6 +3191,10 @@ function helpText2() {
|
|
|
3065
3191
|
" scan [flags] Run the scanner pipeline",
|
|
3066
3192
|
" scaffold widget <id> Emit a Playwright spec from a widget's acceptance",
|
|
3067
3193
|
" ai <install|uninstall|providers> Manage AI assistant integrations",
|
|
3194
|
+
" api <METHOD> <PATH> Call the uidex API",
|
|
3195
|
+
" api --list Show available API routes",
|
|
3196
|
+
" api login Authenticate via browser",
|
|
3197
|
+
" api login --token <tok> Store an auth token directly",
|
|
3068
3198
|
"",
|
|
3069
3199
|
"Flags:",
|
|
3070
3200
|
" --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
|
|
@@ -3076,7 +3206,7 @@ function helpText2() {
|
|
|
3076
3206
|
].join("\n");
|
|
3077
3207
|
}
|
|
3078
3208
|
function runInit(cwd, w) {
|
|
3079
|
-
const configPath =
|
|
3209
|
+
const configPath = path9.join(cwd, CONFIG_FILENAME);
|
|
3080
3210
|
if (fs7.existsSync(configPath)) {
|
|
3081
3211
|
w.err(`.uidex.json already exists at ${configPath}`);
|
|
3082
3212
|
return w.result(1);
|
|
@@ -3088,7 +3218,7 @@ function runInit(cwd, w) {
|
|
|
3088
3218
|
};
|
|
3089
3219
|
fs7.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
3090
3220
|
w.out(`Created ${configPath}`);
|
|
3091
|
-
const gitignorePath =
|
|
3221
|
+
const gitignorePath = path9.join(cwd, ".gitignore");
|
|
3092
3222
|
const entry = "*.gen.ts";
|
|
3093
3223
|
if (fs7.existsSync(gitignorePath)) {
|
|
3094
3224
|
const existing = fs7.readFileSync(gitignorePath, "utf8");
|
|
@@ -3166,7 +3296,7 @@ function runScaffold(cwd, args, flags, w) {
|
|
|
3166
3296
|
for (const r of results) {
|
|
3167
3297
|
const widget = r.registry.get("widget", id);
|
|
3168
3298
|
if (!widget) continue;
|
|
3169
|
-
const outDir =
|
|
3299
|
+
const outDir = path9.resolve(r.configDir, "e2e");
|
|
3170
3300
|
const result = scaffoldWidgetSpec({
|
|
3171
3301
|
registry: r.registry,
|
|
3172
3302
|
widgetId: id,
|