diff --git a/.changeset/young-turkeys-wave.md b/.changeset/young-turkeys-wave.md new file mode 100644 index 000000000..a0265df29 --- /dev/null +++ b/.changeset/young-turkeys-wave.md @@ -0,0 +1,7 @@ +--- +'@astrojs/compiler': minor +--- + +Support for view transition directives + +This adds support for `transition:animate` and `transition:name` which get passed into the new `renderTransition` runtime function. \ No newline at end of file diff --git a/cmd/astro-wasm/astro-wasm.go b/cmd/astro-wasm/astro-wasm.go index 3ffb91cfa..b400e9eb7 100644 --- a/cmd/astro-wasm/astro-wasm.go +++ b/cmd/astro-wasm/astro-wasm.go @@ -95,6 +95,14 @@ func makeTransformOptions(options js.Value) transform.TransformOptions { if jsBool(options.Get("resultScopedSlot")) { scopedSlot = true } + experimentalTransitions := false + if jsBool(options.Get("experimentalTransitions")) { + experimentalTransitions = true + } + transitionsAnimationURL := jsString(options.Get("transitionsAnimationURL")) + if transitionsAnimationURL == "" { + transitionsAnimationURL = "astro/components/viewtransitions.css" + } var resolvePath any = options.Get("resolvePath") var resolvePathFn func(string) string @@ -117,16 +125,18 @@ func makeTransformOptions(options js.Value) transform.TransformOptions { } return transform.TransformOptions{ - Filename: filename, - NormalizedFilename: normalizedFilename, - InternalURL: internalURL, - SourceMap: sourcemap, - AstroGlobalArgs: astroGlobalArgs, - Compact: compact, - ResolvePath: resolvePathFn, - PreprocessStyle: preprocessStyle, - ResultScopedSlot: scopedSlot, - ScopedStyleStrategy: scopedStyleStrategy, + Filename: filename, + NormalizedFilename: normalizedFilename, + InternalURL: internalURL, + SourceMap: sourcemap, + AstroGlobalArgs: astroGlobalArgs, + Compact: compact, + ResolvePath: resolvePathFn, + PreprocessStyle: preprocessStyle, + ResultScopedSlot: scopedSlot, + ScopedStyleStrategy: scopedStyleStrategy, + ExperimentalTransitions: experimentalTransitions, + TransitionsAnimationURL: transitionsAnimationURL, } } diff --git a/internal/node.go b/internal/node.go index 265bbfe1b..11973b81b 100644 --- a/internal/node.go +++ b/internal/node.go @@ -79,10 +79,12 @@ type HydratedComponentMetadata struct { // Similarly, "math" is short for "http://www.w3.org/1998/Math/MathML", and // "svg" is short for "http://www.w3.org/2000/svg". type Node struct { - Fragment bool - CustomElement bool - Component bool - Expression bool + Fragment bool + CustomElement bool + Component bool + Expression bool + Transition bool + TransitionScope string Parent, FirstChild, LastChild, PrevSibling, NextSibling *Node diff --git a/internal/printer/print-to-js.go b/internal/printer/print-to-js.go index 68d2c6038..e88a68952 100644 --- a/internal/printer/print-to-js.go +++ b/internal/printer/print-to-js.go @@ -11,6 +11,7 @@ import ( "strings" . "github.com/withastro/compiler/internal" + astro "github.com/withastro/compiler/internal" "github.com/withastro/compiler/internal/handler" "github.com/withastro/compiler/internal/js_scanner" "github.com/withastro/compiler/internal/loc" @@ -110,6 +111,10 @@ func emptyTextNodeWithoutSiblings(n *Node) bool { func render1(p *printer, n *Node, opts RenderOptions) { depth := opts.depth + if opts.opts.ExperimentalTransitions && n.Transition { + p.needsTransitionCSS = true + } + // Root of the document, print all children if n.Type == DocumentNode { p.printInternalImports(p.opts.InternalURL, &opts) @@ -129,7 +134,7 @@ func render1(p *printer, n *Node, opts RenderOptions) { } p.printReturnClose() - p.printFuncSuffix(opts.opts) + p.printFuncSuffix(opts.opts, n) return } @@ -429,10 +434,28 @@ func render1(p *printer, n *Node, opts RenderOptions) { } p.print(`]`) } else { + if transform.HasAttr(n, transform.TRANSITION_ANIMATE) || transform.HasAttr(n, transform.TRANSITION_NAME) { + animationName := "" + if transform.HasAttr(n, transform.TRANSITION_ANIMATE) { + animationName = transform.GetAttr(n, transform.TRANSITION_ANIMATE).Val + } + transitionName := "" + if transform.HasAttr(n, transform.TRANSITION_NAME) { + transitionName = transform.GetAttr(n, transform.TRANSITION_NAME).Val + } + + n.Attr = append(n.Attr, astro.Attribute{ + Key: "data-astro-transition-scope", + Val: fmt.Sprintf(`%s(%s, "%s", "%s", "%s")`, RENDER_TRANSITION, RESULT, n.TransitionScope, animationName, transitionName), + Type: astro.ExpressionAttribute, + }) + } + for _, a := range n.Attr { if transform.IsImplicitNodeMarker(a) || a.Key == "is:inline" { continue } + if a.Key == "slot" { if n.Parent.Component || n.Parent.Expression { continue diff --git a/internal/printer/printer.go b/internal/printer/printer.go index 93001e828..25560b82a 100644 --- a/internal/printer/printer.go +++ b/internal/printer/printer.go @@ -28,6 +28,7 @@ type printer struct { hasFuncPrelude bool hasInternalImports bool hasCSSImports bool + needsTransitionCSS bool } var TEMPLATE_TAG = "$$render" @@ -40,6 +41,7 @@ var UNESCAPE_HTML = "$$unescapeHTML" var RENDER_SLOT = "$$renderSlot" var MERGE_SLOTS = "$$mergeSlots" var ADD_ATTRIBUTE = "$$addAttribute" +var RENDER_TRANSITION = "$$renderTransition" var SPREAD_ATTRIBUTES = "$$spreadAttributes" var DEFINE_STYLE_VARS = "$$defineStyleVars" var DEFINE_SCRIPT_VARS = "$$defineScriptVars" @@ -116,6 +118,10 @@ func (p *printer) printInternalImports(importSpecifier string, opts *RenderOptio p.print("defineStyleVars as " + DEFINE_STYLE_VARS + ",\n ") p.addNilSourceMapping() p.print("defineScriptVars as " + DEFINE_SCRIPT_VARS + ",\n ") + if opts.opts.ExperimentalTransitions { + p.addNilSourceMapping() + p.print("renderTransition as " + RENDER_TRANSITION + ",\n ") + } // Only needed if using fallback `resolvePath` as it calls `$$metadata.resolvePath` if opts.opts.ResolvePath == nil { @@ -141,6 +147,10 @@ func (p *printer) printCSSImports(cssLen int) { p.print(fmt.Sprintf("import \"%s?astro&type=style&index=%v&lang.css\";", p.opts.Filename, i)) i++ } + if p.needsTransitionCSS { + p.addNilSourceMapping() + p.print(fmt.Sprintf(`import "%s";`, p.opts.TransitionsAnimationURL)) + } p.print("\n") p.hasCSSImports = true } @@ -250,15 +260,19 @@ func (p *printer) printFuncPrelude(opts transform.TransformOptions) { p.hasFuncPrelude = true } -func (p *printer) printFuncSuffix(opts transform.TransformOptions) { +func (p *printer) printFuncSuffix(opts transform.TransformOptions, n *astro.Node) { componentName := getComponentName(opts.Filename) p.addNilSourceMapping() + filenameArg := "undefined" + propagationArg := "undefined" if len(opts.Filename) > 0 { escapedFilename := strings.ReplaceAll(opts.Filename, "'", "\\'") - p.println(fmt.Sprintf("}, '%s');", escapedFilename)) - } else { - p.println("});") + filenameArg = fmt.Sprintf("'%s'", escapedFilename) + } + if n.Transition { + propagationArg = "'self'" } + p.println(fmt.Sprintf("}, %s, %s);", filenameArg, propagationArg)) p.println(fmt.Sprintf("export default %s;", componentName)) } @@ -323,7 +337,7 @@ func (p *printer) printAttributesToObject(n *astro.Node) { } func (p *printer) printAttribute(attr astro.Attribute, n *astro.Node) { - if attr.Key == "define:vars" || attr.Key == "set:text" || attr.Key == "set:html" || attr.Key == "is:raw" { + if attr.Key == "define:vars" || attr.Key == "set:text" || attr.Key == "set:html" || attr.Key == "is:raw" || attr.Key == "transition:animate" || attr.Key == "transition:name" { return } diff --git a/internal/printer/printer_test.go b/internal/printer/printer_test.go index f5f57243a..6e7d2265f 100644 --- a/internal/printer/printer_test.go +++ b/internal/printer/printer_test.go @@ -37,7 +37,7 @@ const Astro = $$result.createAstro($$Astro, $$props, %s); Astro.self = $$Component;%s`, CREATE_COMPONENT, SLOTS, SLOTS, "\n\n") var RETURN = fmt.Sprintf("return %s%s", TEMPLATE_TAG, BACKTICK) var SUFFIX = fmt.Sprintf("%s;", BACKTICK) + ` -}); +}, undefined, undefined); export default $$Component;` var CREATE_ASTRO_CALL = "const $$Astro = $$createAstro('https://astro.build');\nconst Astro = $$Astro;" var RENDER_HEAD_RESULT = "${$$renderHead($$result)}" @@ -48,7 +48,7 @@ var NON_WHITESPACE_CHARS = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRS func suffixWithFilename(filename string) string { return fmt.Sprintf("%s;", BACKTICK) + fmt.Sprintf(` -}, '%s'); +}, '%s', undefined); export default $$Component;`, filename) } diff --git a/internal/transform/transform.go b/internal/transform/transform.go index c96661a1a..9a3d102b6 100644 --- a/internal/transform/transform.go +++ b/internal/transform/transform.go @@ -13,30 +13,41 @@ import ( a "golang.org/x/net/html/atom" ) +const TRANSITION_ANIMATE = "transition:animate" +const TRANSITION_NAME = "transition:name" + type TransformOptions struct { - Scope string - Filename string - NormalizedFilename string - InternalURL string - SourceMap string - AstroGlobalArgs string - ScopedStyleStrategy string - Compact bool - ResultScopedSlot bool - ResolvePath func(string) string - PreprocessStyle interface{} + Scope string + Filename string + NormalizedFilename string + InternalURL string + SourceMap string + AstroGlobalArgs string + ScopedStyleStrategy string + Compact bool + ResultScopedSlot bool + ExperimentalTransitions bool + TransitionsAnimationURL string + ResolvePath func(string) string + PreprocessStyle interface{} } func Transform(doc *astro.Node, opts TransformOptions, h *handler.Handler) *astro.Node { shouldScope := len(doc.Styles) > 0 && ScopeStyle(doc.Styles, opts) definedVars := GetDefineVars(doc.Styles) didAddDefinedVars := false + i := 0 walk(doc, func(n *astro.Node) { + i++ ExtractScript(doc, n, &opts, h) AddComponentProps(doc, n, &opts) if shouldScope { ScopeElement(n, opts) } + if opts.ExperimentalTransitions && (HasAttr(n, TRANSITION_ANIMATE) || HasAttr(n, TRANSITION_NAME)) { + doc.Transition = true + n.TransitionScope = astro.HashString(fmt.Sprintf("%s-%v", opts.Scope, i)) + } if len(definedVars) > 0 { didAdd := AddDefineVars(n, definedVars) if !didAddDefinedVars { diff --git a/packages/compiler/src/shared/types.ts b/packages/compiler/src/shared/types.ts index f8534c0a1..61f663a51 100644 --- a/packages/compiler/src/shared/types.ts +++ b/packages/compiler/src/shared/types.ts @@ -53,6 +53,8 @@ export interface TransformOptions { * @deprecated "as" has been removed and no longer has any effect! */ as?: 'document' | 'fragment'; + experimentalTransitions?: boolean; + transitionsAnimationURL?: string; resolvePath?: (specifier: string) => Promise; preprocessStyle?: (content: string, attrs: Record) => null | Promise; } diff --git a/packages/compiler/test/basic/trailing-newline.ts b/packages/compiler/test/basic/trailing-newline.ts index 370a8f035..790edfe82 100644 --- a/packages/compiler/test/basic/trailing-newline.ts +++ b/packages/compiler/test/basic/trailing-newline.ts @@ -27,7 +27,7 @@ test.before(async () => { }); test('does not add trailing newline to rendered output', () => { - assert.match(result.code, `}\`;\n}, '');`, 'Does not include a trailing newline in the render function'); + assert.match(result.code, `}\`;\n}, '', undefined);`, 'Does not include a trailing newline in the render function'); }); test.run();