spec-layer 0.7.0 → 0.8.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/README.md +4 -1
- package/dist/cli.js +180 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,7 +62,10 @@ Module 2025.10 files, rather than `ai/foundation.yaml`, needs 0.4.0 or later.
|
|
|
62
62
|
`ai/components/`, `path` in place of `aiPath` in the manifest, and the
|
|
63
63
|
`outputs` block need 0.6.0 or later. `component-specs/` beside `tokens/`,
|
|
64
64
|
`componentSpecsDir`, cwd-relative manifest paths, and the `tokens/` directory
|
|
65
|
-
need 0.7.0 or later.
|
|
65
|
+
need 0.7.0 or later. The `census` and `config_hash` blocks inside
|
|
66
|
+
`resolver.json`, and the `transform` and `resolved` fields in
|
|
67
|
+
`spec-layer.meta.json`, need 0.8.0 or later; an earlier version pulls the same
|
|
68
|
+
files without those fields.
|
|
66
69
|
|
|
67
70
|
## Commands
|
|
68
71
|
|
package/dist/cli.js
CHANGED
|
@@ -816,6 +816,15 @@ var SUPPORTED_DURATION_UNITS = ["ms", "s"];
|
|
|
816
816
|
|
|
817
817
|
// ../extractor/src/v5/canonical.ts
|
|
818
818
|
var import_js_sha2562 = __toESM(require_sha256(), 1);
|
|
819
|
+
var SCHEMA_VERSION = "5.1.0";
|
|
820
|
+
function canonicalJson(value) {
|
|
821
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
822
|
+
if (value && typeof value === "object") {
|
|
823
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => compareCodeUnits(a, b)).map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`);
|
|
824
|
+
return `{${entries.join(",")}}`;
|
|
825
|
+
}
|
|
826
|
+
return JSON.stringify(value);
|
|
827
|
+
}
|
|
819
828
|
|
|
820
829
|
// ../extractor/src/v5/validate.ts
|
|
821
830
|
var ROOT = "<artifact>";
|
|
@@ -1317,6 +1326,7 @@ function validateLevel1(artifact) {
|
|
|
1317
1326
|
}
|
|
1318
1327
|
|
|
1319
1328
|
// ../extractor/src/v5/dtcg.ts
|
|
1329
|
+
var import_js_sha2563 = __toESM(require_sha256(), 1);
|
|
1320
1330
|
function dtcgSegments(name) {
|
|
1321
1331
|
const segments = [];
|
|
1322
1332
|
const notes = [];
|
|
@@ -1435,6 +1445,15 @@ function sortTree(value) {
|
|
|
1435
1445
|
const keys = Object.keys(value).sort((a, b) => rank(a) - rank(b) || compareCodeUnits(a, b));
|
|
1436
1446
|
return Object.fromEntries(keys.map((k) => [k, sortTree(value[k])]));
|
|
1437
1447
|
}
|
|
1448
|
+
function recordFact(p, tokenId, mode, transform, resolved2) {
|
|
1449
|
+
let facts = p.factsById.get(tokenId);
|
|
1450
|
+
if (!facts) {
|
|
1451
|
+
facts = { transform: {}, resolved: {} };
|
|
1452
|
+
p.factsById.set(tokenId, facts);
|
|
1453
|
+
}
|
|
1454
|
+
facts.transform[mode] = transform;
|
|
1455
|
+
if (resolved2 !== void 0) facts.resolved[mode] = resolved2;
|
|
1456
|
+
}
|
|
1438
1457
|
function reportOnce(p, entry2) {
|
|
1439
1458
|
const key = JSON.stringify([entry2.code, entry2.path, entry2.mode ?? null, entry2.details]);
|
|
1440
1459
|
if (p.reportKeys.has(key)) return;
|
|
@@ -1500,15 +1519,47 @@ function projectedLiteral(p, token, resolved2, onOverrideConflict) {
|
|
|
1500
1519
|
const collection = p.collectionById.get(token.collection_id);
|
|
1501
1520
|
const override = collection ? unitOverrideFor(p, token, collection) : void 0;
|
|
1502
1521
|
let literal = resolved2;
|
|
1522
|
+
let overrode = false;
|
|
1503
1523
|
if (override !== void 0 && literal.type === "number") {
|
|
1504
1524
|
if (token.scopes.some((s) => STATED_NUMBER_SCOPES.includes(s))) onOverrideConflict?.(override);
|
|
1505
|
-
else
|
|
1525
|
+
else {
|
|
1526
|
+
literal = { type: "dimension", number: literal.value, unit: override };
|
|
1527
|
+
overrode = true;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
const converted2 = dtcgLiteral(literal, token.scopes, p.options.values);
|
|
1531
|
+
if ("omit" in converted2) return { converted: converted2, transform: null };
|
|
1532
|
+
return {
|
|
1533
|
+
converted: converted2,
|
|
1534
|
+
transform: overrode ? "number-unit-override" : literalTransform(literal, token.scopes)
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
function literalTransform(value, scopes) {
|
|
1538
|
+
switch (value.type) {
|
|
1539
|
+
case "color":
|
|
1540
|
+
return "color";
|
|
1541
|
+
case "dimension":
|
|
1542
|
+
return "dimension";
|
|
1543
|
+
case "duration":
|
|
1544
|
+
return "duration";
|
|
1545
|
+
case "number":
|
|
1546
|
+
return scopes.includes("FONT_WEIGHT") ? "font-weight" : "number";
|
|
1547
|
+
case "cubic_bezier":
|
|
1548
|
+
return "cubic-bezier";
|
|
1549
|
+
case "font_family":
|
|
1550
|
+
return "font-family";
|
|
1551
|
+
case "string":
|
|
1552
|
+
case "boolean":
|
|
1553
|
+
return null;
|
|
1554
|
+
default: {
|
|
1555
|
+
const exhaustive = value;
|
|
1556
|
+
return exhaustive;
|
|
1557
|
+
}
|
|
1506
1558
|
}
|
|
1507
|
-
return dtcgLiteral(literal, token.scopes, p.options.values);
|
|
1508
1559
|
}
|
|
1509
1560
|
function aliasLeafType(p, token, chain, resolved2) {
|
|
1510
1561
|
const terminal = chain.length > 0 ? p.tokenById.get(chain[chain.length - 1].token_id) : void 0;
|
|
1511
|
-
return projectedLiteral(p, terminal ?? token, resolved2);
|
|
1562
|
+
return projectedLiteral(p, terminal ?? token, resolved2).converted;
|
|
1512
1563
|
}
|
|
1513
1564
|
function modeLabels(collection) {
|
|
1514
1565
|
const counts = /* @__PURE__ */ new Map();
|
|
@@ -1547,6 +1598,17 @@ function reportCollectionNameCollisions(p) {
|
|
|
1547
1598
|
function asJson(value) {
|
|
1548
1599
|
return JSON.parse(JSON.stringify(value));
|
|
1549
1600
|
}
|
|
1601
|
+
function transformField(p, token) {
|
|
1602
|
+
const facts = p.factsById.get(token.id);
|
|
1603
|
+
if (!facts) return {};
|
|
1604
|
+
const sorted = (source) => {
|
|
1605
|
+
const keys = Object.keys(source).sort(compareCodeUnits);
|
|
1606
|
+
return keys.length === 0 ? void 0 : Object.fromEntries(keys.map((k) => [k, source[k]]));
|
|
1607
|
+
};
|
|
1608
|
+
const transform = sorted(facts.transform);
|
|
1609
|
+
const resolved2 = sorted(facts.resolved);
|
|
1610
|
+
return { ...transform ? { transform } : {}, ...resolved2 ? { resolved: resolved2 } : {} };
|
|
1611
|
+
}
|
|
1550
1612
|
function metaEntry(p, token, collection) {
|
|
1551
1613
|
const labels = p.modeLabelsById.get(collection.id) ?? modeLabels(collection);
|
|
1552
1614
|
const omitted = p.omittedIds.has(token.id);
|
|
@@ -1559,6 +1621,7 @@ function metaEntry(p, token, collection) {
|
|
|
1559
1621
|
collection_id: token.collection_id,
|
|
1560
1622
|
type: token.type,
|
|
1561
1623
|
scopes: [...token.scopes],
|
|
1624
|
+
...omitted ? {} : transformField(p, token),
|
|
1562
1625
|
...token.code_syntax ? { code_syntax: token.code_syntax } : {},
|
|
1563
1626
|
...token.publication ? { publication: token.publication } : {},
|
|
1564
1627
|
...omitted ? {
|
|
@@ -1857,6 +1920,64 @@ function annotateGroups(p, tree, collection) {
|
|
|
1857
1920
|
}
|
|
1858
1921
|
}
|
|
1859
1922
|
}
|
|
1923
|
+
var newAccumulator = () => ({
|
|
1924
|
+
tokens: 0,
|
|
1925
|
+
types: /* @__PURE__ */ new Map(),
|
|
1926
|
+
present: 0,
|
|
1927
|
+
missing: 0,
|
|
1928
|
+
aliases: 0,
|
|
1929
|
+
literals: 0,
|
|
1930
|
+
scopes: /* @__PURE__ */ new Map(),
|
|
1931
|
+
codeSyntaxPresent: 0,
|
|
1932
|
+
codeSyntaxMissing: 0,
|
|
1933
|
+
published: 0,
|
|
1934
|
+
hiddenFromPublishing: 0,
|
|
1935
|
+
unstated: 0,
|
|
1936
|
+
omitted: 0,
|
|
1937
|
+
collided: 0
|
|
1938
|
+
});
|
|
1939
|
+
var bump = (counts, key) => {
|
|
1940
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1941
|
+
};
|
|
1942
|
+
var histogram = (counts) => Object.fromEntries([...counts.keys()].sort(compareCodeUnits).map((k) => [k, counts.get(k)]));
|
|
1943
|
+
function censusEntry(a) {
|
|
1944
|
+
return {
|
|
1945
|
+
tokens: a.tokens,
|
|
1946
|
+
types: histogram(a.types),
|
|
1947
|
+
descriptions: { present: a.present, missing: a.missing },
|
|
1948
|
+
aliases: a.aliases,
|
|
1949
|
+
literals: a.literals,
|
|
1950
|
+
scopes: histogram(a.scopes),
|
|
1951
|
+
code_syntax: { present: a.codeSyntaxPresent, missing: a.codeSyntaxMissing },
|
|
1952
|
+
publication: {
|
|
1953
|
+
published: a.published,
|
|
1954
|
+
hidden_from_publishing: a.hiddenFromPublishing,
|
|
1955
|
+
unstated: a.unstated
|
|
1956
|
+
},
|
|
1957
|
+
...a.omitted > 0 ? { omitted: a.omitted } : {},
|
|
1958
|
+
...a.collided > 0 ? { collided: a.collided } : {}
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
function styleCensus(tree) {
|
|
1962
|
+
const a = newAccumulator();
|
|
1963
|
+
const walk = (node) => {
|
|
1964
|
+
if (typeof node !== "object" || node === null || Array.isArray(node)) return;
|
|
1965
|
+
const record = node;
|
|
1966
|
+
if ("$value" in record) {
|
|
1967
|
+
a.tokens += 1;
|
|
1968
|
+
bump(a.types, typeof record.$type === "string" ? record.$type : "unknown");
|
|
1969
|
+
if (typeof record.$description === "string" && record.$description.length > 0) a.present += 1;
|
|
1970
|
+
else a.missing += 1;
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
for (const [key, value] of Object.entries(record)) {
|
|
1974
|
+
if (key.startsWith("$")) continue;
|
|
1975
|
+
walk(value);
|
|
1976
|
+
}
|
|
1977
|
+
};
|
|
1978
|
+
walk(tree);
|
|
1979
|
+
return { tokens: a.tokens, types: histogram(a.types), descriptions: { present: a.present, missing: a.missing } };
|
|
1980
|
+
}
|
|
1860
1981
|
function foundationDtcg(artifact, options = {}) {
|
|
1861
1982
|
const p = {
|
|
1862
1983
|
artifact,
|
|
@@ -1871,7 +1992,8 @@ function foundationDtcg(artifact, options = {}) {
|
|
|
1871
1992
|
omittedIds: /* @__PURE__ */ new Set(),
|
|
1872
1993
|
collidedIds: /* @__PURE__ */ new Set(),
|
|
1873
1994
|
report: [],
|
|
1874
|
-
reportKeys: /* @__PURE__ */ new Set()
|
|
1995
|
+
reportKeys: /* @__PURE__ */ new Set(),
|
|
1996
|
+
factsById: /* @__PURE__ */ new Map()
|
|
1875
1997
|
};
|
|
1876
1998
|
indexPaths(p);
|
|
1877
1999
|
omitInexpressibleTypes(p);
|
|
@@ -1879,23 +2001,46 @@ function foundationDtcg(artifact, options = {}) {
|
|
|
1879
2001
|
reportCollectionNameCollisions(p);
|
|
1880
2002
|
const files = {};
|
|
1881
2003
|
const plans = [];
|
|
2004
|
+
const census = {};
|
|
1882
2005
|
const taken = new Set(RESERVED_FILE_NAMES);
|
|
1883
2006
|
for (const collection of artifact.collections) {
|
|
1884
2007
|
for (const mode of collection.modes) {
|
|
1885
2008
|
const tree = {};
|
|
2009
|
+
const a = newAccumulator();
|
|
1886
2010
|
for (const token of artifact.tokens) {
|
|
1887
|
-
if (token.collection_id !== collection.id
|
|
2011
|
+
if (token.collection_id !== collection.id) continue;
|
|
2012
|
+
if (p.omittedIds.has(token.id)) {
|
|
2013
|
+
a.omitted += 1;
|
|
2014
|
+
if (p.collidedIds.has(token.id)) a.collided += 1;
|
|
2015
|
+
continue;
|
|
2016
|
+
}
|
|
1888
2017
|
const leaf = tokenLeaf(p, token, collection, mode.id);
|
|
1889
|
-
if (leaf)
|
|
2018
|
+
if (!leaf) continue;
|
|
2019
|
+
setLeaf(tree, p.segmentsById.get(token.id) ?? [], leaf);
|
|
2020
|
+
a.tokens += 1;
|
|
2021
|
+
bump(a.types, typeof leaf.$type === "string" ? leaf.$type : "unknown");
|
|
2022
|
+
const modeLabel = modeLabelOf(p, collection, mode.id);
|
|
2023
|
+
if (p.factsById.get(token.id)?.transform[modeLabel] === "alias") a.aliases += 1;
|
|
2024
|
+
else a.literals += 1;
|
|
2025
|
+
if (token.description.length > 0) a.present += 1;
|
|
2026
|
+
else a.missing += 1;
|
|
2027
|
+
for (const scope of token.scopes) bump(a.scopes, scope);
|
|
2028
|
+
if (token.code_syntax) a.codeSyntaxPresent += 1;
|
|
2029
|
+
else a.codeSyntaxMissing += 1;
|
|
2030
|
+
if (token.publication?.published) a.published += 1;
|
|
2031
|
+
if (token.publication?.hidden_from_publishing) a.hiddenFromPublishing += 1;
|
|
2032
|
+
if (!token.publication) a.unstated += 1;
|
|
1890
2033
|
}
|
|
1891
2034
|
annotateGroups(p, tree, collection);
|
|
1892
2035
|
const file = fileNameFor(collection, mode, taken);
|
|
1893
2036
|
plans.push({ collection, modeId: mode.id, file });
|
|
1894
2037
|
files[file] = sortTree(tree);
|
|
2038
|
+
census[file] = censusEntry(a);
|
|
1895
2039
|
}
|
|
1896
2040
|
}
|
|
1897
2041
|
const styles = styleFiles(p);
|
|
1898
2042
|
Object.assign(files, styles);
|
|
2043
|
+
for (const [file, tree] of Object.entries(styles)) census[file] = styleCensus(tree);
|
|
1899
2044
|
const resolver = buildResolver(p, plans, Object.keys(styles).sort(compareCodeUnits));
|
|
1900
2045
|
p.report.sort((a, b) => compareCodeUnits(a.path, b.path) || compareCodeUnits(a.code, b.code) || compareCodeUnits(a.mode ?? "", b.mode ?? ""));
|
|
1901
2046
|
const meta = {};
|
|
@@ -1906,7 +2051,25 @@ function foundationDtcg(artifact, options = {}) {
|
|
|
1906
2051
|
meta[p.collidedIds.has(token.id) ? `${path} [${token.id}]` : path] = metaEntry(p, token, collection);
|
|
1907
2052
|
}
|
|
1908
2053
|
const sortedMeta = Object.fromEntries(Object.entries(meta).sort(([a], [b]) => compareCodeUnits(a, b)));
|
|
1909
|
-
|
|
2054
|
+
const codeSyntax = {};
|
|
2055
|
+
for (const [path, entry2] of Object.entries(sortedMeta)) {
|
|
2056
|
+
if (entry2.code_syntax) codeSyntax[path] = entry2.code_syntax;
|
|
2057
|
+
}
|
|
2058
|
+
const sourceFileName = artifact.spec_layer.source.file_name;
|
|
2059
|
+
const extension = {
|
|
2060
|
+
schema_version: SCHEMA_VERSION,
|
|
2061
|
+
content_hash: artifact.spec_layer.export.content_hash,
|
|
2062
|
+
config_hash: `sha256:${(0, import_js_sha2563.sha256)(canonicalJson(p.options))}`,
|
|
2063
|
+
source: {
|
|
2064
|
+
provider: "figma",
|
|
2065
|
+
...typeof sourceFileName === "string" && sourceFileName.length > 0 ? { file_name: sourceFileName } : {}
|
|
2066
|
+
},
|
|
2067
|
+
completeness: artifact.completeness,
|
|
2068
|
+
code_syntax: codeSyntax,
|
|
2069
|
+
census: Object.fromEntries(Object.keys(census).sort(compareCodeUnits).map((k) => [k, census[k]])),
|
|
2070
|
+
report: p.report
|
|
2071
|
+
};
|
|
2072
|
+
return { files, resolver, meta: sortedMeta, report: p.report, extension };
|
|
1910
2073
|
}
|
|
1911
2074
|
function omitInexpressibleTypes(p) {
|
|
1912
2075
|
for (const token of p.artifact.tokens) {
|
|
@@ -2007,9 +2170,10 @@ function tokenLeaf(p, token, collection, modeId) {
|
|
|
2007
2170
|
});
|
|
2008
2171
|
return null;
|
|
2009
2172
|
}
|
|
2173
|
+
recordFact(p, token.id, mode, "alias", typed.$value);
|
|
2010
2174
|
return { $type: typed.$type, $value: `{${targetPath}}`, ...description };
|
|
2011
2175
|
}
|
|
2012
|
-
const
|
|
2176
|
+
const projected = projectedLiteral(p, token, value.value, (override) => {
|
|
2013
2177
|
reportOnce(p, {
|
|
2014
2178
|
code: "unit_override_conflicts_with_scope",
|
|
2015
2179
|
severity: "warning",
|
|
@@ -2018,6 +2182,7 @@ function tokenLeaf(p, token, collection, modeId) {
|
|
|
2018
2182
|
details: { id: token.id, override, scopes: [...token.scopes] }
|
|
2019
2183
|
});
|
|
2020
2184
|
});
|
|
2185
|
+
const converted2 = projected.converted;
|
|
2021
2186
|
if ("omit" in converted2) {
|
|
2022
2187
|
reportOnce(p, {
|
|
2023
2188
|
code: converted2.omit,
|
|
@@ -2029,6 +2194,7 @@ function tokenLeaf(p, token, collection, modeId) {
|
|
|
2029
2194
|
});
|
|
2030
2195
|
return null;
|
|
2031
2196
|
}
|
|
2197
|
+
if (projected.transform !== null) recordFact(p, token.id, mode, projected.transform);
|
|
2032
2198
|
return { $type: converted2.$type, $value: converted2.$value, ...description };
|
|
2033
2199
|
}
|
|
2034
2200
|
function dtcgExportFiles(out) {
|
|
@@ -2036,7 +2202,10 @@ function dtcgExportFiles(out) {
|
|
|
2036
2202
|
`;
|
|
2037
2203
|
const files = {};
|
|
2038
2204
|
for (const name of Object.keys(out.files).sort(compareCodeUnits)) files[name] = text(out.files[name]);
|
|
2039
|
-
files["resolver.json"] = text(
|
|
2205
|
+
files["resolver.json"] = text({
|
|
2206
|
+
...out.resolver,
|
|
2207
|
+
$extensions: { "com.spec-layer": out.extension }
|
|
2208
|
+
});
|
|
2040
2209
|
files["spec-layer.meta.json"] = text(out.meta);
|
|
2041
2210
|
files["report.json"] = text(out.report);
|
|
2042
2211
|
return files;
|
|
@@ -2546,7 +2715,7 @@ ${imports.join("\n")}
|
|
|
2546
2715
|
}
|
|
2547
2716
|
|
|
2548
2717
|
// ../extractor/src/v5/componentContext.ts
|
|
2549
|
-
var
|
|
2718
|
+
var import_js_sha2564 = __toESM(require_sha256(), 1);
|
|
2550
2719
|
|
|
2551
2720
|
// ../extractor/src/libraryBundle.ts
|
|
2552
2721
|
var LIBRARY_BUNDLE_SCHEMA = "spec-layer-library-bundle";
|
|
@@ -2614,7 +2783,7 @@ function parseLibraryBundle(input) {
|
|
|
2614
2783
|
}
|
|
2615
2784
|
|
|
2616
2785
|
// ../extractor/src/libraryBundleHash.ts
|
|
2617
|
-
var
|
|
2786
|
+
var import_js_sha2565 = __toESM(require_sha256(), 1);
|
|
2618
2787
|
|
|
2619
2788
|
// src/bundle.ts
|
|
2620
2789
|
function parseBundle(raw) {
|