Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/figma-modeless-layer-bare-names.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@adobe/design-data-tui": minor
---

Figma export derives collection specs from mode sets and gains opt-in flags (cn3v.3, .4, .7).

- **sdk/plugins/figma**: specs come from `graph.mode_sets`; variables match per collection.
- **sdk/cli**: `figma export --modeless-layer` writes a modeless alias layer.
- **sdk/cli**: `figma export|audit|diff --no-folder-prefix` use bare token names.
86 changes: 76 additions & 10 deletions sdk/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,19 @@ enum FigmaSub {
/// tokens without a usable replacement keep their literal value.
#[arg(long)]
redirect_deprecated: bool,
/// Also write a modeless alias layer into the existing "Spectrum"
/// collection: one variable per token, aliasing the mode-group variable
/// for tokens that vary on one axis, or holding the concrete value for
/// tokens that don't. Fails if the collection is missing. Off by
/// default; mode-group collections are unaffected either way.
#[arg(long)]
modeless_layer: bool,
/// Name variables by their bare token name instead of
/// `colorTheme/<name>` / `platformScale/<name>`. Only use this once the
/// target library's top-level folders are removed; otherwise every
/// variable is created anew. Off by default.
#[arg(long)]
no_folder_prefix: bool,
#[command(flatten)]
write_options: FigmaWriteOptions,
},
Expand Down Expand Up @@ -668,6 +681,10 @@ enum FigmaSub {
/// defaults to the resolved design-data dataset
#[arg(long)]
token_dir: Option<PathBuf>,
/// Compare against bare token names (as written by `figma export
/// --no-folder-prefix`) instead of `colorTheme/`/`platformScale/` names
#[arg(long)]
no_folder_prefix: bool,
/// Output format (pretty or json)
#[arg(long, value_enum, default_value_t = OutputFormat::Pretty)]
format: OutputFormat,
Expand All @@ -693,6 +710,10 @@ enum FigmaSub {
/// resolved dataset
#[arg(long, value_name = "PATH")]
manifest: Option<PathBuf>,
/// Compare against bare token names (as written by `figma export
/// --no-folder-prefix`) instead of `colorTheme/`/`platformScale/` names
#[arg(long)]
no_folder_prefix: bool,
/// Output format (pretty or json)
#[arg(long, value_enum, default_value_t = OutputFormat::Pretty)]
format: OutputFormat,
Expand Down Expand Up @@ -2305,7 +2326,10 @@ fn write_figma_report(
fn load_figma_tokens(
path: Option<&Path>,
manifest: Option<&Path>,
) -> miette::Result<Vec<figma::mapping::ExportToken>> {
) -> miette::Result<(
Vec<figma::mapping::ExportToken>,
Vec<figma::mapping::CollectionSpec>,
)> {
if path.is_some_and(|dir| !dir_has_cascade_tokens(dir)) {
miette::bail!("Figma export/audit requires canonical .tokens.json data; legacy directories are no longer supported");
}
Expand Down Expand Up @@ -2335,7 +2359,10 @@ fn load_figma_tokens(
manifest::apply_configured(&mut graph, &resolved)
.into_diagnostic()
.wrap_err("failed to apply platform manifest cascade")?;
figma::mapping::export_tokens(&graph).map_err(|e| miette::miette!("{e}"))
let specs =
figma::mapping::collection_specs(&graph.mode_sets).map_err(|e| miette::miette!("{e}"))?;
let tokens = figma::mapping::export_tokens(&graph).map_err(|e| miette::miette!("{e}"))?;
Ok((tokens, specs))
}

fn dir_has_cascade_tokens(dir: &Path) -> bool {
Expand All @@ -2357,6 +2384,8 @@ fn run_figma_export(
manifest: Option<&Path>,
code_syntax_manifests: &[String],
redirect_deprecated: bool,
modeless_layer: bool,
no_folder_prefix: bool,
write_options: &FigmaWriteOptions,
) -> miette::Result<ExitCode> {
use figma::write_guard;
Expand Down Expand Up @@ -2399,7 +2428,12 @@ fn run_figma_export(
platform_formats.push((platform.to_string(), config));
}
// 1. Load tokens (cascade graph projection, or a legacy dir when given).
let tokens = load_figma_tokens(path, manifest)?;
let (tokens, specs) = load_figma_tokens(path, manifest)?;
let specs = if no_folder_prefix {
figma::mapping::without_folder_prefix(specs)
} else {
specs
};

// 2. GET existing variables to obtain collection/mode IDs.
eprintln!("Fetching existing variables from Figma...");
Expand All @@ -2409,14 +2443,26 @@ fn run_figma_export(

// 3. Build the export payload.
eprintln!("Building export payload...");
let (body, summary) = figma::mapping::build_export_payload_with_options(
let (mut body, mut summary) = figma::mapping::build_export_payload_with_options(
&tokens,
&specs,
&response.meta,
overrides.as_ref(),
&platform_formats,
redirect_deprecated,
)
.map_err(|e| miette::miette!("{e}"))?;
if modeless_layer {
figma::mapping::append_modeless_layer(
&mut body,
&mut summary,
&tokens,
&specs,
&response.meta,
overrides.as_ref(),
)
.map_err(|e| miette::miette!("{e}"))?;
}

let payload = serde_json::to_value(&body).into_diagnostic()?;
// 4. Output or guard, post once, then verify.
Expand Down Expand Up @@ -2754,6 +2800,7 @@ fn run_figma_import(
fn run_figma_audit(
snapshot: &Path,
token_dir: Option<&Path>,
no_folder_prefix: bool,
format: OutputFormat,
) -> miette::Result<ExitCode> {
let text = std::fs::read_to_string(snapshot)
Expand All @@ -2763,8 +2810,14 @@ fn run_figma_audit(
.into_diagnostic()
.wrap_err_with(|| format!("failed to parse snapshot {}", snapshot.display()))?;

let tokens = load_figma_tokens(token_dir, None)?;
let report = figma::audit::audit_tokens(&meta, &tokens).map_err(|e| miette::miette!("{e}"))?;
let (tokens, specs) = load_figma_tokens(token_dir, None)?;
let specs = if no_folder_prefix {
figma::mapping::without_folder_prefix(specs)
} else {
specs
};
let report =
figma::audit::audit_tokens(&meta, &tokens, &specs).map_err(|e| miette::miette!("{e}"))?;

match format {
OutputFormat::Json => {
Expand Down Expand Up @@ -2887,14 +2940,20 @@ fn run_figma_diff(
snapshot: Option<&Path>,
mapping: Option<&Path>,
manifest: Option<&Path>,
no_folder_prefix: bool,
format: OutputFormat,
) -> miette::Result<ExitCode> {
let (meta, mapping_map, graph) =
load_figma_diff_inputs(file_key, token, snapshot, mapping, manifest)?;

// 3. Diff.
let report = figma::import::diff_values(&meta, &graph, mapping_map.as_ref())
.map_err(|e| miette::miette!("{e}"))?;
let report = figma::import::diff_values_with_options(
&meta,
&graph,
mapping_map.as_ref(),
no_folder_prefix,
)
.map_err(|e| miette::miette!("{e}"))?;

match format {
OutputFormat::Json => {
Expand Down Expand Up @@ -2932,7 +2991,7 @@ fn run_figma_diff(
/// to cover today.
///
/// These are real-file naming conventions observed in specific collections,
/// not the exporter's own output — `mapping::COLLECTION_SPECS` models what
/// not the exporter's own output — `mapping::collection_specs` models what
/// `figma export` generates (`colorTheme/`, `platformScale/`) and has no
/// entry for these, so there's nothing to derive this list from
/// automatically. If a new prefix group shows up, add it here by hand.
Expand Down Expand Up @@ -3604,6 +3663,8 @@ fn main() -> ExitCode {
manifest,
code_syntax_manifests,
redirect_deprecated,
modeless_layer,
no_folder_prefix,
write_options,
} => run_figma_export(
path.as_deref(),
Expand All @@ -3613,6 +3674,8 @@ fn main() -> ExitCode {
manifest.as_deref(),
&code_syntax_manifests,
redirect_deprecated,
modeless_layer,
no_folder_prefix,
&write_options,
),
FigmaSub::Import {
Expand All @@ -3633,21 +3696,24 @@ fn main() -> ExitCode {
FigmaSub::Audit {
snapshot,
token_dir,
no_folder_prefix,
format,
} => run_figma_audit(&snapshot, token_dir.as_deref(), format),
} => run_figma_audit(&snapshot, token_dir.as_deref(), no_folder_prefix, format),
FigmaSub::Diff {
file_key,
token,
snapshot,
mapping,
manifest,
no_folder_prefix,
format,
} => run_figma_diff(
file_key.as_deref(),
token.as_deref(),
snapshot.as_deref(),
mapping.as_deref(),
manifest.as_deref(),
no_folder_prefix,
format,
),
FigmaSub::Pair {
Expand Down
11 changes: 7 additions & 4 deletions sdk/plugins/figma/src/audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use std::path::Path;

use serde::Serialize;

use super::mapping::{build_export_payload, summarize_variables};
use super::mapping::{build_export_payload, collection_specs, summarize_variables, CollectionSpec};
use super::types::VariablesMeta;
use super::FigmaError;

Expand Down Expand Up @@ -82,16 +82,19 @@ pub fn audit_names(existing: &VariablesMeta, token_dir: &Path) -> Result<AuditRe
);
}
}
audit_tokens(existing, &crate::mapping::export_tokens(&graph)?)
let specs = collection_specs(&graph.mode_sets)?;
audit_tokens(existing, &crate::mapping::export_tokens(&graph)?, &specs)
}

/// Same as [`audit_names`], over already-loaded tokens (e.g. from
/// [`crate::mapping::export_tokens`]).
/// [`crate::mapping::export_tokens`]) and the collection specs derived from
/// the graph's mode sets.
pub fn audit_tokens(
existing: &VariablesMeta,
tokens: &[crate::mapping::ExportToken],
specs: &[CollectionSpec],
) -> Result<AuditReport, FigmaError> {
let (body, summary) = build_export_payload(tokens, existing, None)?;
let (body, summary) = build_export_payload(tokens, specs, existing, None)?;

// Real variable names per collection, from the snapshot (non-remote only,
// matching summarize_variables' existing convention). The real file has
Expand Down
4 changes: 3 additions & 1 deletion sdk/plugins/figma/src/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ mod resolve;
#[cfg(test)]
mod tests;

pub use diff::{diff_values, DiffClass, DiffCounts, DiffEntry, DiffReport, ModeDiff};
pub use diff::{
diff_values, diff_values_with_options, DiffClass, DiffCounts, DiffEntry, DiffReport, ModeDiff,
};
pub use overrides::{build_import_overrides, ImportSummary};
pub use pair::{pair_by_value, PairingCandidate, PairingReport};
28 changes: 25 additions & 3 deletions sdk/plugins/figma/src/import/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,18 @@ pub fn diff_values(
meta: &VariablesMeta,
graph: &TokenGraph,
mapping: Option<&HashMap<String, String>>,
) -> Result<DiffReport, FigmaError> {
diff_values_with_options(meta, graph, mapping, false)
}

/// [`diff_values`] for a file whose generated variables use bare token names
/// (`figma export --no-folder-prefix`): a slash-less Figma name is taken as the
/// legacy key itself, and the design-data-only pass compares unprefixed names.
pub fn diff_values_with_options(
meta: &VariablesMeta,
graph: &TokenGraph,
mapping: Option<&HashMap<String, String>>,
no_folder_prefix: bool,
) -> Result<DiffReport, FigmaError> {
let reversed: Option<HashMap<String, String>> =
mapping.map(|m| m.iter().map(|(k, v)| (v.clone(), k.clone())).collect());
Expand Down Expand Up @@ -287,7 +299,9 @@ pub fn diff_values(
// reach the alias-target fallback below unless that fallback runs
// even when inversion itself fails outright (not just when it
// inverts to something the graph can't resolve).
let inverted = invert_name(&variable.name, reversed.as_ref());
let inverted = invert_name(&variable.name, reversed.as_ref()).or_else(|| {
(no_folder_prefix && !variable.name.contains('/')).then(|| variable.name.clone())
});
let resolved = inverted
.as_ref()
.and_then(|legacy_key| {
Expand Down Expand Up @@ -429,10 +443,18 @@ pub fn diff_values(
// produce an export payload at all; that's a reason to skip this one
// pass, not to discard the Figma-driven diff already computed above.
let export_tokens = crate::mapping::export_tokens(graph)?;
if let Ok((body, _summary)) = build_export_payload(&export_tokens, meta, mapping) {
let specs = crate::mapping::collection_specs(&graph.mode_sets)?;
let specs = if no_folder_prefix {
crate::mapping::without_folder_prefix(specs)
} else {
specs
};
if let Ok((body, _summary)) = build_export_payload(&export_tokens, &specs, meta, mapping) {
for action in &body.variables {
if !seen.contains(&action.name) {
let legacy_key = invert_name(&action.name, reversed.as_ref());
let legacy_key = invert_name(&action.name, reversed.as_ref()).or_else(|| {
(no_folder_prefix && !action.name.contains('/')).then(|| action.name.clone())
});
// A legacy key that never resolved (e.g. a cascade-format
// token with no usable `name`/`legacyKey` field) falls back
// to its synthetic `path:index` graph key, which always
Expand Down
26 changes: 26 additions & 0 deletions sdk/plugins/figma/src/import/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,32 @@ fn diff_values_reports_known_mismatch() {
}
}

#[test]
fn diff_values_with_bare_names_maps_variable_to_its_token() {
let var = mock_variable(
"blue-100",
"COLOR",
vec![("m-light", json!({"r": 0.0, "g": 0.0, "b": 1.0, "a": 1.0}))],
);
let meta = VariablesMeta {
variables: HashMap::from([(var.id.clone(), var)]),
variable_collections: HashMap::new(),
};
let graph = mock_graph("blue-100", "u-blue-100", json!("#ff8000"));

let prefixed = diff_values(&meta, &graph, None).unwrap();
assert_eq!(prefixed.counts.value_mismatch, 0);

let bare = diff_values_with_options(&meta, &graph, None, true).unwrap();
assert_eq!(bare.counts.value_mismatch, 1);
let entry = bare
.entries
.iter()
.find(|e| e.name == "blue-100")
.expect("bare-named variable must be reported");
assert_eq!(entry.legacy_key.as_deref(), Some("blue-100"));
}

/// `S2.Color-theme` is a single-mode collection whose variables are bare
/// names (no `/`) that are `VARIABLE_ALIAS`es into `.Color theme`.
/// `invert_name` can't invert a bare name, so it must not short-circuit
Expand Down
3 changes: 3 additions & 0 deletions sdk/plugins/figma/src/mapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,19 @@ mod convert;
#[cfg(test)]
mod golden_tests;
mod graph;
mod modeless;
mod payload;
mod routing;
#[cfg(test)]
mod tests;

pub(crate) use convert::figma_opacity_to_fraction;
pub use graph::{export_tokens, ExportToken};
pub use modeless::{append_modeless_layer, MODELESS_COLLECTION};
pub use payload::{
build_export_payload, build_export_payload_with_options,
build_export_payload_with_platform_formats, summarize_variables, CollectionSummary,
ExportSummary,
};
pub use routing::{collection_specs, without_folder_prefix, CollectionSpec};
pub(crate) use routing::{FONT_STYLE, FONT_WEIGHT, OPACITY};
Loading
Loading