diff options
author | 2021-07-08 20:07:56 +0200 | |
---|---|---|
committer | 2021-07-08 14:07:56 -0400 | |
commit | 0a7b6deaec9fa32c2cb7139ac9aeaa242c0a7f4c (patch) | |
tree | d8ff047bea3e4496b06990a44d24c05e81e595a3 | |
parent | ea5afcd6334c25c7a38f8f381d400bef1bb6dbc9 (diff) | |
download | astro-0a7b6deaec9fa32c2cb7139ac9aeaa242c0a7f4c.tar.gz astro-0a7b6deaec9fa32c2cb7139ac9aeaa242c0a7f4c.tar.zst astro-0a7b6deaec9fa32c2cb7139ac9aeaa242c0a7f4c.zip |
Move hydration directives to special attributes (#618)
* feat: :recycle: updating hydration to work with the directive syntax
* test: :white_check_mark: Updating tests for the hydration directive syntax
* refactor: Updating example projects for the hydration directive syntax
* test: :white_check_mark: Found a test fixture still needing an update to the hydration directive syntax
* style: Prettier strikes again! Reverting code formatting changes
* refactor: :recycle: moving directive matching to a Set
* refactor: Updating syntax to `client:load`
* refactor: :recycle: Simplifying the `client:` directive match
Per PR feedback from @matthewp
* chore: errant console.warn() snuck into the last commit
* feat: :loud_sound: Adding a super fancy build warning to update to the directive syntax
* refactor: :recycle: Removing unnecessary checks when matching supported hydration directives
`val` isn't being used for now, but leaving it in the attr destructuring as a reminder since it'll be needed for `client:media`
* test: :white_check_mark: Including the original hydration syntax in a test to make sure it builds
* style: :memo: Adding a comment to make it clear why the old hydration syntax is included in a the test markup
* fix: :bug: updating `head` logic to recognize hydration directive syntax
* docs: Adding changeset
* refactor: :fire: Removing unnecessary `!hasComponents` check
* docs: :memo: Adding more detail to the changset
Co-authored-by: Tony Sullivan <tony.f.sullivan@gmail.com>
25 files changed, 117 insertions, 59 deletions
diff --git a/.changeset/famous-years-bow.md b/.changeset/famous-years-bow.md new file mode 100644 index 000000000..77bf5d2c7 --- /dev/null +++ b/.changeset/famous-years-bow.md @@ -0,0 +1,15 @@ +--- +'astro': minor +--- + +## Adds directive syntax for component hydration + +This change updates the syntax for partial hydration from `<Button:load />` to `<Button client:load />`. + +**Why?** + +Partial hydration is about to get super powers! This clears the way for more dynamic partial hydration, i.e. `<MobileMenu client:media="(max-width: 40em)" />`. + +**How to upgrade** + +Just update `:load`, `:idle`, and `:visible` to match the `client:load` format, thats it! Don't worry, the original syntax is still supported but it's recommended to future-proof your project by updating to the newer syntax. diff --git a/examples/blog/src/pages/posts/introducing-astro.md b/examples/blog/src/pages/posts/introducing-astro.md index 3f30087a1..293b3b8f9 100644 --- a/examples/blog/src/pages/posts/introducing-astro.md +++ b/examples/blog/src/pages/posts/introducing-astro.md @@ -54,7 +54,7 @@ Of course, sometimes client-side JavaScript is inevitable. Image carousels, shop In other full-stack web frameworks this level of per-component optimization would be impossible without loading the entire page in JavaScript, delaying interactivity. In Astro, this kind of [partial hydration](https://addyosmani.com/blog/rehydration/) is built into the tool itself. -You can even [automatically defer components](https://codepen.io/jonneal/full/ZELvMvw) to only load once they become visible on the page with the `:visible` modifier. +You can even [automatically defer components](https://codepen.io/jonneal/full/ZELvMvw) to only load once they become visible on the page with the `client:visible` directive. This new approach to web architecture is called [islands architecture](https://jasonformat.com/islands-architecture/). We didn't coin the term, but Astro may have perfected the technique. We are confident that an HTML-first, JavaScript-only-as-needed approach is the best solution for the majority of content-based websites. diff --git a/examples/docs/src/layouts/Main.astro b/examples/docs/src/layouts/Main.astro index a92ffaff4..dbfc90791 100644 --- a/examples/docs/src/layouts/Main.astro +++ b/examples/docs/src/layouts/Main.astro @@ -212,7 +212,7 @@ const githubEditUrl = `https://github.com/USER/REPO/blob/main/${currentFile}` <div /> <div> - <ThemeToggle:idle /> + <ThemeToggle client:idle /> </div> </div> </nav> @@ -232,7 +232,7 @@ const githubEditUrl = `https://github.com/USER/REPO/blob/main/${currentFile}` </article> </div> <aside class="sidebar" id="sidebar-content"> - <DocSidebar:idle headers={headers} editHref={editHref} /> + <DocSidebar client:idle headers={headers} editHref={editHref} /> </aside> </main> </body> diff --git a/examples/framework-multiple/src/pages/index.astro b/examples/framework-multiple/src/pages/index.astro index a2f15764e..826a9d5f9 100644 --- a/examples/framework-multiple/src/pages/index.astro +++ b/examples/framework-multiple/src/pages/index.astro @@ -35,22 +35,22 @@ import SvelteCounter from '../components/SvelteCounter.svelte'; <body> <main> - <react.Counter:visible> + <react.Counter client:visible> <h1>Hello React!</h1> <p>What's up?</p> - </react.Counter:visible> + </react.Counter> - <PreactCounter:visible> + <PreactCounter client:visible> <h1>Hello Preact!</h1> - </PreactCounter:visible> + </PreactCounter> - <VueCounter:visible> + <VueCounter client:visible> <h1>Hello Vue!</h1> - </VueCounter:visible> + </VueCounter> - <SvelteCounter:visible> + <SvelteCounter client:visible> <h1>Hello Svelte!</h1> - </SvelteCounter:visible> + </SvelteCounter> <A /> diff --git a/examples/framework-preact/src/pages/index.astro b/examples/framework-preact/src/pages/index.astro index 8cdccf783..c370d7298 100644 --- a/examples/framework-preact/src/pages/index.astro +++ b/examples/framework-preact/src/pages/index.astro @@ -33,9 +33,9 @@ import Counter from '../components/Counter.jsx' </head> <body> <main> - <Counter:visible> + <Counter client:visible> <h1>Hello Preact!</h1> - </Counter:visible> + </Counter> </main> </body> </html> diff --git a/examples/framework-react/src/pages/index.astro b/examples/framework-react/src/pages/index.astro index 69b9c6d44..a62427dd9 100644 --- a/examples/framework-react/src/pages/index.astro +++ b/examples/framework-react/src/pages/index.astro @@ -33,9 +33,9 @@ import Counter from '../components/Counter.jsx' </head> <body> <main> - <Counter:visible> + <Counter client:visible> <h1>Hello React!</h1> - </Counter:visible> + </Counter> </main> </body> </html> diff --git a/examples/framework-svelte/src/pages/index.astro b/examples/framework-svelte/src/pages/index.astro index b89e12b4a..15d93bbaa 100644 --- a/examples/framework-svelte/src/pages/index.astro +++ b/examples/framework-svelte/src/pages/index.astro @@ -33,9 +33,9 @@ import Counter from '../components/Counter.svelte' </head> <body> <main> - <Counter:visible> + <Counter client:visible> <h1>Hello Svelte!</h1> - </Counter:visible> + </Counter> </main> </body> </html> diff --git a/examples/framework-vue/src/pages/index.astro b/examples/framework-vue/src/pages/index.astro index 1ce7919bd..2dcc11da6 100644 --- a/examples/framework-vue/src/pages/index.astro +++ b/examples/framework-vue/src/pages/index.astro @@ -33,9 +33,9 @@ import Counter from '../components/Counter.vue' </head> <body> <main> - <Counter:visible> + <Counter client:visible> <h1>Hello Vue!</h1> - </Counter:visible> + </Counter> </main> </body> </html> diff --git a/examples/snowpack/src/pages/news.astro b/examples/snowpack/src/pages/news.astro index bae3e5ad7..f51631d28 100644 --- a/examples/snowpack/src/pages/news.astro +++ b/examples/snowpack/src/pages/news.astro @@ -48,7 +48,7 @@ const description = 'Snowpack community news and companies that use Snowpack.'; working on!</div> </article> - {news.reverse().map((item: any) => <Card:idle item={item} />)} + {news.reverse().map((item: any) => <Card client:idle item={item} />)} </div> <div class="content"> diff --git a/examples/snowpack/src/pages/plugins.astro b/examples/snowpack/src/pages/plugins.astro index ddd7632e8..ce70f4cac 100644 --- a/examples/snowpack/src/pages/plugins.astro +++ b/examples/snowpack/src/pages/plugins.astro @@ -68,7 +68,7 @@ let description = 'Snowpack plugins allow for configuration-minimal tooling inte <div style="margin-top:4rem;"></div> - <PluginSearchPage:load /> + <PluginSearchPage client:load /> </MainLayout> </body> diff --git a/examples/with-markdown/src/pages/index.astro b/examples/with-markdown/src/pages/index.astro index 6e402fb1c..b6843f3a5 100644 --- a/examples/with-markdown/src/pages/index.astro +++ b/examples/with-markdown/src/pages/index.astro @@ -32,10 +32,10 @@ const items = ['A', 'B', 'C']; ## Embed framework components - <ReactCounter:visible /> - <PreactCounter:visible /> - <VueCounter:visible /> - <SvelteCounter:visible /> + <ReactCounter client:visible /> + <PreactCounter client:visible /> + <VueCounter client:visible /> + <SvelteCounter client:visible /> ## Use Expressions @@ -43,11 +43,11 @@ const items = ['A', 'B', 'C']; ## Oh yeah... - <ReactCounter:visible> + <ReactCounter client:visible> 🤯 It's also _recursive_! ### Markdown can be embedded in any child component - </ReactCounter:visible> + </ReactCounter> ## Code diff --git a/examples/with-nanostores/src/pages/index.astro b/examples/with-nanostores/src/pages/index.astro index a40897232..a3f0c7c8c 100644 --- a/examples/with-nanostores/src/pages/index.astro +++ b/examples/with-nanostores/src/pages/index.astro @@ -37,10 +37,10 @@ import AdminsPreact from '../components/AdminsPreact.jsx'; <a href="https://github.com/nanostores/nanostores">nanostores</a></h1> </div> </header> - <AdminsReact:load /> - <AdminsSvelte:load /> - <AdminsVue:load /> - <AdminsPreact:load /> + <AdminsReact client:load /> + <AdminsSvelte client:load /> + <AdminsVue client:load /> + <AdminsPreact client:load /> </main> </body> </html> diff --git a/packages/astro/README.md b/packages/astro/README.md index 76e0966c8..df7a33712 100644 --- a/packages/astro/README.md +++ b/packages/astro/README.md @@ -118,9 +118,9 @@ TODO: Astro dynamic components guide By default, Astro outputs zero client-side JS. If you'd like to include an interactive component in the client output, you may use any of the following techniques. - `<MyComponent />` will render an HTML-only version of `MyComponent` (default) -- `<MyComponent:load />` will render `MyComponent` on page load -- `<MyComponent:idle />` will use [requestIdleCallback()][mdn-ric] to render `MyComponent` as soon as main thread is free -- `<MyComponent:visible />` will use an [IntersectionObserver][mdn-io] to render `MyComponent` when the element enters the viewport +- `<MyComponent client:load />` will render `MyComponent` on page load +- `<MyComponent client:idle />` will use [requestIdleCallback()][mdn-ric] to render `MyComponent` as soon as main thread is free +- `<MyComponent client:visible />` will use an [IntersectionObserver][mdn-io] to render `MyComponent` when the element enters the viewport ### ⚛️ State Management diff --git a/packages/astro/src/compiler/codegen/index.ts b/packages/astro/src/compiler/codegen/index.ts index 22ee1816b..af74a368a 100644 --- a/packages/astro/src/compiler/codegen/index.ts +++ b/packages/astro/src/compiler/codegen/index.ts @@ -47,6 +47,23 @@ interface CodeGenOptions { fileID: string; } +interface HydrationAttributes { + method?: 'load' | 'idle' | 'visible'; +} + +/** Searches through attributes to extract hydration-rlated attributes */ +function findHydrationAttributes(attrs: Record<string, string>): HydrationAttributes { + let method: HydrationAttributes['method']; + + const hydrationDirectives = new Set(['client:load', 'client:idle', 'client:visible']); + + for (const [key, val] of Object.entries(attrs)) { + if (hydrationDirectives.has(key)) method = key.slice(7) as HydrationAttributes['method']; + } + + return { method }; +} + /** Retrieve attributes from TemplateNode */ async function getAttributes(attrs: Attribute[], state: CodegenState, compileOptions: CompileOptions): Promise<Record<string, string>> { let result: Record<string, string> = {}; @@ -154,18 +171,32 @@ function getComponentUrl(astroConfig: AstroConfig, url: string, parentUrl: strin interface GetComponentWrapperOptions { filename: string; astroConfig: AstroConfig; + compileOptions: CompileOptions; } const PlainExtensions = new Set(['.js', '.jsx', '.ts', '.tsx']); /** Generate Astro-friendly component import */ -function getComponentWrapper(_name: string, { url, importSpecifier }: ComponentInfo, opts: GetComponentWrapperOptions) { +function getComponentWrapper(_name: string, hydration: HydrationAttributes, { url, importSpecifier }: ComponentInfo, opts: GetComponentWrapperOptions) { const { astroConfig, filename } = opts; - const [name, kind] = _name.split(':'); + + let name = _name; + let method = hydration.method; + + /** Legacy support for original hydration syntax */ + if (name.indexOf(':') > 0) { + const [legacyName, legacyMethod] = _name.split(':'); + name = legacyName; + method = legacyMethod as HydrationAttributes['method']; + + const { compileOptions, filename } = opts; + const shortname = path.posix.relative(compileOptions.astroConfig.projectRoot.pathname, filename); + warn(compileOptions.logging, shortname, yellow(`Deprecation warning: Partial hydration now uses a directive syntax. Please update to "<${name} client:${method} />"`)); + } // Special flow for custom elements - if (isCustomElementTag(name)) { + if (isCustomElementTag(_name)) { return { - wrapper: `__astro_component(...__astro_element_registry.astroComponentArgs("${name}", ${JSON.stringify({ hydrate: kind, displayName: _name })}))`, + wrapper: `__astro_component(...__astro_element_registry.astroComponentArgs("${name}", ${JSON.stringify({ hydrate: method, displayName: _name })}))`, wrapperImports: [ `import {AstroElementRegistry} from 'astro/dist/internal/element-registry.js';`, `import {__astro_component} from 'astro/dist/internal/__astro_component.js';`, @@ -183,21 +214,21 @@ function getComponentWrapper(_name: string, { url, importSpecifier }: ComponentI return { value: importSpecifier.imported.value }; } case 'ImportNamespaceSpecifier': { - const [_, value] = name.split('.'); + const [_, value] = _name.split('.'); return { value }; } } }; - const importInfo = kind - ? { - componentUrl: getComponentUrl(astroConfig, url, pathToFileURL(filename)), - componentExport: getComponentExport(), - } - : {}; + const importInfo = method + ? { + componentUrl: getComponentUrl(astroConfig, url, pathToFileURL(filename)), + componentExport: getComponentExport() + } + : {}; return { - wrapper: `__astro_component(${name}, ${JSON.stringify({ hydrate: kind, displayName: _name, ...importInfo })})`, + wrapper: `__astro_component(${name}, ${JSON.stringify({ hydrate: method, displayName: _name, ...importInfo })})`, wrapperImports: [`import {__astro_component} from 'astro/dist/internal/__astro_component.js';`], }; } @@ -633,6 +664,7 @@ async function compileHtml(enterNode: TemplateNode, state: CodegenState, compile } try { const attributes = await getAttributes(node.attributes, state, compileOptions); + const hydrationAttributes = findHydrationAttributes(attributes); buffers.out += buffers.out === '' ? '' : ','; @@ -671,7 +703,7 @@ async function compileHtml(enterNode: TemplateNode, state: CodegenState, compile curr = 'markdown'; return; } - const { wrapper, wrapperImports } = getComponentWrapper(name, componentInfo ?? ({} as any), { astroConfig, filename }); + const { wrapper, wrapperImports } = getComponentWrapper(name, hydrationAttributes, componentInfo ?? ({} as any), { astroConfig, filename, compileOptions }); if (wrapperImports) { for (let wrapperImport of wrapperImports) { importStatements.add(wrapperImport); diff --git a/packages/astro/src/compiler/transform/head.ts b/packages/astro/src/compiler/transform/head.ts index b081491a3..9a12e395b 100644 --- a/packages/astro/src/compiler/transform/head.ts +++ b/packages/astro/src/compiler/transform/head.ts @@ -21,8 +21,18 @@ export default function (opts: TransformOptions): Transformer { }, InlineComponent: { enter(node) { + if (hasComponents) { + return + } + + if (node.attributes && node.attributes.some(({ name }: any) => name.startsWith('client:'))) { + hasComponents = true; + return; + } + + /** Check for legacy hydration */ const [_name, kind] = node.name.split(':'); - if (kind && !hasComponents) { + if (kind) { hasComponents = true; } }, diff --git a/packages/astro/test/fixtures/astro-basic/src/pages/client.astro b/packages/astro/test/fixtures/astro-basic/src/pages/client.astro index 43d8b373b..a9cac2201 100644 --- a/packages/astro/test/fixtures/astro-basic/src/pages/client.astro +++ b/packages/astro/test/fixtures/astro-basic/src/pages/client.astro @@ -7,6 +7,6 @@ import Tour from '../components/Tour.jsx'; <title>Stuff</title> </head> <body> - <Tour:load /> + <Tour client:load /> </body> </html>
\ No newline at end of file diff --git a/packages/astro/test/fixtures/astro-dynamic/src/pages/default-rename.astro b/packages/astro/test/fixtures/astro-dynamic/src/pages/default-rename.astro index 8d418fc15..95b4bb538 100644 --- a/packages/astro/test/fixtures/astro-dynamic/src/pages/default-rename.astro +++ b/packages/astro/test/fixtures/astro-dynamic/src/pages/default-rename.astro @@ -5,8 +5,8 @@ import SvelteCounterRenamed from '../components/SvelteCounter.svelte'; <html> <head><title>Dynamic pages</title></head> <body> - <CounterRenamed:load /> + <CounterRenamed client:load /> - <SvelteCounterRenamed:load /> + <SvelteCounterRenamed client:load /> </body> </html> diff --git a/packages/astro/test/fixtures/astro-dynamic/src/pages/index.astro b/packages/astro/test/fixtures/astro-dynamic/src/pages/index.astro index c4d0fef17..4dbb7f2c6 100644 --- a/packages/astro/test/fixtures/astro-dynamic/src/pages/index.astro +++ b/packages/astro/test/fixtures/astro-dynamic/src/pages/index.astro @@ -5,8 +5,9 @@ import SvelteCounter from '../components/SvelteCounter.svelte'; <html> <head><title>Dynamic pages</title></head> <body> - <Counter:load /> + <Counter client:load /> + <!-- Including the original hydration syntax to test backwards compatibility --> <SvelteCounter:load /> </body> </html>
\ No newline at end of file diff --git a/packages/astro/test/fixtures/astro-fallback/src/pages/index.astro b/packages/astro/test/fixtures/astro-fallback/src/pages/index.astro index f4f20c322..30183f0a0 100644 --- a/packages/astro/test/fixtures/astro-fallback/src/pages/index.astro +++ b/packages/astro/test/fixtures/astro-fallback/src/pages/index.astro @@ -11,6 +11,6 @@ let title = 'My Page' <body> <h1>{title}</h1> - <Client:load /> + <Client client:load /> </body> </html>
\ No newline at end of file diff --git a/packages/astro/test/fixtures/astro-hmr/src/pages/index.astro b/packages/astro/test/fixtures/astro-hmr/src/pages/index.astro index 754749f90..02e2633ac 100644 --- a/packages/astro/test/fixtures/astro-hmr/src/pages/index.astro +++ b/packages/astro/test/fixtures/astro-hmr/src/pages/index.astro @@ -7,6 +7,6 @@ import Tour from '../components/Tour.jsx'; </head> <body> <div>Hello world</div> - <Tour:load /> + <Tour client:load /> </body> </html>
\ No newline at end of file diff --git a/packages/astro/test/fixtures/astro-hmr/src/pages/manual.astro b/packages/astro/test/fixtures/astro-hmr/src/pages/manual.astro index 29bcb5b23..8fc2b4d55 100644 --- a/packages/astro/test/fixtures/astro-hmr/src/pages/manual.astro +++ b/packages/astro/test/fixtures/astro-hmr/src/pages/manual.astro @@ -10,6 +10,6 @@ import Tour from '../components/Tour.jsx'; </head> <body> <div>Hello world</div> - <Tour:load /> + <Tour client:load /> </body> </html>
\ No newline at end of file diff --git a/packages/astro/test/fixtures/astro-markdown/src/pages/complex.astro b/packages/astro/test/fixtures/astro-markdown/src/pages/complex.astro index f65c60080..6e6c84060 100644 --- a/packages/astro/test/fixtures/astro-markdown/src/pages/complex.astro +++ b/packages/astro/test/fixtures/astro-markdown/src/pages/complex.astro @@ -14,7 +14,7 @@ const description = 'This is a post about some stuff.'; ## Interesting Topic <Hello name={`world`} /> - <Counter:load /> + <Counter client:load /> </Layout> </Markdown> diff --git a/packages/astro/test/fixtures/custom-elements/src/pages/load.astro b/packages/astro/test/fixtures/custom-elements/src/pages/load.astro index 45c9c402a..9cac84321 100644 --- a/packages/astro/test/fixtures/custom-elements/src/pages/load.astro +++ b/packages/astro/test/fixtures/custom-elements/src/pages/load.astro @@ -10,6 +10,6 @@ import '../components/my-element.js'; <body> <h1>{title}</h1> - <my-element:load></my-element:load> + <my-element client:load></my-element> </body> </html>
\ No newline at end of file diff --git a/packages/astro/test/fixtures/no-head-el/src/components/Child.astro b/packages/astro/test/fixtures/no-head-el/src/components/Child.astro index 41cee95c1..e0ffd578c 100644 --- a/packages/astro/test/fixtures/no-head-el/src/components/Child.astro +++ b/packages/astro/test/fixtures/no-head-el/src/components/Child.astro @@ -7,4 +7,4 @@ } </style> <div>Something here</div> -<Something:idle />
\ No newline at end of file +<Something client:idle />
\ No newline at end of file diff --git a/packages/astro/test/fixtures/no-head-el/src/pages/index.astro b/packages/astro/test/fixtures/no-head-el/src/pages/index.astro index 10d8e2e24..091bbb386 100644 --- a/packages/astro/test/fixtures/no-head-el/src/pages/index.astro +++ b/packages/astro/test/fixtures/no-head-el/src/pages/index.astro @@ -10,5 +10,5 @@ import Child from '../components/Child.astro'; </style> <h1>Title of this Blog</h1> -<Something:load /> +<Something client:load /> <Child />
\ No newline at end of file |