Skip to content

Commit

Permalink
Merge branch 'main' into aria-audit
Browse files Browse the repository at this point in the history
  • Loading branch information
ematipico authored Jan 3, 2024
2 parents 31b4529 + 769826e commit 89f3f65
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 13 deletions.
10 changes: 10 additions & 0 deletions .changeset/stupid-peas-juggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'astro': minor
---

Extends the `client:visible` directive by adding an optional `rootMargin` property. This allows a component to be hydrated when it is close to the viewport instead of waiting for it to become visible.

```html
<!-- Load component when it's within 200px away from entering the viewport -->
<Component client:visible="200px" />
```
28 changes: 28 additions & 0 deletions .changeset/tiny-days-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
'astro': minor
---

Cookie encoding / decoding can now be customized

Adds new `encode` and `decode` functions to allow customizing how cookies are encoded and decoded. For example, you can bypass the default encoding via `encodeURIComponent` when adding a URL as part of a cookie:

```astro
---
import { encodeCookieValue } from "./cookies";
Astro.cookies.set('url', Astro.url.toString(), {
// Override the default encoding so that URI components are not encoded
encode: value => encodeCookieValue(value)
});
---
```

Later, you can decode the URL in the same way:

```astro
---
import { decodeCookieValue } from "./cookies";
const url = Astro.cookies.get('url', {
decode: value => decodeCookieValue(value)
});
---
```
4 changes: 2 additions & 2 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,13 @@ export type {
} from '../assets/types.js';
export type { RemotePattern } from '../assets/utils/remotePattern.js';
export type { SSRManifest } from '../core/app/types.js';
export type { AstroCookies } from '../core/cookies/index.js';
export type { AstroCookies, AstroCookieSetOptions, AstroCookieGetOptions } from '../core/cookies/index.js';

export interface AstroBuiltinProps {
'client:load'?: boolean;
'client:idle'?: boolean;
'client:media'?: string;
'client:visible'?: boolean;
'client:visible'?: string|boolean;
'client:only'?: boolean | string;
}

Expand Down
23 changes: 14 additions & 9 deletions packages/astro/src/core/cookies/cookies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@ import type { CookieSerializeOptions } from 'cookie';
import { parse, serialize } from 'cookie';
import { AstroError, AstroErrorData } from '../errors/index.js';

