Draft / June 7, 2026
TSRX
This page is the working language specification for TSRX. It is written for implementors of parsers, language tooling, compilers, and hosts that need a precise account of the syntax and static constraints of the language.
First-edition scope
The current draft fixes the syntax and early-error surface for JSX-shaped TSRX values, JSX statement containers, template control flow, lazy destructuring, sibling-scoped style blocks with themes and apply, and proposal-aligned submodule declarations.
Introduction
TSRX is a TypeScript-compatible syntax extension to ECMAScript for authoring component-oriented user interface programs. It extends the TypeScript source language with JSX-shaped template values, JSX statement containers, template control-flow directives, lazy destructuring patterns, sibling-scoped style elements, and style identifiers.
This specification defines the syntax of TSRX and the static constraints that a conforming implementation is expected to enforce. It does not propose incorporation of TSRX into the ECMAScript standard, and it does not require JavaScript engines or browsers to parse TSRX directly. Instead, it defines a source language intended for compilers, preprocessors, editors, formatters, and other tooling.
Unless this document explicitly states otherwise, a construct that is valid in the TypeScript baseline grammar remains valid in TSRX source text. The grammar additions in this document are therefore additive and TypeScript-compatible.
Rationale
TSRX exists to define a predictable syntax for component-oriented source code while preserving the familiar JSX AST shape. Elements, fragments, text, expression containers, attributes, and spread attributes use the standard JSX node family; TSRX adds only the nodes needed for statement containers, style blocks, and template control flow.
- Returned TSRX templates are represented with JSXElement and JSXFragment nodes.
- Native TSRX values can be assigned, returned, or passed as props using ordinary JSX-shaped nodes.
- Lazy destructuring is represented as source syntax rather than a host-specific library convention.
- Host features such as server execution and stylesheet composition can be specified cleanly on top of a shared grammar.
1 Conformance
An implementation of core TSRX conforms to this specification if it accepts TypeScript source text together with the additional TSRX constructs defined here, rejects source texts that violate the listed early-error rules, and documents any host-defined or profile-defined semantics that are not fixed by the core language.
This specification distinguishes between core TSRX and host profiles. Core TSRX defines the syntax and static rules common to all conforming implementations. A host profile may add profile-specific constructs or strengthen restrictions on core constructs.
2 Notational Conventions
The syntactic and lexical grammar of ECMAScript are incorporated by reference as already extended by a TypeScript-compatible baseline grammar. When this document presents a modified production, it should be read as a delta against that baseline rather than as a complete restatement of the surrounding grammar.
3 Modified Lexical Conventions
TSRX introduces the whitespace-sensitive lazy-pattern introducers &{ and &[, and module declarations that can be imported by identifier source.
The ampersand introducer is part of the syntax of the lazy pattern itself. Implementations must not treat & { or & [ as lazy pattern forms.
4.1 Modified Productions
The following productions define the normative core additions for the first edition. They are additive over the TypeScript-compatible baseline grammar used by TSRX implementations.
PrimaryExpression :
JSXElement
JSXFragment
JSXStyleElement
JSXCodeBlock
JSXIfExpression
JSXForExpression
JSXSwitchExpression
JSXTryExpression
TemplateChild :
JSXText
JSXElement
JSXFragment
JSXStyleElement
JSXExpressionContainer
JSXCodeBlock
JSXIfExpression
JSXForExpression
JSXSwitchExpression
JSXTryExpression
TemplateChildren :
TemplateChildList
TemplateChildList :
TemplateChild
TemplateChildList TemplateChild
TemplateOutput :
JSXElement
JSXFragment
JSXIfExpression
JSXForExpression
JSXSwitchExpression
JSXTryExpression
BindingAtom :
LazyObjectBindingPattern
LazyArrayBindingPattern
LazyObjectBindingPattern :
&{ BindingPropertyListopt }
LazyArrayBindingPattern :
&[ BindingElementListopt ]
JSXFragment :
<> TemplateChildrenopt </>4.2 Function Bodies and Returns
TSRX does not define a component declaration form. Components are ordinary functions. A function can return a TSRX expression from a normal return statement, or use the statement-container body shorthand when the whole body is TypeScript setup followed by one rendered output.
FunctionDeclaration :
function BindingIdentifier ( FormalParametersopt ) { FunctionBody }
function BindingIdentifier ( FormalParametersopt ) JSXCodeBlock
FunctionExpression :
function BindingIdentifieropt ( FormalParametersopt ) { FunctionBody }
function BindingIdentifieropt ( FormalParametersopt ) JSXCodeBlock
ReturnStatement :
return JSXElement ;
return JSXFragment ;
return JSXCodeBlock ;
return Expression ;Class methods, object methods, arrow functions, and function expressions all remain ordinary TypeScript constructs. Hosts may choose which TSRX-producing functions are considered components.
4.3 Template Elements
Within a returned TSRX template, direct element, fragment, style, text, and
expression-container children use the standard JSX node family. JavaScript line and
block comments are permitted between template children and do not render. Template
control flow uses directive-prefixed@if,@for,@switch, and@tryblocks whose bodies render template children.
Every JSX control-flow body is an implicit statement container and must use a{}template block after the directive header.
Each@switch@caseand@defaultmust use a{}template block. Cases are isolated: they do not fall through to later cases, andbreakandreturnare syntax errors inside a JSX switch case.
Plain JSX children are the default. When an expression position, function body,
element child, or fragment child needs local TypeScript statements before rendering,
those statements must be contained in a JSX statement container written as@{...}. A statement container places setup statements first and then exactly one output
node: a JSX element, JSX fragment, or JSX control-flow expression. If the output
needs text, expression containers, or multiple siblings after setup, wrap them in a
JSX fragment. Standalone style elements are not output nodes: any number may sit
among the setup statements or after the output node, and they scope the container
(4.6).
Control-flow block bodies follow the same structural rule as statement containers:
TypeScript first (although optional), then an optional rendered template output. The
ordinary JSX form{ AssignmentExpression }contributes a JSXExpressionContainer node and is not itself a statement container. A
function exit remains an ordinaryreturnstatement.
JSXTextChild :
JSXTextCharacters
JSXExpressionContainer :
{ AssignmentExpression }
JSXCodeBlock :
@{ TemplateSetupListopt TemplateOutput JSXStyleElementListopt }
TemplateSetupList :
TemplateSetupItem
TemplateSetupList TemplateSetupItem
TemplateSetupItem :
StatementListItem
JSXStyleElement
JSXStyleElementList :
JSXStyleElement
JSXStyleElementList JSXStyleElement
TemplateBlock :
{ TemplateChildrenopt }
{ TemplateSetupList TemplateOutput JSXStyleElementListopt }
JSXIfExpression :
@if ( Expression ) TemplateBlock
@if ( Expression ) TemplateBlock @else TemplateBlock
@if ( Expression ) TemplateBlock @else JSXIfExpression
JSXForExpression :
@for ( ForHeader TemplateForOptionsopt ) TemplateBlock
@for ( ForHeader TemplateForOptionsopt ) TemplateBlock @empty TemplateBlock
JSXSwitchExpression :
@switch ( Expression ) { JSXSwitchCaseListopt }
JSXSwitchCase :
@case Expression : TemplateBlock
@default : TemplateBlock
JSXTryExpression :
@try TemplateBlock @pending TemplateBlock
@try TemplateBlock @catch ( CatchParameteropt ) TemplateBlockRuntime dynamic element and component selection uses the dynamic tag syntax: a JSX
expression container in element-name position, written<{expression}>. The expression can evaluate to a string tag name or a component constructor, and a
non-self-closing element repeats the same expression in its closing tag:</{expression}>. No runtime import is required; each target compiler lowers the form to its own
runtime helper.
JSXElementName :
JSXIdentifier
JSXMemberExpression
JSXNamespacedName
JSXExpressionContainer
JSXExpressionContainer :
{ AssignmentExpression }type Tag = 'section' | 'article';
export function Panel({ as = 'section', title }: { as?: Tag; title: string }) @{
<{as} className="panel">
<h2>{title}</h2>
</{as}>
}The tag expression must be able to resolve to an element name: an identifier, member
access, static string, or a runtime expression composed of those. Calls, spreads,
string concatenation, string interpolation, and static non-string literals are not
valid dynamic tag expressions. Forms such as<@tag />and<@Component />are not dynamic tag syntax in current TSRX.
- A JSX text child is represented by
JSXTextand contributes escaped static text. Character references such as"are decoded before the text value is stored. { AssignmentExpression }is the generic template-expression form and is represented byJSXExpressionContainer.@{...}is the template statement form and is represented byJSXCodeBlock.- Identifiers named
textare ordinary identifiers in braced template expressions.
4.3.1 Whitespace
Whitespace in template syntax follows ordinary ECMAScript token-separation rules except at a small number of JSX-like boundaries where the delimiter itself is whitespace-sensitive.
- Within
{ AssignmentExpression }, whitespace and line terminators are admitted wherever the embedded ECMAScript grammar permits them. Thus forms such as{ expr }are well-formed. - The statement-container introducer must be written as the contiguous sequence
@{. Whitespace between@and{does not form a JSXCodeBlock. - A tag or fragment introducer must be written as a contiguous delimiter sequence.
Implementations must not treat
< div>,< /div>,</ div>,< >, or< / >as TSRX template delimiters. - Whitespace and comments are not admitted within a single tag name. Source texts
such as
<Foo . Bar>do not form a single TSRX tag name. - Indentation and line breaks between adjacent template children are permitted as layout. They do not require explicit expression containers merely to separate one element child from the next.
The raw style element is also part of this syntax. A style body is captured as raw text for host-defined stylesheet processing rather than being parsed as nested template children.
4.4 Expression Values
TSRX elements, fragments, style elements, and JSX control-flow expressions are expression values by default. A single element can be returned or assigned directly, and JSX control flow can be assigned directly when the branch or loop itself is the value.
PrimaryExpression :
JSXElement
JSXFragment
JSXStyleElement
JSXCodeBlock
JSXIfExpression
JSXForExpression
JSXSwitchExpression
JSXTryExpressionUse a fragment when an expression value needs text, dynamic expression children, or multiple emitted elements. Use a JSX statement container when local declarations must precede that output, including in arrow expression bodies and statement-container function bodies. Use a single element when the value is already compact.
4.5 Lazy Destructuring
TSRX extends the baseline binding grammar with lazy array and object forms. The syntax is fixed by the language. The runtime observation model is not. Hosts may implement lazy bindings as deferred property reads, reactive accessors, or another equivalent mechanism, provided the syntactic and static constraints remain satisfied.
LazyAssignmentStatement :
LazyObjectBindingPattern = AssignmentExpression ;
LazyArrayBindingPattern = AssignmentExpression ;Lazy binding patterns are permitted in ordinary JavaScript loop headers: in a
classicforinitializer, infor...ofandfor...indeclarations, and infor await...ofdeclarations. Afor...oforfor...inheader may also use a bare target that contains a lazy pattern; it behaves as a
per-iteration binding. Declaration kinds retain their ordinary ECMAScript scope,
including post-loop visibility forvar.
for (const &{ value } of items) { use(value); }
for (let &[key] in table) { use(key); }
for (&{ value } of items) { use(value); }
for (let &{ value } = cursor; value < limit; value++) { use(value); }
for await (const &{ value } of stream) { use(value); }The iterable or right-hand expression is evaluated in the outer binding environment.
The lazy names introduced by the loop target apply to the loop's test, update, and
body only after that target has been established. These are ordinary JavaScript
loops used in setup or imperative logic; rendered list output continues to use the@fortemplate directive.
4.6 Style Elements and Style Identifiers
The syntax of style elements, where a style block may be placed, the sibling scope a
standalone block styles, the value an assigned block evaluates to, the meaning of
theapplyattribute, and the order in which a module's stylesheets are output are part of core
TSRX. A conforming implementation rewrites every selector of a scoped block to
require a hash class: a class it adds to each element the block reaches, so that the
block's selectors match only there. The host is responsible for the text of
generated hash classes, the name of the class attribute it adds them to, selector
rewriting, stylesheet registration, and for injecting a module's CSS before the CSS
of every module that imports it. The selector form:global(...)marks the wrapped part of a selector as unscoped and may appear only at the start or
end of a selector sequence
(tsrx-css-global-placement); the block form:global { ... }marks every rule inside it as unscoped.
JSXStyleElement :
<style JSXAttributesopt> CSSSource </style>
<style JSXAttributesopt />
StyleApplyValue :
{ StyleApplyTarget }
{ [ StyleApplyTargetListopt ] }
StyleApplyTargetList :
StyleApplyTarget
StyleApplyTargetList , StyleApplyTarget
StyleApplyTarget :
IdentifierReference
StyleApplyTarget . IdentifierNameKeyframe names in a scoped style block are scoped by default. The implementation
renames each local@keyframesdeclaration using the block's hash and rewrites matching names inanimationandanimation-namedeclarations within that block. A keyframe name beginning with-global-is emitted with that prefix removed and no hash added: for example,@keyframes -global-fadeInbecomes@keyframes fadeIn. References use the unprefixed
name, including in other components or style blocks once the defining stylesheet is
loaded. Keyframes declared inside a:global { ... }block also retain unscoped names. These rules apply to vendor-prefixed keyframe
at-rules and animation properties as well. Making a keyframe name global does not
change the scope of selectors that reference it.
A style body is captured as raw CSS text. A self-closing style element has no body,
no stylesheet, and no hash class of its own; it exists to carry anapplyattribute. A scoped style block admits exactly two attributes:ref, whose value receives the class-map object of
the block's scope (4.6.3), andapply, whose value must be a StyleApplyValue. Style
elements inside a<head>element and style elements carrying anhrefattribute (resource styles) are outside the scope model of this section: they admit
any attribute, are not scoped, and do not admitapply.
4.6.1 A block is standalone or assigned
A style element is standalone when it is written as template content: a child of a native element or fragment, the output node of a JSXCodeBlock or of a control-flow body, or a bare statement. Only the first placement is valid. A style element is an output node like any other. A block beside the output node of a JSXCodeBlock or a control-flow body is the ordinary multiple-outputs error. A block that is the lone output of such a body, or a statement, is an early error (5.1): wrap it with the output it styles in a fragment. Any number of standalone blocks may appear in one children list. A style element in any other position is assigned: a variable initializer, an object property value, a default export, or any other expression value. Assigned blocks are legal wherever a JavaScript expression is legal, including module scope, function bodies, and nested code blocks. A standalone block at module scope is an early error (5.1).
Raw CSS in a style element is TSRX template syntax. A standalone block with CSS in
it must sit lexically inside a JSXCodeBlock body or a control-flow body, at any
depth of native elements, fragments, expression containers, callbacks, or templates
assigned inside such a body; anywhere else it is an early error (5.1). Plain TSX
keeps the TSX rule: a<style>element whose first child is an expression container, written<style>{css}</style>, is an ordinary JSXElement
with no stylesheet, no scope, and no hash class, and the implementation adds nothing
to it. Head styles and resource styles remain exempt.
4.6.2 A block styles its siblings and everything below them
A standalone block is scoped to its siblings. A sibling scope is the children list
of a native element or fragment that holds at least one standalone block, and a
standalone block belongs to the children list it is written in. It styles the other
children of that list and their descendants, and it never styles the element that
contains it, nor any ancestor. To style an element, make the block and the element
siblings in a fragment. Children lists nested in a scope that hold blocks are nested
scopes, whether they sit directly below or inside a nested JSXCodeBlock or a
control-flow body. Every@ifbranch, the@forbody and its@emptyblock, every@caseand@defaultbody, and the@try,@pending, and@catchblocks render one output node, and a fragment there is a scope of its own.
Sibling blocks share one hash class. All standalone blocks of one list share one scope hash: the position-derived hash of the first block in source order that has a body. Two blocks in one element's children list share a hash class; a block in an enclosing fragment and a block in that element's children list do not. The implementation adds the scope's hash class to every native element among the list's items and their descendants, through nested scopes, so an element carries the hash class of every enclosing scope, outermost first. A composite component element receives no classes of its own, although native elements written as its children still belong to the scope, and no hash class is added past a function boundary. A scope whose blocks are all self-closing has no hash class of its own.
export function Panel() @{
<>
<style>
/* sibling scope A: the children list of this fragment */
div { color: black; }
</style>
<div>Black</div>
<style>
/* still scope A: shares the hash of the first block */
p { margin: 0; }
</style>
<p>No margin</p>
<section>
<style>
/* sibling scope B: the children list of <section>, nested in A. It
styles its siblings and everything below them, never <section>. */
div { font-weight: bold; }
</style>
<div>Black and bold: A and B both reach here</div>
</section>
</>
}
// Stamped classes: the outer div, p, and section carry "<A>"; the nested div carries "<A> <B>".
// Emitted CSS order: div.<A>, p.<A>, div.<B>.Unused selectors are removed per scope, against the list's other children and their
subtrees: a selector of a standalone block that matches no element the block reaches
is unused, including a selector that matches only the element containing the block.
A block inside an@ifor@forbranch styles only the elements that branch renders: its hash class is added only to
the elements of its own branch. Its CSS is still always part of the module's
stylesheet, whether or not the branch ever renders, because CSS is static.
4.6.3 An assigned block evaluates to an object with $class
An assigned block evaluates to an object. Its first property is$class, a string, followed by one property for
every class selector that stands alone as a complete selector in the block; the
value of such a property is the block's hash class, a space, and the class name. A
class selector named$classin an assigned block is an early error.
An assigned block is a theme when it is exported from its module, is the target of
anyapplyin its module, or has its$classproperty read anywhere in its module; otherwise it is a class map. A theme keeps
every selector, each scoped under the block's hash class. A class map keeps only the
class selectors its object exposes; every other selector is removed as unused.
Reading a class property such asstyles.carddoes not make a block a theme.
$classis the space-separated concatenation of the$classof every applied block, inapplyorder, followed by the block's own hash class. A block without a body exposes$classonly and contributes no hash class. A repeated hash class in a class list has no CSS
effect, and an implementation may drop repeats it can resolve statically. A
same-module target whose$classis fully static may be written into the output as a string literal; an imported
target is a runtime read of its$classproperty.
Reading$classopts elements into a theme one at a time. An element whose class attribute includes
a theme's$classmatches the theme's element and descendant selectors exactly as an element of an
applying scope does (4.6.4), and no other element is affected. The value is an
ordinary string, so it may be passed to a component as a prop and placed on that
component's elements; it is then an authored class of those elements and precedes
the hash classes of their own enclosing scopes.$classis the selective counterpart ofapply, which reaches every element of a scope, and
the two may be combined. Listing the$classof several themes on one element composes them there asapply={[a, b]}composes them on a scope.
// theme.tsrx
export const base = <style>
div { font-family: system-ui; }
.muted { color: gray; }
</style>;
// base is { $class: "<base>", muted: "<base> muted" }
export const theme = <style apply={base}>
div { color: green; }
.dark { color: purple; }
</style>;
// theme.$class is "<base> <theme>"; theme.dark is "<theme> dark"
// panel.tsrx
import { theme } from "./theme.tsrx";
export function Panel() @{
<>
<style apply={theme}>
/* scope A; every element of A also carries theme.$class */
div { color: black; }
</style>
<span class={theme.dark}>Purple</span>
<div>Black: the local rule follows the sheet of theme</div>
@{
<>
<style>div { font-weight: bold; }</style>
<div>Carries "<A> <B> " + theme.$class</div>
</>
}
</>
}
// Emitted CSS order: base, theme (theme.tsrx); then scope A, scope B (panel.tsrx).
// card.tsrx: opting elements in with $class
function Card({ parentClass }: { parentClass: string }) @{
<>
<style>.local { padding: 0; }</style>
<article class={`local ${parentClass}`}>
<h2 class={parentClass}>Title</h2>
</article>
</>
}
export function App() @{
const palette = <style>
div { color: blue; }
.card { color: red; }
</style>;
<>
<Card parentClass={palette.$class} />
<div class={palette.$class}>Blue: carries palette.$class</div>
<div class={palette.card}>Red: carries palette.card</div>
<p>Untouched: carries nothing from palette</p>
</>
}
// palette.$class is read, so palette is a theme and div { color: blue } is kept.
// <article> and <h2> carry palette.$class, then the hash of the scope of Card.4.6.4 apply adds a theme's $class to every element of a scope
applyon a standalone block adds the$classof each listed block to every element the block's scope reaches, after every
enclosing scope's hash class, outer scopes' entries first and each scope's entries
in source order.applyon an assigned block merges into that block's$class. Each entry resolves with ordinary lexical
scoping from the position of the style element and must name an assigned block: a
binding whose initializer is a style element, an import, or a non-computed member
chain rooted at an import or at a module-local object literal whose named property
holds a style element. Any other entry, including a call, a conditional, a spread
element, or an array hole, is an early error.
A same-module target must be declared before the block that applies it, by source position. This is deliberately stricter than the ECMAScript temporal dead zone: same-module CSS is output in source order, so a target declared after the block that applies it would win the cascade instead of losing it. Imports are moved to the top of the module by the language, so they always satisfy the rule.
4.6.5 Later rules win: stylesheets are output in source order, outer first
Every generated hash class is a single class, so all scoped rules have equal specificity and document order decides. A conforming implementation outputs a module's stylesheets in source order, outer scopes first, and the following rules are normative.
- Outer before inner. A scope's sheets form one contiguous group in source order, placed where the scope's first block sits and before the groups of the scopes nested inside it, even when one of its blocks is written after a nested scope. Sibling scopes are output in source order, and an assigned block is output at its declaration position.
- Applied theme before the block that applies it. A same-module target precedes the
block that applies it because it must be declared first, and a host must inject an
imported module's CSS before the CSS of the module that imports it. The array
position of an
applyentry decides which classes are added, not their precedence. - Source order within a scope. Of two blocks in one scope, the later wins.
4.7 Host-defined Server Extensions
Submodule declarations are documented in the first edition as a generic extension
surface aligned with the TC39 module declarations proposal. Ripple and Octane both
define host profiles around a module server declaration. Ripple exposes
proposal-aligned imports from server, while Octane uses the file-local module
specifier'server'for RPC imports. The transport, serialization, and runtime behavior remain
host-defined.
A server submodule is a structural boundary, not a directive annotation. Its nested
module scope gives host compilers and tooling an explicit region for isolation and
static analysis, including rejecting implicit cross-boundary captures and keeping
server-only dependencies out of client output. A host-specific"use server"directive, when supported, has separate semantics and is not interchangeable with
module server.
SubmoduleDeclaration :
module Identifier { ModuleItemListopt }
SubmoduleImportDeclaration :
import ImportClause from Identifier ;
The identifier-source import production above describes the proposal-aligned form.
Octane's quoted'server'specifier uses the ordinary TypeScript import grammar.
5 Static Semantics: Early Errors
- A tag or fragment delimiter must not be split by intervening whitespace at the points described in 4.3.1.
- Opening and closing tags for TSRX elements and fragments must match.
- A JSXCodeBlock or template control-flow block that contains TypeScript setup statements and rendered output must place those statements before the output node and must have exactly one output node.
- A statement-container function body follows the same structural rule as any other JSXCodeBlock.
- A standalone
JSXExpressionContaineris not a template output node. Use a JSX fragment when a statement container or control-flow block needs to render text, expression containers, or multiple siblings. - A lazy destructuring assignment must have a directly lazy array or object target,
must be the complete expression statement, and must be a direct item in a Program,
BlockStatement, JSXCodeBlock, or SwitchCase statement list. Braceless control-flow
bodies, nested assignments, sequence operands, call arguments, and non-lazy outer
targets that contain a lazy pattern are early errors reported as
tsrx-unsupported-lazy-assignment-position. Diagnostic-collecting tooling may preserve syntactically valid best-effort output, but that recovery does not make the unsupported assignment conforming TSRX. - In hosts that enable server-oriented submodules, server exports must be imported
before use using the host profile's import form, for example
import { load } from serverin Ripple orimport { load } from 'server'in Octane. - Host profiles may restrict which submodule names are supported and may impose additional restrictions on referenced bindings.
- TSRX template children must not appear outside a JSXElement or JSXFragment body.
5.1 Style blocks
The following static constraints apply to style elements (4.6). Each is reported with the diagnostic code shown. None of them changes the parse, and a diagnostic-collecting implementation may still produce best-effort output, but that output is not conforming TSRX.
- A standalone style block must sit inside a template scope. A standalone block at
module scope is
tsrx-style-standalone-at-module-scope. - A standalone style block must be a child of a native element or fragment. A block
that is the lone output node of a JSXCodeBlock or a control-flow body, or a
statement, is
tsrx-style-standalone-needs-fragment. - A standalone style block with CSS text must sit lexically inside a JSXCodeBlock
body or a control-flow body (4.6.1). Anywhere else, such as a plain TSX return or
an assigned template outside those bodies, it is
tsrx-style-standalone-outside-template. Head styles, resource styles, and self-closing blocks are exempt. - A scoped style block admits only the attributes
refandapply; any other attribute istsrx-style-unknown-attribute. Head styles and resource styles admit any attribute. applyrequires an expression container, writtenapply={theme}orapply={[a, b]}. A bare attribute, a string value, or an empty container istsrx-style-apply-value.- A style element carries at most one
applyattribute; each further one istsrx-style-apply-duplicateand is ignored. applyon a head style or a resource style istsrx-style-apply-unsupported-host.- Every
applyentry must be an identifier or non-computed member chain that resolves, by lexical scoping, to an assigned style block, to an import, or to a member of an import or of a module-local object literal whose property holds a style block. Any other entry istsrx-style-apply-target. Shadowing counts: a binding that hides a style block with another value is not a target. - A same-module target declared after the block that applies it is
tsrx-style-apply-before-declaration, reported at the entry. - An assigned block whose stylesheet declares a class selector named
$classistsrx-style-reserved-class-key. - Within a style body,
:global(...)may begin or end a selector sequence but not sit in its middle, and a bare:globalmust not be nested inside another pseudo-class; both aretsrx-css-global-placement.
The precedence rules of 4.6.5 are constraints on the output stylesheets: outer before inner, applied theme before the block that applies it, and source order within a scope. An implementation that outputs sheets in any other order does not conform.
tsrx-style-standalone-at-module-scope
A standalone <style> block must sit inside a template scope. At module scope
assign it: const theme = <style>...</style>.
tsrx-style-standalone-needs-fragment
A standalone <style> block must be a child of an element or a fragment. As the
lone output of a @{ ... } body or a control-flow body, or as a statement, it
styles nothing: wrap it with the output it styles in a fragment. Beside another
output node it is the ordinary multiple-outputs error.
tsrx-style-standalone-outside-template
Raw CSS in <style> is TSRX template syntax. A standalone block with CSS text
must sit lexically inside a @{ ... } body or an @if/@for/@switch/@try body, at
any depth. In plain TSX give <style> an expression child (<style>{css}</style>)
or assign the block. Head styles and resource styles are exempt.
tsrx-style-unknown-attribute
A scoped <style> block admits only the attributes ref and apply. Head styles
and resource styles (href) admit any attribute.
tsrx-style-apply-value
apply requires an expression container: apply={theme} or apply={[a, b]}.
A bare attribute, a string value, or an empty container is an error.
tsrx-style-apply-duplicate
A <style> block carries at most one apply attribute; pass several targets
as an array.
tsrx-style-apply-unsupported-host
apply is not admitted on a <head> style or on a resource style.
tsrx-style-apply-target
Every apply entry is an identifier or non-computed member chain that resolves
by lexical scoping to an assigned <style> block: a binding initialized with a
block, an import, or a member of an import or of a module-local object literal
whose property holds a block. Calls, conditionals, spreads, and holes are errors.
tsrx-style-apply-before-declaration
A same-module target must be declared before the block that applies it, by
source position; stricter than the temporal dead zone because same-module CSS
is emitted in lexical order.
tsrx-style-reserved-class-key
An assigned block must not declare a class selector named $class.
tsrx-css-global-placement
:global(...) may begin or end a selector sequence but not sit in its middle,
and a bare :global must not be nested inside another pseudo-class.
Emission order (normative): outer scope before inner scope; applied block
before the block that applies it; source order within one scope, later wins.6 Host-defined Semantics
The purpose of the core specification is to make parsers, tooling, and language consumers agree on what TSRX source text means as syntax. The purpose of host documentation is to explain how that syntax is executed, lowered, or bound to runtime facilities.
- How functions returning TSRX lower into executable host code.
- How lazy destructuring is realized at runtime.
- The text of generated scope hashes, the name of the stamped class attribute, and how hashed selectors are rewritten.
- How stylesheets are registered and injected, provided that a module's CSS precedes the CSS of every module that imports it and that sheets keep the emission order of 4.6.5.
- How the object of an assigned block and the value handed to a style
refare delivered at runtime. - How the dynamic tag syntax
<{expression}>is lowered, and how the resolved value selects between string tags and component constructors at runtime. - How submodule declarations and their associated import forms are compiled or executed by a host profile that enables them.
Appendices
The TSRX AST contract exposes ESTree-compatible function nodes and standard JSX-shaped nodes such as JSXElement, JSXFragment, JSXExpressionContainer, JSXText, JSXAttribute, and JSXSpreadAttribute. TSRX-specific additions are limited to JSXCodeBlock, JSXStyleElement, JSXIfExpression, JSXForExpression, JSXSwitchExpression, JSXTryExpression, TSModuleDeclaration, and TSModuleBlock. The grammar in sections 4.1 through 4.7 is normative; the node shapes in this appendix are informative and describe the parser contract exposed to tooling.
A.1 Grammar-to-node correspondence
The reference parser follows the same broad editorial pattern used by the JSX specification: grammar productions define the accepted source forms, and a separate AST layer records those forms in a stable shape for downstream tools. The following correspondence summarizes the first-edition mappings.
Informative grammar-to-node correspondence
FunctionDeclaration, FunctionExpression, ArrowFunctionExpression -> ESTree function nodes
function ... @{ ... } -> ESTree function node with body: JSXCodeBlock
return JSXElement -> ReturnStatement(argument: JSXElement)
return JSXFragment -> ReturnStatement(argument: JSXFragment)
return JSXCodeBlock -> ReturnStatement(argument: JSXCodeBlock)
JSXElement -> ESTree JSXElement
JSXFragment -> ESTree JSXFragment
JSXText -> ESTree JSXText
{ AssignmentExpression } in template position -> JSXExpressionContainer
@{ StatementListItemListopt TemplateOutput } -> JSXCodeBlock
JSXAttributeName JSXAttributeInitializeropt -> JSXAttribute
{ ... AssignmentExpression } in attribute position -> JSXSpreadAttribute
<style> CSSSource </style> -> JSXStyleElement
<style JSXAttributesopt /> -> JSXStyleElement with a self-closing opening element and no StyleSheet child
<style> { AssignmentExpression } </style> -> JSXElement (an ordinary element whose children are expression containers)
@if -> JSXIfExpression
@for -> JSXForExpression
@switch -> JSXSwitchExpression
@try -> JSXTryExpression
module Identifier { ModuleItemListopt } -> TSModuleDeclaration
import ImportClause from Identifier -> ImportDeclaration with Identifier sourceA.2 Function body and return nodes
Functions remain ordinary ESTree function nodes. TSRX structure begins where a JSXElement, JSXFragment, JSXStyleElement, JSXCodeBlock, or JSX control-flow expression appears as an expression value, most commonly as a ReturnStatement argument. The statement-container function body shorthand stores a JSXCodeBlock directly in the function node's body field without introducing a separate component node kind.
interface FunctionDeclaration {
type: 'FunctionDeclaration';
id: Identifier | null;
params: Pattern[];
body: BlockStatement | JSXCodeBlock;
typeParameters?: TSTypeParameterDeclaration;
}
interface ReturnStatement {
type: 'ReturnStatement';
argument: Expression | null;
}- The function id and params fields preserve the ordinary TypeScript function surface, including type annotations and lazy patterns after parsing.
- Ordinary function bodies remain BlockStatement nodes. A statement-container function body is represented as a JSXCodeBlock in the function body's place.
- A returned or expression-position statement container is represented as a JSXCodeBlock expression.
- A returned native fragment is represented by a JSXFragment node whose children store template children in source order.
- Local template setup is represented by JSXCodeBlock nodes rather than by placing ordinary statement nodes directly in JSXElement or JSXFragment children.
- A style element with a body carries a parsed StyleSheet child and
metadata.styleScopeHash, the position-derived hash of that block. Head styles and self-closing blocks carry no hash. A standalone block is emitted under the hash of its scope (4.6.2), which the transform takes from the scope's first bodied block. - Default exports are represented through the ordinary ESTree ExportDefaultDeclaration wrapping the function declaration or expression.
- Implementation metadata may additionally record topScopedClasses for downstream
style-ref analysis. The style analyzer records
metadata.styleExported,metadata.styleApplied, andmetadata.styleClassReadon assigned blocks, derivesmetadata.styleKindfrom them (theme when any of the three is set, otherwise class-map), recordsmetadata.styleApplieson every style element, and summarizes the module's assigned and standalone blocks, in source order, onprogram.metadata.styles.
A.3 Template and attribute nodes
Template children reuse the JSX AST shape. This appendix distinguishes the element node itself from the JSX opening-tag and attribute nodes that refine it.
interface JSXElement {
type: 'JSXElement';
openingElement: JSXOpeningElement;
closingElement: JSXClosingElement | null;
children: TemplateChild[];
metadata?: { native_tsrx?: true };
}
interface JSXOpeningElement {
type: 'JSXOpeningElement';
name: JSXIdentifier | JSXMemberExpression | JSXNamespacedName | JSXExpressionContainer;
attributes: Array<JSXAttribute | JSXSpreadAttribute>;
selfClosing: boolean;
}
interface JSXExpressionContainer {
type: 'JSXExpressionContainer';
expression: Expression | JSXEmptyExpression;
}
interface JSXText {
type: 'JSXText';
value: string;
}- JSXOpeningElement.name is a JSXIdentifier for ordinary tag names, a
JSXMemberExpression for dotted names, a JSXNamespacedName for namespaced names,
and a JSXExpressionContainer for dynamic tags written as
<{expression}>. Dynamic tags additionally mark the element, its opening element, and the name container with anisDynamicflag for downstream tooling. - openingElement and closingElement preserve the original tag delimiters so formatters and source-mapping tools can recover the authored shape.
- JSXOpeningElement.selfClosing records self-closing syntax, while unclosed recovery metadata may be attached by the parser in loose scenarios.
- JSXExpressionContainer wraps the embedded ECMAScript expression from the ordinary {expr} template form.
- JSXText records a raw text child. Static text children decode JSX character references before the text value is stored.
- JSXAttribute.value is null for boolean-style attributes with no initializer. Shorthand attributes are represented as JSXAttribute nodes with parser metadata.
- JSXSpreadAttribute.argument preserves the original ECMAScript expression payload carried by the attribute form.
- A
<style>tag whose content is CSS text is a JSXStyleElement rather than a JSXElement. Its captured stylesheet source text and its styleScopeHash, styleKind, and styleApplies metadata are described in A.5 and are not part of the general JSXElement shape described here. A<style>tag whose first non-whitespace child character is{is an ordinary JSXElement named style with expression-container children.
A.4 Expression value nodes
Expression-position TSRX values use JSXElement, JSXFragment, JSXStyleElement, JSXCodeBlock, JSXIfExpression, JSXForExpression, JSXSwitchExpression, and JSXTryExpression nodes. Native fragments use the standard JSXFragment shape.
interface JSXFragment {
type: 'JSXFragment';
openingFragment: JSXOpeningFragment;
closingFragment: JSXClosingFragment;
children: TemplateChild[];
metadata?: { native_tsrx?: true };
}
type TSRXExpressionValue =
| JSXElement
| JSXFragment
| JSXStyleElement
| JSXCodeBlock
| JSXIfExpression
| JSXForExpression
| JSXSwitchExpression
| JSXTryExpression;
type TemplateOutput =
| JSXElement
| JSXFragment
| JSXIfExpression
| JSXForExpression
| JSXSwitchExpression
| JSXTryExpression;
type TemplateChild =
| JSXText
| JSXExpressionContainer
| JSXCodeBlock
| TemplateOutput
| JSXStyleElement;- JSXFragment corresponds to <> ... </> when that form appears in expression position. Its children array follows the TSRX template-child model.
- openingFragment and closingFragment preserve fragment delimiters for formatter and source-mapping tools.
A.5 TSRX extension nodes
The following nodes are the TSRX-specific additions. Statement containers are represented by JSXCodeBlock nodes in expression, child, and function-body positions. Style blocks are represented as JSXStyleElement nodes. Control-flow directives are represented directly by JSXIfExpression, JSXForExpression, JSXSwitchExpression, and JSXTryExpression nodes.
interface JSXCodeBlock {
type: 'JSXCodeBlock';
body: StatementListItem[];
render: TemplateOutput;
}
interface JSXStyleElement {
type: 'JSXStyleElement';
openingElement: JSXOpeningElement;
closingElement: JSXClosingElement | null;
children: StyleSheet[];
css?: string;
metadata?: {
native_tsrx?: true;
styleScopeHash?: string;
styleKind?: 'theme' | 'class-map';
styleApplies?: StyleApplyResolution[];
};
}
interface StyleApplyResolution {
expression: Identifier | MemberExpression;
target: JSXStyleElement | null;
kind: 'local' | 'import';
}
interface ProgramStyleMetadata {
assigned: JSXStyleElement[];
standalone: JSXStyleElement[];
}
interface JSXIfExpression {
type: 'JSXIfExpression';
statementType: 'IfStatement';
test: Expression;
consequent: Statement;
alternate: Statement | null;
}
interface JSXForExpression {
type: 'JSXForExpression';
statementType: 'ForStatement' | 'ForInStatement' | 'ForOfStatement';
body: Statement;
init?: VariableDeclaration | Expression | null;
test?: Expression | null;
update?: Expression | null;
left?: VariableDeclaration | Pattern;
right?: Expression;
await?: boolean;
index?: Identifier | null;
key?: Expression | null;
empty?: BlockStatement | null;
}
interface JSXSwitchExpression {
type: 'JSXSwitchExpression';
statementType: 'SwitchStatement';
discriminant: Expression;
cases: SwitchCase[];
}
interface JSXTryExpression {
type: 'JSXTryExpression';
statementType: 'TryStatement';
block: BlockStatement;
handler: CatchClause | null;
finalizer: BlockStatement | null;
pending?: BlockStatement | null;
}- The AST contract emits
JSXIfExpression,JSXForExpression,JSXSwitchExpression, andJSXTryExpressionfor template control flow. JSXCodeBlockis a template child and expression value, not a general ECMAScript statement node. Its render field is the one node produced by the container after setup; standalone style elements stay in body, in source order, whether they precede or follow the output.- A bodied
JSXStyleElementholds exactly one StyleSheet child and carries styleScopeHash unless it sits in a head element. A self-closing style element has an empty children array and an empty css string. styleKind is set on assigned blocks only; styleApplies is set on every style element and is empty when the element has no apply attribute. The analyzed Program carries a ProgramStyleMetadata object as metadata.styles.
A.6 Submodules, special identifiers, and stylesheets
TSRX reuses TypeScript-compatible module declaration node shapes for submodules and extends ImportDeclaration sources so a source may be an Identifier. These nodes are intentionally narrow: most of their semantics come from surrounding grammar or from host-defined analysis, not from a wide intrinsic property surface.
interface TSModuleDeclaration {
type: 'TSModuleDeclaration';
id: Identifier;
body: TSModuleBlock;
}
interface TSModuleBlock {
type: 'TSModuleBlock';
body: Array<Statement | ImportDeclaration | ExportNamedDeclaration>;
}
interface CSSStyleSheet {
type: 'StyleSheet';
children: Array<Atrule | Rule>;
source: string;
hash: string;
}- TSModuleDeclaration represents module Identifier { ... } submodules.
- ImportDeclaration.source may be an Identifier for imports from a declared submodule.
- Ripple records exported names from module server declarations for downstream RPC analysis.
- Octane associates string-literal imports from
'server'with the file-local module server declaration for server exports or client RPC stubs. - A StyleSheet child is attached to every style element with a body, wherever it sits, and stores the original source text and the position-derived hash. Standalone blocks of one scope are emitted under the scope's hash; assigned blocks are emitted under their own.
The reference parser is built on Acorn with @sveltejs/acorn-typescript and extended by a custom TSRXPlugin. This is why the grammar in this draft is framed as additive over a TypeScript-compatible baseline rather than as a language unrelated to TypeScript source syntax.