spec-layer 0.6.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 +83 -42
- package/dist/cli.js +501 -151
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -560,7 +560,7 @@ import { parseArgs } from "node:util";
|
|
|
560
560
|
|
|
561
561
|
// src/commands.ts
|
|
562
562
|
import { existsSync as existsSync8 } from "node:fs";
|
|
563
|
-
import { join as
|
|
563
|
+
import { join as join8, resolve as resolve5 } from "node:path";
|
|
564
564
|
|
|
565
565
|
// ../extractor/src/statesMatrix.ts
|
|
566
566
|
var STATE_ORDER = [
|
|
@@ -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 = [];
|
|
@@ -1334,7 +1344,15 @@ function dtcgSegments(name) {
|
|
|
1334
1344
|
}
|
|
1335
1345
|
return { segments, notes };
|
|
1336
1346
|
}
|
|
1337
|
-
|
|
1347
|
+
function trimDashes(s) {
|
|
1348
|
+
let start = 0;
|
|
1349
|
+
let end = s.length;
|
|
1350
|
+
while (start < end && s[start] === "-") start += 1;
|
|
1351
|
+
while (end > start && s[end - 1] === "-") end -= 1;
|
|
1352
|
+
return s.slice(start, end);
|
|
1353
|
+
}
|
|
1354
|
+
var dtcgSlug = (s) => trimDashes(s.toLowerCase().replace(/[^a-z0-9]+/g, "-")) || "unnamed";
|
|
1355
|
+
var slug = dtcgSlug;
|
|
1338
1356
|
var RESERVED_FILE_NAMES = [
|
|
1339
1357
|
"styles.typography.json",
|
|
1340
1358
|
"styles.effects.json",
|
|
@@ -1427,6 +1445,15 @@ function sortTree(value) {
|
|
|
1427
1445
|
const keys = Object.keys(value).sort((a, b) => rank(a) - rank(b) || compareCodeUnits(a, b));
|
|
1428
1446
|
return Object.fromEntries(keys.map((k) => [k, sortTree(value[k])]));
|
|
1429
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
|
+
}
|
|
1430
1457
|
function reportOnce(p, entry2) {
|
|
1431
1458
|
const key = JSON.stringify([entry2.code, entry2.path, entry2.mode ?? null, entry2.details]);
|
|
1432
1459
|
if (p.reportKeys.has(key)) return;
|
|
@@ -1492,15 +1519,47 @@ function projectedLiteral(p, token, resolved2, onOverrideConflict) {
|
|
|
1492
1519
|
const collection = p.collectionById.get(token.collection_id);
|
|
1493
1520
|
const override = collection ? unitOverrideFor(p, token, collection) : void 0;
|
|
1494
1521
|
let literal = resolved2;
|
|
1522
|
+
let overrode = false;
|
|
1495
1523
|
if (override !== void 0 && literal.type === "number") {
|
|
1496
1524
|
if (token.scopes.some((s) => STATED_NUMBER_SCOPES.includes(s))) onOverrideConflict?.(override);
|
|
1497
|
-
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
|
+
}
|
|
1498
1558
|
}
|
|
1499
|
-
return dtcgLiteral(literal, token.scopes, p.options.values);
|
|
1500
1559
|
}
|
|
1501
1560
|
function aliasLeafType(p, token, chain, resolved2) {
|
|
1502
1561
|
const terminal = chain.length > 0 ? p.tokenById.get(chain[chain.length - 1].token_id) : void 0;
|
|
1503
|
-
return projectedLiteral(p, terminal ?? token, resolved2);
|
|
1562
|
+
return projectedLiteral(p, terminal ?? token, resolved2).converted;
|
|
1504
1563
|
}
|
|
1505
1564
|
function modeLabels(collection) {
|
|
1506
1565
|
const counts = /* @__PURE__ */ new Map();
|
|
@@ -1539,6 +1598,17 @@ function reportCollectionNameCollisions(p) {
|
|
|
1539
1598
|
function asJson(value) {
|
|
1540
1599
|
return JSON.parse(JSON.stringify(value));
|
|
1541
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
|
+
}
|
|
1542
1612
|
function metaEntry(p, token, collection) {
|
|
1543
1613
|
const labels = p.modeLabelsById.get(collection.id) ?? modeLabels(collection);
|
|
1544
1614
|
const omitted = p.omittedIds.has(token.id);
|
|
@@ -1551,6 +1621,7 @@ function metaEntry(p, token, collection) {
|
|
|
1551
1621
|
collection_id: token.collection_id,
|
|
1552
1622
|
type: token.type,
|
|
1553
1623
|
scopes: [...token.scopes],
|
|
1624
|
+
...omitted ? {} : transformField(p, token),
|
|
1554
1625
|
...token.code_syntax ? { code_syntax: token.code_syntax } : {},
|
|
1555
1626
|
...token.publication ? { publication: token.publication } : {},
|
|
1556
1627
|
...omitted ? {
|
|
@@ -1849,6 +1920,64 @@ function annotateGroups(p, tree, collection) {
|
|
|
1849
1920
|
}
|
|
1850
1921
|
}
|
|
1851
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
|
+
}
|
|
1852
1981
|
function foundationDtcg(artifact, options = {}) {
|
|
1853
1982
|
const p = {
|
|
1854
1983
|
artifact,
|
|
@@ -1863,7 +1992,8 @@ function foundationDtcg(artifact, options = {}) {
|
|
|
1863
1992
|
omittedIds: /* @__PURE__ */ new Set(),
|
|
1864
1993
|
collidedIds: /* @__PURE__ */ new Set(),
|
|
1865
1994
|
report: [],
|
|
1866
|
-
reportKeys: /* @__PURE__ */ new Set()
|
|
1995
|
+
reportKeys: /* @__PURE__ */ new Set(),
|
|
1996
|
+
factsById: /* @__PURE__ */ new Map()
|
|
1867
1997
|
};
|
|
1868
1998
|
indexPaths(p);
|
|
1869
1999
|
omitInexpressibleTypes(p);
|
|
@@ -1871,23 +2001,46 @@ function foundationDtcg(artifact, options = {}) {
|
|
|
1871
2001
|
reportCollectionNameCollisions(p);
|
|
1872
2002
|
const files = {};
|
|
1873
2003
|
const plans = [];
|
|
2004
|
+
const census = {};
|
|
1874
2005
|
const taken = new Set(RESERVED_FILE_NAMES);
|
|
1875
2006
|
for (const collection of artifact.collections) {
|
|
1876
2007
|
for (const mode of collection.modes) {
|
|
1877
2008
|
const tree = {};
|
|
2009
|
+
const a = newAccumulator();
|
|
1878
2010
|
for (const token of artifact.tokens) {
|
|
1879
|
-
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
|
+
}
|
|
1880
2017
|
const leaf = tokenLeaf(p, token, collection, mode.id);
|
|
1881
|
-
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;
|
|
1882
2033
|
}
|
|
1883
2034
|
annotateGroups(p, tree, collection);
|
|
1884
2035
|
const file = fileNameFor(collection, mode, taken);
|
|
1885
2036
|
plans.push({ collection, modeId: mode.id, file });
|
|
1886
2037
|
files[file] = sortTree(tree);
|
|
2038
|
+
census[file] = censusEntry(a);
|
|
1887
2039
|
}
|
|
1888
2040
|
}
|
|
1889
2041
|
const styles = styleFiles(p);
|
|
1890
2042
|
Object.assign(files, styles);
|
|
2043
|
+
for (const [file, tree] of Object.entries(styles)) census[file] = styleCensus(tree);
|
|
1891
2044
|
const resolver = buildResolver(p, plans, Object.keys(styles).sort(compareCodeUnits));
|
|
1892
2045
|
p.report.sort((a, b) => compareCodeUnits(a.path, b.path) || compareCodeUnits(a.code, b.code) || compareCodeUnits(a.mode ?? "", b.mode ?? ""));
|
|
1893
2046
|
const meta = {};
|
|
@@ -1898,7 +2051,25 @@ function foundationDtcg(artifact, options = {}) {
|
|
|
1898
2051
|
meta[p.collidedIds.has(token.id) ? `${path} [${token.id}]` : path] = metaEntry(p, token, collection);
|
|
1899
2052
|
}
|
|
1900
2053
|
const sortedMeta = Object.fromEntries(Object.entries(meta).sort(([a], [b]) => compareCodeUnits(a, b)));
|
|
1901
|
-
|
|
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 };
|
|
1902
2073
|
}
|
|
1903
2074
|
function omitInexpressibleTypes(p) {
|
|
1904
2075
|
for (const token of p.artifact.tokens) {
|
|
@@ -1999,9 +2170,10 @@ function tokenLeaf(p, token, collection, modeId) {
|
|
|
1999
2170
|
});
|
|
2000
2171
|
return null;
|
|
2001
2172
|
}
|
|
2173
|
+
recordFact(p, token.id, mode, "alias", typed.$value);
|
|
2002
2174
|
return { $type: typed.$type, $value: `{${targetPath}}`, ...description };
|
|
2003
2175
|
}
|
|
2004
|
-
const
|
|
2176
|
+
const projected = projectedLiteral(p, token, value.value, (override) => {
|
|
2005
2177
|
reportOnce(p, {
|
|
2006
2178
|
code: "unit_override_conflicts_with_scope",
|
|
2007
2179
|
severity: "warning",
|
|
@@ -2010,6 +2182,7 @@ function tokenLeaf(p, token, collection, modeId) {
|
|
|
2010
2182
|
details: { id: token.id, override, scopes: [...token.scopes] }
|
|
2011
2183
|
});
|
|
2012
2184
|
});
|
|
2185
|
+
const converted2 = projected.converted;
|
|
2013
2186
|
if ("omit" in converted2) {
|
|
2014
2187
|
reportOnce(p, {
|
|
2015
2188
|
code: converted2.omit,
|
|
@@ -2021,6 +2194,7 @@ function tokenLeaf(p, token, collection, modeId) {
|
|
|
2021
2194
|
});
|
|
2022
2195
|
return null;
|
|
2023
2196
|
}
|
|
2197
|
+
if (projected.transform !== null) recordFact(p, token.id, mode, projected.transform);
|
|
2024
2198
|
return { $type: converted2.$type, $value: converted2.$value, ...description };
|
|
2025
2199
|
}
|
|
2026
2200
|
function dtcgExportFiles(out) {
|
|
@@ -2028,7 +2202,10 @@ function dtcgExportFiles(out) {
|
|
|
2028
2202
|
`;
|
|
2029
2203
|
const files = {};
|
|
2030
2204
|
for (const name of Object.keys(out.files).sort(compareCodeUnits)) files[name] = text(out.files[name]);
|
|
2031
|
-
files["resolver.json"] = text(
|
|
2205
|
+
files["resolver.json"] = text({
|
|
2206
|
+
...out.resolver,
|
|
2207
|
+
$extensions: { "com.spec-layer": out.extension }
|
|
2208
|
+
});
|
|
2032
2209
|
files["spec-layer.meta.json"] = text(out.meta);
|
|
2033
2210
|
files["report.json"] = text(out.report);
|
|
2034
2211
|
return files;
|
|
@@ -2121,6 +2298,7 @@ function resolveNames(paths, meta, rules) {
|
|
|
2121
2298
|
}
|
|
2122
2299
|
|
|
2123
2300
|
// ../extractor/src/v5/outputs/css.ts
|
|
2301
|
+
var CSS_INDEX_FILE = "index.css";
|
|
2124
2302
|
var CSS_DEFAULTS = {
|
|
2125
2303
|
case: "kebab",
|
|
2126
2304
|
root: ":root",
|
|
@@ -2186,6 +2364,23 @@ function sourcesOf(resolver) {
|
|
|
2186
2364
|
}
|
|
2187
2365
|
var modeSlug = (file) => file.replace(/\.json$/, "").split(".").slice(1).join(".");
|
|
2188
2366
|
var collectionSlug = (file) => file.split(".")[0];
|
|
2367
|
+
function cssFileNames(sources) {
|
|
2368
|
+
const taken = /* @__PURE__ */ new Set([CSS_INDEX_FILE]);
|
|
2369
|
+
const out = /* @__PURE__ */ new Map();
|
|
2370
|
+
for (const s of sources) {
|
|
2371
|
+
if (out.has(s.file)) continue;
|
|
2372
|
+
const base = s.mode === null ? dtcgSlug(s.collection) : `${dtcgSlug(s.collection)}.${modeSlug(s.file)}`;
|
|
2373
|
+
let candidate = `${base}.css`;
|
|
2374
|
+
let n = 1;
|
|
2375
|
+
while (taken.has(candidate)) {
|
|
2376
|
+
n += 1;
|
|
2377
|
+
candidate = `${base}-${n}.css`;
|
|
2378
|
+
}
|
|
2379
|
+
taken.add(candidate);
|
|
2380
|
+
out.set(s.file, candidate);
|
|
2381
|
+
}
|
|
2382
|
+
return out;
|
|
2383
|
+
}
|
|
2189
2384
|
var REF = /^\{(.+)\}$/;
|
|
2190
2385
|
function hexChannels(hex) {
|
|
2191
2386
|
const at = (i) => parseInt(hex.slice(i, i + 2), 16);
|
|
@@ -2394,17 +2589,22 @@ function headerText(header, nameCase) {
|
|
|
2394
2589
|
function emitPass(sources, leavesByFile, names, alive, root, template, modes) {
|
|
2395
2590
|
const entries = [];
|
|
2396
2591
|
const declared = /* @__PURE__ */ new Set();
|
|
2592
|
+
const firstFile = /* @__PURE__ */ new Map();
|
|
2397
2593
|
const blocks = /* @__PURE__ */ new Map();
|
|
2398
2594
|
for (const s of sources) {
|
|
2399
2595
|
const perCollection = modes?.[s.collection];
|
|
2400
2596
|
const selector = s.isDefault ? root : (perCollection ?? template).replace(/\{mode\}/g, modeSlug(s.file)).replace(/\{collection\}/g, collectionSlug(s.file));
|
|
2401
2597
|
const decls = [];
|
|
2598
|
+
const declaredHere = [];
|
|
2402
2599
|
for (const leaf of leavesByFile.get(s.file) ?? []) {
|
|
2403
2600
|
const ctx = { names, alive, report: entries, path: leaf.path, ...s.mode !== null ? { mode: s.mode } : {} };
|
|
2404
2601
|
if (leaf.type === "typography") {
|
|
2405
2602
|
const t = typographyDecls(ctx, leaf, names);
|
|
2406
2603
|
decls.push(...t.decls);
|
|
2407
|
-
for (const p of t.declaredPaths)
|
|
2604
|
+
for (const p of t.declaredPaths) {
|
|
2605
|
+
declared.add(p);
|
|
2606
|
+
declaredHere.push(p);
|
|
2607
|
+
}
|
|
2408
2608
|
} else {
|
|
2409
2609
|
const name = names.get(leaf.path);
|
|
2410
2610
|
if (name === void 0) continue;
|
|
@@ -2413,22 +2613,26 @@ function emitPass(sources, leavesByFile, names, alive, root, template, modes) {
|
|
|
2413
2613
|
if (d !== null) {
|
|
2414
2614
|
decls.push(d);
|
|
2415
2615
|
declared.add(leaf.path);
|
|
2616
|
+
declaredHere.push(leaf.path);
|
|
2416
2617
|
}
|
|
2417
2618
|
} else {
|
|
2418
2619
|
const v = cssValue(ctx, leaf.type, leaf.value);
|
|
2419
2620
|
if (v !== null) {
|
|
2420
2621
|
decls.push(`${name}: ${v};`);
|
|
2421
2622
|
declared.add(leaf.path);
|
|
2623
|
+
declaredHere.push(leaf.path);
|
|
2422
2624
|
}
|
|
2423
2625
|
}
|
|
2424
2626
|
}
|
|
2425
2627
|
}
|
|
2426
2628
|
if (decls.length === 0) continue;
|
|
2427
|
-
const
|
|
2428
|
-
|
|
2429
|
-
blocks.
|
|
2629
|
+
for (const p of declaredHere) if (!firstFile.has(p)) firstFile.set(p, s.file);
|
|
2630
|
+
const comment = `/* ${commentSafe(`${s.collection}${s.mode !== null ? `, ${s.mode}` : ""}`)} */`;
|
|
2631
|
+
const existing = blocks.get(s.file);
|
|
2632
|
+
if (existing) existing.decls.push(...decls);
|
|
2633
|
+
else blocks.set(s.file, { selector, comment, decls });
|
|
2430
2634
|
}
|
|
2431
|
-
return { blocks, entries, declared };
|
|
2635
|
+
return { blocks, entries, declared, firstFile };
|
|
2432
2636
|
}
|
|
2433
2637
|
function cssOutput(exp, header, options = {}) {
|
|
2434
2638
|
const nameCase = options.case ?? CSS_DEFAULTS.case;
|
|
@@ -2458,7 +2662,7 @@ function cssOutput(exp, header, options = {}) {
|
|
|
2458
2662
|
const resolved2 = resolveNames([...candidatePaths], exp.meta, {
|
|
2459
2663
|
codeSyntaxKey: "WEB",
|
|
2460
2664
|
acceptDeclared: acceptCssDeclared,
|
|
2461
|
-
affix: (
|
|
2665
|
+
affix: (body) => `--${body}`,
|
|
2462
2666
|
nameCase
|
|
2463
2667
|
});
|
|
2464
2668
|
const names = resolved2.names;
|
|
@@ -2470,9 +2674,11 @@ function cssOutput(exp, header, options = {}) {
|
|
|
2470
2674
|
alive = pass.declared;
|
|
2471
2675
|
pass = emit(alive);
|
|
2472
2676
|
}
|
|
2677
|
+
const fileNames = cssFileNames(sources);
|
|
2473
2678
|
const map = {};
|
|
2474
2679
|
for (const [path, entry2] of Object.entries(resolved2.map)) {
|
|
2475
|
-
|
|
2680
|
+
const from = pass.firstFile.get(path);
|
|
2681
|
+
if (alive.has(path) && from !== void 0) map[path] = { ...entry2, file: fileNames.get(from) };
|
|
2476
2682
|
}
|
|
2477
2683
|
const entries = [...resolved2.report, ...pass.entries];
|
|
2478
2684
|
const shared = [...new Set(sources.filter((s) => !s.isDefault && options.modes?.[s.collection] === void 0).map((s) => s.collection))].sort(compareCodeUnits);
|
|
@@ -2487,14 +2693,29 @@ function cssOutput(exp, header, options = {}) {
|
|
|
2487
2693
|
});
|
|
2488
2694
|
}
|
|
2489
2695
|
}
|
|
2490
|
-
const
|
|
2491
|
-
const
|
|
2696
|
+
const head = headerText(header, nameCase);
|
|
2697
|
+
const files = {};
|
|
2698
|
+
const imports = [];
|
|
2699
|
+
for (const [source, block] of pass.blocks) {
|
|
2700
|
+
const name = fileNames.get(source);
|
|
2701
|
+
files[name] = `${head}
|
|
2702
|
+
|
|
2703
|
+
${block.selector} {
|
|
2704
|
+
${block.comment}
|
|
2705
|
+
${block.decls.map((d) => ` ${d}`).join("\n")}
|
|
2706
|
+
}
|
|
2492
2707
|
`;
|
|
2493
|
-
|
|
2708
|
+
imports.push(block.comment, `@import "./${name}";`);
|
|
2709
|
+
}
|
|
2710
|
+
if (imports.length > 0) files[CSS_INDEX_FILE] = `${head}
|
|
2711
|
+
|
|
2712
|
+
${imports.join("\n")}
|
|
2713
|
+
`;
|
|
2714
|
+
return { files, map, report: sortReport(entries) };
|
|
2494
2715
|
}
|
|
2495
2716
|
|
|
2496
2717
|
// ../extractor/src/v5/componentContext.ts
|
|
2497
|
-
var
|
|
2718
|
+
var import_js_sha2564 = __toESM(require_sha256(), 1);
|
|
2498
2719
|
|
|
2499
2720
|
// ../extractor/src/libraryBundle.ts
|
|
2500
2721
|
var LIBRARY_BUNDLE_SCHEMA = "spec-layer-library-bundle";
|
|
@@ -2562,7 +2783,7 @@ function parseLibraryBundle(input) {
|
|
|
2562
2783
|
}
|
|
2563
2784
|
|
|
2564
2785
|
// ../extractor/src/libraryBundleHash.ts
|
|
2565
|
-
var
|
|
2786
|
+
var import_js_sha2565 = __toESM(require_sha256(), 1);
|
|
2566
2787
|
|
|
2567
2788
|
// src/bundle.ts
|
|
2568
2789
|
function parseBundle(raw) {
|
|
@@ -2585,7 +2806,7 @@ function parseBundle(raw) {
|
|
|
2585
2806
|
|
|
2586
2807
|
// src/config.ts
|
|
2587
2808
|
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "node:fs";
|
|
2588
|
-
import { join as
|
|
2809
|
+
import { join as join4 } from "node:path";
|
|
2589
2810
|
|
|
2590
2811
|
// src/credentials.ts
|
|
2591
2812
|
import { readFileSync, writeFileSync, existsSync, chmodSync } from "node:fs";
|
|
@@ -2768,17 +2989,93 @@ function isAgentHost(value) {
|
|
|
2768
2989
|
}
|
|
2769
2990
|
|
|
2770
2991
|
// src/outputs.ts
|
|
2992
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
2993
|
+
import { resolve as resolve2 } from "node:path";
|
|
2994
|
+
|
|
2995
|
+
// src/visibleDir.ts
|
|
2771
2996
|
import {
|
|
2997
|
+
closeSync,
|
|
2772
2998
|
existsSync as existsSync3,
|
|
2999
|
+
lstatSync,
|
|
2773
3000
|
mkdirSync,
|
|
2774
|
-
|
|
3001
|
+
openSync,
|
|
3002
|
+
readSync,
|
|
3003
|
+
readdirSync as readdirSync2,
|
|
2775
3004
|
renameSync,
|
|
2776
3005
|
rmSync,
|
|
2777
3006
|
writeFileSync as writeFileSync2
|
|
2778
3007
|
} from "node:fs";
|
|
2779
|
-
import {
|
|
3008
|
+
import { isAbsolute, join as join3, relative, resolve } from "node:path";
|
|
3009
|
+
var inside = (parent, child) => {
|
|
3010
|
+
const rel = relative(parent, child);
|
|
3011
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
3012
|
+
};
|
|
3013
|
+
var isDotfile = (name) => name.startsWith(".");
|
|
3014
|
+
function carriesMarker(abs, marker) {
|
|
3015
|
+
const newlines = (marker.match(/\n/g) ?? []).length;
|
|
3016
|
+
const wantBytes = Buffer.byteLength(marker, "utf8") + newlines;
|
|
3017
|
+
const fd = openSync(abs, "r");
|
|
3018
|
+
try {
|
|
3019
|
+
const buf = Buffer.alloc(wantBytes);
|
|
3020
|
+
const read = readSync(fd, buf, 0, wantBytes, 0);
|
|
3021
|
+
const text = buf.subarray(0, read).toString("utf8").replace(/\r\n/g, "\n");
|
|
3022
|
+
return text.startsWith(marker);
|
|
3023
|
+
} finally {
|
|
3024
|
+
closeSync(fd);
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
function visibleDirProblem(cwd, outDir, dir, marker, others, configKey) {
|
|
3028
|
+
const root = resolve(cwd);
|
|
3029
|
+
const abs = resolve(cwd, dir);
|
|
3030
|
+
if (!inside(root, abs) || abs === root) return `${dir} is outside this directory. Choose a path inside the repository.`;
|
|
3031
|
+
if (inside(resolve(cwd, outDir), abs)) return `${dir} is inside ${outDir}, which pull replaces wholesale. Choose a path outside it.`;
|
|
3032
|
+
for (const other of others) {
|
|
3033
|
+
const otherAbs = resolve(cwd, other);
|
|
3034
|
+
if (inside(abs, otherAbs) || inside(otherAbs, abs)) return `${dir} and ${other} overlap. Give each output its own directory.`;
|
|
3035
|
+
}
|
|
3036
|
+
if (!existsSync3(abs)) return null;
|
|
3037
|
+
if (!lstatSync(abs).isDirectory()) return `${dir} exists and is not a directory. Choose another path or remove the file.`;
|
|
3038
|
+
const foreign = `${dir} holds files spec-layer did not write. Set ${configKey} in speclayer.json to another path, or move them.`;
|
|
3039
|
+
for (const entry2 of readdirSync2(abs, { withFileTypes: true })) {
|
|
3040
|
+
if (isDotfile(entry2.name)) continue;
|
|
3041
|
+
if (!entry2.isFile()) return foreign;
|
|
3042
|
+
try {
|
|
3043
|
+
if (!carriesMarker(join3(abs, entry2.name), marker)) return foreign;
|
|
3044
|
+
} catch {
|
|
3045
|
+
return `${dir}/${entry2.name} could not be read.`;
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
return null;
|
|
3049
|
+
}
|
|
3050
|
+
function writeAtomically(abs, text) {
|
|
3051
|
+
const partial = `${abs}.partial`;
|
|
3052
|
+
writeFileSync2(partial, text);
|
|
3053
|
+
try {
|
|
3054
|
+
renameSync(partial, abs);
|
|
3055
|
+
} catch (err) {
|
|
3056
|
+
rmSync(partial, { force: true });
|
|
3057
|
+
throw err;
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
function writeVisibleDir(cwd, dir, marker, files, last) {
|
|
3061
|
+
const abs = resolve(cwd, dir);
|
|
3062
|
+
const names = Object.keys(files).filter((n) => n !== last);
|
|
3063
|
+
if (last !== void 0 && last in files) names.push(last);
|
|
3064
|
+
if (names.length === 0 && !existsSync3(abs)) return [];
|
|
3065
|
+
mkdirSync(abs, { recursive: true });
|
|
3066
|
+
for (const name of names) writeAtomically(join3(abs, name), files[name]);
|
|
3067
|
+
const keep = new Set(names);
|
|
3068
|
+
for (const entry2 of readdirSync2(abs, { withFileTypes: true })) {
|
|
3069
|
+
if (isDotfile(entry2.name) || keep.has(entry2.name) || !entry2.isFile()) continue;
|
|
3070
|
+
const path = join3(abs, entry2.name);
|
|
3071
|
+
if (carriesMarker(path, marker)) rmSync(path);
|
|
3072
|
+
}
|
|
3073
|
+
return names;
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
// src/outputs.ts
|
|
2780
3077
|
var FORMATS = [
|
|
2781
|
-
{ platform: "web", format: "css", defaultPath: "
|
|
3078
|
+
{ platform: "web", format: "css", defaultPath: "tokens", defaultCase: "kebab", headerPrefix: CSS_HEADER_PREFIX }
|
|
2782
3079
|
];
|
|
2783
3080
|
var knownFormats = () => FORMATS.map((f) => `${f.platform}/${f.format}`).join(", ");
|
|
2784
3081
|
var specOf = (platform, format) => FORMATS.find((f) => f.platform === platform && f.format === format) ?? null;
|
|
@@ -2844,45 +3141,27 @@ function renderOutput(exp, o, header) {
|
|
|
2844
3141
|
}
|
|
2845
3142
|
}
|
|
2846
3143
|
}
|
|
2847
|
-
|
|
2848
|
-
const
|
|
2849
|
-
|
|
2850
|
-
};
|
|
2851
|
-
function outputPathProblem(cwd, outDir, o) {
|
|
2852
|
-
const root = resolve(cwd);
|
|
2853
|
-
const abs = resolve(cwd, o.path);
|
|
2854
|
-
if (!inside(root, abs) || abs === root) return `${o.path} is outside this directory. Choose a path inside the repository.`;
|
|
2855
|
-
if (inside(resolve(cwd, outDir), abs)) {
|
|
2856
|
-
return `${o.path} is inside ${outDir}, which pull replaces wholesale. Choose a path outside it.`;
|
|
2857
|
-
}
|
|
2858
|
-
if (existsSync3(abs)) {
|
|
2859
|
-
const prefix = specOf(o.platform, o.format)?.headerPrefix ?? CSS_HEADER_PREFIX;
|
|
2860
|
-
let head;
|
|
2861
|
-
try {
|
|
2862
|
-
head = readFileSync3(abs, "utf8").slice(0, prefix.length);
|
|
2863
|
-
} catch {
|
|
2864
|
-
return `${o.path} exists and could not be read.`;
|
|
2865
|
-
}
|
|
2866
|
-
if (head !== prefix) return `${o.path} exists and was not written by spec-layer. Choose another path or remove the file.`;
|
|
2867
|
-
}
|
|
2868
|
-
return null;
|
|
2869
|
-
}
|
|
2870
|
-
function writeOutputFile(cwd, o, text) {
|
|
2871
|
-
const abs = resolve(cwd, o.path);
|
|
2872
|
-
mkdirSync(dirname(abs), { recursive: true });
|
|
2873
|
-
const partial = `${abs}.partial`;
|
|
2874
|
-
writeFileSync2(partial, text);
|
|
3144
|
+
function readIndexImports(cwd, o) {
|
|
3145
|
+
const path = resolve2(cwd, o.path, CSS_INDEX_FILE);
|
|
3146
|
+
let text;
|
|
2875
3147
|
try {
|
|
2876
|
-
|
|
2877
|
-
} catch
|
|
2878
|
-
|
|
2879
|
-
throw err;
|
|
3148
|
+
text = readFileSync3(path, "utf8");
|
|
3149
|
+
} catch {
|
|
3150
|
+
return null;
|
|
2880
3151
|
}
|
|
3152
|
+
return [...text.matchAll(/^@import "\.\/([^"\n]+)";$/gm)].map((m) => m[1]);
|
|
3153
|
+
}
|
|
3154
|
+
var LEGACY_CSS_PATH_NOTE = (path) => `${path} names a file; spec-layer 0.7.0 writes a directory. Set outputs[].path to a directory, for example "tokens", delete the old file, and pull again.`;
|
|
3155
|
+
function outputPathProblem(cwd, outDir, o, others = []) {
|
|
3156
|
+
if (/\.css$/i.test(o.path)) return LEGACY_CSS_PATH_NOTE(o.path);
|
|
3157
|
+
const marker = specOf(o.platform, o.format)?.headerPrefix ?? CSS_HEADER_PREFIX;
|
|
3158
|
+
return visibleDirProblem(cwd, outDir, o.path, marker, others, "outputs[].path");
|
|
2881
3159
|
}
|
|
2882
3160
|
|
|
2883
3161
|
// src/config.ts
|
|
2884
3162
|
var DEFAULT_API = "https://api.spec-layer.com";
|
|
2885
3163
|
var DEFAULT_OUT_DIR = ".speclayer";
|
|
3164
|
+
var DEFAULT_COMPONENT_SPECS_DIR = "component-specs";
|
|
2886
3165
|
var CONFIG_NAME = "speclayer.json";
|
|
2887
3166
|
var invalidConfig = () => new Error(`${CONFIG_NAME} is not valid JSON. Fix or delete it, then retry.`);
|
|
2888
3167
|
function parseInclude(value) {
|
|
@@ -2914,6 +3193,14 @@ function parseDtcg(value) {
|
|
|
2914
3193
|
}
|
|
2915
3194
|
return out;
|
|
2916
3195
|
}
|
|
3196
|
+
function parseComponentSpecsDir(value) {
|
|
3197
|
+
if (typeof value !== "string" || value.length === 0) throw new Error('speclayer.json "componentSpecsDir" must be a non-empty string.');
|
|
3198
|
+
let dir = value.replace(/\\/g, "/");
|
|
3199
|
+
if (dir.startsWith("./")) dir = dir.slice(2);
|
|
3200
|
+
dir = dir.replace(/\/+$/, "");
|
|
3201
|
+
if (dir.length === 0) throw new Error('speclayer.json "componentSpecsDir" must be a non-empty string.');
|
|
3202
|
+
return dir;
|
|
3203
|
+
}
|
|
2917
3204
|
function parsePlatforms(value) {
|
|
2918
3205
|
if (!Array.isArray(value) || !value.every((p) => typeof p === "string" && isPlatform(p))) {
|
|
2919
3206
|
throw new Error(`speclayer.json "platforms" must be an array of ${PLATFORMS.join(", ")}.`);
|
|
@@ -2934,7 +3221,7 @@ function parseOutputs(value) {
|
|
|
2934
3221
|
return outputs;
|
|
2935
3222
|
}
|
|
2936
3223
|
function readConfig(cwd) {
|
|
2937
|
-
const path =
|
|
3224
|
+
const path = join4(cwd, CONFIG_NAME);
|
|
2938
3225
|
if (!existsSync4(path)) return null;
|
|
2939
3226
|
let parsed;
|
|
2940
3227
|
try {
|
|
@@ -2947,6 +3234,7 @@ function readConfig(cwd) {
|
|
|
2947
3234
|
return {
|
|
2948
3235
|
...typeof record.libraryId === "string" ? { libraryId: record.libraryId } : {},
|
|
2949
3236
|
...typeof record.outDir === "string" ? { outDir: record.outDir } : {},
|
|
3237
|
+
...record.componentSpecsDir !== void 0 ? { componentSpecsDir: parseComponentSpecsDir(record.componentSpecsDir) } : {},
|
|
2950
3238
|
...record.include !== void 0 ? { include: parseInclude(record.include) } : {},
|
|
2951
3239
|
...record.dtcg !== void 0 ? { dtcg: parseDtcg(record.dtcg) } : {},
|
|
2952
3240
|
...record.platforms !== void 0 ? { platforms: parsePlatforms(record.platforms) } : {},
|
|
@@ -2957,18 +3245,19 @@ function writeConfig(cwd, config) {
|
|
|
2957
3245
|
const body = {
|
|
2958
3246
|
libraryId: config.libraryId,
|
|
2959
3247
|
outDir: config.outDir,
|
|
3248
|
+
...config.componentSpecsDir ? { componentSpecsDir: config.componentSpecsDir } : {},
|
|
2960
3249
|
...config.include ? { include: config.include } : {},
|
|
2961
3250
|
...config.dtcg ? { dtcg: config.dtcg } : {},
|
|
2962
3251
|
...config.platforms && config.platforms.length > 0 ? { platforms: config.platforms } : {},
|
|
2963
3252
|
...config.outputs ? { outputs: config.outputs } : {}
|
|
2964
3253
|
};
|
|
2965
|
-
writeFileSync3(
|
|
3254
|
+
writeFileSync3(join4(cwd, CONFIG_NAME), `${JSON.stringify(body, null, 2)}
|
|
2966
3255
|
`);
|
|
2967
3256
|
}
|
|
2968
3257
|
function resolveOptions(cwd, flags, env, manifestLibraryId) {
|
|
2969
3258
|
const config = readConfig(cwd);
|
|
2970
3259
|
const outDir = flags.out ?? config?.outDir ?? DEFAULT_OUT_DIR;
|
|
2971
|
-
const libraryId = flags.id ?? config?.libraryId ?? manifestLibraryId(
|
|
3260
|
+
const libraryId = flags.id ?? config?.libraryId ?? manifestLibraryId(join4(cwd, outDir));
|
|
2972
3261
|
const supplied = flags.key || env.SPEC_LAYER_KEY || null;
|
|
2973
3262
|
let storedKey = null;
|
|
2974
3263
|
let storedKeyFor;
|
|
@@ -2982,6 +3271,7 @@ function resolveOptions(cwd, flags, env, manifestLibraryId) {
|
|
|
2982
3271
|
return {
|
|
2983
3272
|
libraryId,
|
|
2984
3273
|
outDir,
|
|
3274
|
+
componentSpecsDir: config?.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR,
|
|
2985
3275
|
// A trailing slash would build "//v1/..." paths the proxy router 404s on.
|
|
2986
3276
|
api: (flags.api ?? env.SPEC_LAYER_API ?? DEFAULT_API).replace(/\/+$/, ""),
|
|
2987
3277
|
key: supplied ?? storedKey,
|
|
@@ -3027,8 +3317,8 @@ async function fetchBundle(opts) {
|
|
|
3027
3317
|
}
|
|
3028
3318
|
|
|
3029
3319
|
// src/files.ts
|
|
3030
|
-
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4, readFileSync as readFileSync5, readdirSync as
|
|
3031
|
-
import { join as
|
|
3320
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4, readFileSync as readFileSync5, readdirSync as readdirSync3, rmSync as rmSync2, renameSync as renameSync2, existsSync as existsSync5 } from "node:fs";
|
|
3321
|
+
import { join as join5, dirname, relative as relative2, resolve as resolve3, isAbsolute as isAbsolute2, sep } from "node:path";
|
|
3032
3322
|
|
|
3033
3323
|
// src/selection.ts
|
|
3034
3324
|
var DEFAULT_SELECTION = { foundation: true, components: null };
|
|
@@ -3066,8 +3356,9 @@ function slugify(name) {
|
|
|
3066
3356
|
const slug2 = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3067
3357
|
return slug2 || "component";
|
|
3068
3358
|
}
|
|
3359
|
+
var COMPONENT_SPEC_MARKER = "spec_layer:\n kind: component";
|
|
3069
3360
|
function readManifest(outDir) {
|
|
3070
|
-
const path =
|
|
3361
|
+
const path = join5(outDir, "manifest.json");
|
|
3071
3362
|
if (!existsSync5(path)) return null;
|
|
3072
3363
|
try {
|
|
3073
3364
|
const parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
@@ -3084,7 +3375,7 @@ function readManifest(outDir) {
|
|
|
3084
3375
|
}
|
|
3085
3376
|
}
|
|
3086
3377
|
function readLocalBundle(outDir) {
|
|
3087
|
-
const path =
|
|
3378
|
+
const path = join5(outDir, "bundle.json");
|
|
3088
3379
|
if (!existsSync5(path)) return null;
|
|
3089
3380
|
try {
|
|
3090
3381
|
return parseBundle(readFileSync5(path, "utf8"));
|
|
@@ -3114,11 +3405,11 @@ function componentSlugs(bundle) {
|
|
|
3114
3405
|
});
|
|
3115
3406
|
}
|
|
3116
3407
|
function assertReplaceable(outDir, cwd) {
|
|
3117
|
-
const rel = relative2(
|
|
3408
|
+
const rel = relative2(resolve3(cwd), resolve3(outDir));
|
|
3118
3409
|
if (rel === "" || rel.startsWith("..") || isAbsolute2(rel)) {
|
|
3119
3410
|
throw new Error('The output directory must sit inside the current directory, not be "." or a parent of it.');
|
|
3120
3411
|
}
|
|
3121
|
-
if (existsSync5(outDir) && !existsSync5(
|
|
3412
|
+
if (existsSync5(outDir) && !existsSync5(join5(outDir, "manifest.json")) && readdirSync3(outDir).length > 0) {
|
|
3122
3413
|
throw new Error(`${outDir} exists and was not written by spec-layer pull. Choose an empty or new directory.`);
|
|
3123
3414
|
}
|
|
3124
3415
|
}
|
|
@@ -3128,13 +3419,23 @@ function writeBundleFiles(opts) {
|
|
|
3128
3419
|
const selected = selectComponents(opts.bundle, selection);
|
|
3129
3420
|
const slugs = componentSlugs(opts.bundle);
|
|
3130
3421
|
const outputs = opts.outputs ?? [];
|
|
3131
|
-
const
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3422
|
+
const componentSpecsDir = opts.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR;
|
|
3423
|
+
const outDirRel = relative2(resolve3(opts.cwd), resolve3(opts.outDir)).split(sep).join("/");
|
|
3424
|
+
const outputPaths = outputs.map((o) => o.path);
|
|
3425
|
+
const specsProblem = visibleDirProblem(opts.cwd, outDirRel, componentSpecsDir, COMPONENT_SPEC_MARKER, outputPaths, "componentSpecsDir");
|
|
3426
|
+
if (specsProblem) throw new Error(specsProblem);
|
|
3427
|
+
for (const o of outputs) {
|
|
3428
|
+
const problem = outputPathProblem(opts.cwd, outDirRel, o, [componentSpecsDir, ...outputPaths.filter((p) => p !== o.path)]);
|
|
3429
|
+
if (problem) throw new Error(problem);
|
|
3430
|
+
}
|
|
3431
|
+
const briefs = {};
|
|
3432
|
+
opts.bundle.components.forEach((component, i) => {
|
|
3433
|
+
if (!selected[i]) return;
|
|
3434
|
+
if (!component.ai.startsWith(COMPONENT_SPEC_MARKER)) {
|
|
3435
|
+
throw new Error(`The published brief for ${component.name} does not begin with the Spec Layer marker. Republish from the plugin, then pull again.`);
|
|
3136
3436
|
}
|
|
3137
|
-
|
|
3437
|
+
briefs[`${slugs[i]}.yaml`] = component.ai;
|
|
3438
|
+
});
|
|
3138
3439
|
const staging = `${opts.outDir}.partial`;
|
|
3139
3440
|
rmSync2(staging, { recursive: true, force: true });
|
|
3140
3441
|
const written = [];
|
|
@@ -3142,8 +3443,8 @@ function writeBundleFiles(opts) {
|
|
|
3142
3443
|
const json = (v) => `${JSON.stringify(v, null, 2)}
|
|
3143
3444
|
`;
|
|
3144
3445
|
const put = (rel, content) => {
|
|
3145
|
-
const path =
|
|
3146
|
-
mkdirSync2(
|
|
3446
|
+
const path = join5(staging, rel);
|
|
3447
|
+
mkdirSync2(dirname(path), { recursive: true });
|
|
3147
3448
|
writeFileSync4(path, content);
|
|
3148
3449
|
written.push(rel);
|
|
3149
3450
|
};
|
|
@@ -3159,13 +3460,13 @@ function writeBundleFiles(opts) {
|
|
|
3159
3460
|
}
|
|
3160
3461
|
const exp = foundationDtcg(artifact, opts.dtcg ?? {});
|
|
3161
3462
|
for (const [name, text] of Object.entries(dtcgExportFiles(exp))) put(`tokens/${name}`, text);
|
|
3162
|
-
path =
|
|
3463
|
+
path = `${outDirRel}/tokens/resolver.json`;
|
|
3163
3464
|
const header = { libraryId: opts.libraryId, contentHash: opts.bundle.foundation.artifact.spec_layer.export.content_hash };
|
|
3164
3465
|
for (const output of outputs) {
|
|
3165
3466
|
const rendered = renderOutput(exp, output, header);
|
|
3166
3467
|
put(`outputs/${outputId(output)}.map.json`, json(rendered.map));
|
|
3167
3468
|
put(`outputs/${outputId(output)}.report.json`, json(rendered.report));
|
|
3168
|
-
deliverables.push({ output,
|
|
3469
|
+
deliverables.push({ output, files: rendered.files });
|
|
3169
3470
|
}
|
|
3170
3471
|
}
|
|
3171
3472
|
artifacts.push({
|
|
@@ -3176,13 +3477,11 @@ function writeBundleFiles(opts) {
|
|
|
3176
3477
|
});
|
|
3177
3478
|
}
|
|
3178
3479
|
opts.bundle.components.forEach((component, i) => {
|
|
3179
|
-
const path = selected[i] ? `components/${slugs[i]}.yaml` : null;
|
|
3180
|
-
if (path) put(path, component.ai);
|
|
3181
3480
|
artifacts.push({
|
|
3182
3481
|
kind: "component",
|
|
3183
3482
|
name: component.name,
|
|
3184
3483
|
contentHash: component.artifact.spec_layer.export.content_hash,
|
|
3185
|
-
path
|
|
3484
|
+
path: selected[i] ? `${componentSpecsDir}/${slugs[i]}.yaml` : null
|
|
3186
3485
|
});
|
|
3187
3486
|
});
|
|
3188
3487
|
const manifest = {
|
|
@@ -3192,6 +3491,7 @@ function writeBundleFiles(opts) {
|
|
|
3192
3491
|
pluginVersion: opts.bundle.pluginVersion,
|
|
3193
3492
|
extractorVersion: opts.bundle.extractorVersion,
|
|
3194
3493
|
selection,
|
|
3494
|
+
componentSpecsDir,
|
|
3195
3495
|
artifacts,
|
|
3196
3496
|
...opts.dtcg && Object.keys(opts.dtcg).length > 0 ? { dtcg: opts.dtcg } : {},
|
|
3197
3497
|
...opts.platforms && opts.platforms.length > 0 ? { platforms: opts.platforms } : {},
|
|
@@ -3204,18 +3504,18 @@ function writeBundleFiles(opts) {
|
|
|
3204
3504
|
}
|
|
3205
3505
|
rmSync2(opts.outDir, { recursive: true, force: true });
|
|
3206
3506
|
renameSync2(staging, opts.outDir);
|
|
3207
|
-
const
|
|
3507
|
+
const componentSpecs = { path: componentSpecsDir, files: writeVisibleDir(opts.cwd, componentSpecsDir, COMPONENT_SPEC_MARKER, briefs) };
|
|
3508
|
+
const outputResults = [];
|
|
3208
3509
|
for (const d of deliverables) {
|
|
3209
|
-
|
|
3210
|
-
outputPaths.push(d.output.path);
|
|
3510
|
+
outputResults.push({ path: d.output.path, files: writeVisibleDir(opts.cwd, d.output.path, CSS_HEADER_PREFIX, d.files, CSS_INDEX_FILE) });
|
|
3211
3511
|
}
|
|
3212
|
-
return { written, outputs:
|
|
3512
|
+
return { written, componentSpecs, outputs: outputResults };
|
|
3213
3513
|
}
|
|
3214
3514
|
|
|
3215
3515
|
// src/gitignore.ts
|
|
3216
3516
|
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync6 } from "node:fs";
|
|
3217
3517
|
import { spawnSync } from "node:child_process";
|
|
3218
|
-
import { join as
|
|
3518
|
+
import { join as join6, dirname as dirname2, resolve as resolve4 } from "node:path";
|
|
3219
3519
|
var COMMENT = "# Spec Layer pull key, not for committing";
|
|
3220
3520
|
function git(cwd, args) {
|
|
3221
3521
|
const res = spawnSync("git", args, { cwd, stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" });
|
|
@@ -3223,10 +3523,10 @@ function git(cwd, args) {
|
|
|
3223
3523
|
return { ranGit: true, status: res.status, stdout: res.stdout ?? "" };
|
|
3224
3524
|
}
|
|
3225
3525
|
function insideWorkTreeWithoutGit(cwd) {
|
|
3226
|
-
let dir =
|
|
3526
|
+
let dir = resolve4(cwd);
|
|
3227
3527
|
for (; ; ) {
|
|
3228
|
-
if (existsSync6(
|
|
3229
|
-
const parent =
|
|
3528
|
+
if (existsSync6(join6(dir, ".git"))) return true;
|
|
3529
|
+
const parent = dirname2(dir);
|
|
3230
3530
|
if (parent === dir) return false;
|
|
3231
3531
|
dir = parent;
|
|
3232
3532
|
}
|
|
@@ -3242,7 +3542,7 @@ function ensureIgnored(cwd, fileName) {
|
|
|
3242
3542
|
if (inWorkTree.status !== 0 || inWorkTree.stdout.trim() !== "true") return { kind: "not-a-repo" };
|
|
3243
3543
|
const checkIgnore = git(cwd, ["check-ignore", "-q", fileName]);
|
|
3244
3544
|
if (checkIgnore.ranGit && checkIgnore.status === 0) return { kind: "already" };
|
|
3245
|
-
const path =
|
|
3545
|
+
const path = join6(cwd, ".gitignore");
|
|
3246
3546
|
const existed = existsSync6(path);
|
|
3247
3547
|
try {
|
|
3248
3548
|
if (!existed) {
|
|
@@ -3267,8 +3567,8 @@ ${fileName}
|
|
|
3267
3567
|
}
|
|
3268
3568
|
|
|
3269
3569
|
// src/skill.ts
|
|
3270
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as
|
|
3271
|
-
import { dirname as
|
|
3570
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
|
|
3571
|
+
import { dirname as dirname3, join as join7 } from "node:path";
|
|
3272
3572
|
|
|
3273
3573
|
// src/tools.ts
|
|
3274
3574
|
var OK_OR_ERROR = { "0": "success", "1": "usage error, bad key or id, or a network or server failure" };
|
|
@@ -3286,7 +3586,8 @@ var TOOLS = [
|
|
|
3286
3586
|
"speclayer.local.json",
|
|
3287
3587
|
".gitignore (one line, when inside a git repo)",
|
|
3288
3588
|
"<outDir>/",
|
|
3289
|
-
"outputs[].path from speclayer.json (default
|
|
3589
|
+
"outputs[].path from speclayer.json (default tokens/ for web), a directory written in place",
|
|
3590
|
+
"componentSpecsDir from speclayer.json (default component-specs/), written in place"
|
|
3290
3591
|
],
|
|
3291
3592
|
exits: OK_OR_ERROR
|
|
3292
3593
|
},
|
|
@@ -3303,11 +3604,15 @@ var TOOLS = [
|
|
|
3303
3604
|
{
|
|
3304
3605
|
name: "pull",
|
|
3305
3606
|
usage: "spec-layer pull [--id lib_...] [--key sl_...] [--out DIR] [--platform web|ios|android|flutter]... [--only foundation|components] [--component NAME]...",
|
|
3306
|
-
summary: "Fetches the published library and writes
|
|
3307
|
-
when: "After setup, whenever status says the local copy is behind, or after changing the include
|
|
3607
|
+
summary: "Fetches the published library and writes the record under the output directory (default .speclayer/), the briefs under componentSpecsDir, and the token files under outputs[].path.",
|
|
3608
|
+
when: "After setup, whenever status says the local copy is behind, or after changing the include, dtcg, outputs, or componentSpecsDir blocks.",
|
|
3308
3609
|
network: true,
|
|
3309
3610
|
needsKey: true,
|
|
3310
|
-
writes: [
|
|
3611
|
+
writes: [
|
|
3612
|
+
"<outDir>/",
|
|
3613
|
+
"outputs[].path from speclayer.json (default tokens/ for web), a directory written in place",
|
|
3614
|
+
"componentSpecsDir from speclayer.json (default component-specs/), written in place"
|
|
3615
|
+
],
|
|
3311
3616
|
exits: OK_OR_ERROR
|
|
3312
3617
|
},
|
|
3313
3618
|
{
|
|
@@ -3416,24 +3721,24 @@ function readJson(path) {
|
|
|
3416
3721
|
}
|
|
3417
3722
|
function summarizePull(cwd, outDir, manifest) {
|
|
3418
3723
|
if (!manifest) return null;
|
|
3419
|
-
const absOut =
|
|
3420
|
-
const components = manifest.artifacts.filter((a) => a.kind === "component").map((a) => ({ name: a.name, path: a.path
|
|
3724
|
+
const absOut = join7(cwd, outDir);
|
|
3725
|
+
const components = manifest.artifacts.filter((a) => a.kind === "component").map((a) => ({ name: a.name, path: a.path }));
|
|
3421
3726
|
const foundationEntry = manifest.artifacts.find((a) => a.kind === "foundation") ?? null;
|
|
3422
3727
|
let foundation = null;
|
|
3423
3728
|
if (foundationEntry) {
|
|
3424
|
-
const tokensDir =
|
|
3425
|
-
const resolver = readJson(
|
|
3426
|
-
const report2 = readJson(
|
|
3729
|
+
const tokensDir = join7(absOut, "tokens");
|
|
3730
|
+
const resolver = readJson(join7(tokensDir, "resolver.json"));
|
|
3731
|
+
const report2 = readJson(join7(tokensDir, "report.json"));
|
|
3427
3732
|
let tokenFiles = [];
|
|
3428
3733
|
try {
|
|
3429
|
-
tokenFiles =
|
|
3734
|
+
tokenFiles = readdirSync4(tokensDir).filter((f) => f.endsWith(".json") && !RESERVED.has(f)).sort();
|
|
3430
3735
|
} catch {
|
|
3431
3736
|
tokenFiles = [];
|
|
3432
3737
|
}
|
|
3433
3738
|
let unitlessNumbers = 0;
|
|
3434
3739
|
for (const file of tokenFiles) {
|
|
3435
3740
|
if (file.startsWith("styles.")) continue;
|
|
3436
|
-
unitlessNumbers += countNumberTokens(readJson(
|
|
3741
|
+
unitlessNumbers += countNumberTokens(readJson(join7(tokensDir, file)));
|
|
3437
3742
|
}
|
|
3438
3743
|
const reportCounts = {};
|
|
3439
3744
|
if (Array.isArray(report2)) {
|
|
@@ -3459,20 +3764,33 @@ function summarizePull(cwd, outDir, manifest) {
|
|
|
3459
3764
|
libraryId: manifest.libraryId,
|
|
3460
3765
|
publishedAt: manifest.publishedAt,
|
|
3461
3766
|
pluginVersion: manifest.pluginVersion,
|
|
3767
|
+
componentSpecsDir: manifest.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR,
|
|
3462
3768
|
components,
|
|
3463
3769
|
foundation,
|
|
3464
|
-
outputs: (manifest.outputs ?? []).map((o) =>
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3770
|
+
outputs: (manifest.outputs ?? []).map((o) => {
|
|
3771
|
+
const mapPath = join7(absOut, "outputs", `${o.platform}-${o.format}.map.json`);
|
|
3772
|
+
const map = readJson(mapPath);
|
|
3773
|
+
const imports = map ? readIndexImports(cwd, o) : null;
|
|
3774
|
+
const files = imports !== null ? [...imports, CSS_INDEX_FILE] : [];
|
|
3775
|
+
return {
|
|
3776
|
+
platform: o.platform,
|
|
3777
|
+
format: o.format,
|
|
3778
|
+
path: o.path,
|
|
3779
|
+
case: o.case,
|
|
3780
|
+
modeSelector: o.modeSelector ?? '[data-theme="{mode}"]',
|
|
3781
|
+
modes: o.modes ?? {},
|
|
3782
|
+
// index.css must be readable, not just the map, or a deleted index.css
|
|
3783
|
+
// (with the map still on disk from an interrupted pull) would report
|
|
3784
|
+
// written with an empty file list, a sentence that claims files exist.
|
|
3785
|
+
written: map !== null && imports !== null,
|
|
3786
|
+
// Distinguishes "the map is on disk but index.css is gone" from "the
|
|
3787
|
+
// map itself never existed" (the Foundation was excluded), so the
|
|
3788
|
+
// guide can name the actual cause instead of always blaming the
|
|
3789
|
+
// Foundation.
|
|
3790
|
+
indexMissing: map !== null && imports === null,
|
|
3791
|
+
files
|
|
3792
|
+
};
|
|
3793
|
+
})
|
|
3476
3794
|
};
|
|
3477
3795
|
}
|
|
3478
3796
|
var code = (s) => `\`${s}\``;
|
|
@@ -3511,16 +3829,17 @@ function stackSection(input) {
|
|
|
3511
3829
|
if (cssOut) {
|
|
3512
3830
|
const mapPath = `${input.outDir}/outputs/web-css.map.json`;
|
|
3513
3831
|
lines.push(
|
|
3514
|
-
`The CSS custom property for every token is in ${code(mapPath)}: source "code_syntax" when the designer declared it in Figma, "derived" when the CLI built it from the DTCG path by the stated rule (${cssOut.case} case, collection root included). Use those names; never invent a third. ${code(`${tokensDir}spec-layer.meta.json`)} still holds the raw ${code("code_syntax.WEB")} the designer declared.`,
|
|
3832
|
+
`The CSS custom property for every token is in ${code(mapPath)}: source "code_syntax" when the designer declared it in Figma, "derived" when the CLI built it from the DTCG path by the stated rule (${cssOut.case} case, collection root included). Each entry names the file that declares the property. Use those names; never invent a third. ${code(`${tokensDir}spec-layer.meta.json`)} still holds the raw ${code("code_syntax.WEB")} the designer declared.`,
|
|
3515
3833
|
""
|
|
3516
3834
|
);
|
|
3835
|
+
const partFiles = cssOut.files.filter((f) => f !== CSS_INDEX_FILE);
|
|
3517
3836
|
lines.push(
|
|
3518
|
-
`Import ${code(cssOut.path)} from the root stylesheet. It
|
|
3837
|
+
`Import ${code(`${cssOut.path}/index.css`)} from the root stylesheet. It imports one file per collection and mode: ${partFiles.join(", ")}. Sets and default modes are at ${code(":root")}; every other mode is a block under ${code(cssOut.modeSelector)} in its own file, so a mode can also be imported alone. To switch, set ${code("data-theme")} on ${code("<html>")} (or whatever the selector names). To let the OS choose, set that collection's selector to ${code(":root")} under ${code("outputs[].modes")} and import the mode's file yourself under ${code("@media (prefers-color-scheme: dark)")}; the CLI never assumes that.`,
|
|
3519
3838
|
""
|
|
3520
3839
|
);
|
|
3521
3840
|
if (profile.tokenTools.includes("style-dictionary") || profile.tokenTools.includes("tokens-studio")) {
|
|
3522
3841
|
lines.push(
|
|
3523
|
-
`${code(cssOut.path)} is a projection of the same ${code(
|
|
3842
|
+
`${code(`${cssOut.path}/`)} is a projection of the same ${code(tokensDir)} files, not a second source. Import one or the other.`,
|
|
3524
3843
|
""
|
|
3525
3844
|
);
|
|
3526
3845
|
}
|
|
@@ -3530,14 +3849,19 @@ function stackSection(input) {
|
|
|
3530
3849
|
""
|
|
3531
3850
|
);
|
|
3532
3851
|
const configuredNotWritten = pull?.outputs.find((o) => o.platform === "web" && !o.written) ?? null;
|
|
3533
|
-
if (configuredNotWritten) {
|
|
3852
|
+
if (configuredNotWritten?.indexMissing) {
|
|
3853
|
+
lines.push(
|
|
3854
|
+
`A web/css output is configured at ${code(`${configuredNotWritten.path}/`)} but its ${code("index.css")} is missing, so the file list is unknown. Run ${code("npx spec-layer pull")} to write it again.`,
|
|
3855
|
+
""
|
|
3856
|
+
);
|
|
3857
|
+
} else if (configuredNotWritten) {
|
|
3534
3858
|
lines.push(
|
|
3535
|
-
`A web/css output is configured at ${code(configuredNotWritten.path)} but was not written, because the last pull did not write the Foundation. Pull with the Foundation selected to write it.`,
|
|
3859
|
+
`A web/css output is configured at ${code(`${configuredNotWritten.path}/`)} but was not written, because the last pull did not write the Foundation. Pull with the Foundation selected to write it.`,
|
|
3536
3860
|
""
|
|
3537
3861
|
);
|
|
3538
3862
|
} else if (pull?.foundation?.written) {
|
|
3539
3863
|
lines.push(
|
|
3540
|
-
`No token file was written for web. Add \`"outputs"\` in \`speclayer.json\` (or run \`spec-layer pull --platform web\` once) and pull again; the default lands at ${code("
|
|
3864
|
+
`No token file was written for web. Add \`"outputs"\` in \`speclayer.json\` (or run \`spec-layer pull --platform web\` once) and pull again; the default lands at ${code("tokens/")}.`,
|
|
3541
3865
|
""
|
|
3542
3866
|
);
|
|
3543
3867
|
}
|
|
@@ -3619,9 +3943,9 @@ function pullSection(input) {
|
|
|
3619
3943
|
} else {
|
|
3620
3944
|
lines.push("- This library has no Foundation, so there is no tokens/ directory.");
|
|
3621
3945
|
}
|
|
3622
|
-
lines.push(`- ${code(`${
|
|
3946
|
+
lines.push(`- ${code(`${pull.componentSpecsDir}/`)}: one YAML per component.`);
|
|
3623
3947
|
for (const o of pull.outputs) {
|
|
3624
|
-
lines.push(o.written ? `- ${code(o.path)}: ${o.platform}/${o.format} token
|
|
3948
|
+
lines.push(o.written ? `- ${code(`${o.path}/`)}: ${o.platform}/${o.format} token files, ${o.case} names: ${o.files.join(", ")}. Non-default modes are under ${code(o.modeSelector)}, each in its own file. Names and provenance: ${code(`${outDir}/outputs/${o.platform}-${o.format}.map.json`)}; what it could not express: ${code(`${outDir}/outputs/${o.platform}-${o.format}.report.json`)}.` : o.indexMissing ? `- ${code(`${o.path}/`)}: ${o.platform}/${o.format} token files, but ${code("index.css")} is missing; run ${code("npx spec-layer pull")} to restore the directory.` : `- ${code(`${o.path}/`)}: ${o.platform}/${o.format} token files, configured but not written by the last pull (the Foundation was not written). Nothing is on disk at that path from Spec Layer.`);
|
|
3625
3949
|
}
|
|
3626
3950
|
lines.push("");
|
|
3627
3951
|
if (pull.foundation && (pull.foundation.sets.length || pull.foundation.modifiers.length)) {
|
|
@@ -3676,20 +4000,21 @@ function buildSkillGuide(input) {
|
|
|
3676
4000
|
`The Spec Layer Figma plugin publishes a design system's components, variables, and styles as data. The ${code("spec-layer")} CLI (version ${input.version}) pulls that data into this repository under ${code(outDir + "/")}. Everything in those files is extracted deterministically from Figma and validated against a published schema; no model wrote any of it. Treat it as the source of truth for what the design system contains, and treat anything it does not state as unknown rather than as something to infer.`,
|
|
3677
4001
|
""
|
|
3678
4002
|
);
|
|
4003
|
+
const componentSpecsDir = input.pull?.componentSpecsDir ?? input.config?.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR;
|
|
3679
4004
|
lines.push("## How to use it", "");
|
|
3680
4005
|
lines.push(`1. Run ${code("npx spec-layer status")}. Exit 0 means the local copy is current; exit 2 means run ${code("npx spec-layer pull")} first.`);
|
|
3681
|
-
lines.push(`2. Building or changing a component: read its YAML under ${code(`${
|
|
4006
|
+
lines.push(`2. Building or changing a component: read its YAML under ${code(`${componentSpecsDir}/`)}, or ${code("npx spec-layer show component NAME")}. ${code("api")} gives variants, states, booleans, and slots; ${code("anatomy")} names the parts; ${code("references.bindings")} says which token each part's property uses and under which ${code("when")} conditions; ${code("unbound")} lists values that are hardcoded in Figma.`);
|
|
3682
4007
|
lines.push(`3. Working with colors, spacing, type, or effects: start at ${code(`${outDir}/tokens/resolver.json`)}, load the set and mode files it names, and look up ${code("code_syntax")} in ${code("spec-layer.meta.json")} for the name the designer declared for your platform.`);
|
|
3683
4008
|
lines.push(`4. Reference tokens by name in code; never paste a resolved value where a token exists. A value the design system does not define is not a token: say so in your change rather than adding one.`);
|
|
3684
4009
|
lines.push(`5. An ${code("unbound")} entry is design debt reported from Figma. Do not silently promote it to a token; keep the literal and note that Figma has no binding for it.`);
|
|
3685
4010
|
const writtenOutputs = input.pull?.outputs.filter((o) => o.written) ?? [];
|
|
3686
|
-
const outputNote = writtenOutputs.length ? ` Never edit ${writtenOutputs.map((o) => code(o.path)).join(", ")} either: pull replaces
|
|
3687
|
-
lines.push(`6. Never edit files under ${code(outDir + "/")}: the next pull replaces
|
|
4011
|
+
const outputNote = writtenOutputs.length ? ` Never edit ${writtenOutputs.map((o) => code(`${o.path}/`)).join(", ")} either: pull replaces or removes files there.` : "";
|
|
4012
|
+
lines.push(`6. Never edit files under ${code(outDir + "/")} or ${code(componentSpecsDir + "/")}: the next pull replaces or removes them.${outputNote} Configuration lives in ${code("speclayer.json")}. Never commit ${code(CREDENTIALS_NAME)}, and never print or copy the pull key.`);
|
|
3688
4013
|
lines.push("");
|
|
3689
4014
|
lines.push(...pullSection(input));
|
|
3690
4015
|
lines.push(...stackSection(input));
|
|
3691
4016
|
lines.push(...commandsSection());
|
|
3692
|
-
lines.push(`Generated by ${code("spec-layer skill")}. Re-run ${code("npx spec-layer skill --install")} after a pull that adds components or when the codebase changes stack; the file is replaced, not appended.`);
|
|
4017
|
+
lines.push(`Generated by ${code("spec-layer skill")}. Re-run ${code("npx spec-layer skill --install")} after a pull that adds components, after changing ${code("outputs")} or ${code("componentSpecsDir")}, or when the codebase changes stack; the file is replaced, not appended.`);
|
|
3693
4018
|
return `${lines.join("\n")}
|
|
3694
4019
|
`;
|
|
3695
4020
|
}
|
|
@@ -3759,16 +4084,16 @@ ${BLOCK_END}
|
|
|
3759
4084
|
const after = existing.slice(end + BLOCK_END.length).replace(/^\n/, "");
|
|
3760
4085
|
return `${existing.slice(0, begin)}${block}${after}`;
|
|
3761
4086
|
}
|
|
3762
|
-
const
|
|
3763
|
-
return `${existing}${
|
|
4087
|
+
const sep2 = existing.length === 0 ? "" : existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
4088
|
+
return `${existing}${sep2}${block}`;
|
|
3764
4089
|
}
|
|
3765
4090
|
function installSkill(cwd, host, guide) {
|
|
3766
4091
|
const target = installTarget(host);
|
|
3767
|
-
const abs =
|
|
4092
|
+
const abs = join7(cwd, target.path);
|
|
3768
4093
|
const existing = existsSync7(abs) ? readFileSync7(abs, "utf8") : null;
|
|
3769
4094
|
const next = target.mode === "file" ? renderForHost(host, guide) : upsertBlock(existing, renderForHost(host, guide));
|
|
3770
4095
|
if (existing === next) return { path: target.path, result: "unchanged" };
|
|
3771
|
-
mkdirSync3(
|
|
4096
|
+
mkdirSync3(dirname3(abs), { recursive: true });
|
|
3772
4097
|
writeFileSync6(abs, next);
|
|
3773
4098
|
return { path: target.path, result: existing === null ? "created" : "updated" };
|
|
3774
4099
|
}
|
|
@@ -3796,7 +4121,7 @@ function manifestReader() {
|
|
|
3796
4121
|
function sameOutput(a, b) {
|
|
3797
4122
|
const selectionKey = (s) => JSON.stringify([s.foundation, s.components === null ? null : [...new Set(s.components.map(slugify))].sort()]);
|
|
3798
4123
|
const key = (v) => JSON.stringify(sortKeys(v ?? {}));
|
|
3799
|
-
return selectionKey(a.selection) === selectionKey(b.selection) && key(a.dtcg) === key(b.dtcg) && key(a.outputs ?? []) === key(b.outputs ?? []);
|
|
4124
|
+
return selectionKey(a.selection) === selectionKey(b.selection) && key(a.dtcg) === key(b.dtcg) && key(a.outputs ?? []) === key(b.outputs ?? []) && (a.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR) === (b.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR);
|
|
3800
4125
|
}
|
|
3801
4126
|
function sortKeys(value) {
|
|
3802
4127
|
if (Array.isArray(value)) return value.map(sortKeys);
|
|
@@ -3827,12 +4152,12 @@ function outputsForRun(fromFlags, config, platforms) {
|
|
|
3827
4152
|
if (fromFlags) return withDefaults(config?.outputs ?? [], fromFlags);
|
|
3828
4153
|
return config?.outputs ?? defaultOutputs(platforms);
|
|
3829
4154
|
}
|
|
3830
|
-
var NO_PLATFORM_NOTE = `No target platform detected, so no token
|
|
4155
|
+
var NO_PLATFORM_NOTE = `No target platform detected, so no token files were written for your code. Pass --platform ${PLATFORMS.join("|")}, or add outputs to speclayer.json.`;
|
|
3831
4156
|
function platformsMissingFormat(platforms) {
|
|
3832
4157
|
return platforms.filter((p) => !FORMATS.some((f) => f.platform === p));
|
|
3833
4158
|
}
|
|
3834
4159
|
function missingFormatNote(platforms) {
|
|
3835
|
-
return `No token
|
|
4160
|
+
return `No token files exist yet for ${platforms.join(", ")}: no output format is available for that platform. Web has css.`;
|
|
3836
4161
|
}
|
|
3837
4162
|
var errorText = (err) => err instanceof Error ? err.message : String(err);
|
|
3838
4163
|
function runInit(cwd, flags, io2) {
|
|
@@ -3855,12 +4180,13 @@ function runInit(cwd, flags, io2) {
|
|
|
3855
4180
|
writeConfig(cwd, {
|
|
3856
4181
|
libraryId: flags.id,
|
|
3857
4182
|
outDir,
|
|
4183
|
+
componentSpecsDir: DEFAULT_COMPONENT_SPECS_DIR,
|
|
3858
4184
|
...include ? { include } : {},
|
|
3859
4185
|
...platforms.length > 0 ? { platforms } : {},
|
|
3860
4186
|
...outputs.length > 0 ? { outputs } : {}
|
|
3861
4187
|
});
|
|
3862
4188
|
io2.out(`Wrote speclayer.json (library ${flags.id}, output ${outDir}${platforms.length > 0 ? `, platforms ${platforms.join(", ")}` : ""}).`);
|
|
3863
|
-
for (const o of outputs) io2.out(`Token
|
|
4189
|
+
for (const o of outputs) io2.out(`Token files for ${o.platform}: ${o.path}/ (${o.format}, ${o.case} names), written by the next pull.`);
|
|
3864
4190
|
if (source === "flag" || source === "detected") {
|
|
3865
4191
|
const missing = platformsMissingFormat(platforms);
|
|
3866
4192
|
if (missing.length > 0) io2.out(missingFormatNote(missing));
|
|
@@ -3888,7 +4214,7 @@ function resolved(cwd, flags, env, io2, manifestAt) {
|
|
|
3888
4214
|
}
|
|
3889
4215
|
function resolvedOutDir(cwd, flags, io2) {
|
|
3890
4216
|
try {
|
|
3891
|
-
return
|
|
4217
|
+
return join8(cwd, flags.out ?? readConfig(cwd)?.outDir ?? DEFAULT_OUT_DIR);
|
|
3892
4218
|
} catch (err) {
|
|
3893
4219
|
io2.err(errorText(err));
|
|
3894
4220
|
return null;
|
|
@@ -3927,6 +4253,7 @@ async function runSetup(cwd, flags, env, io2, fetcher) {
|
|
|
3927
4253
|
const fromFlags = platformsFromFlags(flags, io2);
|
|
3928
4254
|
if (fromFlags === null) return 1;
|
|
3929
4255
|
const outDir = flags.out ?? existing?.outDir ?? DEFAULT_OUT_DIR;
|
|
4256
|
+
const componentSpecsDir = existing?.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR;
|
|
3930
4257
|
const keptInclude = include ?? existing?.include ?? null;
|
|
3931
4258
|
const keptDtcg = existing?.dtcg ?? null;
|
|
3932
4259
|
const { platforms } = resolvePlatforms(cwd, fromFlags, existing);
|
|
@@ -3934,6 +4261,7 @@ async function runSetup(cwd, flags, env, io2, fetcher) {
|
|
|
3934
4261
|
writeConfig(cwd, {
|
|
3935
4262
|
libraryId: flags.id,
|
|
3936
4263
|
outDir,
|
|
4264
|
+
componentSpecsDir,
|
|
3937
4265
|
...keptInclude ? { include: keptInclude } : {},
|
|
3938
4266
|
...keptDtcg ? { dtcg: keptDtcg } : {},
|
|
3939
4267
|
...platforms.length > 0 ? { platforms } : {},
|
|
@@ -3986,6 +4314,12 @@ git rm --cached ${ignored.line}`);
|
|
|
3986
4314
|
io2.out("spec-layer skill prints the same guide; spec-layer tools lists every command.");
|
|
3987
4315
|
return 0;
|
|
3988
4316
|
}
|
|
4317
|
+
function outputFilesOnDisk(cwd, outDir, o) {
|
|
4318
|
+
const mapPath = join8(cwd, outDir, "outputs", `${outputId(o)}.map.json`);
|
|
4319
|
+
if (!existsSync8(mapPath)) return false;
|
|
4320
|
+
const imports = readIndexImports(cwd, o);
|
|
4321
|
+
return imports !== null && imports.every((f) => existsSync8(resolve5(cwd, o.path, f)));
|
|
4322
|
+
}
|
|
3989
4323
|
async function runPull(cwd, flags, env, io2, fetcher) {
|
|
3990
4324
|
const manifestAt = manifestReader();
|
|
3991
4325
|
const opts = resolved(cwd, flags, env, io2, manifestAt);
|
|
@@ -4001,13 +4335,14 @@ async function runPull(cwd, flags, env, io2, fetcher) {
|
|
|
4001
4335
|
if (fromFlags === null) return 1;
|
|
4002
4336
|
const { platforms, source } = resolvePlatforms(cwd, fromFlags, opts);
|
|
4003
4337
|
const outputs = outputsForRun(fromFlags, opts, platforms);
|
|
4004
|
-
const manifest = manifestAt(
|
|
4338
|
+
const manifest = manifestAt(join8(cwd, opts.outDir));
|
|
4005
4339
|
const foundationOnDisk = Boolean(manifest?.artifacts.find((a) => a.kind === "foundation")?.path);
|
|
4006
4340
|
const willWriteFoundation = selection.foundation && foundationOnDisk;
|
|
4341
|
+
const briefsOnDisk = (manifest?.artifacts ?? []).filter((a) => a.kind === "component" && a.path !== null).every((a) => existsSync8(resolve5(cwd, a.path)));
|
|
4007
4342
|
const etag = manifest && sameOutput(
|
|
4008
|
-
{ selection: manifest.selection ?? DEFAULT_SELECTION, dtcg: manifest.dtcg, outputs: manifest.outputs },
|
|
4009
|
-
{ selection, dtcg: opts.dtcg, outputs }
|
|
4010
|
-
) && (!willWriteFoundation || outputs.every((o) =>
|
|
4343
|
+
{ selection: manifest.selection ?? DEFAULT_SELECTION, dtcg: manifest.dtcg, outputs: manifest.outputs, componentSpecsDir: manifest.componentSpecsDir },
|
|
4344
|
+
{ selection, dtcg: opts.dtcg, outputs, componentSpecsDir: opts.componentSpecsDir }
|
|
4345
|
+
) && briefsOnDisk && (!willWriteFoundation || outputs.every((o) => outputFilesOnDisk(cwd, opts.outDir, o))) ? manifest.bundleHash : void 0;
|
|
4011
4346
|
const result = await fetchBundle({
|
|
4012
4347
|
api: opts.api,
|
|
4013
4348
|
libraryId: opts.libraryId,
|
|
@@ -4024,12 +4359,13 @@ async function runPull(cwd, flags, env, io2, fetcher) {
|
|
|
4024
4359
|
return 0;
|
|
4025
4360
|
}
|
|
4026
4361
|
let written;
|
|
4027
|
-
let
|
|
4362
|
+
let componentSpecs;
|
|
4363
|
+
let outputResults;
|
|
4028
4364
|
try {
|
|
4029
4365
|
const bundle = parseBundle(result.raw);
|
|
4030
4366
|
const selected = selectComponents(bundle, selection);
|
|
4031
4367
|
const writeResult = writeBundleFiles({
|
|
4032
|
-
outDir:
|
|
4368
|
+
outDir: join8(cwd, opts.outDir),
|
|
4033
4369
|
cwd,
|
|
4034
4370
|
raw: result.raw,
|
|
4035
4371
|
bundle,
|
|
@@ -4039,10 +4375,12 @@ async function runPull(cwd, flags, env, io2, fetcher) {
|
|
|
4039
4375
|
bundleHash: result.bundleHash,
|
|
4040
4376
|
dtcg: opts.dtcg,
|
|
4041
4377
|
platforms,
|
|
4042
|
-
outputs
|
|
4378
|
+
outputs,
|
|
4379
|
+
componentSpecsDir: opts.componentSpecsDir
|
|
4043
4380
|
});
|
|
4044
4381
|
written = writeResult.written;
|
|
4045
|
-
|
|
4382
|
+
componentSpecs = writeResult.componentSpecs;
|
|
4383
|
+
outputResults = writeResult.outputs;
|
|
4046
4384
|
io2.out(
|
|
4047
4385
|
`Pulled ${bundle.fileName ?? opts.libraryId}: ${describePull(bundle, selection, selected)} (published ${result.publishedAt}).`
|
|
4048
4386
|
);
|
|
@@ -4050,12 +4388,24 @@ async function runPull(cwd, flags, env, io2, fetcher) {
|
|
|
4050
4388
|
io2.err(errorText(err));
|
|
4051
4389
|
return 1;
|
|
4052
4390
|
}
|
|
4391
|
+
const count = (n) => `${n} file${n === 1 ? "" : "s"}`;
|
|
4053
4392
|
io2.out(`Wrote ${written.length} files under ${opts.outDir}/.`);
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4393
|
+
if (componentSpecs.files.length > 0) io2.out(`Wrote ${componentSpecs.path}/ (${count(componentSpecs.files.length)}).`);
|
|
4394
|
+
for (const r of outputResults) {
|
|
4395
|
+
const o = outputs.find((x) => x.path === r.path);
|
|
4396
|
+
if (o) io2.out(`Wrote ${r.path}/ (${count(r.files.length)}, ${o.platform}/${o.format}, ${o.case} names).`);
|
|
4397
|
+
}
|
|
4398
|
+
const staleDirNote = (previous, current) => {
|
|
4399
|
+
if (previous !== current && existsSync8(resolve5(cwd, previous))) {
|
|
4400
|
+
io2.out(`The previous pull wrote ${previous}/; this one wrote ${current}/. Delete ${previous}/ if nothing else uses it.`);
|
|
4401
|
+
}
|
|
4402
|
+
};
|
|
4403
|
+
if (manifest) staleDirNote(manifest.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR, opts.componentSpecsDir);
|
|
4404
|
+
for (const prev of manifest?.outputs ?? []) {
|
|
4405
|
+
const current = outputs.find((o) => outputId(o) === outputId(prev));
|
|
4406
|
+
if (current) staleDirNote(prev.path, current.path);
|
|
4057
4407
|
}
|
|
4058
|
-
if (
|
|
4408
|
+
if (outputResults.length === 0 && selection.foundation && source === "none" && opts.outputs === void 0) io2.out(NO_PLATFORM_NOTE);
|
|
4059
4409
|
if (source === "flag" || source === "config") {
|
|
4060
4410
|
const missing = platformsMissingFormat(platforms);
|
|
4061
4411
|
if (missing.length > 0) io2.out(missingFormatNote(missing));
|
|
@@ -4066,7 +4416,7 @@ async function runStatus(cwd, flags, env, io2, fetcher) {
|
|
|
4066
4416
|
const manifestAt = manifestReader();
|
|
4067
4417
|
const opts = resolved(cwd, flags, env, io2, manifestAt);
|
|
4068
4418
|
if (!opts) return 1;
|
|
4069
|
-
const manifest = manifestAt(
|
|
4419
|
+
const manifest = manifestAt(join8(cwd, opts.outDir));
|
|
4070
4420
|
if (!manifest) {
|
|
4071
4421
|
io2.err(NO_LOCAL_PULL);
|
|
4072
4422
|
return 2;
|
|
@@ -4104,7 +4454,7 @@ function runList(cwd, flags, io2) {
|
|
|
4104
4454
|
io2.out(row.map((cell, i) => i < 3 ? cell.padEnd(widths[i]) : cell).join(" "));
|
|
4105
4455
|
}
|
|
4106
4456
|
for (const o of manifest.outputs ?? []) {
|
|
4107
|
-
const written = existsSync8(
|
|
4457
|
+
const written = existsSync8(join8(outDir, "outputs", `${o.platform}-${o.format}.map.json`));
|
|
4108
4458
|
io2.out(["output".padEnd(widths[0]), `${o.platform}/${o.format}`.padEnd(widths[1]), written ? o.path : "not written"].join(" "));
|
|
4109
4459
|
}
|
|
4110
4460
|
return 0;
|
|
@@ -4174,7 +4524,7 @@ function collectSkillInput(cwd, flags, io2) {
|
|
|
4174
4524
|
const fromFlags = platformsFromFlags(flags, io2);
|
|
4175
4525
|
if (fromFlags === null) return null;
|
|
4176
4526
|
const { platforms, source: platformSource } = resolvePlatforms(cwd, fromFlags, config);
|
|
4177
|
-
const pull = summarizePull(cwd, outDir, readManifest(
|
|
4527
|
+
const pull = summarizePull(cwd, outDir, readManifest(join8(cwd, outDir)));
|
|
4178
4528
|
return { profile, platforms, platformSource, outDir, config, pull, version: cliVersion() };
|
|
4179
4529
|
}
|
|
4180
4530
|
function skillHosts(flags, input, io2) {
|