interface AstroCookieSetOptions {
export interface AstroCookieSetOptions {
domain?: string;
expires?: Date;
httpOnly?: boolean;
maxAge?: number;
path?: string;
sameSite?: boolean | 'lax' | 'none' | 'strict';
secure?: boolean;
encode?: (value: string) => string;
}

export interface AstroCookieGetOptions {
decode?: (value: string) => string;
};

type AstroCookieDeleteOptions = Pick<AstroCookieSetOptions, 'domain' | 'path'>;

interface AstroCookieInterface {
Expand Down Expand Up @@ -97,7 +102,7 @@ class AstroCookies implements AstroCookiesInterface {
* @param key The cookie to get.
* @returns An object containing the cookie value as well as convenience methods for converting its value.
*/
get(key: string): AstroCookie | undefined {
get(key: string, options: AstroCookieGetOptions | undefined = undefined): AstroCookie | undefined {
// Check for outgoing Set-Cookie values first
if (this.#outgoing?.has(key)) {
let [serializedValue, , isSetValue] = this.#outgoing.get(key)!;
Expand All @@ -108,7 +113,7 @@ class AstroCookies implements AstroCookiesInterface {
}
}

const values = this.#ensureParsed();
const values = this.#ensureParsed(options);
if (key in values) {
const value = values[key];
return new AstroCookie(value);
Expand All @@ -121,12 +126,12 @@ class AstroCookies implements AstroCookiesInterface {
* @param key The cookie to check for.
* @returns
*/
has(key: string): boolean {
has(key: string, options: AstroCookieGetOptions | undefined = undefined): boolean {
if (this.#outgoing?.has(key)) {
let [, , isSetValue] = this.#outgoing.get(key)!;
return isSetValue;
}
const values = this.#ensureParsed();
const values = this.#ensureParsed(options);
return !!values[key];
}

Expand Down Expand Up @@ -185,9 +190,9 @@ class AstroCookies implements AstroCookiesInterface {
}
}

#ensureParsed(): Record<string, string> {
#ensureParsed(options: AstroCookieGetOptions | undefined = undefined): Record<string, string> {
if (!this.#requestValues) {
this.#parse();
this.#parse(options);
}
if (!this.#requestValues) {
this.#requestValues = {};
Expand All @@ -202,13 +207,13 @@ class AstroCookies implements AstroCookiesInterface {
return this.#outgoing;
}

#parse() {
#parse(options: AstroCookieGetOptions | undefined = undefined) {
const raw = this.#request.headers.get('cookie');
if (!raw) {
return;
}

this.#requestValues = parse(raw);
this.#requestValues = parse(raw, options);
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/cookies/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export {
getSetCookiesFromResponse,
responseHasCookies,
} from './response.js';
export type { AstroCookieSetOptions, AstroCookieGetOptions } from "./cookies.js";
8 changes: 6 additions & 2 deletions packages/astro/src/runtime/client/visible.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import type { ClientDirective } from '../../@types/astro.js';
* We target the children because `astro-island` is set to `display: contents`
* which doesn't work with IntersectionObserver
*/
const visibleDirective: ClientDirective = (load, _options, el) => {
const visibleDirective: ClientDirective = (load, options, el) => {
const cb = async () => {
const hydrate = await load();
await hydrate();
};

const ioOptions = {
rootMargin: typeof options.value === 'string' ? options.value : undefined,
};

const io = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
Expand All @@ -19,7 +23,7 @@ const visibleDirective: ClientDirective = (load, _options, el) => {
cb();
break; // break loop on first match
}
});
}, ioOptions);

for (const child of el.children) {
io.observe(child);
Expand Down
24 changes: 24 additions & 0 deletions packages/astro/test/units/cookies/get.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,30 @@ describe('astro/src/core/cookies', () => {
expect(cookies.get('foo').value).to.equal('bar');
});

it('gets the cookie value with default decode', () => {
const url = 'http://localhost';
const req = new Request('http://example.com/', {
headers: {
cookie: `url=${encodeURIComponent(url)}`,
},
});
const cookies = new AstroCookies(req);
// by default decodeURIComponent is used on the value
expect(cookies.get('url').value).to.equal(url);
});

it('gets the cookie value with custom decode', () => {
const url = 'http://localhost';
const req = new Request('http://example.com/', {
headers: {
cookie: `url=${encodeURIComponent(url)}`,
},
});
const cookies = new AstroCookies(req);
// set decode to the identity function to prevent decodeURIComponent on the value
expect(cookies.get('url', { decode: o => o }).value).to.equal(encodeURIComponent(url));
});

it("Returns undefined is the value doesn't exist", () => {
const req = new Request('http://example.com/');
let cookies = new AstroCookies(req);
Expand Down
23 changes: 23 additions & 0 deletions packages/astro/test/units/cookies/set.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,29 @@ describe('astro/src/core/cookies', () => {
expect(headers[0]).to.equal('foo=bar');
});

it('Sets a cookie value that can be serialized w/ defaultencodeURIComponent behavior', () => {
let req = new Request('http://example.com/');
let cookies = new AstroCookies(req);
const url = 'http://localhost/path';
cookies.set('url', url);
let headers = Array.from(cookies.headers());
expect(headers).to.have.a.lengthOf(1);
// by default cookie value is URI encoded
expect(headers[0]).to.equal(`url=${encodeURIComponent(url)}`);
});

it('Sets a cookie value that can be serialized w/ custom encode behavior', () => {
let req = new Request('http://example.com/');
let cookies = new AstroCookies(req);
const url = 'http://localhost/path';
// set encode option to the identity function
cookies.set('url', url, { encode: o => o });
let headers = Array.from(cookies.headers());
expect(headers).to.have.a.lengthOf(1);
// no longer URI encoded
expect(headers[0]).to.equal(`url=${url}`);
});

it('Can set cookie options', () => {
let req = new Request('http://example.com/');
let cookies = new AstroCookies(req);
Expand Down

0 comments on commit 89f3f65

Please sign in to comment.