diff --git a/.changeset/ninety-monkeys-complain.md b/.changeset/ninety-monkeys-complain.md new file mode 100644 index 0000000000..c8a06da1f3 --- /dev/null +++ b/.changeset/ninety-monkeys-complain.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Clear content layer cache when astro version changes diff --git a/.changeset/swift-snakes-hope.md b/.changeset/swift-snakes-hope.md new file mode 100644 index 0000000000..34c626b394 --- /dev/null +++ b/.changeset/swift-snakes-hope.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Allows special characters in Action names diff --git a/packages/astro/src/actions/runtime/virtual/get-action.ts b/packages/astro/src/actions/runtime/virtual/get-action.ts index b547b57c10..59db34cbc0 100644 --- a/packages/astro/src/actions/runtime/virtual/get-action.ts +++ b/packages/astro/src/actions/runtime/virtual/get-action.ts @@ -11,7 +11,10 @@ import type { ActionAccept, ActionClient } from './server.js'; export async function getAction( path: string, ): Promise> { - const pathKeys = path.replace('/_actions/', '').split('.'); + const pathKeys = path + .replace('/_actions/', '') + .split('.') + .map((key) => decodeURIComponent(key)); // @ts-expect-error virtual module let { server: actionLookup } = await import('astro:internal-actions'); diff --git a/packages/astro/src/content/content-layer.ts b/packages/astro/src/content/content-layer.ts index dcb7aca9fb..df67d49a8b 100644 --- a/packages/astro/src/content/content-layer.ts +++ b/packages/astro/src/content/content-layer.ts @@ -146,13 +146,27 @@ export class ContentLayer { const { digest: currentConfigDigest } = contentConfig.config; this.#lastConfigDigest = currentConfigDigest; + let shouldClear = false; const previousConfigDigest = await this.#store.metaStore().get('config-digest'); + const previousAstroVersion = await this.#store.metaStore().get('astro-version'); if (currentConfigDigest && previousConfigDigest !== currentConfigDigest) { - logger.info('Content config changed, clearing cache'); + logger.info('Content config changed'); + shouldClear = true; + } + if (process.env.ASTRO_VERSION && previousAstroVersion !== process.env.ASTRO_VERSION) { + logger.info('Astro version changed'); + shouldClear = true; + } + if (shouldClear) { + logger.info('Clearing content store'); this.#store.clearAll(); + } + if (process.env.ASTRO_VERSION) { + await this.#store.metaStore().set('astro-version', process.env.ASTRO_VERSION); + } + if (currentConfigDigest) { await this.#store.metaStore().set('config-digest', currentConfigDigest); } - await Promise.all( Object.entries(contentConfig.config.collections).map(async ([name, collection]) => { if (collection.type !== CONTENT_LAYER_TYPE) { diff --git a/packages/astro/templates/actions.mjs b/packages/astro/templates/actions.mjs index 3ff65cecf5..b83cc4a39c 100644 --- a/packages/astro/templates/actions.mjs +++ b/packages/astro/templates/actions.mjs @@ -5,13 +5,17 @@ import { getActionQueryString, } from 'astro:actions'; +const ENCODED_DOT = '%2E'; + function toActionProxy(actionCallback = {}, aggregatedPath = '') { return new Proxy(actionCallback, { get(target, objKey) { if (objKey in target || typeof objKey === 'symbol') { return target[objKey]; } - const path = aggregatedPath + objKey.toString(); + // Add the key, encoding dots so they're not interpreted as nested properties. + const path = + aggregatedPath + encodeURIComponent(objKey.toString()).replaceAll('.', ENCODED_DOT); function action(param) { return handleAction(param, path, this); } diff --git a/packages/astro/test/actions.test.js b/packages/astro/test/actions.test.js index 17758e82c8..793e6ebe77 100644 --- a/packages/astro/test/actions.test.js +++ b/packages/astro/test/actions.test.js @@ -115,6 +115,23 @@ describe('Astro Actions', () => { assert.equal(data.success, true); assert.equal(data.isFormData, true, 'Should receive plain FormData'); }); + + it('Handles special characters in action names', async () => { + for (const name of ['with%2Fslash', 'with%20space', 'with%2Edot']) { + const res = await fixture.fetch(`/_actions/${name}`, { + method: 'POST', + body: JSON.stringify({ name: 'ben' }), + headers: { + 'Content-Type': 'application/json', + }, + }); + assert.equal(res.ok, true); + const text = await res.text(); + assert.equal(res.headers.get('Content-Type'), 'application/json+devalue'); + const data = devalue.parse(text); + assert.equal(data, 'Hello, ben!'); + } + }); }); describe('build', () => { @@ -428,6 +445,24 @@ describe('Astro Actions', () => { const dataRest = devalue.parse(await resRest.text()); assert.equal('fake', dataRest?.uploadId); }); + + it('Handles special characters in action names', async () => { + for (const name of ['with%2Fslash', 'with%20space', 'with%2Edot']) { + const req = new Request(`http://example.com/_actions/${name}`, { + method: 'POST', + body: JSON.stringify({ name: 'ben' }), + headers: { + 'Content-Type': 'application/json', + }, + }); + const res = await app.render(req); + assert.equal(res.ok, true); + const text = await res.text(); + assert.equal(res.headers.get('Content-Type'), 'application/json+devalue'); + const data = devalue.parse(text); + assert.equal(data, 'Hello, ben!'); + } + }); }); }); diff --git a/packages/astro/test/fixtures/actions/src/actions/index.ts b/packages/astro/test/fixtures/actions/src/actions/index.ts index 4e6120309f..78cc39620b 100644 --- a/packages/astro/test/fixtures/actions/src/actions/index.ts +++ b/packages/astro/test/fixtures/actions/src/actions/index.ts @@ -161,4 +161,28 @@ export const server = { }; }, }), + "with.dot": defineAction({ + input: z.object({ + name: z.string(), + }), + handler: async (input) => { + return `Hello, ${input.name}!` + } + }), + "with space": defineAction({ + input: z.object({ + name: z.string(), + }), + handler: async (input) => { + return `Hello, ${input.name}!` + } + }), + "with/slash": defineAction({ + input: z.object({ + name: z.string(), + }), + handler: async (input) => { + return `Hello, ${input.name}!` + } + }), };