diff --git a/.changeset/purple-chairs-relax.md b/.changeset/purple-chairs-relax.md new file mode 100644 index 000000000..dbdeac389 --- /dev/null +++ b/.changeset/purple-chairs-relax.md @@ -0,0 +1,5 @@ +--- +'@astrojs/compiler': minor +--- + +Support the transition:persist directive diff --git a/cmd/astro-wasm/astro-wasm.go b/cmd/astro-wasm/astro-wasm.go index b400e9eb7..1b08f2364 100644 --- a/cmd/astro-wasm/astro-wasm.go +++ b/cmd/astro-wasm/astro-wasm.go @@ -99,6 +99,10 @@ func makeTransformOptions(options js.Value) transform.TransformOptions { if jsBool(options.Get("experimentalTransitions")) { experimentalTransitions = true } + experimentalPersistence := false + if jsBool(options.Get("experimentalPersistence")) { + experimentalPersistence = true + } transitionsAnimationURL := jsString(options.Get("transitionsAnimationURL")) if transitionsAnimationURL == "" { transitionsAnimationURL = "astro/components/viewtransitions.css" @@ -136,6 +140,7 @@ func makeTransformOptions(options js.Value) transform.TransformOptions { ResultScopedSlot: scopedSlot, ScopedStyleStrategy: scopedStyleStrategy, ExperimentalTransitions: experimentalTransitions, + ExperimentalPersistence: experimentalPersistence, TransitionsAnimationURL: transitionsAnimationURL, } } diff --git a/internal/printer/printer.go b/internal/printer/printer.go index 9c2c472bd..d6436e708 100644 --- a/internal/printer/printer.go +++ b/internal/printer/printer.go @@ -42,6 +42,7 @@ var RENDER_SLOT = "$$renderSlot" var MERGE_SLOTS = "$$mergeSlots" var ADD_ATTRIBUTE = "$$addAttribute" var RENDER_TRANSITION = "$$renderTransition" +var CREATE_TRANSITION_SCOPE = "$$createTransitionScope" var SPREAD_ATTRIBUTES = "$$spreadAttributes" var DEFINE_STYLE_VARS = "$$defineStyleVars" var DEFINE_SCRIPT_VARS = "$$defineScriptVars" @@ -122,6 +123,10 @@ func (p *printer) printInternalImports(importSpecifier string, opts *RenderOptio p.addNilSourceMapping() p.print("renderTransition as " + RENDER_TRANSITION + ",\n ") } + if opts.opts.ExperimentalPersistence { + p.addNilSourceMapping() + p.print("createTransitionScope as " + CREATE_TRANSITION_SCOPE + ",\n ") + } // Only needed if using fallback `resolvePath` as it calls `$$metadata.resolvePath` if opts.opts.ResolvePath == nil { @@ -276,6 +281,25 @@ func (p *printer) printFuncSuffix(opts transform.TransformOptions, n *astro.Node p.println(fmt.Sprintf("export default %s;", componentName)) } +var skippedAttributes = map[string]bool{ + "define:vars": true, + "set:text": true, + "set:html": true, + "is:raw": true, + "transition:animate": true, + "transition:name": true, + "transition:persist": true, +} + +var skippedAttributesToObject = map[string]bool{ + "set:text": true, + "set:html": true, + "is:raw": true, + "transition:animate": true, + "transition:name": true, + "transition:persist": true, +} + func (p *printer) printAttributesToObject(n *astro.Node) { lastAttributeSkipped := false p.print("{") @@ -283,7 +307,7 @@ func (p *printer) printAttributesToObject(n *astro.Node) { if i != 0 && !lastAttributeSkipped { p.print(",") } - if a.Key == "set:text" || a.Key == "set:html" || a.Key == "is:raw" || a.Key == "transition:animate" || a.Key == "transition:name" { + if _, ok := skippedAttributesToObject[a.Key]; ok { lastAttributeSkipped = true continue } @@ -338,7 +362,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" || attr.Key == "transition:animate" || attr.Key == "transition:name" { + if _, ok := skippedAttributes[attr.Key]; ok { return } @@ -458,6 +482,26 @@ func maybeConvertTransition(n *astro.Node) { Type: astro.ExpressionAttribute, }) } + if transform.HasAttr(n, transform.TRANSITION_PERSIST) { + transitionPersistIndex := transform.AttrIndex(n, transform.TRANSITION_PERSIST) + // If there no value, create a transition scope for this element + if n.Attr[transitionPersistIndex].Val != "" { + // Just rename the attribute + n.Attr[transitionPersistIndex].Key = "data-astro-transition-persist" + + } else if transform.HasAttr(n, transform.TRANSITION_NAME) { + transitionNameAttr := transform.GetAttr(n, transform.TRANSITION_NAME) + n.Attr[transitionPersistIndex].Key = "data-astro-transition-persist" + n.Attr[transitionPersistIndex].Val = transitionNameAttr.Val + n.Attr[transitionPersistIndex].Type = transitionNameAttr.Type + } else { + n.Attr = append(n.Attr, astro.Attribute{ + Key: "data-astro-transition-persist", + Val: fmt.Sprintf(`%s(%s, "%s")`, CREATE_TRANSITION_SCOPE, RESULT, n.TransitionScope), + Type: astro.ExpressionAttribute, + }) + } + } } func (p *printer) printComponentMetadata(doc *astro.Node, opts transform.TransformOptions, source []byte) { diff --git a/internal/printer/printer_test.go b/internal/printer/printer_test.go index 485fa8dea..4ebe7638f 100644 --- a/internal/printer/printer_test.go +++ b/internal/printer/printer_test.go @@ -39,17 +39,23 @@ var RETURN = fmt.Sprintf("return %s%s", TEMPLATE_TAG, BACKTICK) var SUFFIX = fmt.Sprintf("%s;", BACKTICK) + ` }, undefined, undefined); export default $$Component;` +var SUFFIX_EXP_TRANSITIONS = fmt.Sprintf("%s;", BACKTICK) + ` +}, undefined, 'self'); +export default $$Component;` var CREATE_ASTRO_CALL = "const $$Astro = $$createAstro('https://astro.build');\nconst Astro = $$Astro;" var RENDER_HEAD_RESULT = "${$$renderHead($$result)}" // SPECIAL TEST FIXTURES var NON_WHITESPACE_CHARS = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+[];:'\",.?") -func suffixWithFilename(filename string) string { - +func suffixWithFilename(filename string, transitions bool) string { + propagationArg := "undefined" + if transitions { + propagationArg = `'self'` + } return fmt.Sprintf("%s;", BACKTICK) + fmt.Sprintf(` -}, '%s', undefined); -export default $$Component;`, filename) +}, '%s', %s); +export default $$Component;`, filename, propagationArg) } type want struct { @@ -69,11 +75,12 @@ type metadata struct { } type testcase struct { - name string - source string - only bool - filename string - want want + name string + source string + only bool + transitions bool + filename string + want want } type jsonTestcase struct { @@ -2792,35 +2799,55 @@ const items = ["Dog", "Cat", "Platipus"]; }, }, { - name: "transition:name with an expression", - source: `
`, - filename: "/projects/app/src/pages/page.astro", + name: "transition:name with an expression", + source: `
`, + filename: "/projects/app/src/pages/page.astro", + transitions: true, want: want{ - code: `${$$maybeRenderHead($$result)}`, + code: `${$$maybeRenderHead($$result)}`, }, }, { - name: "transition:name with an template literal", - source: "
", - filename: "/projects/app/src/pages/page.astro", + name: "transition:name with an template literal", + source: "
", + filename: "/projects/app/src/pages/page.astro", + transitions: true, want: want{ - code: `${$$maybeRenderHead($$result)}`, + code: `${$$maybeRenderHead($$result)}`, }, }, { - name: "transition:animate with an expression", - source: "
", - filename: "/projects/app/src/pages/page.astro", + name: "transition:animate with an expression", + source: "
", + filename: "/projects/app/src/pages/page.astro", + transitions: true, want: want{ - code: `${$$maybeRenderHead($$result)}`, + code: `${$$maybeRenderHead($$result)}`, }, }, { - name: "transition:animate on Component", - source: ``, - filename: "/projects/app/src/pages/page.astro", + name: "transition:animate on Component", + source: ``, + filename: "/projects/app/src/pages/page.astro", + transitions: true, + want: want{ + code: `${$$renderComponent($$result,'Component',Component,{"class":"bar","data-astro-transition-scope":($$renderTransition($$result, "X2GW3NPO", "morph", ""))})}`, + }, + }, + { + name: "transition:persist converted to a data attribute", + source: `
`, + transitions: true, + want: want{ + code: `${$$maybeRenderHead($$result)}`, + }, + }, + { + name: "transition:persist uses transition:name if defined", + source: `
`, + transitions: true, want: want{ - code: `${$$renderComponent($$result,'Component',Component,{"class":"bar","data-astro-transition-scope":($$renderTransition($$result, "", "morph", ""))})}`, + code: `${$$maybeRenderHead($$result)}
`, }, }, } @@ -2847,7 +2874,12 @@ const items = ["Dog", "Cat", "Platipus"]; hash := astro.HashString(code) transform.ExtractStyles(doc) - transform.Transform(doc, transform.TransformOptions{Scope: hash}, h) // note: we want to test Transform in context here, but more advanced cases could be tested separately + transformOptions := transform.TransformOptions{ + Scope: hash, + ExperimentalTransitions: true, + ExperimentalPersistence: true, + } + transform.Transform(doc, transformOptions, h) // note: we want to test Transform in context here, but more advanced cases could be tested separately result := PrintToJS(code, doc, 0, transform.TransformOptions{ Scope: "XXXX", InternalURL: "http://localhost:3000/", @@ -2964,8 +2996,10 @@ const items = ["Dog", "Cat", "Platipus"]; if len(tt.filename) > 0 { escapedFilename := strings.ReplaceAll(tt.filename, "'", "\\'") - toMatch += suffixWithFilename(escapedFilename) + toMatch += suffixWithFilename(escapedFilename, tt.transitions) toMatch = strings.Replace(toMatch, "$$Component", getComponentName(tt.filename), -1) + } else if tt.transitions { + toMatch += SUFFIX_EXP_TRANSITIONS } else { toMatch += SUFFIX } diff --git a/internal/transform/transform.go b/internal/transform/transform.go index 9a3d102b6..e710a5560 100644 --- a/internal/transform/transform.go +++ b/internal/transform/transform.go @@ -15,6 +15,7 @@ import ( const TRANSITION_ANIMATE = "transition:animate" const TRANSITION_NAME = "transition:name" +const TRANSITION_PERSIST = "transition:persist" type TransformOptions struct { Scope string @@ -27,6 +28,7 @@ type TransformOptions struct { Compact bool ResultScopedSlot bool ExperimentalTransitions bool + ExperimentalPersistence bool TransitionsAnimationURL string ResolvePath func(string) string PreprocessStyle interface{} @@ -44,9 +46,9 @@ func Transform(doc *astro.Node, opts TransformOptions, h *handler.Handler) *astr if shouldScope { ScopeElement(n, opts) } - if opts.ExperimentalTransitions && (HasAttr(n, TRANSITION_ANIMATE) || HasAttr(n, TRANSITION_NAME)) { + if opts.ExperimentalTransitions && (HasAttr(n, TRANSITION_ANIMATE) || HasAttr(n, TRANSITION_NAME) || HasAttr(n, TRANSITION_PERSIST)) { doc.Transition = true - n.TransitionScope = astro.HashString(fmt.Sprintf("%s-%v", opts.Scope, i)) + getOrCreateTransitionScope(n, &opts, i) } if len(definedVars) > 0 { didAdd := AddDefineVars(n, definedVars) @@ -550,3 +552,11 @@ func mergeClassList(doc *astro.Node, n *astro.Node, opts *TransformOptions) { func remove(slice []astro.Attribute, s int) []astro.Attribute { return append(slice[:s], slice[s+1:]...) } + +func getOrCreateTransitionScope(n *astro.Node, opts *TransformOptions, i int) string { + if n.TransitionScope != "" { + return n.TransitionScope + } + n.TransitionScope = astro.HashString(fmt.Sprintf("%s-%v", opts.Scope, i)) + return n.TransitionScope +} diff --git a/internal/transform/transform_test.go b/internal/transform/transform_test.go index ceb846b59..d4b5fe0bd 100644 --- a/internal/transform/transform_test.go +++ b/internal/transform/transform_test.go @@ -65,7 +65,7 @@ func transformScopingFixtures() []struct { name: "empty (space)", source: `
`, @@ -253,7 +253,9 @@ func TestFullTransform(t *testing.T) { ExtractStyles(doc) // Clear doc.Styles to avoid scoping behavior, we're not testing that here doc.Styles = make([]*astro.Node, 0) - Transform(doc, TransformOptions{}, handler.NewHandler(tt.source, "/test.astro")) + Transform(doc, TransformOptions{ + ExperimentalTransitions: true, + }, handler.NewHandler(tt.source, "/test.astro")) astro.PrintToSource(&b, doc) got := strings.TrimSpace(b.String()) if tt.want != got { diff --git a/internal/transform/utils.go b/internal/transform/utils.go index b9184e005..1a5e082ba 100644 --- a/internal/transform/utils.go +++ b/internal/transform/utils.go @@ -25,13 +25,17 @@ func HasInlineDirective(n *astro.Node) bool { return HasAttr(n, "is:inline") } -func HasAttr(n *astro.Node, key string) bool { - for _, attr := range n.Attr { +func AttrIndex(n *astro.Node, key string) int { + for i, attr := range n.Attr { if attr.Key == key { - return true + return i } } - return false + return -1 +} + +func HasAttr(n *astro.Node, key string) bool { + return AttrIndex(n, key) != -1 } func GetAttr(n *astro.Node, key string) *astro.Attribute { diff --git a/packages/compiler/src/shared/types.ts b/packages/compiler/src/shared/types.ts index 61f663a51..cbe122550 100644 --- a/packages/compiler/src/shared/types.ts +++ b/packages/compiler/src/shared/types.ts @@ -54,6 +54,7 @@ export interface TransformOptions { */ as?: 'document' | 'fragment'; experimentalTransitions?: boolean; + experimentalPersistence?: boolean; transitionsAnimationURL?: string; resolvePath?: (specifier: string) => Promise; preprocessStyle?: (content: string, attrs: Record) => null | Promise;