From e0503a02ab5213e937f835129746bbd6f68fccd3 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 8 Oct 2026 00:46:55 +0000 Subject: [PATCH 1/3] Record StyleX theme imports in functional modules StyleX resolves imported defineVars, defineConsts, and defineMarker values only through imports of the defining module's theme file. Validation already proves which imported values StyleX evaluates statically; keep that set on the functional module so code generation can choose their import paths. Amp-Thread-ID: https://ampcode.com/threads/T-01a118ca-d95d-702f-85cb-95686f6ce5ed Co-authored-by: Amp --- compiler-backend/functional/src/convert.rs | 3 ++- .../functional/src/convert/stylex.rs | 16 +++++++--------- compiler-backend/functional/src/stylex.rs | 11 +++++++++++ compiler-backend/functional/src/tree.rs | 4 ++++ 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/compiler-backend/functional/src/convert.rs b/compiler-backend/functional/src/convert.rs index 35cc7195f..13e7dfd11 100644 --- a/compiler-backend/functional/src/convert.rs +++ b/compiler-backend/functional/src/convert.rs @@ -203,7 +203,7 @@ fn convert(mut context: Context<'_, impl checking::ExternalQueries>) -> Conversi } } // StyleX consumes the emitted expressions, including literals substituted for local bindings. - context.validate_stylex_uses(&declarations)?; + let stylex_theme_imports = context.validate_stylex_uses(&declarations)?; let dependencies = context.dependencies.iter().map(|(&file_id, dependency)| ModuleDependency { file_id, @@ -220,6 +220,7 @@ fn convert(mut context: Context<'_, impl checking::ExternalQueries>) -> Conversi dependencies: dependencies.into(), surface, declarations: declarations.into(), + stylex_theme_imports, storage: context.storage, }) } diff --git a/compiler-backend/functional/src/convert/stylex.rs b/compiler-backend/functional/src/convert/stylex.rs index d46ad2a5c..9acd93c40 100644 --- a/compiler-backend/functional/src/convert/stylex.rs +++ b/compiler-backend/functional/src/convert/stylex.rs @@ -297,13 +297,14 @@ where Ok(intrinsic) } + /// Returns the imported theme values that StyleX evaluates statically in this module. pub(super) fn validate_stylex_uses( &self, declarations: &[Declaration], - ) -> ConversionResult<()> { + ) -> ConversionResult> { // StyleX intrinsics and expressions only arise from references to the virtual modules. if !self.references_stylex_module && !self.module_is_virtual(self.file_id) { - return Ok(()); + return Ok(FxHashSet::default()); } let mut bindings = StyleXStaticBindings::default(); for declaration in declarations { @@ -334,7 +335,8 @@ where &mut bindings, )?; } - Ok(()) + let theme_imports = bindings.imports.into_iter().filter(|&(_, theme)| theme); + Ok(theme_imports.map(|(global, _)| global).collect()) } fn validate_stylex_expression( @@ -768,13 +770,9 @@ where Ok(matches!( context.storage[expression].kind, ExpressionKind::StyleX(StyleXExpression::Call { - target: StyleXCallTarget::Root( - StyleXRootCall::DefineVars - | StyleXRootCall::DefineConsts - | StyleXRootCall::DefineMarker - ), + target: StyleXCallTarget::Root(call), .. - }) + }) if call.defines_theme_value() )) } diff --git a/compiler-backend/functional/src/stylex.rs b/compiler-backend/functional/src/stylex.rs index d455dea8a..81250b395 100644 --- a/compiler-backend/functional/src/stylex.rs +++ b/compiler-backend/functional/src/stylex.rs @@ -130,6 +130,17 @@ impl StyleXRootCall { StyleXRootCall::FirstThatWorks => "firstThatWorks", } } + + /// StyleX hashes these definitions by file path, so it accepts them only in theme files and + /// resolves references to them only through imports of theme files. + pub fn defines_theme_value(self) -> bool { + matches!( + self, + StyleXRootCall::DefineVars + | StyleXRootCall::DefineConsts + | StyleXRootCall::DefineMarker + ) + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/compiler-backend/functional/src/tree.rs b/compiler-backend/functional/src/tree.rs index 2ec0ca8a3..2bd8bf6f2 100644 --- a/compiler-backend/functional/src/tree.rs +++ b/compiler-backend/functional/src/tree.rs @@ -7,6 +7,7 @@ use files::FileId; use indexing::{DeriveId, InstanceId, TermItemId, TypeItemId}; use la_arena::{Arena, Idx}; use lowering::TypeId as SourceTypeId; +use rustc_hash::FxHashSet; use smol_str::SmolStr; use crate::stylex::StyleXExpression; @@ -21,6 +22,9 @@ pub struct Module { pub dependencies: Arc<[ModuleDependency]>, pub surface: ModuleSurface, pub declarations: Arc<[Declaration]>, + /// Imported `defineVars`, `defineConsts`, and `defineMarker` values that StyleX evaluates + /// statically, which it resolves only through imports of the defining module's theme file. + pub stylex_theme_imports: FxHashSet, pub storage: Storage, } From bc066155b5405effae11a6a0e9f159992b35cf7f Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 8 Oct 2026 00:47:01 +0000 Subject: [PATCH 2/3] Write StyleX theme definitions to theme files StyleX accepts defineVars, defineConsts, and defineMarker only in files named *.stylex.js, so projects previously had to mark every generated index.js as a theme file through unstable_moduleResolution.themeFileExtension. Write a module's exported theme definitions to index.stylex.js, which index.js re-exports. Same-module values that the definitions read are copied into the theme file: StyleX evaluates every import of a theme file as a theme value, so importing them from it would break the definitions. Modules import statically evaluated theme values from the defining module's theme file and everything else from its index.js. With @stylexjs/unplugin, Iris output now needs no module resolution options. Amp-Thread-ID: https://ampcode.com/threads/T-01a118ca-d95d-702f-85cb-95686f6ce5ed Co-authored-by: Amp --- .../convert/generator/functional/render.rs | 213 ++++++++++++++---- .../generator/functional/render/stylex.rs | 57 +++++ compiler-backend/javascript/src/lib.rs | 1 + compiler-backend/javascript/src/module.rs | 16 ++ compiler-executable/iris-build/src/compile.rs | 7 + .../iris-cli/skills/stylex/SKILL.md | 31 ++- tests-e2e/tests/stylex.rs | 14 +- tests-e2e/tools/verify-stylex.mjs | 31 ++- .../output/Main/index.js | 22 +- .../output/Main/index.stylex.js | 21 ++ .../Main.checking.snap | 2 + .../Main.functional.snap | 9 +- .../Main.purs | 5 +- .../Main.semantic.snap | 7 +- .../Tokens.purs | 13 +- .../output/Main/index.js | 15 +- .../output/Tokens/index.js | 12 +- .../output/Tokens/index.stylex.js | 5 + .../output/Main/index.js | 9 +- .../output/Main/index.stylex.js | 8 + .../output/Main/index.js | 3 +- .../output/Main/index.stylex.js | 2 + .../output/Main/index.js | 3 +- .../output/Main/index.stylex.js | 2 + .../output/Main/index.js | 2 +- .../output/Tokens/index.js | 3 +- .../output/Tokens/index.stylex.js | 2 + tests-integration/src/fixtures.rs | 10 + 28 files changed, 401 insertions(+), 124 deletions(-) create mode 100644 tests-integration/fixtures/compiler/1788370980_style_x_extended_intrinsics/output/Main/index.stylex.js create mode 100644 tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/output/Tokens/index.stylex.js create mode 100644 tests-integration/fixtures/compiler/1788371940_style_x_theme_overrides/output/Main/index.stylex.js create mode 100644 tests-integration/fixtures/compiler/1791340800_stylex_marker_composition/output/Main/index.stylex.js create mode 100644 tests-integration/fixtures/compiler/1791340920_stylex_marker_style_in_marker_case/output/Main/index.stylex.js create mode 100644 tests-integration/fixtures/compiler/1791347580_stylex_conditional_case/output/Tokens/index.stylex.js diff --git a/compiler-backend/javascript/src/convert/generator/functional/render.rs b/compiler-backend/javascript/src/convert/generator/functional/render.rs index d459e6195..b0fdc835f 100644 --- a/compiler-backend/javascript/src/convert/generator/functional/render.rs +++ b/compiler-backend/javascript/src/convert/generator/functional/render.rs @@ -26,7 +26,9 @@ use smol_str::{SmolStr, format_smolstr}; use super::super::names::NameAllocator; use crate::error::{ModuleDiagnostic, ModuleError, ModuleResult, UnsupportedState}; -use crate::module::{Module, module_filename, runtime_filename}; +use crate::module::{ + Module, STYLEX_THEME_FILENAME, module_filename, runtime_filename, stylex_theme_module_filename, +}; use crate::tree::{BinaryOperator, ExpressionId, ObjectProperty, Tree, UnaryOperator}; use crate::writer::{BindingCallTarget, Writer}; @@ -34,7 +36,7 @@ use self::inline::{is_abstraction, pattern_parameter}; use self::structure::{ collect_module_references, cyclic_instance_initializers, has_local_lazy_initializers, }; -use self::stylex::collect_stylex_references; +use self::stylex::{StyleXTheme, collect_stylex_references, collect_stylex_theme}; use self::syntax::{ binary_expression, combine_conditions, constructor_expression, curried_call_expression, literal_expression, synthesized_evidence_expression, unary_expression, @@ -55,6 +57,7 @@ pub(crate) struct Generator<'m> { external_named_imports: FxHashMap, external_references: Vec, stylex_namespace: Option, + stylex_theme: Option, foreign_import: Option, runtime_namespace: Option, lazy_global_names: FxHashMap, @@ -177,6 +180,14 @@ struct ModuleRenderer<'a, 'm, 't> { generator: &'a Generator<'m>, tree: &'a mut Tree<'t>, writer: &'a mut Writer<'t>, + file: OutputFile, +} + +/// The files generated for a module. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum OutputFile { + Index, + StyleXTheme, } struct FunctionRenderer<'a, 'm, 't> { @@ -309,6 +320,7 @@ impl<'m> Generator<'m> { } let stylex_namespace = has_stylex.then(|| allocator.allocate("$stylex")); + let stylex_theme = if has_stylex { collect_stylex_theme(module) } else { None }; let has_foreign = module .declarations @@ -367,6 +379,7 @@ impl<'m> Generator<'m> { external_named_imports, external_references, stylex_namespace, + stylex_theme, foreign_import, runtime_namespace, lazy_global_names, @@ -381,8 +394,12 @@ impl<'m> Generator<'m> { let mut tree = Tree::new(&allocator); let mut writer = Writer::new(&allocator); let initializer_cycle = { - let mut renderer = - ModuleRenderer { generator: &self, tree: &mut tree, writer: &mut writer }; + let mut renderer = ModuleRenderer { + generator: &self, + tree: &mut tree, + writer: &mut writer, + file: OutputFile::Index, + }; render_imports(&mut renderer); render_constructors(&mut renderer); render_source_functions(&mut renderer)?; @@ -392,6 +409,25 @@ impl<'m> Generator<'m> { render_exports(&mut renderer); initializer_cycle }; + let stylex_theme_source = if self.stylex_theme.is_some() { + let mut writer = Writer::new(&allocator); + let mut renderer = ModuleRenderer { + generator: &self, + tree: &mut tree, + writer: &mut writer, + file: OutputFile::StyleXTheme, + }; + render_imports(&mut renderer); + let initializer_cycle = render_value_declarations(&mut renderer)?; + debug_assert!( + initializer_cycle.is_empty(), + "invariant violated: statically evaluable StyleX theme values form a cycle" + ); + render_exports(&mut renderer); + Some(writer.finish()) + } else { + None + }; let dependencies = self.module.dependencies.iter().map(|dependency| dependency.file_id); let dependencies = dependencies.collect_vec(); @@ -409,6 +445,7 @@ impl<'m> Generator<'m> { dependencies, diagnostics, self.foreign_import.as_ref().map(|foreign_import| foreign_import.kind), + stylex_theme_source, requires_runtime, )) } @@ -424,7 +461,19 @@ impl<'m> Generator<'m> { } fn render_imports(renderer: &mut ModuleRenderer<'_, '_, '_>) { - let ModuleRenderer { generator, writer, .. } = renderer; + let ModuleRenderer { generator, writer, file, .. } = renderer; + if *file == OutputFile::StyleXTheme { + let theme = generator + .stylex_theme + .as_ref() + .expect("invariant violated: StyleX theme file rendered without theme definitions"); + render_named_imports(generator, writer, theme.imports.iter().copied()); + if let Some(namespace) = &generator.stylex_namespace { + writer.import_namespace(namespace, "@stylexjs/stylex"); + } + writer.blank(); + return; + } let mut files = generator .external_module_namespaces .keys() @@ -439,34 +488,19 @@ fn render_imports(renderer: &mut ModuleRenderer<'_, '_, '_>) { let path = format!("../{}", module_filename(module_name)); writer.import_namespace(namespace, &path); } - let mut named_files = generator - .external_named_imports - .keys() - .map(|global| global_file(*global)) - .collect::>() - .into_iter() - .collect_vec(); - named_files.sort_by_key(|file_id| generator.module_dependency(*file_id).module_name.as_str()); - for file_id in named_files { - let dependency = generator.module_dependency(file_id); - let path = format!("../{}", module_filename(&dependency.module_name)); - let bindings = generator - .external_named_imports + render_named_imports(generator, writer, generator.external_named_imports.keys().copied()); + if let Some(theme) = &generator.stylex_theme { + let definitions = generator + .module + .declarations .iter() - .filter(|(global, _)| global_file(**global) == file_id) - .map(|(global, local)| { - let imported = generator - .external_references - .iter() - .find(|reference| reference.id == *global) - .expect("invariant violated: named import has no external reference") - .item_name - .as_str(); - (imported, local.as_str()) + .filter(|declaration| theme.definitions.contains(&declaration.global.id)) + .map(|declaration| { + let local = generator.global_name(declaration.global.id); + (declaration.global.item_name.as_str(), local) }); - let mut bindings = bindings.collect_vec(); - bindings.sort_unstable(); - writer.import_named(&bindings, &path); + let definitions = definitions.collect_vec(); + writer.import_named(&definitions, &format!("./{STYLEX_THEME_FILENAME}")); } if let Some(namespace) = &generator.stylex_namespace { writer.import_namespace(namespace, "@stylexjs/stylex"); @@ -489,8 +523,48 @@ fn render_imports(renderer: &mut ModuleRenderer<'_, '_, '_>) { } } +/// StyleX resolves an imported theme value only when the import names the defining module's +/// theme file. Other values are imported from the module itself, which re-exports its theme. +fn render_named_imports( + generator: &Generator<'_>, + writer: &mut Writer<'_>, + globals: impl Iterator, +) { + let mut groups = FxHashMap::<_, Vec<_>>::default(); + for global in globals { + let local = generator + .external_named_imports + .get(&global) + .expect("invariant violated: imported StyleX value has no named import"); + let imported = generator + .external_references + .iter() + .find(|reference| reference.id == global) + .expect("invariant violated: named import has no external reference") + .item_name + .as_str(); + let theme = generator.module.stylex_theme_imports.contains(&global); + let group = groups.entry((global_file(global), theme)).or_default(); + group.push((imported, local.as_str())); + } + let mut groups = groups.into_iter().collect_vec(); + groups.sort_by_key(|&((file_id, theme), _)| { + (generator.module_dependency(file_id).module_name.as_str(), theme) + }); + for ((file_id, theme), mut bindings) in groups { + let module_name = &generator.module_dependency(file_id).module_name; + let filename = if theme { + stylex_theme_module_filename(module_name) + } else { + module_filename(module_name) + }; + bindings.sort_unstable(); + writer.import_named(&bindings, &format!("../{filename}")); + } +} + fn render_constructors(renderer: &mut ModuleRenderer<'_, '_, '_>) { - let ModuleRenderer { generator, tree, writer } = renderer; + let ModuleRenderer { generator, tree, writer, .. } = renderer; let mut rendered = false; for declaration in generator.module.declarations.iter() { let DeclarationKind::Constructor { arity } = declaration.kind else { @@ -1033,7 +1107,7 @@ impl Generator<'_> { } fn render_foreign_declarations(renderer: &mut ModuleRenderer<'_, '_, '_>) { - let ModuleRenderer { generator, tree, writer } = renderer; + let ModuleRenderer { generator, tree, writer, .. } = renderer; let Some(foreign_import) = &generator.foreign_import else { return; }; @@ -1103,7 +1177,9 @@ fn render_value_declarations( let DeclarationKind::Value(expression) = declaration.kind else { unreachable!("invariant violated: sorted JavaScript declaration is not a value") }; - if is_abstraction(&generator.module.storage[expression].kind) { + if is_abstraction(&generator.module.storage[expression].kind) + || !renderer.renders_value(declaration) + { continue; } @@ -1113,7 +1189,7 @@ fn render_value_declarations( } let name = generator.global_name(declaration.global.id); - let exported = generator.declaration_is_inline_exported(declaration); + let exported = renderer.declaration_is_inline_exported(declaration); if cyclic { initializer_cycle.push(declaration.global.id); @@ -3237,31 +3313,70 @@ fn sorted_value_declarations<'m>(generator: &'m Generator<'_>) -> Vec<(&'m Decla } fn render_exports(renderer: &mut ModuleRenderer<'_, '_, '_>) { - let ModuleRenderer { generator, writer, .. } = renderer; let mut rendered = false; - for declaration in generator.module.declarations.iter() { - if !declaration.exported { - continue; - } - let local = generator.global_name(declaration.global.id); - if local == declaration.global.item_name { + for declaration in renderer.generator.module.declarations.iter() { + if !renderer.exports(declaration) || renderer.declaration_is_inline_exported(declaration) { continue; } - writer.export(local, &declaration.global.item_name); + let local = renderer.generator.global_name(declaration.global.id); + renderer.writer.export(local, &declaration.global.item_name); rendered = true; } - for exports in generator.module.surface.indirect.iter() { - let specifiers = exports.globals.iter().map(|global| global.item_name.to_string()); - let dependency = generator.module_dependency(exports.file_id); - let path = format!("../{}", module_filename(&dependency.module_name)); - writer.re_export(specifiers.collect_vec(), &path); - rendered = true; + let ModuleRenderer { generator, writer, file, .. } = renderer; + if *file == OutputFile::Index { + if let Some(theme) = &generator.stylex_theme { + let definitions = generator + .module + .declarations + .iter() + .filter(|declaration| theme.definitions.contains(&declaration.global.id)) + .map(|declaration| declaration.global.item_name.to_string()); + writer.re_export(definitions.collect_vec(), &format!("./{STYLEX_THEME_FILENAME}")); + rendered = true; + } + for exports in generator.module.surface.indirect.iter() { + let specifiers = exports.globals.iter().map(|global| global.item_name.to_string()); + let dependency = generator.module_dependency(exports.file_id); + let path = format!("../{}", module_filename(&dependency.module_name)); + writer.re_export(specifiers.collect_vec(), &path); + rendered = true; + } } if rendered { writer.blank(); } } +impl ModuleRenderer<'_, '_, '_> { + /// Whether this file renders the declaration, if it is a value: a module's theme definitions + /// render only in its theme file, which renders nothing else except the values they read. + fn renders_value(&self, declaration: &Declaration) -> bool { + let id = declaration.global.id; + match (self.file, &self.generator.stylex_theme) { + (OutputFile::Index, None) => true, + (OutputFile::Index, Some(theme)) => !theme.definitions.contains(&id), + (OutputFile::StyleXTheme, Some(theme)) => theme.declarations.contains(&id), + (OutputFile::StyleXTheme, None) => false, + } + } + + /// A theme file exports only the theme definitions; the values they read stay private to it, + /// while the module's own file re-exports the definitions. + fn exports(&self, declaration: &Declaration) -> bool { + match (self.file, &self.generator.stylex_theme) { + (OutputFile::StyleXTheme, Some(theme)) => { + theme.definitions.contains(&declaration.global.id) + } + _ => declaration.exported && self.renders_value(declaration), + } + } + + fn declaration_is_inline_exported(&self, declaration: &Declaration) -> bool { + let local = self.generator.global_name(declaration.global.id); + self.exports(declaration) && local == declaration.global.item_name + } +} + impl Generator<'_> { fn declaration_is_inline_exported(&self, declaration: &Declaration) -> bool { let local = self.global_name(declaration.global.id); diff --git a/compiler-backend/javascript/src/convert/generator/functional/render/stylex.rs b/compiler-backend/javascript/src/convert/generator/functional/render/stylex.rs index 919726b71..2719823fb 100644 --- a/compiler-backend/javascript/src/convert/generator/functional/render/stylex.rs +++ b/compiler-backend/javascript/src/convert/generator/functional/render/stylex.rs @@ -64,6 +64,63 @@ pub(super) fn collect_stylex_references(module: &Module) -> Vec { references } +/// The declarations rendered into a module's StyleX theme file. +/// +/// StyleX evaluates every import of a theme file as a theme value without reading the file, so +/// the same-module values that the definitions read are rendered again in the theme file rather +/// than imported from it. These values are statically evaluable data, so duplicating them does not +/// change their meaning. +pub(super) struct StyleXTheme { + /// Exported `defineVars`, `defineConsts`, and `defineMarker` values. + pub(super) definitions: FxHashSet, + /// The definitions and the same-module values that they read. + pub(super) declarations: FxHashSet, + /// Other modules' values that the declarations read. + pub(super) imports: FxHashSet, +} + +pub(super) fn collect_stylex_theme(module: &Module) -> Option { + let mut values = FxHashMap::default(); + let mut definitions = FxHashSet::default(); + for declaration in module.declarations.iter() { + let DeclarationKind::Value(expression) = declaration.kind else { continue }; + values.insert(declaration.global.id, expression); + if declaration.exported + && let ExpressionKind::StyleX(StyleXExpression::Call { + target: StyleXCallTarget::Root(call), + .. + }) = module.storage[expression].kind + && call.defines_theme_value() + { + definitions.insert(declaration.global.id); + } + } + if definitions.is_empty() { + return None; + } + let mut declarations = FxHashSet::default(); + let mut imports = FxHashSet::default(); + let mut pending = definitions.iter().copied().collect::>(); + while let Some(id) = pending.pop() { + if !declarations.insert(id) { + continue; + } + let expression = values + .get(&id) + .expect("invariant violated: StyleX theme definition reads a non-value declaration"); + let mut globals = FxHashSet::default(); + super::collect_expression_globals(module, *expression, true, &mut globals); + for global in globals { + if global_file(global) == module.file_id { + pending.push(global); + } else { + imports.insert(global); + } + } + } + Some(StyleXTheme { definitions, declarations, imports }) +} + fn global_file(id: GlobalId) -> files::FileId { match id { GlobalId::Term(file_id, _) | GlobalId::Generated(file_id, _) => file_id, diff --git a/compiler-backend/javascript/src/lib.rs b/compiler-backend/javascript/src/lib.rs index fca3723e0..40cd43228 100644 --- a/compiler-backend/javascript/src/lib.rs +++ b/compiler-backend/javascript/src/lib.rs @@ -15,6 +15,7 @@ pub use convert::convert_module; pub use error::{ModuleDiagnostic, ModuleError, ModuleResult, UnsupportedState}; pub use module::{ Module, foreign_module_filename, module_filename, runtime_filename, runtime_source, + stylex_theme_module_filename, }; use building_types::QueryResult; diff --git a/compiler-backend/javascript/src/module.rs b/compiler-backend/javascript/src/module.rs index 87d54c5a8..2bebf27b0 100644 --- a/compiler-backend/javascript/src/module.rs +++ b/compiler-backend/javascript/src/module.rs @@ -12,6 +12,7 @@ pub struct Module { dependencies: Arc<[FileId]>, diagnostics: Arc<[ModuleDiagnostic]>, foreign_kind: Option, + stylex_theme_source: Option>, requires_runtime: bool, } @@ -23,6 +24,7 @@ impl Module { dependencies: Vec, diagnostics: Vec, foreign_kind: Option, + stylex_theme_source: Option, requires_runtime: bool, ) -> Module { Module { @@ -32,6 +34,7 @@ impl Module { dependencies: dependencies.into(), diagnostics: diagnostics.into(), foreign_kind, + stylex_theme_source: stylex_theme_source.map(Arc::from), requires_runtime, } } @@ -64,6 +67,11 @@ impl Module { self.foreign_kind } + /// The module's StyleX theme definitions, written to [`stylex_theme_module_filename`]. + pub fn stylex_theme_source(&self) -> Option<&str> { + self.stylex_theme_source.as_deref() + } + pub fn requires_runtime(&self) -> bool { self.requires_runtime } @@ -81,6 +89,14 @@ pub fn module_filename(module_name: &str) -> String { format!("{module_name}/index.js") } +/// StyleX accepts `defineVars`, `defineConsts`, and `defineMarker` only in files named +/// `*.stylex.js`, which its default module resolution recognizes without configuration. +pub fn stylex_theme_module_filename(module_name: &str) -> String { + format!("{module_name}/{STYLEX_THEME_FILENAME}") +} + +pub(crate) const STYLEX_THEME_FILENAME: &str = "index.stylex.js"; + pub fn foreign_module_filename(module_name: &str, kind: ForeignSourceKind) -> String { format!("{module_name}/foreign.{}", kind.extension()) } diff --git a/compiler-executable/iris-build/src/compile.rs b/compiler-executable/iris-build/src/compile.rs index 1b136da57..ec7456df0 100644 --- a/compiler-executable/iris-build/src/compile.rs +++ b/compiler-executable/iris-build/src/compile.rs @@ -484,6 +484,13 @@ fn write_module( return ModuleWrite { outputs: Vec::new(), result: Err(error.into()) }; } let mut outputs = vec![output_path]; + if let Some(source) = module.stylex_theme_source() { + let output_path = output.join(javascript::stylex_theme_module_filename(module.name())); + if let Err(error) = write_if_changed(&output_path, source.as_bytes()) { + return ModuleWrite { outputs, result: Err(error.into()) }; + } + outputs.push(output_path); + } if let Some(kind) = module.foreign_kind() { let output_path = output.join(javascript::foreign_module_filename(module.name(), kind)); let foreign = compilation diff --git a/compiler-executable/iris-cli/skills/stylex/SKILL.md b/compiler-executable/iris-cli/skills/stylex/SKILL.md index d014251cf..9d0bbe4c1 100644 --- a/compiler-executable/iris-cli/skills/stylex/SKILL.md +++ b/compiler-executable/iris-cli/skills/stylex/SKILL.md @@ -50,7 +50,7 @@ import Iris.StyleX.Types as Types | `stylex.firstThatWorks('sticky', 'fixed')` | `StyleX.firstThatWorks [ "sticky", "fixed" ]` | | `stylex.types.color('red')` | `Types.color "red"` | | `stylex.keyframes`, `defineVars`, `defineConsts`, `createTheme`, `viewTransitionClass`, `positionTry`, `attrs` | The same name under `StyleX` | -| `tokens.stylex.js` exporting `defineVars` or `defineConsts` | An exported top-level value in any module, with the bundler setting below | +| `tokens.stylex.js` exporting `defineVars` or `defineConsts` | An exported top-level value in any module; Iris writes it to the module's theme file | - Quote record labels that are not PureScript identifiers: `":hover"`, `"::placeholder"`, `"@media (min-width: 768px)"`, `"WebkitBackdropFilter"`. @@ -155,23 +155,18 @@ as well as the Iris build. Follow StyleX's installation guide for the bundler, then add what Iris output needs: - Generated modules import `@stylexjs/stylex`, so the project depends on it at runtime. -- Iris writes each module to `output//index.js`, but the plugin accepts `defineVars`, - `defineConsts`, `defineMarker`, and `createTheme` only in theme files, `*.stylex.js` by default. - Mark Iris output as theme files; `@stylexjs/unplugin` forwards this option to the Babel plugin: - - ```js - unstable_moduleResolution: { - type: "commonJS", - rootDir: process.cwd(), - themeFileExtension: "index", - }, - ``` - - Without it, those calls fail with `Unable to generate hash` or `Only static values are allowed`, - and imports between modules in static calls fail with `Could not resolve the path to the - imported file`. When calling the Babel plugin yourself, pass real file paths: the plugin - resolves imports through symlinks, so a symlinked filename gives a variable a different hash in - the module that imports it. +- Iris writes each module to `output//index.js`. The plugin accepts `defineVars`, + `defineConsts`, and `defineMarker` only in theme files named `*.stylex.js`, so Iris writes a + module's exported definitions to `output//index.stylex.js`, which `index.js` + re-exports. Generated modules import these values from the theme file, which the plugin's + default module resolution follows. `@stylexjs/unplugin` needs no resolution options; when + calling the Babel plugin yourself, pass `unstable_moduleResolution: { type: "commonJS", + rootDir }`, since the plugin cannot hash these definitions without it. Remove any + `themeFileExtension: "index"` setting from older Iris versions: it no longer matches the + generated files. +- When calling the Babel plugin yourself, also transform each `index.stylex.js` and pass real + file paths: the plugin resolves imports through symlinks, so a symlinked filename gives a + variable a different hash in the module that imports it. - Run `iris watch` beside the dev server, with the same `--output` the bundler imports from. - Vite may miss `@stylexjs/stylex` in its first scan of generated modules; add it to `optimizeDeps.include` if the dev server reloads on first navigation. diff --git a/tests-e2e/tests/stylex.rs b/tests-e2e/tests/stylex.rs index dbf475256..81a31a49e 100644 --- a/tests-e2e/tests/stylex.rs +++ b/tests-e2e/tests/stylex.rs @@ -19,19 +19,25 @@ package: ); workspace.write( "src/Tokens.purs", - r#"module Tokens (await, constants, rowMarker, variables) where + r#"module Tokens (await, constants, gapProps, rowMarker, variables) where import Iris.StyleX as StyleX await :: StyleX.Style await = StyleX.defaultMarker -constants = StyleX.defineConsts { spacing: "13px", small: "@media (max-width: 600px)" } +gap = "21px" + +constants = StyleX.defineConsts { spacing: "13px", small: "@media (max-width: 600px)", gap } variables = StyleX.defineVars { accent: "blue" } rowMarker :: StyleX.Marker rowMarker = StyleX.defineMarker + +gapStyles = StyleX.create { root: { margin: gap, color: variables.accent } } + +gapProps = StyleX.props gapStyles.root "#, ); workspace.write( @@ -106,8 +112,8 @@ conditionalProps enabled = StyleX.props assert_success(&output); let generated = workspace.read("output/Main/index.js"); assert!( - generated - .contains("import { \"await\" as Tokens_await, constants as Tokens_constants, rowMarker as Tokens_rowMarker, variables as Tokens_variables }"), + generated.contains("import { \"await\" as Tokens_await } from \"../Tokens/index.js\";") + && generated.contains("import { constants as Tokens_constants, rowMarker as Tokens_rowMarker, variables as Tokens_variables } from \"../Tokens/index.stylex.js\";"), "unexpected generated JavaScript:\n{generated}" ); diff --git a/tests-e2e/tools/verify-stylex.mjs b/tests-e2e/tools/verify-stylex.mjs index 453ac039d..d3cdf17d4 100644 --- a/tests-e2e/tools/verify-stylex.mjs +++ b/tests-e2e/tools/verify-stylex.mjs @@ -12,12 +12,12 @@ import * as stylex from "@stylexjs/stylex"; // The plugin resolves imports to real paths and hashes them relative to `rootDir`, so a // symlinked temporary directory (macOS `/var`) must be canonical on both sides. const outputRoot = fs.realpathSync(path.resolve(process.argv[2])); -const modules = ["Tokens", "Main"]; +const files = ["Tokens/index.stylex.js", "Tokens/index.js", "Main/index.js"]; const styles = []; const transformed = new Map(); -for (const moduleName of modules) { - const filename = path.join(outputRoot, moduleName, "index.js"); +for (const file of files) { + const filename = path.join(outputRoot, file); const source = fs.readFileSync(filename, "utf8"); const result = transformSync(source, { filename, @@ -28,11 +28,8 @@ for (const moduleName of modules) { stylexPlugin, { dev: false, - unstable_moduleResolution: { - type: "commonJS", - rootDir: outputRoot, - themeFileExtension: "index", - }, + // The default that `@stylexjs/unplugin` passes; theme files need no further setup. + unstable_moduleResolution: { type: "commonJS", rootDir: outputRoot }, }, ], ], @@ -49,7 +46,7 @@ for (const moduleName of modules) { "viewTransitionClass", ]; if (staticCalls.some((call) => result.code.includes(`$stylex.${call}(`))) { - throw new Error(`${moduleName} retains uncompiled static StyleX calls`); + throw new Error(`${file} retains uncompiled static StyleX calls`); } styles.push(...result.metadata.stylex); transformed.set(filename, result.code); @@ -73,6 +70,7 @@ for (const expected of [ "padding:8px", "@media (max-width: 600px)", "padding:4px", + "margin:21px", ]) { if (!css.includes(expected)) { throw new Error(`StyleX CSS does not contain ${JSON.stringify(expected)}:\n${css}`); @@ -81,8 +79,12 @@ for (const expected of [ // A cross-module reference must hash to the variable its defining module declares. const accent = css.match(/(--[\w-]+):blue/)?.[1]; -if (accent === undefined || !css.includes(`border-color:var(${accent})`)) { - throw new Error(`border-color does not use the variable Tokens defines:\n${css}`); +if ( + accent === undefined || + !css.includes(`border-color:var(${accent})`) || + !css.includes(`{color:var(${accent})`) +) { + throw new Error(`Styles do not use the variable Tokens defines:\n${css}`); } for (const [filename, code] of transformed) { @@ -101,7 +103,14 @@ registerHooks({ const tokens = await import(pathToFileURL(path.join(outputRoot, "Tokens/index.js"))); const main = await import(pathToFileURL(path.join(outputRoot, "Main/index.js"))); +const theme = await import(pathToFileURL(path.join(outputRoot, "Tokens/index.stylex.js"))); +assert.equal(tokens.variables, theme.variables); const classNames = (value) => new Set(value.trim().split(/\s+/)); +const gapClasses = classNames(tokens.gapProps.className); +assert.equal(gapClasses.size, 2); +for (const gapClass of gapClasses) { + assert.ok(css.includes(`.${gapClass}`)); +} const expected = classNames(stylex.props(main.styles.row, tokens.rowMarker).className); const markerClass = stylex.props(tokens.rowMarker).className; assert.ok(markerClass.length > 0); diff --git a/tests-integration/fixtures/compiler/1788370980_style_x_extended_intrinsics/output/Main/index.js b/tests-integration/fixtures/compiler/1788370980_style_x_extended_intrinsics/output/Main/index.js index 3de04296b..913c1d0a8 100644 --- a/tests-integration/fixtures/compiler/1788370980_style_x_extended_intrinsics/output/Main/index.js +++ b/tests-integration/fixtures/compiler/1788370980_style_x_extended_intrinsics/output/Main/index.js @@ -1,28 +1,9 @@ +import { constants, variables, rowMarker } from "./index.stylex.js"; import * as $stylex from "@stylexjs/stylex"; -export const constants = $stylex.defineConsts({ - compact: "@media (max-width: 40rem)", - columns: 12 | 0 -}); -export const variables = $stylex.defineVars({ - accent: $stylex.types.color("royalblue"), - angle: $stylex.types.angle("45deg"), - image: $stylex.types.image("linear-gradient(red, blue)"), - integer: $stylex.types.integer(1 | 0), - length: $stylex.types.length("8px"), - lengthPercentage: $stylex.types.lengthPercentage("10%"), - number: $stylex.types.number(.5), - percentage: $stylex.types.percentage("50%"), - resolution: $stylex.types.resolution("2dppx"), - time: $stylex.types.time("200ms"), - transformFunction: $stylex.types.transformFunction("scale(1)"), - transformList: $stylex.types.transformList("scale(1) rotate(2deg)"), - url: $stylex.types.url("url(image.png)") -}); export const theme = $stylex.createTheme(variables, { accent: $stylex.types.color("white"), length: $stylex.types.length("12px") }); -export const rowMarker = $stylex.defineMarker(); export const styles = $stylex.create({ root: { color: { default: "blue", @@ -51,3 +32,4 @@ export const fallback = $stylex.positionTry({ positionArea: "block-start", margin: 8 | 0 }); +export { constants, variables, rowMarker } from "./index.stylex.js"; diff --git a/tests-integration/fixtures/compiler/1788370980_style_x_extended_intrinsics/output/Main/index.stylex.js b/tests-integration/fixtures/compiler/1788370980_style_x_extended_intrinsics/output/Main/index.stylex.js new file mode 100644 index 000000000..7e5ef6f34 --- /dev/null +++ b/tests-integration/fixtures/compiler/1788370980_style_x_extended_intrinsics/output/Main/index.stylex.js @@ -0,0 +1,21 @@ +import * as $stylex from "@stylexjs/stylex"; +export const constants = $stylex.defineConsts({ + compact: "@media (max-width: 40rem)", + columns: 12 | 0 +}); +export const variables = $stylex.defineVars({ + accent: $stylex.types.color("royalblue"), + angle: $stylex.types.angle("45deg"), + image: $stylex.types.image("linear-gradient(red, blue)"), + integer: $stylex.types.integer(1 | 0), + length: $stylex.types.length("8px"), + lengthPercentage: $stylex.types.lengthPercentage("10%"), + number: $stylex.types.number(.5), + percentage: $stylex.types.percentage("50%"), + resolution: $stylex.types.resolution("2dppx"), + time: $stylex.types.time("200ms"), + transformFunction: $stylex.types.transformFunction("scale(1)"), + transformList: $stylex.types.transformList("scale(1) rotate(2deg)"), + url: $stylex.types.url("url(image.png)") +}); +export const rowMarker = $stylex.defineMarker(); diff --git a/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/Main.checking.snap b/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/Main.checking.snap index 34f64006c..0c367db10 100644 --- a/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/Main.checking.snap +++ b/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/Main.checking.snap @@ -1,8 +1,10 @@ --- source: tests-integration/src/fixtures.rs +assertion_line: 311 --- Terms theme :: Iris.StyleX.Style styles :: { root :: Iris.StyleX.Style } +awaitProps :: { className :: Prim.String } Types diff --git a/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/Main.functional.snap b/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/Main.functional.snap index 3e82b4993..87a966567 100644 --- a/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/Main.functional.snap +++ b/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/Main.functional.snap @@ -1,8 +1,15 @@ --- source: tests-integration/src/fixtures.rs +assertion_line: 314 --- @export theme = stylex.createTheme variables { accent: "white" } @export styles = stylex.create { - root: { color: stylex.conditionalValue "blue" [stylex.when.ancestor(":hover", rowMarker, "red")] } + root: + { + color: stylex.conditionalValue "blue" [stylex.when.ancestor(":hover", rowMarker, "red")], + padding: spacing.gap + } } + +@export awaitProps = stylex.props await diff --git a/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/Main.purs b/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/Main.purs index 206de808d..b6cec2666 100644 --- a/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/Main.purs +++ b/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/Main.purs @@ -2,7 +2,7 @@ module Main where import Iris.StyleX as StyleX import Iris.StyleX.When as When -import Tokens (rowMarker, variables) +import Tokens (await, rowMarker, spacing, variables) theme = StyleX.createTheme variables { accent: "white" } @@ -10,5 +10,8 @@ styles = StyleX.create { root: { color: StyleX.conditionalValue "blue" [ When.ancestorMarker ":hover" rowMarker "red" ] + , padding: spacing.gap } } + +awaitProps = StyleX.props await diff --git a/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/Main.semantic.snap b/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/Main.semantic.snap index 49d65d24a..c19e57bfb 100644 --- a/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/Main.semantic.snap +++ b/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/Main.semantic.snap @@ -1,5 +1,6 @@ --- source: tests-integration/src/fixtures.rs +assertion_line: 313 --- theme :: Iris.StyleX.Style theme = @@ -10,4 +11,8 @@ theme = styles :: { root :: Iris.StyleX.Style } styles = create {compileStyles {trivial} {compileStyleListConsTypeRecord {compileStyleListNilTypeRow} {trivial}}} - { root: { color: conditionalValue "blue" [ancestorMarker ":hover" rowMarker "red"] } } + { root: { color: conditionalValue "blue" + [ancestorMarker ":hover" rowMarker "red"], padding: spacing.gap } } + +awaitProps :: { className :: Prim.String } +awaitProps = props {propsInputStyleProps} await diff --git a/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/Tokens.purs b/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/Tokens.purs index 6c15b0c7b..a89695c0c 100644 --- a/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/Tokens.purs +++ b/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/Tokens.purs @@ -1,8 +1,19 @@ -module Tokens (rowMarker, variables) where +module Tokens (await, gapProps, rowMarker, spacing, variables) where import Iris.StyleX as StyleX +await :: StyleX.Style +await = StyleX.defaultMarker + +gap = "21px" + +spacing = StyleX.defineConsts { gap } + variables = StyleX.defineVars { accent: "blue" } rowMarker :: StyleX.Marker rowMarker = StyleX.defineMarker + +gapStyles = StyleX.create { root: { margin: gap, color: variables.accent } } + +gapProps = StyleX.props gapStyles.root diff --git a/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/output/Main/index.js b/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/output/Main/index.js index 8936b4b9e..cc61e5d8f 100644 --- a/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/output/Main/index.js +++ b/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/output/Main/index.js @@ -1,7 +1,12 @@ -import { rowMarker as Tokens_rowMarker, variables as Tokens_variables } from "../Tokens/index.js"; +import { "await" as Tokens_await } from "../Tokens/index.js"; +import { rowMarker as Tokens_rowMarker, spacing as Tokens_spacing, variables as Tokens_variables } from "../Tokens/index.stylex.js"; import * as $stylex from "@stylexjs/stylex"; export const theme = $stylex.createTheme(Tokens_variables, { accent: "white" }); -export const styles = $stylex.create({ root: { color: { - default: "blue", - [$stylex.when.ancestor(":hover", Tokens_rowMarker)]: "red" -} } }); +export const styles = $stylex.create({ root: { + color: { + default: "blue", + [$stylex.when.ancestor(":hover", Tokens_rowMarker)]: "red" + }, + padding: Tokens_spacing.gap +} }); +export const awaitProps = $stylex.props(Tokens_await); diff --git a/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/output/Tokens/index.js b/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/output/Tokens/index.js index 90e9a2671..99e84dc10 100644 --- a/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/output/Tokens/index.js +++ b/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/output/Tokens/index.js @@ -1,3 +1,11 @@ +import { spacing, variables, rowMarker } from "./index.stylex.js"; import * as $stylex from "@stylexjs/stylex"; -export const variables = $stylex.defineVars({ accent: "blue" }); -export const rowMarker = $stylex.defineMarker(); +const $await = $stylex.defaultMarker(); +const gap = "21px"; +const gapStyles = $stylex.create({ root: { + margin: gap, + color: variables.accent +} }); +export const gapProps = $stylex.props(gapStyles.root); +export { $await as "await" }; +export { spacing, variables, rowMarker } from "./index.stylex.js"; diff --git a/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/output/Tokens/index.stylex.js b/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/output/Tokens/index.stylex.js new file mode 100644 index 000000000..688a0db23 --- /dev/null +++ b/tests-integration/fixtures/compiler/1788371460_style_x_cross_module_values/output/Tokens/index.stylex.js @@ -0,0 +1,5 @@ +import * as $stylex from "@stylexjs/stylex"; +const gap = "21px"; +export const spacing = $stylex.defineConsts({ gap }); +export const variables = $stylex.defineVars({ accent: "blue" }); +export const rowMarker = $stylex.defineMarker(); diff --git a/tests-integration/fixtures/compiler/1788371940_style_x_theme_overrides/output/Main/index.js b/tests-integration/fixtures/compiler/1788371940_style_x_theme_overrides/output/Main/index.js index 5d621fa89..a6c4f0f7c 100644 --- a/tests-integration/fixtures/compiler/1788371940_style_x_theme_overrides/output/Main/index.js +++ b/tests-integration/fixtures/compiler/1788371940_style_x_theme_overrides/output/Main/index.js @@ -1,11 +1,6 @@ +import { variables, invalidInteger, invalidNumber, invalidColor } from "./index.stylex.js"; import * as $stylex from "@stylexjs/stylex"; -export const variables = $stylex.defineVars({ - accent: $stylex.types.color("blue"), - spacing: $stylex.types.length("8px") -}); export const valid = $stylex.createTheme(variables, { accent: $stylex.types.color("white") }); export const wrongType = $stylex.createTheme(variables, { accent: $stylex.types.length("12px") }); export const unknown = $stylex.createTheme(variables, { missing: $stylex.types.color("red") }); -export const invalidInteger = $stylex.defineVars({ value: $stylex.types.integer(true) }); -export const invalidNumber = $stylex.defineVars({ value: $stylex.types.number("1") }); -export const invalidColor = $stylex.defineVars({ value: $stylex.types.color(42 | 0) }); +export { variables, invalidInteger, invalidNumber, invalidColor } from "./index.stylex.js"; diff --git a/tests-integration/fixtures/compiler/1788371940_style_x_theme_overrides/output/Main/index.stylex.js b/tests-integration/fixtures/compiler/1788371940_style_x_theme_overrides/output/Main/index.stylex.js new file mode 100644 index 000000000..9296984f7 --- /dev/null +++ b/tests-integration/fixtures/compiler/1788371940_style_x_theme_overrides/output/Main/index.stylex.js @@ -0,0 +1,8 @@ +import * as $stylex from "@stylexjs/stylex"; +export const variables = $stylex.defineVars({ + accent: $stylex.types.color("blue"), + spacing: $stylex.types.length("8px") +}); +export const invalidInteger = $stylex.defineVars({ value: $stylex.types.integer(true) }); +export const invalidNumber = $stylex.defineVars({ value: $stylex.types.number("1") }); +export const invalidColor = $stylex.defineVars({ value: $stylex.types.color(42 | 0) }); diff --git a/tests-integration/fixtures/compiler/1791340800_stylex_marker_composition/output/Main/index.js b/tests-integration/fixtures/compiler/1791340800_stylex_marker_composition/output/Main/index.js index defc86a33..0aa974889 100644 --- a/tests-integration/fixtures/compiler/1791340800_stylex_marker_composition/output/Main/index.js +++ b/tests-integration/fixtures/compiler/1791340800_stylex_marker_composition/output/Main/index.js @@ -1,5 +1,6 @@ +import { rowMarker } from "./index.stylex.js"; import * as $stylex from "@stylexjs/stylex"; -export const rowMarker = $stylex.defineMarker(); export const styles = $stylex.create({ row: { color: "red" } }); export const rowProps = $stylex.props([styles.row, rowMarker]); export const rowAttrs = $stylex.attrs([rowMarker, styles.row]); +export { rowMarker } from "./index.stylex.js"; diff --git a/tests-integration/fixtures/compiler/1791340800_stylex_marker_composition/output/Main/index.stylex.js b/tests-integration/fixtures/compiler/1791340800_stylex_marker_composition/output/Main/index.stylex.js new file mode 100644 index 000000000..98cb887db --- /dev/null +++ b/tests-integration/fixtures/compiler/1791340800_stylex_marker_composition/output/Main/index.stylex.js @@ -0,0 +1,2 @@ +import * as $stylex from "@stylexjs/stylex"; +export const rowMarker = $stylex.defineMarker(); diff --git a/tests-integration/fixtures/compiler/1791340920_stylex_marker_style_in_marker_case/output/Main/index.js b/tests-integration/fixtures/compiler/1791340920_stylex_marker_style_in_marker_case/output/Main/index.js index c5e9940e3..f85f60f02 100644 --- a/tests-integration/fixtures/compiler/1791340920_stylex_marker_style_in_marker_case/output/Main/index.js +++ b/tests-integration/fixtures/compiler/1791340920_stylex_marker_style_in_marker_case/output/Main/index.js @@ -1,6 +1,7 @@ +import { rowMarker } from "./index.stylex.js"; import * as $stylex from "@stylexjs/stylex"; -export const rowMarker = $stylex.defineMarker(); export const styles = $stylex.create({ row: { color: { default: "blue", [$stylex.when.ancestor(":hover", rowMarker)]: "red" } } }); +export { rowMarker } from "./index.stylex.js"; diff --git a/tests-integration/fixtures/compiler/1791340920_stylex_marker_style_in_marker_case/output/Main/index.stylex.js b/tests-integration/fixtures/compiler/1791340920_stylex_marker_style_in_marker_case/output/Main/index.stylex.js new file mode 100644 index 000000000..98cb887db --- /dev/null +++ b/tests-integration/fixtures/compiler/1791340920_stylex_marker_style_in_marker_case/output/Main/index.stylex.js @@ -0,0 +1,2 @@ +import * as $stylex from "@stylexjs/stylex"; +export const rowMarker = $stylex.defineMarker(); diff --git a/tests-integration/fixtures/compiler/1791347580_stylex_conditional_case/output/Main/index.js b/tests-integration/fixtures/compiler/1791347580_stylex_conditional_case/output/Main/index.js index 6bf788fb1..530adc260 100644 --- a/tests-integration/fixtures/compiler/1791347580_stylex_conditional_case/output/Main/index.js +++ b/tests-integration/fixtures/compiler/1791347580_stylex_conditional_case/output/Main/index.js @@ -1,4 +1,4 @@ -import { breakpoints as Tokens_breakpoints } from "../Tokens/index.js"; +import { breakpoints as Tokens_breakpoints } from "../Tokens/index.stylex.js"; import * as $stylex from "@stylexjs/stylex"; export const print = "@media print"; export const styles = $stylex.create({ root: { padding: { diff --git a/tests-integration/fixtures/compiler/1791347580_stylex_conditional_case/output/Tokens/index.js b/tests-integration/fixtures/compiler/1791347580_stylex_conditional_case/output/Tokens/index.js index a7ccd99bb..227361386 100644 --- a/tests-integration/fixtures/compiler/1791347580_stylex_conditional_case/output/Tokens/index.js +++ b/tests-integration/fixtures/compiler/1791347580_stylex_conditional_case/output/Tokens/index.js @@ -1,2 +1,3 @@ +import { breakpoints } from "./index.stylex.js"; import * as $stylex from "@stylexjs/stylex"; -export const breakpoints = $stylex.defineConsts({ small: "@media (max-width: 600px)" }); +export { breakpoints } from "./index.stylex.js"; diff --git a/tests-integration/fixtures/compiler/1791347580_stylex_conditional_case/output/Tokens/index.stylex.js b/tests-integration/fixtures/compiler/1791347580_stylex_conditional_case/output/Tokens/index.stylex.js new file mode 100644 index 000000000..a7ccd99bb --- /dev/null +++ b/tests-integration/fixtures/compiler/1791347580_stylex_conditional_case/output/Tokens/index.stylex.js @@ -0,0 +1,2 @@ +import * as $stylex from "@stylexjs/stylex"; +export const breakpoints = $stylex.defineConsts({ small: "@media (max-width: 600px)" }); diff --git a/tests-integration/src/fixtures.rs b/tests-integration/src/fixtures.rs index edc048cae..5d1504a6b 100644 --- a/tests-integration/src/fixtures.rs +++ b/tests-integration/src/fixtures.rs @@ -66,6 +66,11 @@ impl JavaScriptModules { let output_parent = output_path.parent().expect("module filename has no parent"); std::fs::create_dir_all(output_parent)?; std::fs::write(output_path, module.source())?; + if let Some(source) = module.stylex_theme_source() { + let output_path = + output.join(javascript::stylex_theme_module_filename(module.name())); + std::fs::write(output_path, source)?; + } if let Some(kind) = module.foreign_kind() { let source_url = Url::parse(&files.path(module.file_id()))?; let source_path = source_url.to_file_path().map_err(|()| { @@ -322,6 +327,11 @@ pub fn compiler(path: &Path) -> FixtureResult { continue; } expected_paths.insert(PathBuf::from(module.filename())); + if module.stylex_theme_source().is_some() { + expected_paths.insert(PathBuf::from(javascript::stylex_theme_module_filename( + module.name(), + ))); + } if let Some(kind) = module.foreign_kind() { expected_paths.insert(PathBuf::from(javascript::foreign_module_filename( module.name(), From 9014665c5ec589c7e531b0bc3a014037bce88add Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 8 Oct 2026 00:47:06 +0000 Subject: [PATCH 3/3] Show StyleX theme files in watch JavaScript queries `iris watch query javascript` shows what the StyleX plugin receives, which now includes a module's theme file. Label both files when the module has one. Amp-Thread-ID: https://ampcode.com/threads/T-01a118ca-d95d-702f-85cb-95686f6ce5ed Co-authored-by: Amp --- Cargo.lock | 1 + compiler-executable/iris-watch-query/Cargo.toml | 1 + compiler-executable/iris-watch-query/src/lib.rs | 14 +++++++++++--- compiler-executable/iris-watch-query/src/lookup.rs | 5 ++++- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b48ed3c10..f4adb17f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1990,6 +1990,7 @@ dependencies = [ "iris-build", "iris-diagnostics", "iris-watch-server", + "javascript", "serde", "serde_json", "url", diff --git a/compiler-executable/iris-watch-query/Cargo.toml b/compiler-executable/iris-watch-query/Cargo.toml index 612b5737d..745cba242 100644 --- a/compiler-executable/iris-watch-query/Cargo.toml +++ b/compiler-executable/iris-watch-query/Cargo.toml @@ -14,6 +14,7 @@ iris-analysis = { version = "0.1.0", path = "../../compiler-services/iris-analys iris-build = { version = "0.1.0", path = "../iris-build" } iris-diagnostics = { version = "0.1.0", path = "../../compiler-services/iris-diagnostics" } iris-watch-server = { version = "0.1.0", path = "../iris-watch-server" } +javascript = { version = "0.1.0", path = "../../compiler-backend/javascript" } lsp-types.workspace = true serde = { version = "1.0.229", features = ["derive"] } serde_json = "1.0.151" diff --git a/compiler-executable/iris-watch-query/src/lib.rs b/compiler-executable/iris-watch-query/src/lib.rs index 2f114a577..a4a4044e2 100644 --- a/compiler-executable/iris-watch-query/src/lib.rs +++ b/compiler-executable/iris-watch-query/src/lib.rs @@ -164,6 +164,7 @@ impl DiagnosticSeverity { #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct JavascriptAnswer { pub source: String, + pub stylex_theme_source: Option, } /// Serializes an answer for the protocol. @@ -292,9 +293,16 @@ pub fn render(query: &Query, value: Value) -> Result }); diagnostics.collect::>().join("\n") } - Query::Javascript { .. } => { - let JavascriptAnswer { source } = serde_json::from_value(value)?; - source + Query::Javascript { name } => { + let JavascriptAnswer { source, stylex_theme_source } = serde_json::from_value(value)?; + match stylex_theme_source { + Some(stylex_theme_source) => { + let filename = javascript::module_filename(name); + let theme_filename = javascript::stylex_theme_module_filename(name); + format!("// {filename}\n{source}\n// {theme_filename}\n{stylex_theme_source}") + } + None => source, + } } }; Ok(text) diff --git a/compiler-executable/iris-watch-query/src/lookup.rs b/compiler-executable/iris-watch-query/src/lookup.rs index 649b6175d..6f8ab3e8e 100644 --- a/compiler-executable/iris-watch-query/src/lookup.rs +++ b/compiler-executable/iris-watch-query/src/lookup.rs @@ -221,7 +221,10 @@ pub(crate) fn javascript( ))); } match context.engine.javascript(file_id)? { - Ok(module) => Ok(JavascriptAnswer { source: module.source().to_string() }), + Ok(module) => Ok(JavascriptAnswer { + source: module.source().to_string(), + stylex_theme_source: module.stylex_theme_source().map(str::to_string), + }), Err(_) => Err(QueryFailure::Failed(format!("no JavaScript was generated for {name}"))), } }