From 07f89429a1ef5173d3321e0b362a9dc71fc74fe5 Mon Sep 17 00:00:00 2001 From: Erika <3019731+Princesseuh@users.noreply.github.com> Date: Fri, 1 Mar 2024 10:23:07 +0100 Subject: [PATCH 01/13] fix(assets): Solidify Node endpoint (#10284) * fix(assets): Solidify Node endpoint * chore: changeset --- .changeset/soft-boxes-allow.md | 7 ++ packages/astro/src/assets/endpoint/node.ts | 56 +++++++++--- .../astro/src/assets/vite-plugin-assets.ts | 3 +- packages/astro/test/core-image.test.js | 90 +++++++++++++++++++ 4 files changed, 143 insertions(+), 13 deletions(-) create mode 100644 .changeset/soft-boxes-allow.md diff --git a/.changeset/soft-boxes-allow.md b/.changeset/soft-boxes-allow.md new file mode 100644 index 000000000000..54ff50ea853d --- /dev/null +++ b/.changeset/soft-boxes-allow.md @@ -0,0 +1,7 @@ +--- +"astro": patch +--- + +Fixes an issue where in Node SSR, the image endpoint could be used maliciously to reveal unintended information about the underlying system. + +Thanks to Google Security Team for reporting this issue. diff --git a/packages/astro/src/assets/endpoint/node.ts b/packages/astro/src/assets/endpoint/node.ts index cabf02a76e6b..d06066fae417 100644 --- a/packages/astro/src/assets/endpoint/node.ts +++ b/packages/astro/src/assets/endpoint/node.ts @@ -1,4 +1,7 @@ -import os from 'os'; +/* eslint-disable no-console */ +import os from 'node:os'; +import { isAbsolute } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import { isRemotePath, removeQueryString } from '@astrojs/internal-helpers/path'; import { readFile } from 'fs/promises'; import mime from 'mime/lite.js'; @@ -7,23 +10,44 @@ import { getConfiguredImageService } from '../internal.js'; import { etag } from '../utils/etag.js'; import { isRemoteAllowed } from '../utils/remotePattern.js'; // @ts-expect-error -import { assetsDir, imageConfig } from 'astro:assets'; +import { assetsDir, outDir, imageConfig } from 'astro:assets'; function replaceFileSystemReferences(src: string) { return os.platform().includes('win32') ? src.replace(/^\/@fs\//, '') : src.replace(/^\/@fs/, ''); } async function loadLocalImage(src: string, url: URL) { - const filePath = import.meta.env.DEV - ? removeQueryString(replaceFileSystemReferences(src)) - : new URL('.' + src, assetsDir); + const assetsDirPath = fileURLToPath(assetsDir); + + let fileUrl; + if (import.meta.env.DEV) { + fileUrl = pathToFileURL(removeQueryString(replaceFileSystemReferences(src))); + } else { + try { + fileUrl = new URL('.' + src, outDir); + const filePath = fileURLToPath(fileUrl); + + if (!isAbsolute(filePath) || !filePath.startsWith(assetsDirPath)) { + return undefined; + } + } catch (err: unknown) { + return undefined; + } + } + let buffer: Buffer | undefined = undefined; try { - buffer = await readFile(filePath); + buffer = await readFile(fileUrl); } catch (e) { - const sourceUrl = new URL(src, url.origin); - buffer = await loadRemoteImage(sourceUrl); + // Fallback to try to load the file using `fetch` + try { + const sourceUrl = new URL(src, url.origin); + buffer = await loadRemoteImage(sourceUrl); + } catch (err: unknown) { + console.error('Could not process image request:', err); + return undefined; + } } return buffer; @@ -58,7 +82,11 @@ export const GET: APIRoute = async ({ request }) => { const transform = await imageService.parseURL(url, imageConfig); if (!transform?.src) { - throw new Error('Incorrect transform returned by `parseURL`'); + const err = new Error( + 'Incorrect transform returned by `parseURL`. Expected a transform with a `src` property.' + ); + console.error('Could not parse image transform from URL:', err); + return new Response('Internal Server Error', { status: 500 }); } let inputBuffer: Buffer | undefined = undefined; @@ -74,7 +102,7 @@ export const GET: APIRoute = async ({ request }) => { } if (!inputBuffer) { - return new Response('Not Found', { status: 404 }); + return new Response('Internal Server Error', { status: 500 }); } const { data, format } = await imageService.transform(inputBuffer, transform, imageConfig); @@ -89,8 +117,12 @@ export const GET: APIRoute = async ({ request }) => { }, }); } catch (err: unknown) { - // eslint-disable-next-line no-console console.error('Could not process image request:', err); - return new Response(`Server Error: ${err}`, { status: 500 }); + return new Response( + import.meta.env.DEV ? `Could not process image request: ${err}` : `Internal Server Error`, + { + status: 500, + } + ); } }; diff --git a/packages/astro/src/assets/vite-plugin-assets.ts b/packages/astro/src/assets/vite-plugin-assets.ts index 5a56e76a6838..6afd01c2a6ba 100644 --- a/packages/astro/src/assets/vite-plugin-assets.ts +++ b/packages/astro/src/assets/vite-plugin-assets.ts @@ -66,13 +66,14 @@ export default function assets({ export { default as Picture } from "astro/components/Picture.astro"; export const imageConfig = ${JSON.stringify(settings.config.image)}; - export const assetsDir = new URL(${JSON.stringify( + export const outDir = new URL(${JSON.stringify( new URL( isServerLikeOutput(settings.config) ? settings.config.build.client : settings.config.outDir ) )}); + export const assetsDir = new URL(${JSON.stringify(settings.config.build.assets)}, outDir); export const getImage = async (options) => await getImageInternal(options, imageConfig); `; } diff --git a/packages/astro/test/core-image.test.js b/packages/astro/test/core-image.test.js index 590e77000557..3f53fba96180 100644 --- a/packages/astro/test/core-image.test.js +++ b/packages/astro/test/core-image.test.js @@ -1103,6 +1103,96 @@ describe('astro:image', () => { assert.equal(response.status, 200); }); + it('endpoint handle malformed requests', async () => { + const badPaths = [ + '../../../../../../../../../../../../etc/hosts%00', + '../../../../../../../../../../../../etc/hosts', + '../../boot.ini', + '/../../../../../../../../%2A', + '../../../../../../../../../../../../etc/passwd%00', + '../../../../../../../../../../../../etc/passwd', + '../../../../../../../../../../../../etc/shadow%00', + '../../../../../../../../../../../../etc/shadow', + '/../../../../../../../../../../etc/passwd^^', + '/../../../../../../../../../../etc/shadow^^', + '/../../../../../../../../../../etc/passwd', + '/../../../../../../../../../../etc/shadow', + '/./././././././././././etc/passwd', + '/./././././././././././etc/shadow', + '....................etcpasswd', + '....................etcshadow', + '....................etcpasswd', + '....................etcshadow', + '/..../..../..../..../..../..../etc/passwd', + '/..../..../..../..../..../..../etc/shadow', + '.\\./.\\./.\\./.\\./.\\./.\\./etc/passwd', + '.\\./.\\./.\\./.\\./.\\./.\\./etc/shadow', + '....................etcpasswd%00', + '....................etcshadow%00', + '....................etcpasswd%00', + '....................etcshadow%00', + '%0a/bin/cat%20/etc/passwd', + '%0a/bin/cat%20/etc/shadow', + '%00/etc/passwd%00', + '%00/etc/shadow%00', + '%00../../../../../../etc/passwd', + '%00../../../../../../etc/shadow', + '/../../../../../../../../../../../etc/passwd%00.jpg', + '/../../../../../../../../../../../etc/passwd%00.html', + '/..%c0%af../..%c0%af../..%c0%af../..%c0%af../..%c0%af../..%c0%af../etc/passwd', + '/..%c0%af../..%c0%af../..%c0%af../..%c0%af../..%c0%af../..%c0%af../etc/shadow', + '/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd,', + '/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/shadow,', + '%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%,25%5c..%25%5c..%25%5c..%25%5c..%00', + '/%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..,%25%5c..%25%5c..%25%5c..%25%5c..%00', + '%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%,25%5c..%25%5c..% 25%5c..%25%5c..%00', + '%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%,25%5c..%25%5c..% 25%5c..%25%5c..%255cboot.ini', + '/%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..,%25%5c..%25%5c..%25%5c..%25%5c..winnt/desktop.ini', + '\\'/bin/cat%20/etc/passwd\\'', + '\\'/bin/cat%20/etc/shadow\\'', + '../../../../../../../../conf/server.xml', + '/../../../../../../../../bin/id|', + 'C:/inetpub/wwwroot/global.asa', + 'C:inetpubwwwrootglobal.asa', + 'C:/boot.ini', + 'C:\boot.ini', + '../../../../../../../../../../../../localstart.asp%00', + '../../../../../../../../../../../../localstart.asp', + '../../../../../../../../../../../../boot.ini%00', + '../../../../../../../../../../../../boot.ini', + '/./././././././././././boot.ini', + '/../../../../../../../../../../../boot.ini%00', + '/../../../../../../../../../../../boot.ini', + '/..../..../..../..../..../..../boot.ini', + '/.\\./.\\./.\\./.\\./.\\./.\\./boot.ini', + '....................\boot.ini', + '....................\boot.ini%00', + '....................\boot.ini', + '/../../../../../../../../../../../boot.ini%00.html', + '/../../../../../../../../../../../boot.ini%00.jpg', + '/.../.../.../.../.../ ', + '..%c0%af../..%c0%af../..%c0%af../..%c0%af../..%c0%af../..%c0%af../boot.ini', + '/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/boot.ini', + '../prerender/index.html', + ]; + + const app = await fixture.loadTestAdapterApp(); + + for (const path of badPaths) { + let request = new Request('http://example.com/_image?href=' + path); + let response = await app.render(request); + const body = await response.text(); + + assert.equal(response.status, 500); + assert.equal(body.includes('Internal Server Error'), true); + } + + // Server should still be running + let request = new Request('http://example.com/'); + let response = await app.render(request); + assert.equal(response.status, 200); + }); + it('prerendered routes images are built', async () => { const html = await fixture.readFile('/client/prerender/index.html'); const $ = cheerio.load(html); From a3ebfad0cc812e410f664d58373f77e6cbaeb7aa Mon Sep 17 00:00:00 2001 From: Erika Date: Fri, 1 Mar 2024 09:24:22 +0000 Subject: [PATCH 02/13] [ci] format --- packages/astro/src/assets/endpoint/node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/src/assets/endpoint/node.ts b/packages/astro/src/assets/endpoint/node.ts index d06066fae417..4d29a7fadd02 100644 --- a/packages/astro/src/assets/endpoint/node.ts +++ b/packages/astro/src/assets/endpoint/node.ts @@ -10,7 +10,7 @@ import { getConfiguredImageService } from '../internal.js'; import { etag } from '../utils/etag.js'; import { isRemoteAllowed } from '../utils/remotePattern.js'; // @ts-expect-error -import { assetsDir, outDir, imageConfig } from 'astro:assets'; +import { assetsDir, imageConfig, outDir } from 'astro:assets'; function replaceFileSystemReferences(src: string) { return os.platform().includes('win32') ? src.replace(/^\/@fs\//, '') : src.replace(/^\/@fs/, ''); From afd41cc28bd449e82831e91f6302221945bd6019 Mon Sep 17 00:00:00 2001 From: "Houston (Bot)" <108291165+astrobot-houston@users.noreply.github.com> Date: Fri, 1 Mar 2024 01:31:12 -0800 Subject: [PATCH 03/13] [ci] release (#10265) Co-authored-by: github-actions[bot] --- .changeset/curvy-donkeys-knock.md | 5 -- .changeset/few-worms-rush.md | 5 -- .changeset/smooth-singers-kiss.md | 5 -- .changeset/soft-boxes-allow.md | 7 --- examples/basics/package.json | 2 +- examples/blog/package.json | 2 +- examples/component/package.json | 2 +- examples/framework-alpine/package.json | 2 +- examples/framework-lit/package.json | 2 +- examples/framework-multiple/package.json | 2 +- examples/framework-preact/package.json | 2 +- examples/framework-react/package.json | 2 +- examples/framework-solid/package.json | 2 +- examples/framework-svelte/package.json | 2 +- examples/framework-vue/package.json | 2 +- examples/hackernews/package.json | 4 +- examples/integration/package.json | 2 +- examples/middleware/package.json | 4 +- examples/minimal/package.json | 2 +- examples/non-html-pages/package.json | 2 +- examples/portfolio/package.json | 2 +- examples/ssr/package.json | 4 +- examples/starlog/package.json | 2 +- examples/view-transitions/package.json | 4 +- examples/with-markdoc/package.json | 2 +- examples/with-markdown-plugins/package.json | 2 +- examples/with-markdown-shiki/package.json | 2 +- examples/with-mdx/package.json | 2 +- examples/with-nanostores/package.json | 2 +- examples/with-tailwindcss/package.json | 2 +- examples/with-vitest/package.json | 2 +- packages/astro/CHANGELOG.md | 12 ++++ packages/astro/package.json | 2 +- packages/integrations/node/CHANGELOG.md | 6 ++ packages/integrations/node/package.json | 2 +- pnpm-lock.yaml | 62 ++++++++++----------- 36 files changed, 82 insertions(+), 86 deletions(-) delete mode 100644 .changeset/curvy-donkeys-knock.md delete mode 100644 .changeset/few-worms-rush.md delete mode 100644 .changeset/smooth-singers-kiss.md delete mode 100644 .changeset/soft-boxes-allow.md diff --git a/.changeset/curvy-donkeys-knock.md b/.changeset/curvy-donkeys-knock.md deleted file mode 100644 index 0197a800e052..000000000000 --- a/.changeset/curvy-donkeys-knock.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"astro": patch ---- - -Fixes a regression introduced in v4.4.5 where image optimization did not work in dev mode when a base was configured. diff --git a/.changeset/few-worms-rush.md b/.changeset/few-worms-rush.md deleted file mode 100644 index 3ca708f94865..000000000000 --- a/.changeset/few-worms-rush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"astro": patch ---- - -Adds auto completion for `astro:` event names when adding or removing event listeners on `document`. diff --git a/.changeset/smooth-singers-kiss.md b/.changeset/smooth-singers-kiss.md deleted file mode 100644 index 56d1ea893af4..000000000000 --- a/.changeset/smooth-singers-kiss.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@astrojs/node": patch ---- - -Fixes the `server.host` option to properly listen on all network interfaces when set to `true` diff --git a/.changeset/soft-boxes-allow.md b/.changeset/soft-boxes-allow.md deleted file mode 100644 index 54ff50ea853d..000000000000 --- a/.changeset/soft-boxes-allow.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"astro": patch ---- - -Fixes an issue where in Node SSR, the image endpoint could be used maliciously to reveal unintended information about the underlying system. - -Thanks to Google Security Team for reporting this issue. diff --git a/examples/basics/package.json b/examples/basics/package.json index 86f892f41087..b190f951dae2 100644 --- a/examples/basics/package.json +++ b/examples/basics/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.4.6" + "astro": "^4.4.7" } } diff --git a/examples/blog/package.json b/examples/blog/package.json index c51c8592d4bc..91cf97616a89 100644 --- a/examples/blog/package.json +++ b/examples/blog/package.json @@ -14,6 +14,6 @@ "@astrojs/mdx": "^2.1.1", "@astrojs/rss": "^4.0.5", "@astrojs/sitemap": "^3.1.1", - "astro": "^4.4.6" + "astro": "^4.4.7" } } diff --git a/examples/component/package.json b/examples/component/package.json index 1cdb6b33662d..4687a572c3a7 100644 --- a/examples/component/package.json +++ b/examples/component/package.json @@ -15,7 +15,7 @@ ], "scripts": {}, "devDependencies": { - "astro": "^4.4.6" + "astro": "^4.4.7" }, "peerDependencies": { "astro": "^4.0.0" diff --git a/examples/framework-alpine/package.json b/examples/framework-alpine/package.json index b203a9e4167f..10733f4fd0c7 100644 --- a/examples/framework-alpine/package.json +++ b/examples/framework-alpine/package.json @@ -14,6 +14,6 @@ "@astrojs/alpinejs": "^0.4.0", "@types/alpinejs": "^3.13.5", "alpinejs": "^3.13.3", - "astro": "^4.4.6" + "astro": "^4.4.7" } } diff --git a/examples/framework-lit/package.json b/examples/framework-lit/package.json index 6d8d7ff1291a..216fe53cb2fe 100644 --- a/examples/framework-lit/package.json +++ b/examples/framework-lit/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/lit": "^4.0.1", "@webcomponents/template-shadowroot": "^0.2.1", - "astro": "^4.4.6", + "astro": "^4.4.7", "lit": "^3.1.2" } } diff --git a/examples/framework-multiple/package.json b/examples/framework-multiple/package.json index 65bb39f7d3e8..25e278979660 100644 --- a/examples/framework-multiple/package.json +++ b/examples/framework-multiple/package.json @@ -16,7 +16,7 @@ "@astrojs/solid-js": "^4.0.1", "@astrojs/svelte": "^5.2.0", "@astrojs/vue": "^4.0.8", - "astro": "^4.4.6", + "astro": "^4.4.7", "preact": "^10.19.2", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/framework-preact/package.json b/examples/framework-preact/package.json index 5f61f5790fc4..fc9089470e3d 100644 --- a/examples/framework-preact/package.json +++ b/examples/framework-preact/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/preact": "^3.1.1", "@preact/signals": "^1.2.1", - "astro": "^4.4.6", + "astro": "^4.4.7", "preact": "^10.19.2" } } diff --git a/examples/framework-react/package.json b/examples/framework-react/package.json index ad2b47014a5f..04f61c34c8d4 100644 --- a/examples/framework-react/package.json +++ b/examples/framework-react/package.json @@ -14,7 +14,7 @@ "@astrojs/react": "^3.0.10", "@types/react": "^18.2.37", "@types/react-dom": "^18.2.15", - "astro": "^4.4.6", + "astro": "^4.4.7", "react": "^18.2.0", "react-dom": "^18.2.0" } diff --git a/examples/framework-solid/package.json b/examples/framework-solid/package.json index f4a1cd063707..0f98a817722a 100644 --- a/examples/framework-solid/package.json +++ b/examples/framework-solid/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/solid-js": "^4.0.1", - "astro": "^4.4.6", + "astro": "^4.4.7", "solid-js": "^1.8.5" } } diff --git a/examples/framework-svelte/package.json b/examples/framework-svelte/package.json index c4b7e3b43332..297dd9b4dd26 100644 --- a/examples/framework-svelte/package.json +++ b/examples/framework-svelte/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/svelte": "^5.2.0", - "astro": "^4.4.6", + "astro": "^4.4.7", "svelte": "^4.2.5" } } diff --git a/examples/framework-vue/package.json b/examples/framework-vue/package.json index 5864c5a006a1..3e577afe22dc 100644 --- a/examples/framework-vue/package.json +++ b/examples/framework-vue/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/vue": "^4.0.8", - "astro": "^4.4.6", + "astro": "^4.4.7", "vue": "^3.3.8" } } diff --git a/examples/hackernews/package.json b/examples/hackernews/package.json index 3560057d1e79..b883f6e413a6 100644 --- a/examples/hackernews/package.json +++ b/examples/hackernews/package.json @@ -11,7 +11,7 @@ "astro": "astro" }, "dependencies": { - "@astrojs/node": "^8.2.1", - "astro": "^4.4.6" + "@astrojs/node": "^8.2.2", + "astro": "^4.4.7" } } diff --git a/examples/integration/package.json b/examples/integration/package.json index bd2cd6879db1..f770a92bbace 100644 --- a/examples/integration/package.json +++ b/examples/integration/package.json @@ -15,7 +15,7 @@ ], "scripts": {}, "devDependencies": { - "astro": "^4.4.6" + "astro": "^4.4.7" }, "peerDependencies": { "astro": "^4.0.0" diff --git a/examples/middleware/package.json b/examples/middleware/package.json index 9ee9f9fa601b..112b26151f2e 100644 --- a/examples/middleware/package.json +++ b/examples/middleware/package.json @@ -12,8 +12,8 @@ "server": "node dist/server/entry.mjs" }, "dependencies": { - "@astrojs/node": "^8.2.1", - "astro": "^4.4.6", + "@astrojs/node": "^8.2.2", + "astro": "^4.4.7", "html-minifier": "^4.0.0" }, "devDependencies": { diff --git a/examples/minimal/package.json b/examples/minimal/package.json index 85580d9cd383..a690ec757c8c 100644 --- a/examples/minimal/package.json +++ b/examples/minimal/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.4.6" + "astro": "^4.4.7" } } diff --git a/examples/non-html-pages/package.json b/examples/non-html-pages/package.json index f00fb4b5b97a..1c5a175c2667 100644 --- a/examples/non-html-pages/package.json +++ b/examples/non-html-pages/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.4.6" + "astro": "^4.4.7" } } diff --git a/examples/portfolio/package.json b/examples/portfolio/package.json index e52d98f04c54..015be58aa96e 100644 --- a/examples/portfolio/package.json +++ b/examples/portfolio/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.4.6" + "astro": "^4.4.7" } } diff --git a/examples/ssr/package.json b/examples/ssr/package.json index 592fc7077b67..37861914cc20 100644 --- a/examples/ssr/package.json +++ b/examples/ssr/package.json @@ -12,9 +12,9 @@ "server": "node dist/server/entry.mjs" }, "dependencies": { - "@astrojs/node": "^8.2.1", + "@astrojs/node": "^8.2.2", "@astrojs/svelte": "^5.2.0", - "astro": "^4.4.6", + "astro": "^4.4.7", "svelte": "^4.2.5" } } diff --git a/examples/starlog/package.json b/examples/starlog/package.json index 83df3e1f30c6..ef9962f6ee49 100644 --- a/examples/starlog/package.json +++ b/examples/starlog/package.json @@ -10,7 +10,7 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.4.6", + "astro": "^4.4.7", "sass": "^1.69.5", "sharp": "^0.32.6" } diff --git a/examples/view-transitions/package.json b/examples/view-transitions/package.json index a50877a080aa..6ee02205dfab 100644 --- a/examples/view-transitions/package.json +++ b/examples/view-transitions/package.json @@ -11,7 +11,7 @@ }, "devDependencies": { "@astrojs/tailwind": "^5.1.0", - "@astrojs/node": "^8.2.1", - "astro": "^4.4.6" + "@astrojs/node": "^8.2.2", + "astro": "^4.4.7" } } diff --git a/examples/with-markdoc/package.json b/examples/with-markdoc/package.json index 2246568f6c20..a08d735de871 100644 --- a/examples/with-markdoc/package.json +++ b/examples/with-markdoc/package.json @@ -12,6 +12,6 @@ }, "dependencies": { "@astrojs/markdoc": "^0.9.0", - "astro": "^4.4.6" + "astro": "^4.4.7" } } diff --git a/examples/with-markdown-plugins/package.json b/examples/with-markdown-plugins/package.json index 14caff03e549..07e94e6e70c3 100644 --- a/examples/with-markdown-plugins/package.json +++ b/examples/with-markdown-plugins/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/markdown-remark": "^4.2.1", - "astro": "^4.4.6", + "astro": "^4.4.7", "hast-util-select": "^6.0.2", "rehype-autolink-headings": "^7.1.0", "rehype-slug": "^6.0.0", diff --git a/examples/with-markdown-shiki/package.json b/examples/with-markdown-shiki/package.json index 2a295880e3ab..decc8acf15e9 100644 --- a/examples/with-markdown-shiki/package.json +++ b/examples/with-markdown-shiki/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.4.6" + "astro": "^4.4.7" } } diff --git a/examples/with-mdx/package.json b/examples/with-mdx/package.json index 4ceaf2373c7a..189c2cf43a90 100644 --- a/examples/with-mdx/package.json +++ b/examples/with-mdx/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/mdx": "^2.1.1", "@astrojs/preact": "^3.1.1", - "astro": "^4.4.6", + "astro": "^4.4.7", "preact": "^10.19.2" } } diff --git a/examples/with-nanostores/package.json b/examples/with-nanostores/package.json index 909328a27997..84c90742adab 100644 --- a/examples/with-nanostores/package.json +++ b/examples/with-nanostores/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/preact": "^3.1.1", "@nanostores/preact": "^0.5.0", - "astro": "^4.4.6", + "astro": "^4.4.7", "nanostores": "^0.9.5", "preact": "^10.19.2" } diff --git a/examples/with-tailwindcss/package.json b/examples/with-tailwindcss/package.json index 3d9a75496793..7439fb66bf03 100644 --- a/examples/with-tailwindcss/package.json +++ b/examples/with-tailwindcss/package.json @@ -14,7 +14,7 @@ "@astrojs/mdx": "^2.1.1", "@astrojs/tailwind": "^5.1.0", "@types/canvas-confetti": "^1.6.3", - "astro": "^4.4.6", + "astro": "^4.4.7", "autoprefixer": "^10.4.15", "canvas-confetti": "^1.9.1", "postcss": "^8.4.28", diff --git a/examples/with-vitest/package.json b/examples/with-vitest/package.json index f337edc99d6e..875e1a16b7e2 100644 --- a/examples/with-vitest/package.json +++ b/examples/with-vitest/package.json @@ -12,7 +12,7 @@ "test": "vitest" }, "dependencies": { - "astro": "^4.4.6", + "astro": "^4.4.7", "vitest": "^1.3.1" } } diff --git a/packages/astro/CHANGELOG.md b/packages/astro/CHANGELOG.md index 901e49d22925..05e54a5a4b5c 100644 --- a/packages/astro/CHANGELOG.md +++ b/packages/astro/CHANGELOG.md @@ -1,5 +1,17 @@ # astro +## 4.4.7 + +### Patch Changes + +- [#10274](https://github.com/withastro/astro/pull/10274) [`e556151603a2f0173059d0f98fdcbec0610b48ff`](https://github.com/withastro/astro/commit/e556151603a2f0173059d0f98fdcbec0610b48ff) Thanks [@lilnasy](https://github.com/lilnasy)! - Fixes a regression introduced in v4.4.5 where image optimization did not work in dev mode when a base was configured. + +- [#10263](https://github.com/withastro/astro/pull/10263) [`9bdbed723e0aa4243d7d6ee64d1c1df3b75b9aeb`](https://github.com/withastro/astro/commit/9bdbed723e0aa4243d7d6ee64d1c1df3b75b9aeb) Thanks [@martrapp](https://github.com/martrapp)! - Adds auto completion for `astro:` event names when adding or removing event listeners on `document`. + +- [#10284](https://github.com/withastro/astro/pull/10284) [`07f89429a1ef5173d3321e0b362a9dc71fc74fe5`](https://github.com/withastro/astro/commit/07f89429a1ef5173d3321e0b362a9dc71fc74fe5) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fixes an issue where in Node SSR, the image endpoint could be used maliciously to reveal unintended information about the underlying system. + + Thanks to Google Security Team for reporting this issue. + ## 4.4.6 ### Patch Changes diff --git a/packages/astro/package.json b/packages/astro/package.json index 561ba56dbff8..15e4acb8f2c1 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,6 +1,6 @@ { "name": "astro", - "version": "4.4.6", + "version": "4.4.7", "description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.", "type": "module", "author": "withastro", diff --git a/packages/integrations/node/CHANGELOG.md b/packages/integrations/node/CHANGELOG.md index d95cb355c814..9f5e216ebd11 100644 --- a/packages/integrations/node/CHANGELOG.md +++ b/packages/integrations/node/CHANGELOG.md @@ -1,5 +1,11 @@ # @astrojs/node +## 8.2.2 + +### Patch Changes + +- [#10282](https://github.com/withastro/astro/pull/10282) [`b47dcaa25968ec85ba96fce23381c94a94e389f6`](https://github.com/withastro/astro/commit/b47dcaa25968ec85ba96fce23381c94a94e389f6) Thanks [@SatanshuMishra](https://github.com/SatanshuMishra)! - Fixes the `server.host` option to properly listen on all network interfaces when set to `true` + ## 8.2.1 ### Patch Changes diff --git a/packages/integrations/node/package.json b/packages/integrations/node/package.json index fea5c39b1cb4..95b5601f8ecd 100644 --- a/packages/integrations/node/package.json +++ b/packages/integrations/node/package.json @@ -1,7 +1,7 @@ { "name": "@astrojs/node", "description": "Deploy your site to a Node.js server", - "version": "8.2.1", + "version": "8.2.2", "type": "module", "types": "./dist/index.d.ts", "author": "withastro", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0eb28d9453b..86188e8d72ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -134,7 +134,7 @@ importers: examples/basics: dependencies: astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro examples/blog: @@ -149,13 +149,13 @@ importers: specifier: ^3.1.1 version: link:../../packages/integrations/sitemap astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro examples/component: devDependencies: astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro examples/framework-alpine: @@ -170,7 +170,7 @@ importers: specifier: ^3.13.3 version: 3.13.3 astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro examples/framework-lit: @@ -182,7 +182,7 @@ importers: specifier: ^0.2.1 version: 0.2.1 astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro lit: specifier: ^3.1.2 @@ -206,7 +206,7 @@ importers: specifier: ^4.0.8 version: link:../../packages/integrations/vue astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro preact: specifier: ^10.19.2 @@ -236,7 +236,7 @@ importers: specifier: ^1.2.1 version: 1.2.1(preact@10.19.3) astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro preact: specifier: ^10.19.2 @@ -254,7 +254,7 @@ importers: specifier: ^18.2.15 version: 18.2.18 astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro react: specifier: ^18.2.0 @@ -269,7 +269,7 @@ importers: specifier: ^4.0.1 version: link:../../packages/integrations/solid astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro solid-js: specifier: ^1.8.5 @@ -281,7 +281,7 @@ importers: specifier: ^5.2.0 version: link:../../packages/integrations/svelte astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro svelte: specifier: ^4.2.5 @@ -293,7 +293,7 @@ importers: specifier: ^4.0.8 version: link:../../packages/integrations/vue astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro vue: specifier: ^3.3.8 @@ -302,25 +302,25 @@ importers: examples/hackernews: dependencies: '@astrojs/node': - specifier: ^8.2.1 + specifier: ^8.2.2 version: link:../../packages/integrations/node astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro examples/integration: devDependencies: astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro examples/middleware: dependencies: '@astrojs/node': - specifier: ^8.2.1 + specifier: ^8.2.2 version: link:../../packages/integrations/node astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro html-minifier: specifier: ^4.0.0 @@ -333,31 +333,31 @@ importers: examples/minimal: dependencies: astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro examples/non-html-pages: dependencies: astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro examples/portfolio: dependencies: astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro examples/ssr: dependencies: '@astrojs/node': - specifier: ^8.2.1 + specifier: ^8.2.2 version: link:../../packages/integrations/node '@astrojs/svelte': specifier: ^5.2.0 version: link:../../packages/integrations/svelte astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro svelte: specifier: ^4.2.5 @@ -366,7 +366,7 @@ importers: examples/starlog: dependencies: astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro sass: specifier: ^1.69.5 @@ -378,13 +378,13 @@ importers: examples/view-transitions: devDependencies: '@astrojs/node': - specifier: ^8.2.1 + specifier: ^8.2.2 version: link:../../packages/integrations/node '@astrojs/tailwind': specifier: ^5.1.0 version: link:../../packages/integrations/tailwind astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro examples/with-markdoc: @@ -393,7 +393,7 @@ importers: specifier: ^0.9.0 version: link:../../packages/integrations/markdoc astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro examples/with-markdown-plugins: @@ -402,7 +402,7 @@ importers: specifier: ^4.2.1 version: link:../../packages/markdown/remark astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro hast-util-select: specifier: ^6.0.2 @@ -423,7 +423,7 @@ importers: examples/with-markdown-shiki: dependencies: astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro examples/with-mdx: @@ -435,7 +435,7 @@ importers: specifier: ^3.1.1 version: link:../../packages/integrations/preact astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro preact: specifier: ^10.19.2 @@ -450,7 +450,7 @@ importers: specifier: ^0.5.0 version: 0.5.0(nanostores@0.9.5)(preact@10.19.3) astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro nanostores: specifier: ^0.9.5 @@ -471,7 +471,7 @@ importers: specifier: ^1.6.3 version: 1.6.4 astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro autoprefixer: specifier: ^10.4.15 @@ -489,7 +489,7 @@ importers: examples/with-vitest: dependencies: astro: - specifier: ^4.4.6 + specifier: ^4.4.7 version: link:../../packages/astro vitest: specifier: ^1.3.1 From d5277df5a4d1e9a8a7b6c8d7b87912e13a163f7f Mon Sep 17 00:00:00 2001 From: Erika <3019731+Princesseuh@users.noreply.github.com> Date: Fri, 1 Mar 2024 11:26:28 +0100 Subject: [PATCH 04/13] fix(node): Safely create requests (#10285) * fix(node): Wrap request creation in try catch * chore: changeset --- .changeset/perfect-poets-teach.md | 5 +++++ packages/integrations/node/src/serve-app.ts | 10 +++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 .changeset/perfect-poets-teach.md diff --git a/.changeset/perfect-poets-teach.md b/.changeset/perfect-poets-teach.md new file mode 100644 index 000000000000..2e91fb67852a --- /dev/null +++ b/.changeset/perfect-poets-teach.md @@ -0,0 +1,5 @@ +--- +"@astrojs/node": patch +--- + +Fixes an issue where malformed requests could cause the server to error in certain cases. diff --git a/packages/integrations/node/src/serve-app.ts b/packages/integrations/node/src/serve-app.ts index f2fc61f010d4..a9840b72148c 100644 --- a/packages/integrations/node/src/serve-app.ts +++ b/packages/integrations/node/src/serve-app.ts @@ -8,7 +8,15 @@ import type { RequestHandler } from './types.js'; */ export function createAppHandler(app: NodeApp): RequestHandler { return async (req, res, next, locals) => { - const request = NodeApp.createRequest(req); + let request; + try { + request = NodeApp.createRequest(req); + } catch (err) { + res.statusCode = 500; + res.end('Internal Server Error'); + return; + } + const routeData = app.match(request); if (routeData) { const response = await app.render(request, { From 5e3e74b61daa2ba44c761c9ab5745818661a656e Mon Sep 17 00:00:00 2001 From: Erika <3019731+Princesseuh@users.noreply.github.com> Date: Fri, 1 Mar 2024 11:41:43 +0100 Subject: [PATCH 05/13] fix(audits): Don't warn about loading on data URIs (#10275) --- .changeset/warm-buttons-agree.md | 5 +++++ .../astro/src/runtime/client/dev-toolbar/apps/audit/perf.ts | 6 ++++++ 2 files changed, 11 insertions(+) create mode 100644 .changeset/warm-buttons-agree.md diff --git a/.changeset/warm-buttons-agree.md b/.changeset/warm-buttons-agree.md new file mode 100644 index 000000000000..9186f02828a2 --- /dev/null +++ b/.changeset/warm-buttons-agree.md @@ -0,0 +1,5 @@ +--- +"astro": patch +--- + +Fixes dev toolbar warning about using the proper loading attributes on images using `data:` URIs diff --git a/packages/astro/src/runtime/client/dev-toolbar/apps/audit/perf.ts b/packages/astro/src/runtime/client/dev-toolbar/apps/audit/perf.ts index 197553a25fc7..4b67fcbf5575 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/apps/audit/perf.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/apps/audit/perf.ts @@ -38,6 +38,9 @@ export const perf: AuditRuleWithSelector[] = [ // Ignore elements that are above the fold, they should be loaded eagerly if (htmlElement.offsetTop < window.innerHeight) return false; + // Ignore elements using `data:` URI, the `loading` attribute doesn't do anything for these + if (htmlElement.src.startsWith('data:')) return false; + return true; }, }, @@ -53,6 +56,9 @@ export const perf: AuditRuleWithSelector[] = [ // Ignore elements that are below the fold, they should be loaded lazily if (htmlElement.offsetTop > window.innerHeight) return false; + // Ignore elements using `data:` URI, the `loading` attribute doesn't do anything for these + if (htmlElement.src.startsWith('data:')) return false; + return true; }, }, From 87a3d51f2ca8661babbb76956e54bf389eb86d8f Mon Sep 17 00:00:00 2001 From: "Houston (Bot)" <108291165+astrobot-houston@users.noreply.github.com> Date: Fri, 1 Mar 2024 04:10:51 -0800 Subject: [PATCH 06/13] [ci] release (#10286) Co-authored-by: github-actions[bot] --- .changeset/perfect-poets-teach.md | 5 -- .changeset/warm-buttons-agree.md | 5 -- examples/basics/package.json | 2 +- examples/blog/package.json | 2 +- examples/component/package.json | 2 +- examples/framework-alpine/package.json | 2 +- examples/framework-lit/package.json | 2 +- examples/framework-multiple/package.json | 2 +- examples/framework-preact/package.json | 2 +- examples/framework-react/package.json | 2 +- examples/framework-solid/package.json | 2 +- examples/framework-svelte/package.json | 2 +- examples/framework-vue/package.json | 2 +- examples/hackernews/package.json | 4 +- examples/integration/package.json | 2 +- examples/middleware/package.json | 4 +- examples/minimal/package.json | 2 +- examples/non-html-pages/package.json | 2 +- examples/portfolio/package.json | 2 +- examples/ssr/package.json | 4 +- examples/starlog/package.json | 2 +- examples/view-transitions/package.json | 4 +- examples/with-markdoc/package.json | 2 +- examples/with-markdown-plugins/package.json | 2 +- examples/with-markdown-shiki/package.json | 2 +- examples/with-mdx/package.json | 2 +- examples/with-nanostores/package.json | 2 +- examples/with-tailwindcss/package.json | 2 +- examples/with-vitest/package.json | 2 +- packages/astro/CHANGELOG.md | 6 ++ packages/astro/package.json | 2 +- packages/integrations/node/CHANGELOG.md | 6 ++ packages/integrations/node/package.json | 2 +- pnpm-lock.yaml | 62 ++++++++++----------- 34 files changed, 76 insertions(+), 74 deletions(-) delete mode 100644 .changeset/perfect-poets-teach.md delete mode 100644 .changeset/warm-buttons-agree.md diff --git a/.changeset/perfect-poets-teach.md b/.changeset/perfect-poets-teach.md deleted file mode 100644 index 2e91fb67852a..000000000000 --- a/.changeset/perfect-poets-teach.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@astrojs/node": patch ---- - -Fixes an issue where malformed requests could cause the server to error in certain cases. diff --git a/.changeset/warm-buttons-agree.md b/.changeset/warm-buttons-agree.md deleted file mode 100644 index 9186f02828a2..000000000000 --- a/.changeset/warm-buttons-agree.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"astro": patch ---- - -Fixes dev toolbar warning about using the proper loading attributes on images using `data:` URIs diff --git a/examples/basics/package.json b/examples/basics/package.json index b190f951dae2..7a0b6444b171 100644 --- a/examples/basics/package.json +++ b/examples/basics/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.4.7" + "astro": "^4.4.8" } } diff --git a/examples/blog/package.json b/examples/blog/package.json index 91cf97616a89..08ff9cbfe5e7 100644 --- a/examples/blog/package.json +++ b/examples/blog/package.json @@ -14,6 +14,6 @@ "@astrojs/mdx": "^2.1.1", "@astrojs/rss": "^4.0.5", "@astrojs/sitemap": "^3.1.1", - "astro": "^4.4.7" + "astro": "^4.4.8" } } diff --git a/examples/component/package.json b/examples/component/package.json index 4687a572c3a7..031ca9733acf 100644 --- a/examples/component/package.json +++ b/examples/component/package.json @@ -15,7 +15,7 @@ ], "scripts": {}, "devDependencies": { - "astro": "^4.4.7" + "astro": "^4.4.8" }, "peerDependencies": { "astro": "^4.0.0" diff --git a/examples/framework-alpine/package.json b/examples/framework-alpine/package.json index 10733f4fd0c7..e397fa085b24 100644 --- a/examples/framework-alpine/package.json +++ b/examples/framework-alpine/package.json @@ -14,6 +14,6 @@ "@astrojs/alpinejs": "^0.4.0", "@types/alpinejs": "^3.13.5", "alpinejs": "^3.13.3", - "astro": "^4.4.7" + "astro": "^4.4.8" } } diff --git a/examples/framework-lit/package.json b/examples/framework-lit/package.json index 216fe53cb2fe..594a709e24f0 100644 --- a/examples/framework-lit/package.json +++ b/examples/framework-lit/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/lit": "^4.0.1", "@webcomponents/template-shadowroot": "^0.2.1", - "astro": "^4.4.7", + "astro": "^4.4.8", "lit": "^3.1.2" } } diff --git a/examples/framework-multiple/package.json b/examples/framework-multiple/package.json index 25e278979660..12d57d8410cc 100644 --- a/examples/framework-multiple/package.json +++ b/examples/framework-multiple/package.json @@ -16,7 +16,7 @@ "@astrojs/solid-js": "^4.0.1", "@astrojs/svelte": "^5.2.0", "@astrojs/vue": "^4.0.8", - "astro": "^4.4.7", + "astro": "^4.4.8", "preact": "^10.19.2", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/framework-preact/package.json b/examples/framework-preact/package.json index fc9089470e3d..a7aaa6022935 100644 --- a/examples/framework-preact/package.json +++ b/examples/framework-preact/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/preact": "^3.1.1", "@preact/signals": "^1.2.1", - "astro": "^4.4.7", + "astro": "^4.4.8", "preact": "^10.19.2" } } diff --git a/examples/framework-react/package.json b/examples/framework-react/package.json index 04f61c34c8d4..f8b6117e4507 100644 --- a/examples/framework-react/package.json +++ b/examples/framework-react/package.json @@ -14,7 +14,7 @@ "@astrojs/react": "^3.0.10", "@types/react": "^18.2.37", "@types/react-dom": "^18.2.15", - "astro": "^4.4.7", + "astro": "^4.4.8", "react": "^18.2.0", "react-dom": "^18.2.0" } diff --git a/examples/framework-solid/package.json b/examples/framework-solid/package.json index 0f98a817722a..e337a679342f 100644 --- a/examples/framework-solid/package.json +++ b/examples/framework-solid/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/solid-js": "^4.0.1", - "astro": "^4.4.7", + "astro": "^4.4.8", "solid-js": "^1.8.5" } } diff --git a/examples/framework-svelte/package.json b/examples/framework-svelte/package.json index 297dd9b4dd26..986e936e9025 100644 --- a/examples/framework-svelte/package.json +++ b/examples/framework-svelte/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/svelte": "^5.2.0", - "astro": "^4.4.7", + "astro": "^4.4.8", "svelte": "^4.2.5" } } diff --git a/examples/framework-vue/package.json b/examples/framework-vue/package.json index 3e577afe22dc..51dfa0ce769d 100644 --- a/examples/framework-vue/package.json +++ b/examples/framework-vue/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/vue": "^4.0.8", - "astro": "^4.4.7", + "astro": "^4.4.8", "vue": "^3.3.8" } } diff --git a/examples/hackernews/package.json b/examples/hackernews/package.json index b883f6e413a6..4f6b7213fb6f 100644 --- a/examples/hackernews/package.json +++ b/examples/hackernews/package.json @@ -11,7 +11,7 @@ "astro": "astro" }, "dependencies": { - "@astrojs/node": "^8.2.2", - "astro": "^4.4.7" + "@astrojs/node": "^8.2.3", + "astro": "^4.4.8" } } diff --git a/examples/integration/package.json b/examples/integration/package.json index f770a92bbace..aad853c1ef81 100644 --- a/examples/integration/package.json +++ b/examples/integration/package.json @@ -15,7 +15,7 @@ ], "scripts": {}, "devDependencies": { - "astro": "^4.4.7" + "astro": "^4.4.8" }, "peerDependencies": { "astro": "^4.0.0" diff --git a/examples/middleware/package.json b/examples/middleware/package.json index 112b26151f2e..849cdf143add 100644 --- a/examples/middleware/package.json +++ b/examples/middleware/package.json @@ -12,8 +12,8 @@ "server": "node dist/server/entry.mjs" }, "dependencies": { - "@astrojs/node": "^8.2.2", - "astro": "^4.4.7", + "@astrojs/node": "^8.2.3", + "astro": "^4.4.8", "html-minifier": "^4.0.0" }, "devDependencies": { diff --git a/examples/minimal/package.json b/examples/minimal/package.json index a690ec757c8c..b7fa4d9c4817 100644 --- a/examples/minimal/package.json +++ b/examples/minimal/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.4.7" + "astro": "^4.4.8" } } diff --git a/examples/non-html-pages/package.json b/examples/non-html-pages/package.json index 1c5a175c2667..86ca1e0ab70e 100644 --- a/examples/non-html-pages/package.json +++ b/examples/non-html-pages/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.4.7" + "astro": "^4.4.8" } } diff --git a/examples/portfolio/package.json b/examples/portfolio/package.json index 015be58aa96e..b82aa53b52cd 100644 --- a/examples/portfolio/package.json +++ b/examples/portfolio/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.4.7" + "astro": "^4.4.8" } } diff --git a/examples/ssr/package.json b/examples/ssr/package.json index 37861914cc20..6e530d7115f5 100644 --- a/examples/ssr/package.json +++ b/examples/ssr/package.json @@ -12,9 +12,9 @@ "server": "node dist/server/entry.mjs" }, "dependencies": { - "@astrojs/node": "^8.2.2", + "@astrojs/node": "^8.2.3", "@astrojs/svelte": "^5.2.0", - "astro": "^4.4.7", + "astro": "^4.4.8", "svelte": "^4.2.5" } } diff --git a/examples/starlog/package.json b/examples/starlog/package.json index ef9962f6ee49..883712151f33 100644 --- a/examples/starlog/package.json +++ b/examples/starlog/package.json @@ -10,7 +10,7 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.4.7", + "astro": "^4.4.8", "sass": "^1.69.5", "sharp": "^0.32.6" } diff --git a/examples/view-transitions/package.json b/examples/view-transitions/package.json index 6ee02205dfab..d74f5ec9eaf7 100644 --- a/examples/view-transitions/package.json +++ b/examples/view-transitions/package.json @@ -11,7 +11,7 @@ }, "devDependencies": { "@astrojs/tailwind": "^5.1.0", - "@astrojs/node": "^8.2.2", - "astro": "^4.4.7" + "@astrojs/node": "^8.2.3", + "astro": "^4.4.8" } } diff --git a/examples/with-markdoc/package.json b/examples/with-markdoc/package.json index a08d735de871..6e1c487ccbd5 100644 --- a/examples/with-markdoc/package.json +++ b/examples/with-markdoc/package.json @@ -12,6 +12,6 @@ }, "dependencies": { "@astrojs/markdoc": "^0.9.0", - "astro": "^4.4.7" + "astro": "^4.4.8" } } diff --git a/examples/with-markdown-plugins/package.json b/examples/with-markdown-plugins/package.json index 07e94e6e70c3..8074dfefac76 100644 --- a/examples/with-markdown-plugins/package.json +++ b/examples/with-markdown-plugins/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/markdown-remark": "^4.2.1", - "astro": "^4.4.7", + "astro": "^4.4.8", "hast-util-select": "^6.0.2", "rehype-autolink-headings": "^7.1.0", "rehype-slug": "^6.0.0", diff --git a/examples/with-markdown-shiki/package.json b/examples/with-markdown-shiki/package.json index decc8acf15e9..f1519cc4d834 100644 --- a/examples/with-markdown-shiki/package.json +++ b/examples/with-markdown-shiki/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.4.7" + "astro": "^4.4.8" } } diff --git a/examples/with-mdx/package.json b/examples/with-mdx/package.json index 189c2cf43a90..cd3c57ee062a 100644 --- a/examples/with-mdx/package.json +++ b/examples/with-mdx/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/mdx": "^2.1.1", "@astrojs/preact": "^3.1.1", - "astro": "^4.4.7", + "astro": "^4.4.8", "preact": "^10.19.2" } } diff --git a/examples/with-nanostores/package.json b/examples/with-nanostores/package.json index 84c90742adab..e82990cb4afc 100644 --- a/examples/with-nanostores/package.json +++ b/examples/with-nanostores/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/preact": "^3.1.1", "@nanostores/preact": "^0.5.0", - "astro": "^4.4.7", + "astro": "^4.4.8", "nanostores": "^0.9.5", "preact": "^10.19.2" } diff --git a/examples/with-tailwindcss/package.json b/examples/with-tailwindcss/package.json index 7439fb66bf03..e9edc6b0ff89 100644 --- a/examples/with-tailwindcss/package.json +++ b/examples/with-tailwindcss/package.json @@ -14,7 +14,7 @@ "@astrojs/mdx": "^2.1.1", "@astrojs/tailwind": "^5.1.0", "@types/canvas-confetti": "^1.6.3", - "astro": "^4.4.7", + "astro": "^4.4.8", "autoprefixer": "^10.4.15", "canvas-confetti": "^1.9.1", "postcss": "^8.4.28", diff --git a/examples/with-vitest/package.json b/examples/with-vitest/package.json index 875e1a16b7e2..7e9e64d9b20c 100644 --- a/examples/with-vitest/package.json +++ b/examples/with-vitest/package.json @@ -12,7 +12,7 @@ "test": "vitest" }, "dependencies": { - "astro": "^4.4.7", + "astro": "^4.4.8", "vitest": "^1.3.1" } } diff --git a/packages/astro/CHANGELOG.md b/packages/astro/CHANGELOG.md index 05e54a5a4b5c..fcc699317d00 100644 --- a/packages/astro/CHANGELOG.md +++ b/packages/astro/CHANGELOG.md @@ -1,5 +1,11 @@ # astro +## 4.4.8 + +### Patch Changes + +- [#10275](https://github.com/withastro/astro/pull/10275) [`5e3e74b61daa2ba44c761c9ab5745818661a656e`](https://github.com/withastro/astro/commit/5e3e74b61daa2ba44c761c9ab5745818661a656e) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fixes dev toolbar warning about using the proper loading attributes on images using `data:` URIs + ## 4.4.7 ### Patch Changes diff --git a/packages/astro/package.json b/packages/astro/package.json index 15e4acb8f2c1..731033eeb384 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,6 +1,6 @@ { "name": "astro", - "version": "4.4.7", + "version": "4.4.8", "description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.", "type": "module", "author": "withastro", diff --git a/packages/integrations/node/CHANGELOG.md b/packages/integrations/node/CHANGELOG.md index 9f5e216ebd11..5d4f27635f64 100644 --- a/packages/integrations/node/CHANGELOG.md +++ b/packages/integrations/node/CHANGELOG.md @@ -1,5 +1,11 @@ # @astrojs/node +## 8.2.3 + +### Patch Changes + +- [#10285](https://github.com/withastro/astro/pull/10285) [`d5277df5a4d1e9a8a7b6c8d7b87912e13a163f7f`](https://github.com/withastro/astro/commit/d5277df5a4d1e9a8a7b6c8d7b87912e13a163f7f) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fixes an issue where malformed requests could cause the server to error in certain cases. + ## 8.2.2 ### Patch Changes diff --git a/packages/integrations/node/package.json b/packages/integrations/node/package.json index 95b5601f8ecd..0f9d55cc7207 100644 --- a/packages/integrations/node/package.json +++ b/packages/integrations/node/package.json @@ -1,7 +1,7 @@ { "name": "@astrojs/node", "description": "Deploy your site to a Node.js server", - "version": "8.2.2", + "version": "8.2.3", "type": "module", "types": "./dist/index.d.ts", "author": "withastro", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 86188e8d72ac..9e35467d7405 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -134,7 +134,7 @@ importers: examples/basics: dependencies: astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro examples/blog: @@ -149,13 +149,13 @@ importers: specifier: ^3.1.1 version: link:../../packages/integrations/sitemap astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro examples/component: devDependencies: astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro examples/framework-alpine: @@ -170,7 +170,7 @@ importers: specifier: ^3.13.3 version: 3.13.3 astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro examples/framework-lit: @@ -182,7 +182,7 @@ importers: specifier: ^0.2.1 version: 0.2.1 astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro lit: specifier: ^3.1.2 @@ -206,7 +206,7 @@ importers: specifier: ^4.0.8 version: link:../../packages/integrations/vue astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro preact: specifier: ^10.19.2 @@ -236,7 +236,7 @@ importers: specifier: ^1.2.1 version: 1.2.1(preact@10.19.3) astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro preact: specifier: ^10.19.2 @@ -254,7 +254,7 @@ importers: specifier: ^18.2.15 version: 18.2.18 astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro react: specifier: ^18.2.0 @@ -269,7 +269,7 @@ importers: specifier: ^4.0.1 version: link:../../packages/integrations/solid astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro solid-js: specifier: ^1.8.5 @@ -281,7 +281,7 @@ importers: specifier: ^5.2.0 version: link:../../packages/integrations/svelte astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro svelte: specifier: ^4.2.5 @@ -293,7 +293,7 @@ importers: specifier: ^4.0.8 version: link:../../packages/integrations/vue astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro vue: specifier: ^3.3.8 @@ -302,25 +302,25 @@ importers: examples/hackernews: dependencies: '@astrojs/node': - specifier: ^8.2.2 + specifier: ^8.2.3 version: link:../../packages/integrations/node astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro examples/integration: devDependencies: astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro examples/middleware: dependencies: '@astrojs/node': - specifier: ^8.2.2 + specifier: ^8.2.3 version: link:../../packages/integrations/node astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro html-minifier: specifier: ^4.0.0 @@ -333,31 +333,31 @@ importers: examples/minimal: dependencies: astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro examples/non-html-pages: dependencies: astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro examples/portfolio: dependencies: astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro examples/ssr: dependencies: '@astrojs/node': - specifier: ^8.2.2 + specifier: ^8.2.3 version: link:../../packages/integrations/node '@astrojs/svelte': specifier: ^5.2.0 version: link:../../packages/integrations/svelte astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro svelte: specifier: ^4.2.5 @@ -366,7 +366,7 @@ importers: examples/starlog: dependencies: astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro sass: specifier: ^1.69.5 @@ -378,13 +378,13 @@ importers: examples/view-transitions: devDependencies: '@astrojs/node': - specifier: ^8.2.2 + specifier: ^8.2.3 version: link:../../packages/integrations/node '@astrojs/tailwind': specifier: ^5.1.0 version: link:../../packages/integrations/tailwind astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro examples/with-markdoc: @@ -393,7 +393,7 @@ importers: specifier: ^0.9.0 version: link:../../packages/integrations/markdoc astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro examples/with-markdown-plugins: @@ -402,7 +402,7 @@ importers: specifier: ^4.2.1 version: link:../../packages/markdown/remark astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro hast-util-select: specifier: ^6.0.2 @@ -423,7 +423,7 @@ importers: examples/with-markdown-shiki: dependencies: astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro examples/with-mdx: @@ -435,7 +435,7 @@ importers: specifier: ^3.1.1 version: link:../../packages/integrations/preact astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro preact: specifier: ^10.19.2 @@ -450,7 +450,7 @@ importers: specifier: ^0.5.0 version: 0.5.0(nanostores@0.9.5)(preact@10.19.3) astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro nanostores: specifier: ^0.9.5 @@ -471,7 +471,7 @@ importers: specifier: ^1.6.3 version: 1.6.4 astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro autoprefixer: specifier: ^10.4.15 @@ -489,7 +489,7 @@ importers: examples/with-vitest: dependencies: astro: - specifier: ^4.4.7 + specifier: ^4.4.8 version: link:../../packages/astro vitest: specifier: ^1.3.1 From a548a3a99c2835c19662fc38636f92b2bda26614 Mon Sep 17 00:00:00 2001 From: Erika <3019731+Princesseuh@users.noreply.github.com> Date: Fri, 1 Mar 2024 15:48:38 +0100 Subject: [PATCH 07/13] fix(markdoc & mdx): Proxy crimes (#10278) * fix(markdoc & mdx): Proxy cimes * chore: changeset --- .changeset/giant-spies-type.md | 6 +++++ packages/astro/package.json | 1 + packages/astro/src/assets/build/generate.ts | 8 ++++++- packages/astro/src/assets/utils/proxy.ts | 6 ++++- packages/astro/src/core/logger/core.ts | 1 + .../core-image-deletion/astro.config.mjs | 7 ++++++ .../fixtures/core-image-deletion/package.json | 4 +++- .../src/assets/markdocStillExists.jpg | Bin 0 -> 37287 bytes .../src/assets/mdxDontExist.jpg | Bin 0 -> 36105 bytes .../src/content/blog/markdoc.mdoc | 7 ++++++ .../src/content/blog/mdx.mdx | 7 ++++++ .../core-image-deletion/src/content/config.ts | 12 ++++++++++ .../src/pages/blog/[slug].astro | 22 ++++++++++++++++++ packages/astro/test/image-deletion.test.js | 12 +++++++++- .../markdoc/src/content-entry-type.ts | 14 ++++++++++- pnpm-lock.yaml | 6 +++++ 16 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 .changeset/giant-spies-type.md create mode 100644 packages/astro/test/fixtures/core-image-deletion/astro.config.mjs create mode 100644 packages/astro/test/fixtures/core-image-deletion/src/assets/markdocStillExists.jpg create mode 100644 packages/astro/test/fixtures/core-image-deletion/src/assets/mdxDontExist.jpg create mode 100644 packages/astro/test/fixtures/core-image-deletion/src/content/blog/markdoc.mdoc create mode 100644 packages/astro/test/fixtures/core-image-deletion/src/content/blog/mdx.mdx create mode 100644 packages/astro/test/fixtures/core-image-deletion/src/content/config.ts create mode 100644 packages/astro/test/fixtures/core-image-deletion/src/pages/blog/[slug].astro diff --git a/.changeset/giant-spies-type.md b/.changeset/giant-spies-type.md new file mode 100644 index 000000000000..97018591a63f --- /dev/null +++ b/.changeset/giant-spies-type.md @@ -0,0 +1,6 @@ +--- +"@astrojs/markdoc": patch +"astro": patch +--- + +Fixes original images sometimes being kept / deleted when they shouldn't in both MDX and Markdoc diff --git a/packages/astro/package.json b/packages/astro/package.json index 731033eeb384..36a0483cf54e 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -108,6 +108,7 @@ "dev": "astro-scripts dev --copy-wasm --prebuild \"src/runtime/server/astro-island.ts\" --prebuild \"src/runtime/client/{idle,load,media,only,visible}.ts\" \"src/**/*.{ts,js}\"", "postbuild": "astro-scripts copy \"src/**/*.astro\" && astro-scripts copy \"src/**/*.wasm\"", "test": "pnpm run test:node", + "test:match": "pnpm run test:node --match", "test:e2e": "playwright test", "test:e2e:match": "playwright test -g", "test:node": "astro-scripts test \"test/**/*.test.js\"" diff --git a/packages/astro/src/assets/build/generate.ts b/packages/astro/src/assets/build/generate.ts index 1c73c1592cd8..496dc1e6e4a7 100644 --- a/packages/astro/src/assets/build/generate.ts +++ b/packages/astro/src/assets/build/generate.ts @@ -119,7 +119,13 @@ export async function generateImagesForPath( !globalThis.astroAsset.referencedImages?.has(transformsAndPath.originalSrcPath) ) { try { - await fs.promises.unlink(getFullImagePath(originalFilePath, env)); + if (transformsAndPath.originalSrcPath) { + env.logger.debug( + 'assets', + `Deleting ${originalFilePath} as it's not referenced outside of image processing.` + ); + await fs.promises.unlink(getFullImagePath(originalFilePath, env)); + } } catch (e) { /* No-op, it's okay if we fail to delete one of the file, we're not too picky. */ } diff --git a/packages/astro/src/assets/utils/proxy.ts b/packages/astro/src/assets/utils/proxy.ts index 2b4389e4d142..e5c7ce7a094d 100644 --- a/packages/astro/src/assets/utils/proxy.ts +++ b/packages/astro/src/assets/utils/proxy.ts @@ -11,7 +11,11 @@ export function getProxyCode(options: ImageMetadata, isSSR: boolean): string { if (name === 'fsPath') { return ${stringifiedFSPath}; } - ${!isSSR ? `globalThis.astroAsset.referencedImages.add(${stringifiedFSPath});` : ''} + ${ + !isSSR + ? `if (target[name] !== undefined) globalThis.astroAsset.referencedImages.add(${stringifiedFSPath});` + : '' + } return target[name]; } }) diff --git a/packages/astro/src/core/logger/core.ts b/packages/astro/src/core/logger/core.ts index 3f9c9f4177b5..45ab41ec92ce 100644 --- a/packages/astro/src/core/logger/core.ts +++ b/packages/astro/src/core/logger/core.ts @@ -28,6 +28,7 @@ export type LoggerLabel = | 'preferences' | 'redirects' | 'toolbar' + | 'assets' // SKIP_FORMAT: A special label that tells the logger not to apply any formatting. // Useful for messages that are already formatted, like the server start message. | 'SKIP_FORMAT'; diff --git a/packages/astro/test/fixtures/core-image-deletion/astro.config.mjs b/packages/astro/test/fixtures/core-image-deletion/astro.config.mjs new file mode 100644 index 000000000000..e39cf9fc68fc --- /dev/null +++ b/packages/astro/test/fixtures/core-image-deletion/astro.config.mjs @@ -0,0 +1,7 @@ +import mdx from '@astrojs/mdx'; +import markdoc from '@astrojs/markdoc'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + integrations: [mdx(), markdoc()], +}); diff --git a/packages/astro/test/fixtures/core-image-deletion/package.json b/packages/astro/test/fixtures/core-image-deletion/package.json index 40a2ee1a64b1..e55fc847dce8 100644 --- a/packages/astro/test/fixtures/core-image-deletion/package.json +++ b/packages/astro/test/fixtures/core-image-deletion/package.json @@ -3,7 +3,9 @@ "version": "0.0.0", "private": true, "dependencies": { - "astro": "workspace:*" + "astro": "workspace:*", + "@astrojs/mdx": "workspace:*", + "@astrojs/markdoc": "workspace:*" }, "scripts": { "dev": "astro dev" diff --git a/packages/astro/test/fixtures/core-image-deletion/src/assets/markdocStillExists.jpg b/packages/astro/test/fixtures/core-image-deletion/src/assets/markdocStillExists.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8e38327233fc6d1f813e3a10aa2cd6c576995614 GIT binary patch literal 37287 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 Q>s$(`;C#&aJ^yC^2W2S`-2eap literal 0 HcmV?d00001 diff --git a/packages/astro/test/fixtures/core-image-deletion/src/assets/mdxDontExist.jpg b/packages/astro/test/fixtures/core-image-deletion/src/assets/mdxDontExist.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8c5ba062c2f0fdd7ffeedf1a123df7acb7211fa7 GIT binary patch literal 36105 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 { + const blog = await getCollection("blog"); + return blog.map((post) => ({ + params: { + slug: post.slug, + }, + props: { + post + } + })); +}) satisfies GetStaticPaths; + +const { post } = Astro.props; + +const { Content } = await post.render(); +--- + + diff --git a/packages/astro/test/image-deletion.test.js b/packages/astro/test/image-deletion.test.js index 4283e8c06f3b..cb4b464bf6aa 100644 --- a/packages/astro/test/image-deletion.test.js +++ b/packages/astro/test/image-deletion.test.js @@ -3,7 +3,7 @@ import { before, describe, it } from 'node:test'; import { testImageService } from './test-image-service.js'; import { loadFixture } from './test-utils.js'; -describe('astro:assets - delete images that are unused', () => { +describe('astro:assets - delete images that are unused zzz', () => { /** @type {import('./test-utils.js').Fixture} */ let fixture; @@ -33,5 +33,15 @@ describe('astro:assets - delete images that are unused', () => { const imagesUsedElsewhere = await fixture.glob('_astro/url.*.*'); assert.equal(imagesUsedElsewhere.length, 2); }); + + it('should delete MDX images only used for optimization', async () => { + const imagesOnlyOptimized = await fixture.glob('_astro/mdxDontExist.*.*'); + assert.equal(imagesOnlyOptimized.length, 1); + }); + + it('should always keep Markdoc images', async () => { + const imagesUsedElsewhere = await fixture.glob('_astro/markdocStillExists.*.*'); + assert.equal(imagesUsedElsewhere.length, 2); + }); }); }); diff --git a/packages/integrations/markdoc/src/content-entry-type.ts b/packages/integrations/markdoc/src/content-entry-type.ts index fe15d03b01ea..89f9f9e8687b 100644 --- a/packages/integrations/markdoc/src/content-entry-type.ts +++ b/packages/integrations/markdoc/src/content-entry-type.ts @@ -196,7 +196,19 @@ async function emitOptimizedImages( ctx.pluginContext.meta.watchMode, ctx.pluginContext.emitFile ); - node.attributes[attributeName] = src; + + const fsPath = resolved.id; + + if (src) { + // We cannot track images in Markdoc, Markdoc rendering always strips out the proxy. As such, we'll always + // assume that the image is referenced elsewhere, to be on safer side. + if (ctx.astroConfig.output === 'static') { + if (globalThis.astroAsset.referencedImages) + globalThis.astroAsset.referencedImages.add(fsPath); + } + + node.attributes[attributeName] = { ...src, fsPath }; + } } else { throw new MarkdocError({ message: `Could not resolve image ${JSON.stringify( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9e35467d7405..62588e2fc44c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2516,6 +2516,12 @@ importers: packages/astro/test/fixtures/core-image-deletion: dependencies: + '@astrojs/markdoc': + specifier: workspace:* + version: link:../../../../integrations/markdoc + '@astrojs/mdx': + specifier: workspace:* + version: link:../../../../integrations/mdx astro: specifier: workspace:* version: link:../../.. From 4b6e2fb69b241c982c82a2f13d603b2057c345af Mon Sep 17 00:00:00 2001 From: Erika Date: Fri, 1 Mar 2024 15:29:39 +0000 Subject: [PATCH 08/13] [ci] format --- .../astro/test/fixtures/core-image-deletion/astro.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/test/fixtures/core-image-deletion/astro.config.mjs b/packages/astro/test/fixtures/core-image-deletion/astro.config.mjs index e39cf9fc68fc..8ba730fc91bc 100644 --- a/packages/astro/test/fixtures/core-image-deletion/astro.config.mjs +++ b/packages/astro/test/fixtures/core-image-deletion/astro.config.mjs @@ -1,5 +1,5 @@ -import mdx from '@astrojs/mdx'; import markdoc from '@astrojs/markdoc'; +import mdx from '@astrojs/mdx'; import { defineConfig } from 'astro/config'; export default defineConfig({ From 3488be9b59d1cb65325b0e087c33bcd74aaa4926 Mon Sep 17 00:00:00 2001 From: Ben Holmes Date: Fri, 1 Mar 2024 19:29:55 -0500 Subject: [PATCH 09/13] finalize WIP API (#10280) * feat: no more readable / writable * fix: table typegen * wip: move data seeding * chore: add scripts to basics * feat: data() -> seed file * refactor: ensure precedence of file name * feat: db execute command * fix: test imports * chore: remove old readable error tests * feat: support local db with `db execute` * refactor: remove integrations from test for now * chore: stray comment * chore: remove `table` config object * feat: `db.batch`! * refactor: move migrations/ inside db/ * fix: move ticketing-example to seed file * fix: disable foreign keys when recreating tables * refactor: standardize migrations dir * feat: move to db/config.ts * feat: file watching for db/config.ts dependencies * feat: remove unsafeDisableStudio * chroe: remove bad import * feat: parse config.ts from cli * chore: remove async from localDatabaseClient * fix: update recipes config and seed * chore: update unit tests * chore: update tests to dev server * refactor: collectionToTable -> asDrizzleTable * chore: tidy up collection -> table error states * refactor: regexp -> endsWith * feat: pretty error inserting into table * refactor: try/catch -> catch() * feat: expose utils for integration seed files * fix: add config import to db client modules * fix: just use generic "seeding database" error * chore: remove unused link args * fix: migration queries im,port * chore: remove irrelevant glob/ example * feat: format migration file path * feat: support all config file names * chore: remove db.batch() for now * chore: remove `db` object * core: remove unused integration file * chore: changeset * fix: foreign key empty error message * chore: remove old TODO * fix: bad context reference * refactor: seedDev -> seedLocal * wip: throw some console logs at github * wip: avoid seeding astro:db imported by seed file * wip: use anything in db/ * refactor: only seed when loaded within srcDir * refactor: avoid resolution when not seeding * chore: remove logs * refactor: seed within create local db client * refactor: use normalizePath * wip: logs * wip: logs * refactor: early return * chore: more logs * refactor: no batch * fix: use beforeAll * refactor: move all tests to base block * wip: log dev server starting * chore: remove logs * wip: demo ready * chore: remove duplicate recreateTables() call * Revert "wip: demo ready" This reverts commit 37585ce5cb4cce8dcc750d8752e0eb02418b5c87. * refactor: beforeEach to isolate dev servers * chore: remove useBundledDbUrl * refactor: naming and seed scope * chore: remove stray console logs * wip: fix windows file import * wip: try fileURLToPath * Revert "wip: try fileURLToPath" This reverts commit 46fd65d61a8a285c2d507d524734369a3b97a1a0. * Revert "wip: fix windows file import" This reverts commit 1a669ea646e2dc91ca120539431c10f0793a20f3. * refactor: dir -> directory * refactor: move execute file to cli * refactor: remove seed.dev convention * wip: attempt fileURLToPath * wip: debug the file exists * fix: use mjs?? * chore: remove duplicate seedLocal * chore: remove log check * refactor: use in memory db for tests * chore: clean up test comment * fix: avoid file writes for db setup on in memory db * chore: bump db changeset to minor --------- Co-authored-by: Nate Moore --- .changeset/rich-turtles-live.md | 6 + packages/astro/src/core/config/schema.ts | 1 - packages/db/config-augment.d.ts | 4 - packages/db/index.d.ts | 8 +- packages/db/package.json | 14 + .../db/src/core/cli/commands/execute/index.ts | 40 +++ .../db/src/core/cli/commands/gen/index.ts | 29 +- .../db/src/core/cli/commands/link/index.ts | 4 +- .../db/src/core/cli/commands/login/index.ts | 9 +- .../db/src/core/cli/commands/logout/index.ts | 4 +- .../db/src/core/cli/commands/push/index.ts | 121 ++------ .../db/src/core/cli/commands/shell/index.ts | 9 +- .../db/src/core/cli/commands/verify/index.ts | 13 +- packages/db/src/core/cli/index.ts | 52 ++-- packages/db/src/core/cli/migration-queries.ts | 2 +- packages/db/src/core/cli/migrations.ts | 57 ++-- packages/db/src/core/consts.ts | 3 + packages/db/src/core/errors.ts | 57 ++-- packages/db/src/core/integration/index.ts | 112 +++---- packages/db/src/core/integration/typegen.ts | 14 +- .../db/src/core/integration/vite-plugin-db.ts | 110 +++++-- packages/db/src/core/load-file.ts | 117 +++++++ packages/db/src/core/queries.ts | 292 ------------------ packages/db/src/core/types.ts | 160 ++-------- packages/db/src/core/utils.ts | 8 + packages/db/src/index.ts | 5 +- packages/db/src/runtime/config.ts | 48 +++ packages/db/src/runtime/db-client.ts | 56 +--- packages/db/src/runtime/index.ts | 17 +- packages/db/src/runtime/queries.ts | 244 +++++++++++++++ packages/db/src/runtime/types.ts | 4 +- packages/db/src/utils.ts | 1 + packages/db/test/basics.test.js | 98 ++---- .../db/test/fixtures/basics/astro.config.mjs | 7 + .../db/test/fixtures/basics/astro.config.ts | 28 -- packages/db/test/fixtures/basics/db/config.ts | 12 + packages/db/test/fixtures/basics/db/seed.ts | 19 ++ packages/db/test/fixtures/basics/db/theme.ts | 15 + packages/db/test/fixtures/basics/package.json | 5 + .../fixtures/basics/src/pages/index.astro | 1 + .../src/pages/insert-into-readonly.astro | 14 - .../src/pages/insert-into-writable.astro | 12 - .../fixtures/basics/themes-integration.ts | 36 --- .../db/test/fixtures/glob/astro.config.ts | 25 -- packages/db/test/fixtures/glob/package.json | 21 -- .../db/test/fixtures/glob/quotes/erika.json | 4 - .../db/test/fixtures/glob/quotes/tony.json | 4 - .../test/fixtures/glob/src/pages/index.astro | 25 -- packages/db/test/fixtures/glob/utils.ts | 60 ---- .../db/test/fixtures/recipes/astro.config.ts | 78 +---- .../db/test/fixtures/recipes/db/config.ts | 26 ++ packages/db/test/fixtures/recipes/db/seed.ts | 60 ++++ .../ticketing-example/astro.config.ts | 44 +-- .../fixtures/ticketing-example/db/config.ts | 27 ++ .../fixtures/ticketing-example/db/seed.ts | 10 + ...queries.test.js => column-queries.test.js} | 61 ++-- packages/db/test/unit/index-queries.test.js | 5 +- .../db/test/unit/reference-queries.test.js | 17 +- 58 files changed, 1096 insertions(+), 1239 deletions(-) create mode 100644 .changeset/rich-turtles-live.md delete mode 100644 packages/db/config-augment.d.ts create mode 100644 packages/db/src/core/cli/commands/execute/index.ts create mode 100644 packages/db/src/core/load-file.ts delete mode 100644 packages/db/src/core/queries.ts create mode 100644 packages/db/src/runtime/config.ts create mode 100644 packages/db/src/runtime/queries.ts create mode 100644 packages/db/src/utils.ts create mode 100644 packages/db/test/fixtures/basics/astro.config.mjs delete mode 100644 packages/db/test/fixtures/basics/astro.config.ts create mode 100644 packages/db/test/fixtures/basics/db/config.ts create mode 100644 packages/db/test/fixtures/basics/db/seed.ts create mode 100644 packages/db/test/fixtures/basics/db/theme.ts delete mode 100644 packages/db/test/fixtures/basics/src/pages/insert-into-readonly.astro delete mode 100644 packages/db/test/fixtures/basics/src/pages/insert-into-writable.astro delete mode 100644 packages/db/test/fixtures/basics/themes-integration.ts delete mode 100644 packages/db/test/fixtures/glob/astro.config.ts delete mode 100644 packages/db/test/fixtures/glob/package.json delete mode 100644 packages/db/test/fixtures/glob/quotes/erika.json delete mode 100644 packages/db/test/fixtures/glob/quotes/tony.json delete mode 100644 packages/db/test/fixtures/glob/src/pages/index.astro delete mode 100644 packages/db/test/fixtures/glob/utils.ts create mode 100644 packages/db/test/fixtures/recipes/db/config.ts create mode 100644 packages/db/test/fixtures/recipes/db/seed.ts create mode 100644 packages/db/test/fixtures/ticketing-example/db/config.ts create mode 100644 packages/db/test/fixtures/ticketing-example/db/seed.ts rename packages/db/test/unit/{field-queries.test.js => column-queries.test.js} (89%) diff --git a/.changeset/rich-turtles-live.md b/.changeset/rich-turtles-live.md new file mode 100644 index 000000000000..95802f8842f4 --- /dev/null +++ b/.changeset/rich-turtles-live.md @@ -0,0 +1,6 @@ +--- +"astro": patch +"@astrojs/db": minor +--- + +Finalize db API to a shared db/ directory. diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 7fd6e7d0e947..fcfd91639b1b 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -108,7 +108,6 @@ export const AstroConfigSchema = z.object({ .optional() .default('attribute'), adapter: z.object({ name: z.string(), hooks: z.object({}).passthrough().default({}) }).optional(), - db: z.object({}).passthrough().default({}).optional(), integrations: z.preprocess( // preprocess (val) => (Array.isArray(val) ? val.flat(Infinity).filter(Boolean) : val), diff --git a/packages/db/config-augment.d.ts b/packages/db/config-augment.d.ts deleted file mode 100644 index 3278e6c2abd1..000000000000 --- a/packages/db/config-augment.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare namespace Config { - type DBUserConfig = import('./dist/core/types.js').DBUserConfig; - export interface Database extends DBUserConfig {} -} diff --git a/packages/db/index.d.ts b/packages/db/index.d.ts index 3a2134b9fc22..81af4fb43cfc 100644 --- a/packages/db/index.d.ts +++ b/packages/db/index.d.ts @@ -1,3 +1,5 @@ -/// -export * from './dist/index.js'; -export { default } from './dist/index.js'; +export { default, cli } from './dist/index.js'; + +declare module 'astro:db' { + export { defineTable, defineDB, column, sql, NOW, TRUE, FALSE } from './dist/index.js'; +} diff --git a/packages/db/package.json b/packages/db/package.json index b2055f20d452..270376784f90 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -12,6 +12,10 @@ "types": "./index.d.ts", "import": "./dist/index.js" }, + "./utils": { + "types": "./dist/utils.d.ts", + "import": "./dist/utils.js" + }, "./runtime": { "types": "./dist/runtime/index.d.ts", "import": "./dist/runtime/index.js" @@ -20,6 +24,10 @@ "types": "./dist/runtime/drizzle.d.ts", "import": "./dist/runtime/drizzle.js" }, + "./runtime/config": { + "types": "./dist/runtime/config.d.ts", + "import": "./dist/runtime/config.js" + }, "./package.json": "./package.json" }, "typesVersions": { @@ -27,11 +35,17 @@ ".": [ "./index.d.ts" ], + "utils": [ + "./dist/utils.d.ts" + ], "runtime": [ "./dist/runtime/index.d.ts" ], "runtime/drizzle": [ "./dist/runtime/drizzle.d.ts" + ], + "runtime/config": [ + "./dist/runtime/config.d.ts" ] } }, diff --git a/packages/db/src/core/cli/commands/execute/index.ts b/packages/db/src/core/cli/commands/execute/index.ts new file mode 100644 index 000000000000..a81696702f62 --- /dev/null +++ b/packages/db/src/core/cli/commands/execute/index.ts @@ -0,0 +1,40 @@ +import type { AstroConfig } from 'astro'; +import type { Arguments } from 'yargs-parser'; +import { MISSING_EXECUTE_PATH_ERROR, FILE_NOT_FOUND_ERROR } from '../../../errors.js'; +import { existsSync } from 'node:fs'; +import { getManagedAppTokenOrExit } from '../../../tokens.js'; +import { type DBConfig } from '../../../types.js'; +import { bundleFile, importBundledFile } from '../../../load-file.js'; +import { getStudioVirtualModContents } from '../../../integration/vite-plugin-db.js'; + +export async function cmd({ + astroConfig, + dbConfig, + flags, +}: { + astroConfig: AstroConfig; + dbConfig: DBConfig; + flags: Arguments; +}) { + const filePath = flags._[4]; + if (typeof filePath !== 'string') { + console.error(MISSING_EXECUTE_PATH_ERROR); + process.exit(1); + } + + const fileUrl = new URL(filePath, astroConfig.root); + if (!existsSync(fileUrl)) { + console.error(FILE_NOT_FOUND_ERROR(filePath)); + process.exit(1); + } + + const appToken = await getManagedAppTokenOrExit(flags.token); + + const virtualModContents = getStudioVirtualModContents({ + tables: dbConfig.tables ?? {}, + appToken: appToken.token, + }); + const { code } = await bundleFile({ virtualModContents, root: astroConfig.root, fileUrl }); + // Executable files use top-level await. Importing will run the file. + await importBundledFile({ code, root: astroConfig.root }); +} diff --git a/packages/db/src/core/cli/commands/gen/index.ts b/packages/db/src/core/cli/commands/gen/index.ts index 25f59be45a83..c28f697d86e6 100644 --- a/packages/db/src/core/cli/commands/gen/index.ts +++ b/packages/db/src/core/cli/commands/gen/index.ts @@ -1,6 +1,7 @@ +import { fileURLToPath } from 'node:url'; import { writeFile } from 'node:fs/promises'; import type { AstroConfig } from 'astro'; -import { bgRed, red, reset } from 'kleur/colors'; +import { bold, bgRed, red, reset } from 'kleur/colors'; import type { Arguments } from 'yargs-parser'; import { getMigrationQueries } from '../../migration-queries.js'; import { @@ -9,12 +10,23 @@ import { getMigrationStatus, initializeMigrationsDirectory, } from '../../migrations.js'; +import { getMigrationsDirectoryUrl } from '../../../utils.js'; +import type { DBConfig } from '../../../types.js'; +import { relative } from 'node:path'; -export async function cmd({ config }: { config: AstroConfig; flags: Arguments }) { - const migration = await getMigrationStatus(config); +export async function cmd({ + astroConfig, + dbConfig, +}: { + astroConfig: AstroConfig; + dbConfig: DBConfig; + flags: Arguments; +}) { + const migration = await getMigrationStatus({ dbConfig, root: astroConfig.root }); + const migrationsDir = getMigrationsDirectoryUrl(astroConfig.root); if (migration.state === 'no-migrations-found') { - await initializeMigrationsDirectory(migration.currentSnapshot); + await initializeMigrationsDirectory(migration.currentSnapshot, migrationsDir); console.log(MIGRATIONS_CREATED); return; } else if (migration.state === 'up-to-date') { @@ -30,14 +42,15 @@ export async function cmd({ config }: { config: AstroConfig; flags: Arguments }) // Warn the user about any changes that lead to data-loss. // When the user runs `db push`, they will be prompted to confirm these changes. confirmations.map((message) => console.log(bgRed(' !!! ') + ' ' + red(message))); - const migrationFileContent = { + const content = { diff, db: migrationQueries, // TODO(fks): Encode the relevant data, instead of the raw message. // This will give `db push` more control over the formatting of the message. confirm: confirmations.map((c) => reset(c)), }; - const migrationFileName = `./migrations/${newFilename}`; - await writeFile(migrationFileName, JSON.stringify(migrationFileContent, undefined, 2)); - console.log(migrationFileName + ' created!'); + const fileUrl = new URL(newFilename, migrationsDir); + const relativePath = relative(fileURLToPath(astroConfig.root), fileURLToPath(fileUrl)); + await writeFile(fileUrl, JSON.stringify(content, undefined, 2)); + console.log(bold(relativePath) + ' created!'); } diff --git a/packages/db/src/core/cli/commands/link/index.ts b/packages/db/src/core/cli/commands/link/index.ts index 4acfa7ec4849..f92a1818ca41 100644 --- a/packages/db/src/core/cli/commands/link/index.ts +++ b/packages/db/src/core/cli/commands/link/index.ts @@ -1,17 +1,15 @@ import { mkdir, writeFile } from 'node:fs/promises'; import { homedir } from 'node:os'; import { basename } from 'node:path'; -import type { AstroConfig } from 'astro'; import { slug } from 'github-slugger'; import { bgRed, cyan } from 'kleur/colors'; import ora from 'ora'; import prompts from 'prompts'; -import type { Arguments } from 'yargs-parser'; import { MISSING_SESSION_ID_ERROR } from '../../../errors.js'; import { PROJECT_ID_FILE, getSessionIdFromFile } from '../../../tokens.js'; import { getAstroStudioUrl } from '../../../utils.js'; -export async function cmd({}: { config: AstroConfig; flags: Arguments }) { +export async function cmd() { const sessionToken = await getSessionIdFromFile(); if (!sessionToken) { console.error(MISSING_SESSION_ID_ERROR); diff --git a/packages/db/src/core/cli/commands/login/index.ts b/packages/db/src/core/cli/commands/login/index.ts index 215d5723e158..2a3f16447d87 100644 --- a/packages/db/src/core/cli/commands/login/index.ts +++ b/packages/db/src/core/cli/commands/login/index.ts @@ -7,6 +7,7 @@ import open from 'open'; import ora from 'ora'; import type { Arguments } from 'yargs-parser'; import { SESSION_LOGIN_FILE } from '../../../tokens.js'; +import type { DBConfig } from '../../../types.js'; import { getAstroStudioUrl } from '../../../utils.js'; // NOTE(fks): How the Astro CLI login process works: @@ -47,7 +48,13 @@ async function createServer(): Promise<{ url: string; promise: Promise } return { url: serverUrl, promise: sessionPromise }; } -export async function cmd({ flags }: { config: AstroConfig; flags: Arguments }) { +export async function cmd({ + flags, +}: { + astroConfig: AstroConfig; + dbConfig: DBConfig; + flags: Arguments; +}) { let session = flags.session; if (!session) { diff --git a/packages/db/src/core/cli/commands/logout/index.ts b/packages/db/src/core/cli/commands/logout/index.ts index 0881dcf4c82e..fbc4ba78cd83 100644 --- a/packages/db/src/core/cli/commands/logout/index.ts +++ b/packages/db/src/core/cli/commands/logout/index.ts @@ -1,9 +1,7 @@ import { unlink } from 'node:fs/promises'; -import type { AstroConfig } from 'astro'; -import type { Arguments } from 'yargs-parser'; import { SESSION_LOGIN_FILE } from '../../../tokens.js'; -export async function cmd({}: { config: AstroConfig; flags: Arguments }) { +export async function cmd() { await unlink(SESSION_LOGIN_FILE); console.log('Successfully logged out of Astro Studio.'); } diff --git a/packages/db/src/core/cli/commands/push/index.ts b/packages/db/src/core/cli/commands/push/index.ts index 8c3f21e4d773..b6bd773e2f03 100644 --- a/packages/db/src/core/cli/commands/push/index.ts +++ b/packages/db/src/core/cli/commands/push/index.ts @@ -1,32 +1,36 @@ -import { type InStatement, createClient } from '@libsql/client'; import type { AstroConfig } from 'astro'; -import { drizzle as drizzleLibsql } from 'drizzle-orm/libsql'; -import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; -import { drizzle as drizzleProxy } from 'drizzle-orm/sqlite-proxy'; import { red } from 'kleur/colors'; import prompts from 'prompts'; import type { Arguments } from 'yargs-parser'; -import { MISSING_SESSION_ID_ERROR } from '../../../errors.js'; -import { recreateTables, seedData } from '../../../queries.js'; import { getManagedAppTokenOrExit } from '../../../tokens.js'; -import { type AstroConfigWithDB, type DBSnapshot, tablesSchema } from '../../../types.js'; -import { getRemoteDatabaseUrl } from '../../../utils.js'; +import { type DBConfig, type DBSnapshot } from '../../../types.js'; +import { getMigrationsDirectoryUrl, getRemoteDatabaseUrl } from '../../../utils.js'; import { getMigrationQueries } from '../../migration-queries.js'; import { - MIGRATIONS_NOT_INITIALIZED, - MIGRATIONS_UP_TO_DATE, - MIGRATION_NEEDED, createEmptySnapshot, - getMigrationStatus, getMigrations, + getMigrationStatus, + INITIAL_SNAPSHOT, loadInitialSnapshot, loadMigration, + MIGRATION_NEEDED, + MIGRATIONS_NOT_INITIALIZED, + MIGRATIONS_UP_TO_DATE, } from '../../migrations.js'; +import { MISSING_SESSION_ID_ERROR } from '../../../errors.js'; -export async function cmd({ config, flags }: { config: AstroConfig; flags: Arguments }) { +export async function cmd({ + astroConfig, + dbConfig, + flags, +}: { + astroConfig: AstroConfig; + dbConfig: DBConfig; + flags: Arguments; +}) { const isDryRun = flags.dryRun; const appToken = await getManagedAppTokenOrExit(flags.token); - const migration = await getMigrationStatus(config); + const migration = await getMigrationStatus({ dbConfig, root: astroConfig.root }); if (migration.state === 'no-migrations-found') { console.log(MIGRATIONS_NOT_INITIALIZED); process.exit(1); @@ -34,9 +38,10 @@ export async function cmd({ config, flags }: { config: AstroConfig; flags: Argum console.log(MIGRATION_NEEDED); process.exit(1); } + const migrationsDir = getMigrationsDirectoryUrl(astroConfig.root); // get all migrations from the filesystem - const allLocalMigrations = await getMigrations(); + const allLocalMigrations = await getMigrations(migrationsDir); let missingMigrations: string[] = []; try { const { data } = await prepareMigrateQuery({ @@ -63,14 +68,12 @@ export async function cmd({ config, flags }: { config: AstroConfig; flags: Argum console.log(`Pushing ${missingMigrations.length} migrations...`); await pushSchema({ migrations: missingMigrations, + migrationsDir, appToken: appToken.token, isDryRun, currentSnapshot: migration.currentSnapshot, }); } - // push the database seed data - console.info('Pushing data...'); - await pushData({ config, appToken: appToken.token, isDryRun }); // cleanup and exit await appToken.destroy(); console.info('Push complete!'); @@ -78,25 +81,29 @@ export async function cmd({ config, flags }: { config: AstroConfig; flags: Argum async function pushSchema({ migrations, + migrationsDir, appToken, isDryRun, currentSnapshot, }: { migrations: string[]; + migrationsDir: URL; appToken: string; isDryRun: boolean; currentSnapshot: DBSnapshot; }) { // load all missing migrations - const initialSnapshot = migrations.find((m) => m === '0000_snapshot.json'); - const filteredMigrations = migrations.filter((m) => m !== '0000_snapshot.json'); - const missingMigrationContents = await Promise.all(filteredMigrations.map(loadMigration)); + const initialSnapshot = migrations.find((m) => m === INITIAL_SNAPSHOT); + const filteredMigrations = migrations.filter((m) => m !== INITIAL_SNAPSHOT); + const missingMigrationContents = await Promise.all( + filteredMigrations.map((m) => loadMigration(m, migrationsDir)) + ); // create a migration for the initial snapshot, if needed const initialMigrationBatch = initialSnapshot ? ( await getMigrationQueries({ oldSnapshot: createEmptySnapshot(), - newSnapshot: await loadInitialSnapshot(), + newSnapshot: await loadInitialSnapshot(migrationsDir), }) ).queries : []; @@ -130,76 +137,6 @@ async function pushSchema({ await runMigrateQuery({ queries, migrations, snapshot: currentSnapshot, appToken, isDryRun }); } -const sqlite = new SQLiteAsyncDialect(); - -async function pushData({ - config, - appToken, - isDryRun, -}: { - config: AstroConfigWithDB; - appToken: string; - isDryRun?: boolean; -}) { - const queries: InStatement[] = []; - if (config.db?.data) { - const libsqlClient = createClient({ url: ':memory:' }); - // Stand up tables locally to mirror inserts. - // Needed to generate return values. - await recreateTables({ - db: drizzleLibsql(libsqlClient), - tables: tablesSchema.parse(config.db.tables ?? {}), - }); - - for (const [collectionName, { writable }] of Object.entries(config.db.tables ?? {})) { - if (!writable) { - queries.push({ - sql: `DELETE FROM ${sqlite.escapeName(collectionName)}`, - args: [], - }); - } - } - - // Use proxy to trace all queries to queue up in a batch. - const db = await drizzleProxy(async (sqlQuery, params, method) => { - const stmt: InStatement = { sql: sqlQuery, args: params }; - queries.push(stmt); - // Use in-memory database to generate results for `returning()`. - const { rows } = await libsqlClient.execute(stmt); - const rowValues: unknown[][] = []; - for (const row of rows) { - if (row != null && typeof row === 'object') { - rowValues.push(Object.values(row)); - } - } - if (method === 'get') { - return { rows: rowValues[0] }; - } - return { rows: rowValues }; - }); - await seedData({ - db, - mode: 'build', - data: config.db.data, - }); - } - - const url = new URL('/db/query', getRemoteDatabaseUrl()); - - if (isDryRun) { - console.info('[DRY RUN] Batch data seed:', JSON.stringify(queries, null, 2)); - return new Response(null, { status: 200 }); - } - - return await fetch(url, { - method: 'POST', - headers: new Headers({ - Authorization: `Bearer ${appToken}`, - }), - body: JSON.stringify(queries), - }); -} - async function runMigrateQuery({ queries: baseQueries, migrations, diff --git a/packages/db/src/core/cli/commands/shell/index.ts b/packages/db/src/core/cli/commands/shell/index.ts index 13450bf31144..7d8f13437bbd 100644 --- a/packages/db/src/core/cli/commands/shell/index.ts +++ b/packages/db/src/core/cli/commands/shell/index.ts @@ -4,8 +4,15 @@ import type { Arguments } from 'yargs-parser'; import { createRemoteDatabaseClient } from '../../../../runtime/db-client.js'; import { getManagedAppTokenOrExit } from '../../../tokens.js'; import { getRemoteDatabaseUrl } from '../../../utils.js'; +import type { DBConfigInput } from '../../../types.js'; -export async function cmd({ flags }: { config: AstroConfig; flags: Arguments }) { +export async function cmd({ + flags, +}: { + dbConfig: DBConfigInput; + astroConfig: AstroConfig; + flags: Arguments; +}) { const query = flags.query; const appToken = await getManagedAppTokenOrExit(flags.token); const db = createRemoteDatabaseClient(appToken.token, getRemoteDatabaseUrl()); diff --git a/packages/db/src/core/cli/commands/verify/index.ts b/packages/db/src/core/cli/commands/verify/index.ts index 6d839d60673c..ff182fc53c86 100644 --- a/packages/db/src/core/cli/commands/verify/index.ts +++ b/packages/db/src/core/cli/commands/verify/index.ts @@ -7,9 +7,18 @@ import { MIGRATION_NEEDED, getMigrationStatus, } from '../../migrations.js'; +import type { DBConfig } from '../../../types.js'; -export async function cmd({ config, flags }: { config: AstroConfig; flags: Arguments }) { - const status = await getMigrationStatus(config); +export async function cmd({ + astroConfig, + dbConfig, + flags, +}: { + astroConfig: AstroConfig; + dbConfig: DBConfig; + flags: Arguments; +}) { + const status = await getMigrationStatus({ dbConfig, root: astroConfig.root }); const { state } = status; if (flags.json) { if (state === 'ahead') { diff --git a/packages/db/src/core/cli/index.ts b/packages/db/src/core/cli/index.ts index 5d29b8dda0de..9ffaee7b3059 100644 --- a/packages/db/src/core/cli/index.ts +++ b/packages/db/src/core/cli/index.ts @@ -1,50 +1,56 @@ import type { AstroConfig } from 'astro'; import type { Arguments } from 'yargs-parser'; -import { STUDIO_CONFIG_MISSING_CLI_ERROR } from '../errors.js'; +import { loadDbConfigFile } from '../load-file.js'; +import { dbConfigSchema } from '../types.js'; -export async function cli({ flags, config }: { flags: Arguments; config: AstroConfig }) { +export async function cli({ + flags, + config: astroConfig, +}: { + flags: Arguments; + config: AstroConfig; +}) { const args = flags._ as string[]; // Most commands are `astro db foo`, but for now login/logout // are also handled by this package, so first check if this is a db command. const command = args[2] === 'db' ? args[3] : args[2]; - - switch (command) { - case 'login': { - const { cmd } = await import('./commands/login/index.js'); - return await cmd({ config, flags }); - } - case 'logout': { - const { cmd } = await import('./commands/logout/index.js'); - return await cmd({ config, flags }); - } - } - - if (!config.db?.studio) { - console.log(STUDIO_CONFIG_MISSING_CLI_ERROR); - process.exit(1); - } + const { mod } = await loadDbConfigFile(astroConfig.root); + // TODO: parseConfigOrExit() + const dbConfig = dbConfigSchema.parse(mod?.default ?? {}); switch (command) { case 'shell': { const { cmd } = await import('./commands/shell/index.js'); - return await cmd({ config, flags }); + return await cmd({ astroConfig, dbConfig, flags }); } case 'gen': case 'sync': { const { cmd } = await import('./commands/gen/index.js'); - return await cmd({ config, flags }); + return await cmd({ astroConfig, dbConfig, flags }); } case 'push': { const { cmd } = await import('./commands/push/index.js'); - return await cmd({ config, flags }); + return await cmd({ astroConfig, dbConfig, flags }); } case 'verify': { const { cmd } = await import('./commands/verify/index.js'); - return await cmd({ config, flags }); + return await cmd({ astroConfig, dbConfig, flags }); + } + case 'execute': { + const { cmd } = await import('./commands/execute/index.js'); + return await cmd({ astroConfig, dbConfig, flags }); + } + case 'login': { + const { cmd } = await import('./commands/login/index.js'); + return await cmd({ astroConfig, dbConfig, flags }); + } + case 'logout': { + const { cmd } = await import('./commands/logout/index.js'); + return await cmd(); } case 'link': { const { cmd } = await import('./commands/link/index.js'); - return await cmd({ config, flags }); + return await cmd(); } default: { if (command == null) { diff --git a/packages/db/src/core/cli/migration-queries.ts b/packages/db/src/core/cli/migration-queries.ts index e031108e4747..aa9d7e316805 100644 --- a/packages/db/src/core/cli/migration-queries.ts +++ b/packages/db/src/core/cli/migration-queries.ts @@ -12,7 +12,7 @@ import { getReferencesConfig, hasDefault, schemaTypeToSqlType, -} from '../queries.js'; +} from '../../runtime/queries.js'; import { type BooleanColumn, type ColumnType, diff --git a/packages/db/src/core/cli/migrations.ts b/packages/db/src/core/cli/migrations.ts index 671862b61495..455ebc984fc7 100644 --- a/packages/db/src/core/cli/migrations.ts +++ b/packages/db/src/core/cli/migrations.ts @@ -1,8 +1,8 @@ -import type { AstroConfig } from 'astro'; import deepDiff from 'deep-diff'; import { mkdir, readFile, readdir, writeFile } from 'fs/promises'; +import { type DBSnapshot, type DBConfig } from '../types.js'; import { cyan, green, yellow } from 'kleur/colors'; -import { type DBSnapshot, tablesSchema } from '../types.js'; +import { getMigrationsDirectoryUrl } from '../utils.js'; const { applyChange, diff: generateDiff } = deepDiff; export type MigrationStatus = @@ -24,9 +24,18 @@ export type MigrationStatus = currentSnapshot: DBSnapshot; }; -export async function getMigrationStatus(config: AstroConfig): Promise { - const currentSnapshot = createCurrentSnapshot(config); - const allMigrationFiles = await getMigrations(); +export const INITIAL_SNAPSHOT = '0000_snapshot.json'; + +export async function getMigrationStatus({ + dbConfig, + root, +}: { + dbConfig: DBConfig; + root: URL; +}): Promise { + const currentSnapshot = createCurrentSnapshot(dbConfig); + const dir = getMigrationsDirectoryUrl(root); + const allMigrationFiles = await getMigrations(dir); if (allMigrationFiles.length === 0) { return { @@ -35,7 +44,7 @@ export async function getMigrationStatus(config: AstroConfig): Promise { - const migrationFiles = await readdir('./migrations').catch((err) => { +export async function getMigrations(dir: URL): Promise { + const migrationFiles = await readdir(dir).catch((err) => { if (err.code === 'ENOENT') { return []; } @@ -94,13 +103,14 @@ export async function getMigrations(): Promise { } export async function loadMigration( - migration: string + migration: string, + dir: URL ): Promise<{ diff: any[]; db: string[]; confirm?: string[] }> { - return JSON.parse(await readFile(`./migrations/${migration}`, 'utf-8')); + return JSON.parse(await readFile(new URL(migration, dir), 'utf-8')); } -export async function loadInitialSnapshot(): Promise { - const snapshot = JSON.parse(await readFile('./migrations/0000_snapshot.json', 'utf-8')); +export async function loadInitialSnapshot(dir: URL): Promise { + const snapshot = JSON.parse(await readFile(new URL(INITIAL_SNAPSHOT, dir), 'utf-8')); // `experimentalVersion: 1` -- added the version column if (snapshot.experimentalVersion === 1) { return snapshot; @@ -112,16 +122,19 @@ export async function loadInitialSnapshot(): Promise { throw new Error('Invalid snapshot format'); } -export async function initializeMigrationsDirectory(currentSnapshot: DBSnapshot) { - await mkdir('./migrations', { recursive: true }); - await writeFile('./migrations/0000_snapshot.json', JSON.stringify(currentSnapshot, undefined, 2)); +export async function initializeMigrationsDirectory(currentSnapshot: DBSnapshot, dir: URL) { + await mkdir(dir, { recursive: true }); + await writeFile(new URL(INITIAL_SNAPSHOT, dir), JSON.stringify(currentSnapshot, undefined, 2)); } -export async function initializeFromMigrations(allMigrationFiles: string[]): Promise { - const prevSnapshot = await loadInitialSnapshot(); +export async function initializeFromMigrations( + allMigrationFiles: string[], + dir: URL +): Promise { + const prevSnapshot = await loadInitialSnapshot(dir); for (const migration of allMigrationFiles) { - if (migration === '0000_snapshot.json') continue; - const migrationContent = await loadMigration(migration); + if (migration === INITIAL_SNAPSHOT) continue; + const migrationContent = await loadMigration(migration, dir); migrationContent.diff.forEach((change: any) => { applyChange(prevSnapshot, {}, change); }); @@ -129,10 +142,8 @@ export async function initializeFromMigrations(allMigrationFiles: string[]): Pro return prevSnapshot; } -export function createCurrentSnapshot(config: AstroConfig): DBSnapshot { - // Parse to resolve non-serializable types like () => references - const tablesConfig = tablesSchema.parse(config.db?.tables ?? {}); - const schema = JSON.parse(JSON.stringify(tablesConfig)); +export function createCurrentSnapshot({ tables = {} }: DBConfig): DBSnapshot { + const schema = JSON.parse(JSON.stringify(tables)); return { experimentalVersion: 1, schema }; } export function createEmptySnapshot(): DBSnapshot { diff --git a/packages/db/src/core/consts.ts b/packages/db/src/core/consts.ts index 1f6771b04f85..bf295a2dc9a8 100644 --- a/packages/db/src/core/consts.ts +++ b/packages/db/src/core/consts.ts @@ -6,9 +6,12 @@ export const PACKAGE_NAME = JSON.parse( export const RUNTIME_IMPORT = JSON.stringify(`${PACKAGE_NAME}/runtime`); export const RUNTIME_DRIZZLE_IMPORT = JSON.stringify(`${PACKAGE_NAME}/runtime/drizzle`); +export const RUNTIME_CONFIG_IMPORT = JSON.stringify(`${PACKAGE_NAME}/runtime/config`); export const DB_TYPES_FILE = 'db-types.d.ts'; export const VIRTUAL_MODULE_ID = 'astro:db'; export const DB_PATH = '.astro/content.db'; + +export const CONFIG_FILE_NAMES = ['config.ts', 'config.js', 'config.mts', 'config.mjs']; diff --git a/packages/db/src/core/errors.ts b/packages/db/src/core/errors.ts index 76a66d0dc954..706553a0673e 100644 --- a/packages/db/src/core/errors.ts +++ b/packages/db/src/core/errors.ts @@ -10,44 +10,41 @@ export const MISSING_PROJECT_ID_ERROR = `${red('â–¶ Directory not linked.')} To link this directory to an Astro Studio project, run ${cyan('astro db link')}\n`; -export const STUDIO_CONFIG_MISSING_WRITABLE_TABLE_ERROR = (tableName: string) => `${red( - `â–¶ Writable table ${bold(tableName)} requires Astro Studio or the ${yellow( - 'unsafeWritable' - )} option.` -)} - - Visit ${cyan('https://astro.build/studio')} to create your account - and set ${green('studio: true')} in your astro.config.mjs file to enable Studio.\n`; +export const MIGRATIONS_NOT_INITIALIZED = `${yellow( + 'â–¶ No migrations found!' +)}\n\n To scaffold your migrations folder, run\n ${cyan('astro db sync')}\n`; -export const UNSAFE_WRITABLE_WARNING = `${yellow( - 'unsafeWritable' -)} option is enabled and you are using writable tables. - Redeploying your app may result in wiping away your database. - I hope you know what you are doing.\n`; +export const MISSING_EXECUTE_PATH_ERROR = `${red( + 'â–¶ No file path provided.' +)} Provide a path by running ${cyan('astro db execute ')}\n`; -export const STUDIO_CONFIG_MISSING_CLI_ERROR = `${red('â–¶ This command requires Astro Studio.')} +export const FILE_NOT_FOUND_ERROR = (path: string) => + `${red('â–¶ File not found:')} ${bold(path)}\n`; - Visit ${cyan('https://astro.build/studio')} to create your account - and set ${green('studio: true')} in your astro.config.mjs file to enable Studio.\n`; +export const SEED_ERROR = (error: string) => { + return `${red(`Error while seeding database:`)}\n\n${error}`; +}; -export const MIGRATIONS_NOT_INITIALIZED = `${yellow( - 'â–¶ No migrations found!' -)}\n\n To scaffold your migrations folder, run\n ${cyan('astro db sync')}\n`; +export const REFERENCE_DNE_ERROR = (columnName: string) => { + return `Column ${bold( + columnName + )} references a table that does not exist. Did you apply the referenced table to the \`tables\` object in your db config?`; +}; -export const SEED_WRITABLE_IN_PROD_ERROR = (tableName: string) => { - return `${red( - `Writable tables should not be seeded in production with data().` - )} You can seed ${bold( +export const FOREIGN_KEY_DNE_ERROR = (tableName: string) => { + return `Table ${bold( tableName - )} in development mode only using the "mode" flag. See the docs for more: https://www.notion.so/astroinc/astrojs-db-README-dcf6fa10de9a4f528be56cee96e8c054?pvs=4#278aed3fc37e4cec80240d1552ff6ac5`; + )} references a table that does not exist. Did you apply the referenced table to the \`tables\` object in your db config?`; }; -export const SEED_ERROR = (tableName: string, error: string) => { - return `${red(`Error seeding table ${bold(tableName)}:`)}\n\n${error}`; +export const FOREIGN_KEY_REFERENCES_LENGTH_ERROR = (tableName: string) => { + return `Foreign key on ${bold( + tableName + )} is misconfigured. \`columns\` and \`references\` must be the same length.`; }; -export const SEED_EMPTY_ARRAY_ERROR = (tableName: string) => { - // Drizzle error says "values() must be called with at least one value." - // This is specific to db.insert(). Prettify for seed(). - return SEED_ERROR(tableName, `Empty array was passed. seed() must receive at least one value.`); +export const FOREIGN_KEY_REFERENCES_EMPTY_ERROR = (tableName: string) => { + return `Foreign key on ${bold( + tableName + )} is misconfigured. \`references\` array cannot be empty.`; }; diff --git a/packages/db/src/core/integration/index.ts b/packages/db/src/core/integration/index.ts index d9cddfd72433..2b476a4d3f95 100644 --- a/packages/db/src/core/integration/index.ts +++ b/packages/db/src/core/integration/index.ts @@ -1,28 +1,32 @@ import { existsSync } from 'fs'; +import { CONFIG_FILE_NAMES, DB_PATH } from '../consts.js'; +import { dbConfigSchema, type DBConfig } from '../types.js'; +import { getDbDirectoryUrl, type VitePlugin } from '../utils.js'; +import { errorMap } from './error-map.js'; import { dirname } from 'path'; import { fileURLToPath } from 'url'; import type { AstroIntegration } from 'astro'; import { mkdir, rm, writeFile } from 'fs/promises'; import { blue, yellow } from 'kleur/colors'; -import { createLocalDatabaseClient } from '../../runtime/db-client.js'; -import { DB_PATH } from '../consts.js'; -import { STUDIO_CONFIG_MISSING_WRITABLE_TABLE_ERROR, UNSAFE_WRITABLE_WARNING } from '../errors.js'; -import { recreateTables, seedData } from '../queries.js'; -import { type ManagedAppToken, getManagedAppTokenOrExit } from '../tokens.js'; -import { type DBTables, astroConfigWithDbSchema } from '../types.js'; -import { type VitePlugin } from '../utils.js'; -import { errorMap } from './error-map.js'; import { fileURLIntegration } from './file-url.js'; +import { getManagedAppTokenOrExit, type ManagedAppToken } from '../tokens.js'; +import { loadDbConfigFile } from '../load-file.js'; +import { vitePluginDb, type LateTables } from './vite-plugin-db.js'; import { typegen } from './typegen.js'; -import { vitePluginDb } from './vite-plugin-db.js'; import { vitePluginInjectEnvTs } from './vite-plugin-inject-env-ts.js'; function astroDBIntegration(): AstroIntegration { - let connectedToRemote = false; + let connectToStudio = false; + let configFileDependencies: string[] = []; + let root: URL; let appToken: ManagedAppToken | undefined; - let schemas = { - tables(): DBTables { - throw new Error('tables not found'); + let dbConfig: DBConfig; + + // Make table loading "late" to pass to plugins from `config:setup`, + // but load during `config:done` to wait for integrations to settle. + let tables: LateTables = { + get() { + throw new Error('[astro:db] INTERNAL Tables not loaded yet'); }, }; let command: 'dev' | 'build' | 'preview'; @@ -31,25 +35,28 @@ function astroDBIntegration(): AstroIntegration { hooks: { 'astro:config:setup': async ({ updateConfig, config, command: _command, logger }) => { command = _command; - if (_command === 'preview') return; + root = config.root; + + if (command === 'preview') return; let dbPlugin: VitePlugin | undefined = undefined; - const studio = config.db?.studio ?? false; + connectToStudio = command === 'build'; - if (studio && command === 'build' && process.env.ASTRO_DB_TEST_ENV !== '1') { + if (connectToStudio) { appToken = await getManagedAppTokenOrExit(); - connectedToRemote = true; dbPlugin = vitePluginDb({ - connectToStudio: true, + connectToStudio, appToken: appToken.token, - schemas, + tables, root: config.root, + srcDir: config.srcDir, }); } else { dbPlugin = vitePluginDb({ connectToStudio: false, - schemas, + tables, root: config.root, + srcDir: config.srcDir, }); } @@ -60,67 +67,50 @@ function astroDBIntegration(): AstroIntegration { }, }); }, - 'astro:config:done': async ({ config, logger }) => { + 'astro:config:done': async ({ config }) => { // TODO: refine where we load tables // @matthewp: may want to load tables by path at runtime - const configWithDb = astroConfigWithDbSchema.parse(config, { errorMap }); - const tables = configWithDb.db?.tables ?? {}; - // Redefine getTables so our integration can grab them - schemas.tables = () => tables; - - const studio = configWithDb.db?.studio ?? false; - const unsafeWritable = Boolean(configWithDb.db?.unsafeWritable); - const foundWritableCollection = Object.entries(tables).find(([, c]) => c.writable); - const writableAllowed = studio || unsafeWritable; - if (!writableAllowed && foundWritableCollection) { - logger.error(STUDIO_CONFIG_MISSING_WRITABLE_TABLE_ERROR(foundWritableCollection[0])); - process.exit(1); - } - // Using writable tables with the opt-in flag. Warn them to let them - // know the risk. - else if (unsafeWritable && foundWritableCollection) { - logger.warn(UNSAFE_WRITABLE_WARNING); - } + const { mod, dependencies } = await loadDbConfigFile(config.root); + configFileDependencies = dependencies; + dbConfig = dbConfigSchema.parse(mod?.default ?? {}, { + errorMap, + }); + // TODO: resolve integrations here? + tables.get = () => dbConfig.tables ?? {}; - if (!connectedToRemote) { + if (!connectToStudio && !process.env.TEST_IN_MEMORY_DB) { const dbUrl = new URL(DB_PATH, config.root); if (existsSync(dbUrl)) { await rm(dbUrl); } await mkdir(dirname(fileURLToPath(dbUrl)), { recursive: true }); await writeFile(dbUrl, ''); - - using db = await createLocalDatabaseClient({ - tables, - dbUrl: dbUrl.toString(), - seeding: true, - }); - await recreateTables({ db, tables }); - if (configWithDb.db?.data) { - await seedData({ - db, - data: configWithDb.db.data, - logger, - mode: command === 'dev' ? 'dev' : 'build', - }); - } - logger.debug('Database setup complete.'); } - await typegen({ tables, root: config.root }); + await typegen({ tables: tables.get() ?? {}, root: config.root }); }, 'astro:server:start': async ({ logger }) => { // Wait for the server startup to log, so that this can come afterwards. setTimeout(() => { logger.info( - connectedToRemote ? 'Connected to remote database.' : 'New local database created.' + connectToStudio ? 'Connected to remote database.' : 'New local database created.' ); }, 100); }, + 'astro:server:setup': async ({ server }) => { + const filesToWatch = [ + ...CONFIG_FILE_NAMES.map((c) => new URL(c, getDbDirectoryUrl(root))), + ...configFileDependencies.map((c) => new URL(c, root)), + ]; + server.watcher.on('all', (event, relativeEntry) => { + const entry = new URL(relativeEntry, root); + if (filesToWatch.some((f) => entry.href === f.href)) { + server.restart(); + } + }); + }, 'astro:build:start': async ({ logger }) => { - logger.info( - 'database: ' + (connectedToRemote ? yellow('remote') : blue('local database.')) - ); + logger.info('database: ' + (connectToStudio ? yellow('remote') : blue('local database.'))); }, 'astro:build:done': async ({}) => { await appToken?.destroy(); diff --git a/packages/db/src/core/integration/typegen.ts b/packages/db/src/core/integration/typegen.ts index 0436261e16ba..4fad1bc92189 100644 --- a/packages/db/src/core/integration/typegen.ts +++ b/packages/db/src/core/integration/typegen.ts @@ -28,19 +28,7 @@ ${Object.entries(tables) function generateTableType(name: string, collection: DBTable): string { let tableType = ` export const ${name}: import(${RUNTIME_IMPORT}).Table< ${JSON.stringify(name)}, - ${JSON.stringify( - Object.fromEntries( - Object.entries(collection.columns).map(([columnName, column]) => [ - columnName, - { - // Only select columns Drizzle needs for inference - type: column.type, - optional: column.schema.optional, - default: column.schema.default, - }, - ]) - ) - )} + ${JSON.stringify(collection.columns)} >;`; return tableType; } diff --git a/packages/db/src/core/integration/vite-plugin-db.ts b/packages/db/src/core/integration/vite-plugin-db.ts index 121d0eca1d62..ff97d2b21bf1 100644 --- a/packages/db/src/core/integration/vite-plugin-db.ts +++ b/packages/db/src/core/integration/vite-plugin-db.ts @@ -1,70 +1,125 @@ -import { DB_PATH, RUNTIME_DRIZZLE_IMPORT, RUNTIME_IMPORT, VIRTUAL_MODULE_ID } from '../consts.js'; +import { fileURLToPath } from 'node:url'; +import { SEED_DEV_FILE_NAME } from '../../runtime/queries.js'; +import { + DB_PATH, + RUNTIME_CONFIG_IMPORT, + RUNTIME_DRIZZLE_IMPORT, + RUNTIME_IMPORT, + VIRTUAL_MODULE_ID, +} from '../consts.js'; import type { DBTables } from '../types.js'; -import { type VitePlugin, getRemoteDatabaseUrl } from '../utils.js'; +import { getDbDirectoryUrl, getRemoteDatabaseUrl, type VitePlugin } from '../utils.js'; +import { normalizePath } from 'vite'; + +const LOCAL_DB_VIRTUAL_MODULE_ID = 'astro:local'; const resolvedVirtualModuleId = '\0' + VIRTUAL_MODULE_ID; +const resolvedLocalDbVirtualModuleId = LOCAL_DB_VIRTUAL_MODULE_ID + '/local-db'; +const resolvedSeedVirtualModuleId = '\0' + VIRTUAL_MODULE_ID + '?shouldSeed'; -type LateSchema = { - tables: () => DBTables; +export type LateTables = { + get: () => DBTables; }; type VitePluginDBParams = | { connectToStudio: false; - schemas: LateSchema; + tables: LateTables; + srcDir: URL; root: URL; } | { connectToStudio: true; - schemas: LateSchema; + tables: LateTables; appToken: string; + srcDir: URL; root: URL; }; export function vitePluginDb(params: VitePluginDBParams): VitePlugin { + const srcDirPath = normalizePath(fileURLToPath(params.srcDir)); return { name: 'astro:db', enforce: 'pre', - resolveId(id) { - if (id === VIRTUAL_MODULE_ID) { - return resolvedVirtualModuleId; + async resolveId(id, rawImporter) { + if (id === LOCAL_DB_VIRTUAL_MODULE_ID) return resolvedLocalDbVirtualModuleId; + if (id !== VIRTUAL_MODULE_ID) return; + if (params.connectToStudio) return resolvedVirtualModuleId; + + const importer = rawImporter ? await this.resolve(rawImporter) : null; + if (!importer) return resolvedVirtualModuleId; + + if (importer.id.startsWith(srcDirPath)) { + // Seed only if the importer is in the src directory. + // Otherwise, we may get recursive seed calls (ex. import from db/seed.ts). + return resolvedSeedVirtualModuleId; } + return resolvedVirtualModuleId; }, load(id) { - if (id !== resolvedVirtualModuleId) return; + if (id === resolvedLocalDbVirtualModuleId) { + const dbUrl = new URL(DB_PATH, params.root); + return `import { createLocalDatabaseClient } from ${RUNTIME_IMPORT}; + const dbUrl = ${JSON.stringify(dbUrl)}; + + export const db = createLocalDatabaseClient({ dbUrl });`; + } + + if (id !== resolvedVirtualModuleId && id !== resolvedSeedVirtualModuleId) return; if (params.connectToStudio) { return getStudioVirtualModContents({ appToken: params.appToken, - tables: params.schemas.tables(), + tables: params.tables.get(), }); } - return getVirtualModContents({ + return getLocalVirtualModContents({ root: params.root, - tables: params.schemas.tables(), + tables: params.tables.get(), + shouldSeed: id === resolvedSeedVirtualModuleId, }); }, }; } -export function getVirtualModContents({ tables, root }: { tables: DBTables; root: URL }) { - const dbUrl = new URL(DB_PATH, root); +export function getConfigVirtualModContents() { + return `export * from ${RUNTIME_CONFIG_IMPORT}`; +} + +export function getLocalVirtualModContents({ + tables, + shouldSeed, +}: { + tables: DBTables; + root: URL; + shouldSeed: boolean; +}) { + const seedFilePaths = SEED_DEV_FILE_NAME.map( + // Format as /db/[name].ts + // for Vite import.meta.glob + (name) => new URL(name, getDbDirectoryUrl('file:///')).pathname + ); + return ` -import { collectionToTable, createLocalDatabaseClient } from ${RUNTIME_IMPORT}; -import dbUrl from ${JSON.stringify(`${dbUrl}?fileurl`)}; +import { asDrizzleTable, seedLocal } from ${RUNTIME_IMPORT}; +import { db as _db } from ${JSON.stringify(LOCAL_DB_VIRTUAL_MODULE_ID)}; -const params = ${JSON.stringify({ - tables, - seeding: false, - })}; -params.dbUrl = dbUrl; +export const db = _db; -export const db = await createLocalDatabaseClient(params); +${ + shouldSeed + ? `await seedLocal({ + db: _db, + tables: ${JSON.stringify(tables)}, + fileGlob: import.meta.glob(${JSON.stringify(seedFilePaths)}), +})` + : '' +} export * from ${RUNTIME_DRIZZLE_IMPORT}; +export * from ${RUNTIME_CONFIG_IMPORT}; -${getStringifiedCollectionExports(tables)} -`; +${getStringifiedCollectionExports(tables)}`; } export function getStudioVirtualModContents({ @@ -75,13 +130,14 @@ export function getStudioVirtualModContents({ appToken: string; }) { return ` -import {collectionToTable, createRemoteDatabaseClient} from ${RUNTIME_IMPORT}; +import {asDrizzleTable, createRemoteDatabaseClient} from ${RUNTIME_IMPORT}; export const db = await createRemoteDatabaseClient(${JSON.stringify( appToken // Respect runtime env for user overrides in SSR )}, import.meta.env.ASTRO_STUDIO_REMOTE_DB_URL ?? ${JSON.stringify(getRemoteDatabaseUrl())}); export * from ${RUNTIME_DRIZZLE_IMPORT}; +export * from ${RUNTIME_CONFIG_IMPORT}; ${getStringifiedCollectionExports(tables)} `; @@ -91,7 +147,7 @@ function getStringifiedCollectionExports(tables: DBTables) { return Object.entries(tables) .map( ([name, collection]) => - `export const ${name} = collectionToTable(${JSON.stringify(name)}, ${JSON.stringify( + `export const ${name} = asDrizzleTable(${JSON.stringify(name)}, ${JSON.stringify( collection )}, false)` ) diff --git a/packages/db/src/core/load-file.ts b/packages/db/src/core/load-file.ts new file mode 100644 index 000000000000..591ffa3906d3 --- /dev/null +++ b/packages/db/src/core/load-file.ts @@ -0,0 +1,117 @@ +import { build as esbuild } from 'esbuild'; +import { CONFIG_FILE_NAMES, VIRTUAL_MODULE_ID } from './consts.js'; +import { fileURLToPath } from 'node:url'; +import { getConfigVirtualModContents } from './integration/vite-plugin-db.js'; +import { writeFile, unlink } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; +import { getDbDirectoryUrl } from './utils.js'; + +export async function loadDbConfigFile( + root: URL +): Promise<{ mod: { default?: unknown } | undefined; dependencies: string[] }> { + let configFileUrl: URL | undefined; + for (const fileName of CONFIG_FILE_NAMES) { + const fileUrl = new URL(fileName, getDbDirectoryUrl(root)); + if (existsSync(fileUrl)) { + configFileUrl = fileUrl; + } + } + if (!configFileUrl) { + return { mod: undefined, dependencies: [] }; + } + const { code, dependencies } = await bundleFile({ + virtualModContents: getConfigVirtualModContents(), + root, + fileUrl: configFileUrl, + }); + return { + mod: await importBundledFile({ code, root }), + dependencies, + }; +} + +/** + * Bundle arbitrary `mjs` or `ts` file. + * Simplified fork from Vite's `bundleConfigFile` function. + * + * @see https://github.com/vitejs/vite/blob/main/packages/vite/src/node/config.ts#L961 + */ +export async function bundleFile({ + fileUrl, + root, + virtualModContents, +}: { + fileUrl: URL; + root: URL; + virtualModContents: string; +}) { + const result = await esbuild({ + absWorkingDir: process.cwd(), + entryPoints: [fileURLToPath(fileUrl)], + outfile: 'out.js', + packages: 'external', + write: false, + target: ['node16'], + platform: 'node', + bundle: true, + format: 'esm', + sourcemap: 'inline', + metafile: true, + define: { + 'import.meta.env.ASTRO_STUDIO_REMOTE_DB_URL': 'undefined', + }, + plugins: [ + { + name: 'resolve-astro-db', + setup(build) { + build.onResolve({ filter: /^astro:db$/ }, ({ path }) => { + return { path, namespace: VIRTUAL_MODULE_ID }; + }); + build.onLoad({ namespace: VIRTUAL_MODULE_ID, filter: /.*/ }, () => { + return { + contents: virtualModContents, + // Needed to resolve runtime dependencies + resolveDir: fileURLToPath(root), + }; + }); + }, + }, + ], + }); + + const file = result.outputFiles[0]; + if (!file) { + throw new Error(`Unexpected: no output file`); + } + + return { + code: file.text, + dependencies: Object.keys(result.metafile.inputs), + }; +} + +/** + * Forked from Vite config loader, replacing CJS-based path concat with ESM only + * + * @see https://github.com/vitejs/vite/blob/main/packages/vite/src/node/config.ts#L1074 + */ +export async function importBundledFile({ + code, + root, +}: { + code: string; + root: URL; +}): Promise<{ default?: unknown }> { + // Write it to disk, load it with native Node ESM, then delete the file. + const tmpFileUrl = new URL(`./db.timestamp-${Date.now()}.mjs`, root); + await writeFile(tmpFileUrl, code, { encoding: 'utf8' }); + try { + return await import(/* @vite-ignore */ tmpFileUrl.pathname); + } finally { + try { + await unlink(tmpFileUrl); + } catch { + // already removed if this function is called twice simultaneously + } + } +} diff --git a/packages/db/src/core/queries.ts b/packages/db/src/core/queries.ts deleted file mode 100644 index f699a297a3bf..000000000000 --- a/packages/db/src/core/queries.ts +++ /dev/null @@ -1,292 +0,0 @@ -import type { AstroIntegrationLogger } from 'astro'; -import { type SQL, getTableName, sql } from 'drizzle-orm'; -import { SQLiteAsyncDialect, type SQLiteInsert } from 'drizzle-orm/sqlite-core'; -import type { SqliteRemoteDatabase } from 'drizzle-orm/sqlite-proxy'; -import { bold } from 'kleur/colors'; -import { - type BooleanColumn, - type ColumnType, - type DBColumn, - type DBTable, - type DBTables, - type DateColumn, - type JsonColumn, - type NumberColumn, - type TextColumn, -} from '../core/types.js'; -import type { - ColumnsConfig, - DBUserConfig, - MaybeArray, - ResolvedCollectionConfig, -} from '../core/types.js'; -import { hasPrimaryKey } from '../runtime/index.js'; -import { isSerializedSQL } from '../runtime/types.js'; -import { SEED_EMPTY_ARRAY_ERROR, SEED_ERROR, SEED_WRITABLE_IN_PROD_ERROR } from './errors.js'; - -const sqlite = new SQLiteAsyncDialect(); - -export async function recreateTables({ - db, - tables, -}: { - db: SqliteRemoteDatabase; - tables: DBTables; -}) { - const setupQueries: SQL[] = []; - for (const [name, collection] of Object.entries(tables)) { - const dropQuery = sql.raw(`DROP TABLE IF EXISTS ${sqlite.escapeName(name)}`); - const createQuery = sql.raw(getCreateTableQuery(name, collection)); - const indexQueries = getCreateIndexQueries(name, collection); - setupQueries.push(dropQuery, createQuery, ...indexQueries.map((s) => sql.raw(s))); - } - for (const q of setupQueries) { - await db.run(q); - } -} - -export async function seedData({ - db, - data, - logger, - mode, -}: { - db: SqliteRemoteDatabase; - data: DBUserConfig['data']; - logger?: AstroIntegrationLogger; - mode: 'dev' | 'build'; -}) { - const dataFns = Array.isArray(data) ? data : [data]; - try { - for (const dataFn of dataFns) { - await dataFn({ - seed: async (config, values) => { - seedErrorChecks(mode, config, values); - try { - await db.insert(config.table).values(values as any); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - throw new Error(SEED_ERROR(getTableName(config.table), msg)); - } - }, - seedReturning: async (config, values) => { - seedErrorChecks(mode, config, values); - try { - let result: SQLiteInsert = db - .insert(config.table) - .values(values as any) - .returning(); - if (!Array.isArray(values)) { - result = result.get(); - } - return result; - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - throw new Error(SEED_ERROR(getTableName(config.table), msg)); - } - }, - db, - mode, - }); - } - } catch (e) { - if (!(e instanceof Error)) throw e; - (logger ?? console).error(e.message); - } -} - -function seedErrorChecks( - mode: 'dev' | 'build', - { table, writable }: ResolvedCollectionConfig, - values: MaybeArray -) { - const tableName = getTableName(table); - if (writable && mode === 'build' && process.env.ASTRO_DB_TEST_ENV !== '1') { - throw new Error(SEED_WRITABLE_IN_PROD_ERROR(tableName)); - } - if (Array.isArray(values) && values.length === 0) { - throw new Error(SEED_EMPTY_ARRAY_ERROR(tableName)); - } -} - -export function getCreateTableQuery(collectionName: string, collection: DBTable) { - let query = `CREATE TABLE ${sqlite.escapeName(collectionName)} (`; - - const colQueries = []; - const colHasPrimaryKey = Object.entries(collection.columns).find(([, column]) => - hasPrimaryKey(column) - ); - if (!colHasPrimaryKey) { - colQueries.push('_id INTEGER PRIMARY KEY'); - } - for (const [columnName, column] of Object.entries(collection.columns)) { - const colQuery = `${sqlite.escapeName(columnName)} ${schemaTypeToSqlType( - column.type - )}${getModifiers(columnName, column)}`; - colQueries.push(colQuery); - } - - colQueries.push(...getCreateForeignKeyQueries(collectionName, collection)); - - query += colQueries.join(', ') + ')'; - return query; -} - -export function getCreateIndexQueries( - collectionName: string, - collection: Pick -) { - let queries: string[] = []; - for (const [indexName, indexProps] of Object.entries(collection.indexes ?? {})) { - const onColNames = asArray(indexProps.on); - const onCols = onColNames.map((colName) => sqlite.escapeName(colName)); - - const unique = indexProps.unique ? 'UNIQUE ' : ''; - const indexQuery = `CREATE ${unique}INDEX ${sqlite.escapeName( - indexName - )} ON ${sqlite.escapeName(collectionName)} (${onCols.join(', ')})`; - queries.push(indexQuery); - } - return queries; -} - -export function getCreateForeignKeyQueries(collectionName: string, collection: DBTable) { - let queries: string[] = []; - for (const foreignKey of collection.foreignKeys ?? []) { - const columns = asArray(foreignKey.columns); - const references = asArray(foreignKey.references); - - if (columns.length !== references.length) { - throw new Error( - `Foreign key on ${collectionName} is misconfigured. \`columns\` and \`references\` must be the same length.` - ); - } - const referencedCollection = references[0]?.schema.collection; - if (!referencedCollection) { - throw new Error( - `Foreign key on ${collectionName} is misconfigured. \`references\` cannot be empty.` - ); - } - const query = `FOREIGN KEY (${columns - .map((f) => sqlite.escapeName(f)) - .join(', ')}) REFERENCES ${sqlite.escapeName(referencedCollection)}(${references - .map((r) => sqlite.escapeName(r.schema.name!)) - .join(', ')})`; - queries.push(query); - } - return queries; -} - -function asArray(value: T | T[]) { - return Array.isArray(value) ? value : [value]; -} - -export function schemaTypeToSqlType(type: ColumnType): 'text' | 'integer' { - switch (type) { - case 'date': - case 'text': - case 'json': - return 'text'; - case 'number': - case 'boolean': - return 'integer'; - } -} - -export function getModifiers(columnName: string, column: DBColumn) { - let modifiers = ''; - if (hasPrimaryKey(column)) { - return ' PRIMARY KEY'; - } - if (!column.schema.optional) { - modifiers += ' NOT NULL'; - } - if (column.schema.unique) { - modifiers += ' UNIQUE'; - } - if (hasDefault(column)) { - modifiers += ` DEFAULT ${getDefaultValueSql(columnName, column)}`; - } - const references = getReferencesConfig(column); - if (references) { - const { collection, name } = references.schema; - if (!collection || !name) { - throw new Error( - `Column ${collection}.${name} references a collection that does not exist. Did you apply the referenced collection to the \`tables\` object in your Astro config?` - ); - } - - modifiers += ` REFERENCES ${sqlite.escapeName(collection)} (${sqlite.escapeName(name)})`; - } - return modifiers; -} - -export function getReferencesConfig(column: DBColumn) { - const canHaveReferences = column.type === 'number' || column.type === 'text'; - if (!canHaveReferences) return undefined; - return column.schema.references; -} - -// Using `DBColumn` will not narrow `default` based on the column `type` -// Handle each column separately -type WithDefaultDefined = T & { - schema: Required>; -}; -type DBColumnWithDefault = - | WithDefaultDefined - | WithDefaultDefined - | WithDefaultDefined - | WithDefaultDefined - | WithDefaultDefined; - -// Type narrowing the default fails on union types, so use a type guard -export function hasDefault(column: DBColumn): column is DBColumnWithDefault { - if (column.schema.default !== undefined) { - return true; - } - if (hasPrimaryKey(column) && column.type === 'number') { - return true; - } - return false; -} - -function toDefault(def: T | SQL): string { - const type = typeof def; - if (type === 'string') { - return sqlite.escapeString(def as string); - } else if (type === 'boolean') { - return def ? 'TRUE' : 'FALSE'; - } else { - return def + ''; - } -} - -function getDefaultValueSql(columnName: string, column: DBColumnWithDefault): string { - if (isSerializedSQL(column.schema.default)) { - return column.schema.default.sql; - } - - switch (column.type) { - case 'boolean': - case 'number': - case 'text': - case 'date': - return toDefault(column.schema.default); - case 'json': { - let stringified = ''; - try { - stringified = JSON.stringify(column.schema.default); - } catch (e) { - // eslint-disable-next-line no-console - console.log( - `Invalid default value for column ${bold( - columnName - )}. Defaults must be valid JSON when using the \`json()\` type.` - ); - process.exit(0); - } - - return sqlite.escapeString(stringified); - } - } -} diff --git a/packages/db/src/core/types.ts b/packages/db/src/core/types.ts index a4896a8bead6..f9de8f8ee519 100644 --- a/packages/db/src/core/types.ts +++ b/packages/db/src/core/types.ts @@ -1,8 +1,6 @@ -import type { InferSelectModel } from 'drizzle-orm'; import { SQL } from 'drizzle-orm'; -import { SQLiteAsyncDialect, type SQLiteInsertValue } from 'drizzle-orm/sqlite-core'; +import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; import { type ZodTypeDef, z } from 'zod'; -import { type SqliteDB, type Table, collectionToTable } from '../runtime/index.js'; import { SERIALIZED_SQL_KEY, type SerializedSQL } from '../runtime/types.js'; import { errorMap } from './integration/error-map.js'; @@ -26,6 +24,7 @@ const baseColumnSchema = z.object({ // Defined when `defineReadableTable()` is called name: z.string().optional(), + // TODO: rename to `tableName`. Breaking schema change collection: z.string().optional(), }); @@ -181,41 +180,26 @@ const foreignKeysSchema: z.ZodType>; -const baseCollectionSchema = z.object({ +export const tableSchema = z.object({ columns: columnsSchema, indexes: z.record(indexSchema).optional(), foreignKeys: z.array(foreignKeysSchema).optional(), }); -export const readableCollectionSchema = baseCollectionSchema.extend({ - writable: z.literal(false), -}); - -export const writableCollectionSchema = baseCollectionSchema.extend({ - writable: z.literal(true), -}); - -export const collectionSchema = z.union([readableCollectionSchema, writableCollectionSchema]); -export const tablesSchema = z.preprocess((rawCollections) => { +export const tablesSchema = z.preprocess((rawTables) => { // Use `z.any()` to avoid breaking object references - const tables = z.record(z.any()).parse(rawCollections, { errorMap }); - for (const [collectionName, collection] of Object.entries(tables)) { - // Append `table` object for data seeding. - // Must append at runtime so table name exists. - collection.table = collectionToTable( - collectionName, - collectionSchema.parse(collection, { errorMap }) - ); - // Append collection and column names to columns. - // Used to track collection info for references. - const { columns } = z.object({ columns: z.record(z.any()) }).parse(collection, { errorMap }); + const tables = z.record(z.any()).parse(rawTables, { errorMap }); + for (const [tableName, table] of Object.entries(tables)) { + // Append table and column names to columns. + // Used to track table info for references. + const { columns } = z.object({ columns: z.record(z.any()) }).parse(table, { errorMap }); for (const [columnName, column] of Object.entries(columns)) { column.schema.name = columnName; - column.schema.collection = collectionName; + column.schema.collection = tableName; } } - return rawCollections; -}, z.record(collectionSchema)); + return rawTables; +}, z.record(tableSchema)); export type BooleanColumn = z.infer; export type BooleanColumnInput = z.input; @@ -243,7 +227,7 @@ export type DBColumnInput = | TextColumnInput | JsonColumnInput; export type DBColumns = z.infer; -export type DBTable = z.infer; +export type DBTable = z.infer; export type DBTables = Record; export type DBSnapshot = { schema: Record; @@ -253,62 +237,24 @@ export type DBSnapshot = { */ experimentalVersion: number; }; -export type ReadableDBTable = z.infer; -export type WritableDBTable = z.infer; - -export type DBDataContext = { - db: SqliteDB; - seed: ( - collection: ResolvedCollectionConfig, - data: MaybeArray>> - ) => Promise; - seedReturning: < - TColumns extends ColumnsConfig, - TData extends MaybeArray>>, - >( - collection: ResolvedCollectionConfig, - data: TData - ) => Promise< - TData extends Array>> - ? InferSelectModel>[] - : InferSelectModel> - >; - mode: 'dev' | 'build'; -}; - -export function defineData(fn: (ctx: DBDataContext) => MaybePromise) { - return fn; -} - -const dbDataFn = z.function().returns(z.union([z.void(), z.promise(z.void())])); export const dbConfigSchema = z.object({ - studio: z.boolean().optional(), tables: tablesSchema.optional(), - data: z.union([dbDataFn, z.array(dbDataFn)]).optional(), - unsafeWritable: z.boolean().optional().default(false), }); -type DataFunction = (params: DBDataContext) => MaybePromise; - -export type DBUserConfig = Omit, 'data'> & { - data: DataFunction | DataFunction[]; -}; +export type DBConfigInput = z.input; +export type DBConfig = z.infer; -export const astroConfigWithDbSchema = z.object({ - db: dbConfigSchema.optional(), -}); +export type ColumnsConfig = z.input['columns']; +export type OutputColumnsConfig = z.output['columns']; -export type ColumnsConfig = z.input['columns']; - -interface CollectionConfig +export interface TableConfig // use `extends` to ensure types line up with zod, // only adding generics for type completions. - extends Pick, 'columns' | 'indexes' | 'foreignKeys'> { + extends Pick, 'columns' | 'indexes' | 'foreignKeys'> { columns: TColumns; foreignKeys?: Array<{ columns: MaybeArray>; - // TODO: runtime error if parent collection doesn't match for all columns. Can't put a generic here... references: () => MaybeArray>; }>; indexes?: Record>; @@ -318,69 +264,11 @@ interface IndexConfig extends z.input>; } -export type ResolvedCollectionConfig< - TColumns extends ColumnsConfig = ColumnsConfig, - Writable extends boolean = boolean, -> = CollectionConfig & { - writable: Writable; - table: Table; -}; - -function baseDefineCollection( - userConfig: CollectionConfig, - writable: TWritable -): ResolvedCollectionConfig { - return { - ...userConfig, - writable, - // set at runtime to get the table name - table: null!, - }; -} - -export function defineReadableTable( - userConfig: CollectionConfig -): ResolvedCollectionConfig { - return baseDefineCollection(userConfig, false); -} - -export function defineWritableTable( - userConfig: CollectionConfig -): ResolvedCollectionConfig { - return baseDefineCollection(userConfig, true); -} - -export type AstroConfigWithDB = z.input; +/** @deprecated Use `TableConfig` instead */ +export type ResolvedCollectionConfig = + TableConfig; // We cannot use `Omit`, // since Omit collapses our union type on primary key. -type NumberColumnOpts = z.input; -type TextColumnOpts = z.input; - -function createColumn>(type: S, schema: T) { - return { - type, - /** - * @internal - */ - schema, - }; -} - -export const column = { - number: (opts: T = {} as T) => { - return createColumn('number', opts) satisfies { type: 'number' }; - }, - boolean: (opts: T = {} as T) => { - return createColumn('boolean', opts) satisfies { type: 'boolean' }; - }, - text: (opts: T = {} as T) => { - return createColumn('text', opts) satisfies { type: 'text' }; - }, - date(opts: T = {} as T) { - return createColumn('date', opts) satisfies { type: 'date' }; - }, - json(opts: T = {} as T) { - return createColumn('json', opts) satisfies { type: 'json' }; - }, -}; +export type NumberColumnOpts = z.input; +export type TextColumnOpts = z.input; diff --git a/packages/db/src/core/utils.ts b/packages/db/src/core/utils.ts index b579770400bc..674514711075 100644 --- a/packages/db/src/core/utils.ts +++ b/packages/db/src/core/utils.ts @@ -17,3 +17,11 @@ export function getAstroStudioUrl(): string { const env = getAstroStudioEnv(); return env.ASTRO_STUDIO_URL || 'https://stardate.astro.build'; } + +export function getDbDirectoryUrl(root: URL | string) { + return new URL('db/', root); +} + +export function getMigrationsDirectoryUrl(root: URL | string) { + return new URL('migrations/', getDbDirectoryUrl(root)); +} diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index f280f35587b7..0073f2b33c16 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -1,5 +1,4 @@ -export { defineReadableTable, defineWritableTable, defineData, column } from './core/types.js'; -export type { ResolvedCollectionConfig, DBDataContext } from './core/types.js'; +export type { ResolvedCollectionConfig, TableConfig } from './core/types.js'; export { cli } from './core/cli/index.js'; export { integration as default } from './core/integration/index.js'; -export { sql, NOW, TRUE, FALSE } from './runtime/index.js'; +export { sql, NOW, TRUE, FALSE, defineDB, defineTable, column } from './runtime/config.js'; diff --git a/packages/db/src/runtime/config.ts b/packages/db/src/runtime/config.ts new file mode 100644 index 000000000000..5fb87e7a5c15 --- /dev/null +++ b/packages/db/src/runtime/config.ts @@ -0,0 +1,48 @@ +import type { + BooleanColumnInput, + ColumnsConfig, + DBConfigInput, + DateColumnInput, + JsonColumnInput, + NumberColumnOpts, + TableConfig, + TextColumnOpts, +} from '../core/types.js'; + +function createColumn>(type: S, schema: T) { + return { + type, + /** + * @internal + */ + schema, + }; +} + +export const column = { + number: (opts: T = {} as T) => { + return createColumn('number', opts) satisfies { type: 'number' }; + }, + boolean: (opts: T = {} as T) => { + return createColumn('boolean', opts) satisfies { type: 'boolean' }; + }, + text: (opts: T = {} as T) => { + return createColumn('text', opts) satisfies { type: 'text' }; + }, + date(opts: T = {} as T) { + return createColumn('date', opts) satisfies { type: 'date' }; + }, + json(opts: T = {} as T) { + return createColumn('json', opts) satisfies { type: 'json' }; + }, +}; + +export function defineTable(userConfig: TableConfig) { + return userConfig; +} + +export function defineDB(userConfig: DBConfigInput) { + return userConfig; +} + +export { sql, NOW, TRUE, FALSE } from './index.js'; diff --git a/packages/db/src/runtime/db-client.ts b/packages/db/src/runtime/db-client.ts index 9058206760af..a16a7b7c0540 100644 --- a/packages/db/src/runtime/db-client.ts +++ b/packages/db/src/runtime/db-client.ts @@ -1,59 +1,19 @@ import type { InStatement } from '@libsql/client'; import { createClient } from '@libsql/client'; -import { getTableName } from 'drizzle-orm'; import type { LibSQLDatabase } from 'drizzle-orm/libsql'; import { drizzle as drizzleLibsql } from 'drizzle-orm/libsql'; -import { type SQLiteTable } from 'drizzle-orm/sqlite-core'; import { drizzle as drizzleProxy } from 'drizzle-orm/sqlite-proxy'; import { z } from 'zod'; -import { type DBTables } from '../core/types.js'; const isWebContainer = !!process.versions?.webcontainer; -interface LocalDatabaseClient extends LibSQLDatabase, Disposable {} - -export async function createLocalDatabaseClient({ - tables, - dbUrl, - seeding, -}: { - dbUrl: string; - tables: DBTables; - seeding: boolean; -}): Promise { +export function createLocalDatabaseClient({ dbUrl }: { dbUrl: string }): LibSQLDatabase { const url = isWebContainer ? 'file:content.db' : dbUrl; - const client = createClient({ url }); - const db = Object.assign(drizzleLibsql(client), { - [Symbol.dispose || Symbol.for('Symbol.dispose')]() { - client.close(); - }, - }); - - if (seeding) return db; - - const { insert: drizzleInsert, update: drizzleUpdate, delete: drizzleDelete } = db; - return Object.assign(db, { - insert(Table: SQLiteTable) { - checkIfModificationIsAllowed(tables, Table); - return drizzleInsert.call(this, Table); - }, - update(Table: SQLiteTable) { - checkIfModificationIsAllowed(tables, Table); - return drizzleUpdate.call(this, Table); - }, - delete(Table: SQLiteTable) { - checkIfModificationIsAllowed(tables, Table); - return drizzleDelete.call(this, Table); - }, - }); -} + console.log('memory', process.env.TEST_IN_MEMORY_DB); + const client = createClient({ url: process.env.TEST_IN_MEMORY_DB ? ':memory:' : url }); + const db = drizzleLibsql(client); -function checkIfModificationIsAllowed(tables: DBTables, Table: SQLiteTable) { - const tableName = getTableName(Table); - const collection = tables[tableName]; - if (!collection.writable) { - throw new Error(`The [${tableName}] collection is read-only.`); - } + return db; } export function createRemoteDatabaseClient(appToken: string, remoteDbURL: string) { @@ -61,8 +21,6 @@ export function createRemoteDatabaseClient(appToken: string, remoteDbURL: string const db = drizzleProxy(async (sql, parameters, method) => { const requestBody: InStatement = { sql, args: parameters }; - // eslint-disable-next-line no-console - console.info(JSON.stringify(requestBody)); const res = await fetch(url, { method: 'POST', headers: { @@ -107,5 +65,9 @@ export function createRemoteDatabaseClient(appToken: string, remoteDbURL: string return { rows: rowValues }; }); + + (db as any).batch = (_drizzleQueries: Array>) => { + throw new Error('db.batch() is not currently supported.'); + }; return db; } diff --git a/packages/db/src/runtime/index.ts b/packages/db/src/runtime/index.ts index 7ab5264cdca3..d68521f43511 100644 --- a/packages/db/src/runtime/index.ts +++ b/packages/db/src/runtime/index.ts @@ -1,3 +1,4 @@ +import type { LibSQLDatabase } from 'drizzle-orm/libsql'; import { type ColumnBuilderBaseConfig, type ColumnDataType, sql } from 'drizzle-orm'; import { type IndexBuilder, @@ -8,14 +9,14 @@ import { sqliteTable, text, } from 'drizzle-orm/sqlite-core'; -import type { SqliteRemoteDatabase } from 'drizzle-orm/sqlite-proxy'; import { type DBColumn, type DBTable } from '../core/types.js'; import { type SerializedSQL, isSerializedSQL } from './types.js'; export { sql }; -export type SqliteDB = SqliteRemoteDatabase; +export type SqliteDB = LibSQLDatabase; export type { Table } from './types.js'; export { createRemoteDatabaseClient, createLocalDatabaseClient } from './db-client.js'; +export { seedLocal } from './queries.js'; export function hasPrimaryKey(column: DBColumn) { return 'primaryKey' in column.schema && !!column.schema.primaryKey; @@ -54,17 +55,17 @@ type D1ColumnBuilder = SQLiteColumnBuilderBase< ColumnBuilderBaseConfig & { data: unknown } >; -export function collectionToTable(name: string, collection: DBTable) { +export function asDrizzleTable(name: string, table: DBTable) { const columns: Record = {}; - if (!Object.entries(collection.columns).some(([, column]) => hasPrimaryKey(column))) { + if (!Object.entries(table.columns).some(([, column]) => hasPrimaryKey(column))) { columns['_id'] = integer('_id').primaryKey(); } - for (const [columnName, column] of Object.entries(collection.columns)) { + for (const [columnName, column] of Object.entries(table.columns)) { columns[columnName] = columnMapper(columnName, column); } - const table = sqliteTable(name, columns, (ormTable) => { + const drizzleTable = sqliteTable(name, columns, (ormTable) => { const indexes: Record = {}; - for (const [indexName, indexProps] of Object.entries(collection.indexes ?? {})) { + for (const [indexName, indexProps] of Object.entries(table.indexes ?? {})) { const onColNames = Array.isArray(indexProps.on) ? indexProps.on : [indexProps.on]; const onCols = onColNames.map((colName) => ormTable[colName]); if (!atLeastOne(onCols)) continue; @@ -73,7 +74,7 @@ export function collectionToTable(name: string, collection: DBTable) { } return indexes; }); - return table; + return drizzleTable; } function atLeastOne(arr: T[]): arr is [T, ...T[]] { diff --git a/packages/db/src/runtime/queries.ts b/packages/db/src/runtime/queries.ts new file mode 100644 index 000000000000..b81b06edbe7f --- /dev/null +++ b/packages/db/src/runtime/queries.ts @@ -0,0 +1,244 @@ +import type { + BooleanColumn, + DBTable, + DBTables, + DBColumn, + DateColumn, + ColumnType, + JsonColumn, + NumberColumn, + TextColumn, +} from '../core/types.js'; +import { bold } from 'kleur/colors'; +import { type SQL, sql } from 'drizzle-orm'; +import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; +import { hasPrimaryKey, type SqliteDB } from './index.js'; +import { isSerializedSQL } from './types.js'; +import { + FOREIGN_KEY_REFERENCES_LENGTH_ERROR, + FOREIGN_KEY_REFERENCES_EMPTY_ERROR, + REFERENCE_DNE_ERROR, + FOREIGN_KEY_DNE_ERROR, + SEED_ERROR, +} from '../core/errors.js'; +import { LibsqlError } from '@libsql/client'; + +const sqlite = new SQLiteAsyncDialect(); + +export const SEED_DEV_FILE_NAME = ['seed.ts', 'seed.js', 'seed.mjs', 'seed.mts']; + +export async function seedLocal({ + db, + tables, + // Glob all potential seed files to catch renames and deletions. + fileGlob, +}: { + db: SqliteDB; + tables: DBTables; + fileGlob: Record Promise>; +}) { + await recreateTables({ db, tables }); + for (const fileName of SEED_DEV_FILE_NAME) { + const key = Object.keys(fileGlob).find((f) => f.endsWith(fileName)); + if (key) { + await fileGlob[key]().catch((e) => { + if (e instanceof LibsqlError) { + throw new Error(SEED_ERROR(e.message)); + } + throw e; + }); + return; + } + } +} + +export async function recreateTables({ db, tables }: { db: SqliteDB; tables: DBTables }) { + const setupQueries: SQL[] = []; + for (const [name, table] of Object.entries(tables)) { + const dropQuery = sql.raw(`DROP TABLE IF EXISTS ${sqlite.escapeName(name)}`); + const createQuery = sql.raw(getCreateTableQuery(name, table)); + const indexQueries = getCreateIndexQueries(name, table); + setupQueries.push(dropQuery, createQuery, ...indexQueries.map((s) => sql.raw(s))); + } + await db.batch([ + db.run(sql`pragma defer_foreign_keys=true;`), + ...setupQueries.map((q) => db.run(q)), + ]); +} + +export function getCreateTableQuery(tableName: string, table: DBTable) { + let query = `CREATE TABLE ${sqlite.escapeName(tableName)} (`; + + const colQueries = []; + const colHasPrimaryKey = Object.entries(table.columns).find(([, column]) => + hasPrimaryKey(column) + ); + if (!colHasPrimaryKey) { + colQueries.push('_id INTEGER PRIMARY KEY'); + } + for (const [columnName, column] of Object.entries(table.columns)) { + const colQuery = `${sqlite.escapeName(columnName)} ${schemaTypeToSqlType( + column.type + )}${getModifiers(columnName, column)}`; + colQueries.push(colQuery); + } + + colQueries.push(...getCreateForeignKeyQueries(tableName, table)); + + query += colQueries.join(', ') + ')'; + return query; +} + +export function getCreateIndexQueries(tableName: string, table: Pick) { + let queries: string[] = []; + for (const [indexName, indexProps] of Object.entries(table.indexes ?? {})) { + const onColNames = asArray(indexProps.on); + const onCols = onColNames.map((colName) => sqlite.escapeName(colName)); + + const unique = indexProps.unique ? 'UNIQUE ' : ''; + const indexQuery = `CREATE ${unique}INDEX ${sqlite.escapeName( + indexName + )} ON ${sqlite.escapeName(tableName)} (${onCols.join(', ')})`; + queries.push(indexQuery); + } + return queries; +} + +export function getCreateForeignKeyQueries(tableName: string, table: DBTable) { + let queries: string[] = []; + for (const foreignKey of table.foreignKeys ?? []) { + const columns = asArray(foreignKey.columns); + const references = asArray(foreignKey.references); + + if (columns.length !== references.length) { + throw new Error(FOREIGN_KEY_REFERENCES_LENGTH_ERROR(tableName)); + } + const firstReference = references[0]; + if (!firstReference) { + throw new Error(FOREIGN_KEY_REFERENCES_EMPTY_ERROR(tableName)); + } + const referencedTable = firstReference.schema.collection; + if (!referencedTable) { + throw new Error(FOREIGN_KEY_DNE_ERROR(tableName)); + } + const query = `FOREIGN KEY (${columns + .map((f) => sqlite.escapeName(f)) + .join(', ')}) REFERENCES ${sqlite.escapeName(referencedTable)}(${references + .map((r) => sqlite.escapeName(r.schema.name!)) + .join(', ')})`; + queries.push(query); + } + return queries; +} + +function asArray(value: T | T[]) { + return Array.isArray(value) ? value : [value]; +} + +export function schemaTypeToSqlType(type: ColumnType): 'text' | 'integer' { + switch (type) { + case 'date': + case 'text': + case 'json': + return 'text'; + case 'number': + case 'boolean': + return 'integer'; + } +} + +export function getModifiers(columnName: string, column: DBColumn) { + let modifiers = ''; + if (hasPrimaryKey(column)) { + return ' PRIMARY KEY'; + } + if (!column.schema.optional) { + modifiers += ' NOT NULL'; + } + if (column.schema.unique) { + modifiers += ' UNIQUE'; + } + if (hasDefault(column)) { + modifiers += ` DEFAULT ${getDefaultValueSql(columnName, column)}`; + } + const references = getReferencesConfig(column); + if (references) { + const { collection: tableName, name } = references.schema; + if (!tableName || !name) { + throw new Error(REFERENCE_DNE_ERROR(columnName)); + } + + modifiers += ` REFERENCES ${sqlite.escapeName(tableName)} (${sqlite.escapeName(name)})`; + } + return modifiers; +} + +export function getReferencesConfig(column: DBColumn) { + const canHaveReferences = column.type === 'number' || column.type === 'text'; + if (!canHaveReferences) return undefined; + return column.schema.references; +} + +// Using `DBColumn` will not narrow `default` based on the column `type` +// Handle each column separately +type WithDefaultDefined = T & { + schema: Required>; +}; +type DBColumnWithDefault = + | WithDefaultDefined + | WithDefaultDefined + | WithDefaultDefined + | WithDefaultDefined + | WithDefaultDefined; + +// Type narrowing the default fails on union types, so use a type guard +export function hasDefault(column: DBColumn): column is DBColumnWithDefault { + if (column.schema.default !== undefined) { + return true; + } + if (hasPrimaryKey(column) && column.type === 'number') { + return true; + } + return false; +} + +function toDefault(def: T | SQL): string { + const type = typeof def; + if (type === 'string') { + return sqlite.escapeString(def as string); + } else if (type === 'boolean') { + return def ? 'TRUE' : 'FALSE'; + } else { + return def + ''; + } +} + +function getDefaultValueSql(columnName: string, column: DBColumnWithDefault): string { + if (isSerializedSQL(column.schema.default)) { + return column.schema.default.sql; + } + + switch (column.type) { + case 'boolean': + case 'number': + case 'text': + case 'date': + return toDefault(column.schema.default); + case 'json': { + let stringified = ''; + try { + stringified = JSON.stringify(column.schema.default); + } catch (e) { + // eslint-disable-next-line no-console + console.log( + `Invalid default value for column ${bold( + columnName + )}. Defaults must be valid JSON when using the \`json()\` type.` + ); + process.exit(0); + } + + return sqlite.escapeString(stringified); + } + } +} diff --git a/packages/db/src/runtime/types.ts b/packages/db/src/runtime/types.ts index 9bc35bda61ac..08ab16a0cc71 100644 --- a/packages/db/src/runtime/types.ts +++ b/packages/db/src/runtime/types.ts @@ -1,6 +1,6 @@ import type { ColumnBaseConfig, ColumnDataType } from 'drizzle-orm'; import type { SQLiteColumn, SQLiteTableWithColumns } from 'drizzle-orm/sqlite-core'; -import type { ColumnsConfig, DBColumn } from '../core/types.js'; +import type { ColumnsConfig, DBColumn, OutputColumnsConfig } from '../core/types.js'; type GeneratedConfig = Pick< ColumnBaseConfig, @@ -76,7 +76,7 @@ export type Column = T ex export type Table< TTableName extends string, - TColumns extends ColumnsConfig, + TColumns extends OutputColumnsConfig | ColumnsConfig, > = SQLiteTableWithColumns<{ name: TTableName; schema: undefined; diff --git a/packages/db/src/utils.ts b/packages/db/src/utils.ts new file mode 100644 index 000000000000..0b4c31832f06 --- /dev/null +++ b/packages/db/src/utils.ts @@ -0,0 +1 @@ +export { asDrizzleTable } from './runtime/index.js'; diff --git a/packages/db/test/basics.test.js b/packages/db/test/basics.test.js index 8a8d9caea2e6..19c105532b12 100644 --- a/packages/db/test/basics.test.js +++ b/packages/db/test/basics.test.js @@ -3,10 +3,6 @@ import { load as cheerioLoad } from 'cheerio'; import testAdapter from '../../astro/test/test-adapter.js'; import { loadFixture } from '../../astro/test/test-utils.js'; -// TODO(fks): Rename this to something more generic/generally useful -// like `ASTRO_MONOREPO_TEST_ENV` if @astrojs/db is merged into astro. -process.env.ASTRO_DB_TEST_ENV = '1'; - describe('astro:db', () => { let fixture; before(async () => { @@ -17,16 +13,25 @@ describe('astro:db', () => { }); }); - describe('production', () => { + // Note(bholmesdev): Use in-memory db to avoid + // Multiple dev servers trying to unlink and remount + // the same database file. + process.env.TEST_IN_MEMORY_DB = 'true'; + describe('development', () => { + let devServer; + before(async () => { - await fixture.build(); + console.log('starting dev server'); + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + process.env.TEST_IN_MEMORY_DB = undefined; }); it('Prints the list of authors', async () => { - const app = await fixture.loadTestAdapterApp(); - const request = new Request('http://example.com/'); - const res = await app.render(request); - const html = await res.text(); + const html = await fixture.fetch('/').then((res) => res.text()); const $ = cheerioLoad(html); const ul = $('.authors-list'); @@ -34,71 +39,36 @@ describe('astro:db', () => { expect(ul.children().eq(0).text()).to.equal('Ben'); }); - it('Errors when inserting to a readonly collection', async () => { - const app = await fixture.loadTestAdapterApp(); - const request = new Request('http://example.com/insert-into-readonly'); - const res = await app.render(request); - const html = await res.text(); + it('Allows expression defaults for date columns', async () => { + const html = await fixture.fetch('/').then((res) => res.text()); const $ = cheerioLoad(html); - expect($('#error').text()).to.equal('The [Author] collection is read-only.'); + const themeAdded = $($('.themes-list .theme-added')[0]).text(); + expect(new Date(themeAdded).getTime()).to.not.be.NaN; }); - it('Does not error when inserting into writable collection', async () => { - const app = await fixture.loadTestAdapterApp(); - const request = new Request('http://example.com/insert-into-writable'); - const res = await app.render(request); - const html = await res.text(); + it('Defaults can be overridden for dates', async () => { + const html = await fixture.fetch('/').then((res) => res.text()); const $ = cheerioLoad(html); - expect($('#error').text()).to.equal(''); + const themeAdded = $($('.themes-list .theme-added')[1]).text(); + expect(new Date(themeAdded).getTime()).to.not.be.NaN; }); - describe('Expression defaults', () => { - let app; - before(async () => { - app = await fixture.loadTestAdapterApp(); - }); - - it('Allows expression defaults for date columns', async () => { - const request = new Request('http://example.com/'); - const res = await app.render(request); - const html = await res.text(); - const $ = cheerioLoad(html); - - const themeAdded = $($('.themes-list .theme-added')[0]).text(); - expect(new Date(themeAdded).getTime()).to.not.be.NaN; - }); - - it('Defaults can be overridden for dates', async () => { - const request = new Request('http://example.com/'); - const res = await app.render(request); - const html = await res.text(); - const $ = cheerioLoad(html); - - const themeAdded = $($('.themes-list .theme-added')[1]).text(); - expect(new Date(themeAdded).getTime()).to.not.be.NaN; - }); - - it('Allows expression defaults for text columns', async () => { - const request = new Request('http://example.com/'); - const res = await app.render(request); - const html = await res.text(); - const $ = cheerioLoad(html); + it('Allows expression defaults for text columns', async () => { + const html = await fixture.fetch('/').then((res) => res.text()); + const $ = cheerioLoad(html); - const themeOwner = $($('.themes-list .theme-owner')[0]).text(); - expect(themeOwner).to.equal(''); - }); + const themeOwner = $($('.themes-list .theme-owner')[0]).text(); + expect(themeOwner).to.equal(''); + }); - it('Allows expression defaults for boolean columns', async () => { - const request = new Request('http://example.com/'); - const res = await app.render(request); - const html = await res.text(); - const $ = cheerioLoad(html); + it('Allows expression defaults for boolean columns', async () => { + const html = await fixture.fetch('/').then((res) => res.text()); + const $ = cheerioLoad(html); - const themeDark = $($('.themes-list .theme-dark')[0]).text(); - expect(themeDark).to.equal('dark mode'); - }); + const themeDark = $($('.themes-list .theme-dark')[0]).text(); + expect(themeDark).to.equal('dark mode'); }); }); }); diff --git a/packages/db/test/fixtures/basics/astro.config.mjs b/packages/db/test/fixtures/basics/astro.config.mjs new file mode 100644 index 000000000000..5ff1200e2418 --- /dev/null +++ b/packages/db/test/fixtures/basics/astro.config.mjs @@ -0,0 +1,7 @@ +import db from '@astrojs/db'; +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + integrations: [db()], +}); diff --git a/packages/db/test/fixtures/basics/astro.config.ts b/packages/db/test/fixtures/basics/astro.config.ts deleted file mode 100644 index 665568a82129..000000000000 --- a/packages/db/test/fixtures/basics/astro.config.ts +++ /dev/null @@ -1,28 +0,0 @@ -import db, { defineReadableTable, column } from '@astrojs/db'; -import { defineConfig } from 'astro/config'; -import { themes } from './themes-integration'; - -const Author = defineReadableTable({ - columns: { - name: column.text(), - }, -}); - -// https://astro.build/config -export default defineConfig({ - integrations: [db(), themes()], - db: { - studio: false, - unsafeWritable: true, - tables: { Author }, - async data({ seed }) { - await seed(Author, [ - { name: 'Ben' }, - { name: 'Nate' }, - { name: 'Erika' }, - { name: 'Bjorn' }, - { name: 'Sarah' }, - ]); - }, - }, -}); diff --git a/packages/db/test/fixtures/basics/db/config.ts b/packages/db/test/fixtures/basics/db/config.ts new file mode 100644 index 000000000000..b2ed2066f1e1 --- /dev/null +++ b/packages/db/test/fixtures/basics/db/config.ts @@ -0,0 +1,12 @@ +import { defineDB, defineTable, column } from 'astro:db'; +import { Themes } from './theme'; + +const Author = defineTable({ + columns: { + name: column.text(), + }, +}); + +export default defineDB({ + tables: { Author, Themes }, +}); diff --git a/packages/db/test/fixtures/basics/db/seed.ts b/packages/db/test/fixtures/basics/db/seed.ts new file mode 100644 index 000000000000..c1b61e0996db --- /dev/null +++ b/packages/db/test/fixtures/basics/db/seed.ts @@ -0,0 +1,19 @@ +import { db, Author } from 'astro:db'; +import { Themes as ThemesConfig } from './theme'; +import { asDrizzleTable } from '@astrojs/db/utils'; + +const Themes = asDrizzleTable('Themes', ThemesConfig); + +await db + .insert(Themes) + .values([{ name: 'dracula' }, { name: 'monokai', added: new Date() }]) + .returning({ name: Themes.name }); +await db + .insert(Author) + .values([ + { name: 'Ben' }, + { name: 'Nate' }, + { name: 'Erika' }, + { name: 'Bjorn' }, + { name: 'Sarah' }, + ]); diff --git a/packages/db/test/fixtures/basics/db/theme.ts b/packages/db/test/fixtures/basics/db/theme.ts new file mode 100644 index 000000000000..d3b89b68c4b8 --- /dev/null +++ b/packages/db/test/fixtures/basics/db/theme.ts @@ -0,0 +1,15 @@ +import { defineTable, column, NOW, sql } from 'astro:db'; + +export const Themes = defineTable({ + columns: { + name: column.text(), + added: column.date({ + default: sql`CURRENT_TIMESTAMP`, + }), + updated: column.date({ + default: NOW, + }), + isDark: column.boolean({ default: sql`TRUE` }), + owner: column.text({ optional: true, default: sql`NULL` }), + }, +}); diff --git a/packages/db/test/fixtures/basics/package.json b/packages/db/test/fixtures/basics/package.json index f537f998df79..af7cbe229800 100644 --- a/packages/db/test/fixtures/basics/package.json +++ b/packages/db/test/fixtures/basics/package.json @@ -2,6 +2,11 @@ "name": "@test/db-aliases", "version": "0.0.0", "private": true, + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview" + }, "dependencies": { "@astrojs/db": "workspace:*", "astro": "workspace:*" diff --git a/packages/db/test/fixtures/basics/src/pages/index.astro b/packages/db/test/fixtures/basics/src/pages/index.astro index 4e8882d57e0a..2d21f81103b8 100644 --- a/packages/db/test/fixtures/basics/src/pages/index.astro +++ b/packages/db/test/fixtures/basics/src/pages/index.astro @@ -1,4 +1,5 @@ --- +/// import { Author, db, Themes } from 'astro:db'; const authors = await db.select().from(Author); diff --git a/packages/db/test/fixtures/basics/src/pages/insert-into-readonly.astro b/packages/db/test/fixtures/basics/src/pages/insert-into-readonly.astro deleted file mode 100644 index 7a6bb37bc3db..000000000000 --- a/packages/db/test/fixtures/basics/src/pages/insert-into-readonly.astro +++ /dev/null @@ -1,14 +0,0 @@ ---- -import { Author, db } from 'astro:db'; - -const authors = await db.select().from(Author); - -let error: any = {}; -try { - db.insert(Author).values({ name: 'Person A' }); -} catch (err) { - error = err; -} ---- - -
{error.message}
diff --git a/packages/db/test/fixtures/basics/src/pages/insert-into-writable.astro b/packages/db/test/fixtures/basics/src/pages/insert-into-writable.astro deleted file mode 100644 index 3e3ad2f31ac5..000000000000 --- a/packages/db/test/fixtures/basics/src/pages/insert-into-writable.astro +++ /dev/null @@ -1,12 +0,0 @@ ---- -import { Themes, db } from 'astro:db'; - -let error: any = {}; -try { - db.insert(Themes).values({ name: 'Person A' }); -} catch (err) { - error = err; -} ---- - -
{error.message}
diff --git a/packages/db/test/fixtures/basics/themes-integration.ts b/packages/db/test/fixtures/basics/themes-integration.ts deleted file mode 100644 index d034d840f85b..000000000000 --- a/packages/db/test/fixtures/basics/themes-integration.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { NOW, column, defineWritableTable, sql } from '@astrojs/db'; -import type { AstroIntegration } from 'astro'; - -const Themes = defineWritableTable({ - columns: { - name: column.text(), - added: column.date({ - default: sql`CURRENT_TIMESTAMP`, - }), - updated: column.date({ - default: NOW, - }), - isDark: column.boolean({ default: sql`TRUE` }), - owner: column.text({ optional: true, default: sql`NULL` }), - }, -}); - -export function themes(): AstroIntegration { - return { - name: 'themes-integration', - hooks: { - 'astro:config:setup': ({ updateConfig }) => { - updateConfig({ - db: { - tables: { Themes }, - async data({ seed }) { - // Seed writable tables in dev mode, only - // but in this case we do it for both, due to tests - await seed(Themes, [{ name: 'dracula' }, { name: 'monokai', added: new Date() }]); - }, - }, - }); - }, - }, - }; -} diff --git a/packages/db/test/fixtures/glob/astro.config.ts b/packages/db/test/fixtures/glob/astro.config.ts deleted file mode 100644 index d84117f4e6e3..000000000000 --- a/packages/db/test/fixtures/glob/astro.config.ts +++ /dev/null @@ -1,25 +0,0 @@ -import db, { defineReadableTable, column } from '@astrojs/db'; -import { defineConfig } from 'astro/config'; -import { asJson, createGlob } from './utils'; - -const Quote = defineReadableTable({ - columns: { - author: column.text(), - body: column.text(), - file: column.text({ unique: true }), - }, -}); - -export default defineConfig({ - db: { - tables: { Quote }, - data({ seed, ...ctx }) { - const glob = createGlob(ctx); - glob('quotes/*.json', { - into: Quote, - parse: asJson, - }); - }, - }, - integrations: [db()], -}); diff --git a/packages/db/test/fixtures/glob/package.json b/packages/db/test/fixtures/glob/package.json deleted file mode 100644 index d7db0815c927..000000000000 --- a/packages/db/test/fixtures/glob/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "glob", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "dev": "astro dev", - "build": "astro build", - "preview": "astro preview" - }, - "dependencies": { - "@astrojs/db": "workspace:*", - "astro": "workspace:*", - "chokidar": "^3.5.3", - "drizzle-orm": "^0.28.6", - "fast-glob": "^3.3.2" - }, - "keywords": [], - "author": "", - "license": "ISC" -} diff --git a/packages/db/test/fixtures/glob/quotes/erika.json b/packages/db/test/fixtures/glob/quotes/erika.json deleted file mode 100644 index c7ece9ca6ee3..000000000000 --- a/packages/db/test/fixtures/glob/quotes/erika.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "author": "Erika", - "body": "Put the quote in the database." -} diff --git a/packages/db/test/fixtures/glob/quotes/tony.json b/packages/db/test/fixtures/glob/quotes/tony.json deleted file mode 100644 index 3aaed7746b83..000000000000 --- a/packages/db/test/fixtures/glob/quotes/tony.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "author": "Tony Sull", - "body": "All content is data, but not all data is content." -} diff --git a/packages/db/test/fixtures/glob/src/pages/index.astro b/packages/db/test/fixtures/glob/src/pages/index.astro deleted file mode 100644 index 6d2fdf22c0eb..000000000000 --- a/packages/db/test/fixtures/glob/src/pages/index.astro +++ /dev/null @@ -1,25 +0,0 @@ ---- -/// -import { Quote, db } from 'astro:db'; - -const quotes = await db.select().from(Quote); ---- - - - - - - - Document - - - { - quotes.map((q) => ( -
-
{q.body}
-
{q.author}
-
- )) - } - - diff --git a/packages/db/test/fixtures/glob/utils.ts b/packages/db/test/fixtures/glob/utils.ts deleted file mode 100644 index 5602c610807c..000000000000 --- a/packages/db/test/fixtures/glob/utils.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { type DBDataContext, type ResolvedCollectionConfig } from '@astrojs/db'; -import chokidar from 'chokidar'; -import { eq } from 'drizzle-orm'; -import fastGlob from 'fast-glob'; -import { readFile } from 'fs/promises'; - -export function createGlob({ db, mode }: Pick) { - return async function glob( - pattern: string, - opts: { - into: ResolvedCollectionConfig; - parse: (params: { file: string; content: string }) => Record; - } - ) { - // TODO: expose `table` - const { table } = opts.into as any; - const fileColumn = table.file; - if (!fileColumn) { - throw new Error('`file` column is required for glob tables.'); - } - if (mode === 'dev') { - chokidar - .watch(pattern) - .on('add', async (file) => { - const content = await readFile(file, 'utf-8'); - const parsed = opts.parse({ file, content }); - await db.insert(table).values({ ...parsed, file }); - }) - .on('change', async (file) => { - const content = await readFile(file, 'utf-8'); - const parsed = opts.parse({ file, content }); - await db - .insert(table) - .values({ ...parsed, file }) - .onConflictDoUpdate({ - target: fileColumn, - set: parsed, - }); - }) - .on('unlink', async (file) => { - await db.delete(table).where(eq(fileColumn, file)); - }); - } else { - const files = await fastGlob(pattern); - for (const file of files) { - const content = await readFile(file, 'utf-8'); - const parsed = opts.parse({ file, content }); - await db.insert(table).values({ ...parsed, file }); - } - } - }; -} - -export function asJson(params: { file: string; content: string }) { - try { - return JSON.parse(params.content); - } catch (e) { - throw new Error(`Error parsing ${params.file}: ${e.message}`); - } -} diff --git a/packages/db/test/fixtures/recipes/astro.config.ts b/packages/db/test/fixtures/recipes/astro.config.ts index bb7ad225e4cd..bd6088769ffa 100644 --- a/packages/db/test/fixtures/recipes/astro.config.ts +++ b/packages/db/test/fixtures/recipes/astro.config.ts @@ -1,82 +1,6 @@ -import astroDb, { defineReadableTable, column } from '@astrojs/db'; +import astroDb from '@astrojs/db'; import { defineConfig } from 'astro/config'; -const Recipe = defineReadableTable({ - columns: { - id: column.number({ primaryKey: true }), - title: column.text(), - description: column.text(), - }, -}); - -const Ingredient = defineReadableTable({ - columns: { - id: column.number({ primaryKey: true }), - name: column.text(), - quantity: column.number(), - recipeId: column.number(), - }, - indexes: { - recipeIdx: { on: 'recipeId' }, - }, - foreignKeys: [{ columns: 'recipeId', references: () => [Recipe.columns.id] }], -}); - export default defineConfig({ integrations: [astroDb()], - db: { - tables: { Recipe, Ingredient }, - async data({ seed, seedReturning }) { - const pancakes = await seedReturning(Recipe, { - title: 'Pancakes', - description: 'A delicious breakfast', - }); - - await seed(Ingredient, [ - { - name: 'Flour', - quantity: 1, - recipeId: pancakes.id, - }, - { - name: 'Eggs', - quantity: 2, - recipeId: pancakes.id, - }, - { - name: 'Milk', - quantity: 1, - recipeId: pancakes.id, - }, - ]); - - const pizza = await seedReturning(Recipe, { - title: 'Pizza', - description: 'A delicious dinner', - }); - - await seed(Ingredient, [ - { - name: 'Flour', - quantity: 1, - recipeId: pizza.id, - }, - { - name: 'Eggs', - quantity: 2, - recipeId: pizza.id, - }, - { - name: 'Milk', - quantity: 1, - recipeId: pizza.id, - }, - { - name: 'Tomato Sauce', - quantity: 1, - recipeId: pizza.id, - }, - ]); - }, - }, }); diff --git a/packages/db/test/fixtures/recipes/db/config.ts b/packages/db/test/fixtures/recipes/db/config.ts new file mode 100644 index 000000000000..1cbcaa96e1a6 --- /dev/null +++ b/packages/db/test/fixtures/recipes/db/config.ts @@ -0,0 +1,26 @@ +import { defineTable, defineDB, column } from 'astro:db'; + +const Recipe = defineTable({ + columns: { + id: column.number({ primaryKey: true }), + title: column.text(), + description: column.text(), + }, +}); + +const Ingredient = defineTable({ + columns: { + id: column.number({ primaryKey: true }), + name: column.text(), + quantity: column.number(), + recipeId: column.number(), + }, + indexes: { + recipeIdx: { on: 'recipeId' }, + }, + foreignKeys: [{ columns: 'recipeId', references: () => [Recipe.columns.id] }], +}); + +export default defineDB({ + tables: { Recipe, Ingredient }, +}); diff --git a/packages/db/test/fixtures/recipes/db/seed.ts b/packages/db/test/fixtures/recipes/db/seed.ts new file mode 100644 index 000000000000..b30c708fa43e --- /dev/null +++ b/packages/db/test/fixtures/recipes/db/seed.ts @@ -0,0 +1,60 @@ +import { db, Recipe, Ingredient } from 'astro:db'; + +const pancakes = await db + .insert(Recipe) + .values({ + title: 'Pancakes', + description: 'A delicious breakfast', + }) + .returning() + .get(); + +await db.insert(Ingredient).values([ + { + name: 'Flour', + quantity: 1, + recipeId: pancakes.id, + }, + { + name: 'Eggs', + quantity: 2, + recipeId: pancakes.id, + }, + { + name: 'Milk', + quantity: 1, + recipeId: pancakes.id, + }, +]); + +const pizza = await db + .insert(Recipe) + .values({ + title: 'Pizza', + description: 'A delicious dinner', + }) + .returning() + .get(); + +await db.insert(Ingredient).values([ + { + name: 'Flour', + quantity: 1, + recipeId: pizza.id, + }, + { + name: 'Eggs', + quantity: 2, + recipeId: pizza.id, + }, + { + name: 'Milk', + quantity: 1, + recipeId: pizza.id, + }, + { + name: 'Tomato Sauce', + quantity: 1, + recipeId: pizza.id, + }, +]); diff --git a/packages/db/test/fixtures/ticketing-example/astro.config.ts b/packages/db/test/fixtures/ticketing-example/astro.config.ts index 1f34f6f0d75a..616156f9af9c 100644 --- a/packages/db/test/fixtures/ticketing-example/astro.config.ts +++ b/packages/db/test/fixtures/ticketing-example/astro.config.ts @@ -1,32 +1,9 @@ -import db, { defineReadableTable, defineWritableTable, column } from '@astrojs/db'; +import db from '@astrojs/db'; import node from '@astrojs/node'; import react from '@astrojs/react'; import { defineConfig } from 'astro/config'; import simpleStackForm from 'simple-stack-form'; -const Event = defineReadableTable({ - columns: { - id: column.number({ - primaryKey: true, - }), - name: column.text(), - description: column.text(), - ticketPrice: column.number(), - date: column.date(), - location: column.text(), - }, -}); -const Ticket = defineWritableTable({ - columns: { - eventId: column.number({ references: () => Event.columns.id }), - email: column.text(), - quantity: column.number(), - newsletter: column.boolean({ - default: false, - }), - }, -}); - // https://astro.build/config export default defineConfig({ integrations: [simpleStackForm(), db(), react()], @@ -34,23 +11,4 @@ export default defineConfig({ adapter: node({ mode: 'standalone', }), - db: { - studio: true, - tables: { - Event, - Ticket, - }, - data({ seed }) { - seed(Event, [ - { - name: 'Sampha LIVE in Brooklyn', - description: - 'Sampha is on tour with his new, flawless album Lahai. Come see the live performance outdoors in Prospect Park. Yes, there will be a grand piano 🎹', - date: new Date('2024-01-01'), - ticketPrice: 10000, - location: 'Brooklyn, NY', - }, - ]); - }, - }, }); diff --git a/packages/db/test/fixtures/ticketing-example/db/config.ts b/packages/db/test/fixtures/ticketing-example/db/config.ts new file mode 100644 index 000000000000..28a50b769674 --- /dev/null +++ b/packages/db/test/fixtures/ticketing-example/db/config.ts @@ -0,0 +1,27 @@ +import { defineDB, defineTable, column } from 'astro:db'; + +const Event = defineTable({ + columns: { + id: column.number({ + primaryKey: true, + }), + name: column.text(), + description: column.text(), + ticketPrice: column.number(), + date: column.date(), + location: column.text(), + }, +}); + +const Ticket = defineTable({ + columns: { + eventId: column.number({ references: () => Event.columns.id }), + email: column.text(), + quantity: column.number(), + newsletter: column.boolean({ + default: true, + }), + }, +}); + +export default defineDB({ tables: { Event, Ticket } }); diff --git a/packages/db/test/fixtures/ticketing-example/db/seed.ts b/packages/db/test/fixtures/ticketing-example/db/seed.ts new file mode 100644 index 000000000000..c9789cbd666d --- /dev/null +++ b/packages/db/test/fixtures/ticketing-example/db/seed.ts @@ -0,0 +1,10 @@ +import { Event, db } from 'astro:db'; + +await db.insert(Event).values({ + name: 'Sampha LIVE in Brooklyn', + description: + 'Sampha is on tour with his new, flawless album Lahai. Come see the live performance outdoors in Prospect Park. Yes, there will be a grand piano 🎹', + date: new Date('2024-01-01'), + ticketPrice: 10000, + location: 'Brooklyn, NY', +}); diff --git a/packages/db/test/unit/field-queries.test.js b/packages/db/test/unit/column-queries.test.js similarity index 89% rename from packages/db/test/unit/field-queries.test.js rename to packages/db/test/unit/column-queries.test.js index dc7945f8c8be..96c9c687a140 100644 --- a/packages/db/test/unit/field-queries.test.js +++ b/packages/db/test/unit/column-queries.test.js @@ -4,16 +4,17 @@ import { getCollectionChangeQueries, getMigrationQueries, } from '../../dist/core/cli/migration-queries.js'; -import { getCreateTableQuery } from '../../dist/core/queries.js'; -import { collectionSchema, column, defineReadableTable } from '../../dist/core/types.js'; +import { getCreateTableQuery } from '../../dist/runtime/queries.js'; +import { column, defineTable } from '../../dist/runtime/config.js'; +import { tableSchema } from '../../dist/core/types.js'; import { NOW } from '../../dist/runtime/index.js'; -const COLLECTION_NAME = 'Users'; +const TABLE_NAME = 'Users'; // `parse` to resolve schema transformations // ex. convert column.date() to ISO strings -const userInitial = collectionSchema.parse( - defineReadableTable({ +const userInitial = tableSchema.parse( + defineTable({ columns: { name: column.text(), age: column.number(), @@ -28,15 +29,11 @@ const defaultAmbiguityResponses = { columnRenames: {}, }; -function userChangeQueries( - oldCollection, - newCollection, - ambiguityResponses = defaultAmbiguityResponses -) { +function userChangeQueries(oldTable, newTable, ambiguityResponses = defaultAmbiguityResponses) { return getCollectionChangeQueries({ - collectionName: COLLECTION_NAME, - oldCollection, - newCollection, + collectionName: TABLE_NAME, + oldCollection: oldTable, + newCollection: newTable, ambiguityResponses, }); } @@ -56,35 +53,35 @@ function configChangeQueries( describe('column queries', () => { describe('getMigrationQueries', () => { it('should be empty when tables are the same', async () => { - const oldCollections = { [COLLECTION_NAME]: userInitial }; - const newCollections = { [COLLECTION_NAME]: userInitial }; + const oldCollections = { [TABLE_NAME]: userInitial }; + const newCollections = { [TABLE_NAME]: userInitial }; const { queries } = await configChangeQueries(oldCollections, newCollections); expect(queries).to.deep.equal([]); }); it('should create table for new tables', async () => { const oldCollections = {}; - const newCollections = { [COLLECTION_NAME]: userInitial }; + const newCollections = { [TABLE_NAME]: userInitial }; const { queries } = await configChangeQueries(oldCollections, newCollections); - expect(queries).to.deep.equal([getCreateTableQuery(COLLECTION_NAME, userInitial)]); + expect(queries).to.deep.equal([getCreateTableQuery(TABLE_NAME, userInitial)]); }); it('should drop table for removed tables', async () => { - const oldCollections = { [COLLECTION_NAME]: userInitial }; + const oldCollections = { [TABLE_NAME]: userInitial }; const newCollections = {}; const { queries } = await configChangeQueries(oldCollections, newCollections); - expect(queries).to.deep.equal([`DROP TABLE "${COLLECTION_NAME}"`]); + expect(queries).to.deep.equal([`DROP TABLE "${TABLE_NAME}"`]); }); it('should rename table for renamed tables', async () => { const rename = 'Peeps'; - const oldCollections = { [COLLECTION_NAME]: userInitial }; + const oldCollections = { [TABLE_NAME]: userInitial }; const newCollections = { [rename]: userInitial }; const { queries } = await configChangeQueries(oldCollections, newCollections, { ...defaultAmbiguityResponses, - collectionRenames: { [rename]: COLLECTION_NAME }, + collectionRenames: { [rename]: TABLE_NAME }, }); - expect(queries).to.deep.equal([`ALTER TABLE "${COLLECTION_NAME}" RENAME TO "${rename}"`]); + expect(queries).to.deep.equal([`ALTER TABLE "${TABLE_NAME}" RENAME TO "${rename}"`]); }); }); @@ -95,14 +92,14 @@ describe('column queries', () => { }); it('should be empty when type updated to same underlying SQL type', async () => { - const blogInitial = collectionSchema.parse({ + const blogInitial = tableSchema.parse({ ...userInitial, columns: { title: column.text(), draft: column.boolean(), }, }); - const blogFinal = collectionSchema.parse({ + const blogFinal = tableSchema.parse({ ...userInitial, columns: { ...blogInitial.columns, @@ -114,7 +111,7 @@ describe('column queries', () => { }); it('should respect user primary key without adding a hidden id', async () => { - const user = collectionSchema.parse({ + const user = tableSchema.parse({ ...userInitial, columns: { ...userInitial.columns, @@ -122,7 +119,7 @@ describe('column queries', () => { }, }); - const userFinal = collectionSchema.parse({ + const userFinal = tableSchema.parse({ ...user, columns: { ...user.columns, @@ -155,10 +152,10 @@ describe('column queries', () => { const { queries } = await userChangeQueries(userInitial, userFinal, { collectionRenames: {}, - columnRenames: { [COLLECTION_NAME]: { middleInitial: 'mi' } }, + columnRenames: { [TABLE_NAME]: { middleInitial: 'mi' } }, }); expect(queries).to.deep.equal([ - `ALTER TABLE "${COLLECTION_NAME}" RENAME COLUMN "mi" TO "middleInitial"`, + `ALTER TABLE "${TABLE_NAME}" RENAME COLUMN "mi" TO "middleInitial"`, ]); }); }); @@ -287,7 +284,7 @@ describe('column queries', () => { }); it('when updating to a runtime default', async () => { - const initial = collectionSchema.parse({ + const initial = tableSchema.parse({ ...userInitial, columns: { ...userInitial.columns, @@ -295,7 +292,7 @@ describe('column queries', () => { }, }); - const userFinal = collectionSchema.parse({ + const userFinal = tableSchema.parse({ ...initial, columns: { ...initial.columns, @@ -317,7 +314,7 @@ describe('column queries', () => { }); it('when adding a column with a runtime default', async () => { - const userFinal = collectionSchema.parse({ + const userFinal = tableSchema.parse({ ...userInitial, columns: { ...userInitial.columns, @@ -407,7 +404,7 @@ describe('column queries', () => { it('when adding a required column with default', async () => { const defaultDate = new Date('2023-01-01'); - const userFinal = collectionSchema.parse({ + const userFinal = tableSchema.parse({ ...userInitial, columns: { ...userInitial.columns, diff --git a/packages/db/test/unit/index-queries.test.js b/packages/db/test/unit/index-queries.test.js index 17b2599bdeb2..5e9b2130d960 100644 --- a/packages/db/test/unit/index-queries.test.js +++ b/packages/db/test/unit/index-queries.test.js @@ -1,9 +1,10 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; import { getCollectionChangeQueries } from '../../dist/core/cli/migration-queries.js'; -import { collectionSchema, column } from '../../dist/core/types.js'; +import { column } from '../../dist/runtime/config.js'; +import { tableSchema } from '../../dist/core/types.js'; -const userInitial = collectionSchema.parse({ +const userInitial = tableSchema.parse({ columns: { name: column.text(), age: column.number(), diff --git a/packages/db/test/unit/reference-queries.test.js b/packages/db/test/unit/reference-queries.test.js index 4c2e3af8d6bb..561879f438e4 100644 --- a/packages/db/test/unit/reference-queries.test.js +++ b/packages/db/test/unit/reference-queries.test.js @@ -1,9 +1,10 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; import { getCollectionChangeQueries } from '../../dist/core/cli/migration-queries.js'; -import { column, defineReadableTable, tablesSchema } from '../../dist/core/types.js'; +import { column, defineTable } from '../../dist/runtime/config.js'; +import { tablesSchema } from '../../dist/core/types.js'; -const BaseUser = defineReadableTable({ +const BaseUser = defineTable({ columns: { id: column.number({ primaryKey: true }), name: column.text(), @@ -13,7 +14,7 @@ const BaseUser = defineReadableTable({ }, }); -const BaseSentBox = defineReadableTable({ +const BaseSentBox = defineTable({ columns: { to: column.number(), toName: column.text(), @@ -58,7 +59,7 @@ describe('reference queries', () => { it('adds references with lossless table recreate', async () => { const { SentBox: Initial } = resolveReferences(); const { SentBox: Final } = resolveReferences({ - SentBox: defineReadableTable({ + SentBox: defineTable({ columns: { ...BaseSentBox.columns, to: column.number({ references: () => BaseUser.columns.id }), @@ -82,7 +83,7 @@ describe('reference queries', () => { it('removes references with lossless table recreate', async () => { const { SentBox: Initial } = resolveReferences({ - SentBox: defineReadableTable({ + SentBox: defineTable({ columns: { ...BaseSentBox.columns, to: column.number({ references: () => BaseUser.columns.id }), @@ -108,7 +109,7 @@ describe('reference queries', () => { it('does not use ADD COLUMN when adding optional column with reference', async () => { const { SentBox: Initial } = resolveReferences(); const { SentBox: Final } = resolveReferences({ - SentBox: defineReadableTable({ + SentBox: defineTable({ columns: { ...BaseSentBox.columns, from: column.number({ references: () => BaseUser.columns.id, optional: true }), @@ -131,13 +132,13 @@ describe('reference queries', () => { it('adds and updates foreign key with lossless table recreate', async () => { const { SentBox: InitialWithoutFK } = resolveReferences(); const { SentBox: InitialWithDifferentFK } = resolveReferences({ - SentBox: defineReadableTable({ + SentBox: defineTable({ ...BaseSentBox, foreignKeys: [{ columns: ['to'], references: () => [BaseUser.columns.id] }], }), }); const { SentBox: Final } = resolveReferences({ - SentBox: defineReadableTable({ + SentBox: defineTable({ ...BaseSentBox, foreignKeys: [ { From 25fe5bd04047d741a1ca726999fedde9650a34ef Mon Sep 17 00:00:00 2001 From: Ben Holmes Date: Sat, 2 Mar 2024 00:31:00 +0000 Subject: [PATCH 10/13] [ci] format --- .../db/src/core/cli/commands/execute/index.ts | 8 +++--- .../db/src/core/cli/commands/gen/index.ts | 10 +++---- .../db/src/core/cli/commands/push/index.ts | 12 ++++---- .../db/src/core/cli/commands/shell/index.ts | 2 +- .../db/src/core/cli/commands/verify/index.ts | 2 +- packages/db/src/core/cli/migration-queries.ts | 2 +- packages/db/src/core/cli/migrations.ts | 2 +- packages/db/src/core/integration/index.ts | 14 +++++----- .../db/src/core/integration/vite-plugin-db.ts | 4 +-- packages/db/src/core/load-file.ts | 6 ++-- packages/db/src/runtime/index.ts | 2 +- packages/db/src/runtime/queries.ts | 28 +++++++++---------- packages/db/test/fixtures/basics/db/config.ts | 2 +- packages/db/test/fixtures/basics/db/seed.ts | 4 +-- packages/db/test/fixtures/basics/db/theme.ts | 2 +- .../db/test/fixtures/recipes/db/config.ts | 2 +- packages/db/test/fixtures/recipes/db/seed.ts | 2 +- .../fixtures/ticketing-example/db/config.ts | 2 +- packages/db/test/unit/column-queries.test.js | 4 +-- packages/db/test/unit/index-queries.test.js | 2 +- .../db/test/unit/reference-queries.test.js | 2 +- 21 files changed, 57 insertions(+), 57 deletions(-) diff --git a/packages/db/src/core/cli/commands/execute/index.ts b/packages/db/src/core/cli/commands/execute/index.ts index a81696702f62..1863691e706e 100644 --- a/packages/db/src/core/cli/commands/execute/index.ts +++ b/packages/db/src/core/cli/commands/execute/index.ts @@ -1,11 +1,11 @@ +import { existsSync } from 'node:fs'; import type { AstroConfig } from 'astro'; import type { Arguments } from 'yargs-parser'; -import { MISSING_EXECUTE_PATH_ERROR, FILE_NOT_FOUND_ERROR } from '../../../errors.js'; -import { existsSync } from 'node:fs'; +import { FILE_NOT_FOUND_ERROR, MISSING_EXECUTE_PATH_ERROR } from '../../../errors.js'; +import { getStudioVirtualModContents } from '../../../integration/vite-plugin-db.js'; +import { bundleFile, importBundledFile } from '../../../load-file.js'; import { getManagedAppTokenOrExit } from '../../../tokens.js'; import { type DBConfig } from '../../../types.js'; -import { bundleFile, importBundledFile } from '../../../load-file.js'; -import { getStudioVirtualModContents } from '../../../integration/vite-plugin-db.js'; export async function cmd({ astroConfig, diff --git a/packages/db/src/core/cli/commands/gen/index.ts b/packages/db/src/core/cli/commands/gen/index.ts index c28f697d86e6..be157d4a5545 100644 --- a/packages/db/src/core/cli/commands/gen/index.ts +++ b/packages/db/src/core/cli/commands/gen/index.ts @@ -1,8 +1,11 @@ -import { fileURLToPath } from 'node:url'; import { writeFile } from 'node:fs/promises'; +import { relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; import type { AstroConfig } from 'astro'; -import { bold, bgRed, red, reset } from 'kleur/colors'; +import { bgRed, bold, red, reset } from 'kleur/colors'; import type { Arguments } from 'yargs-parser'; +import type { DBConfig } from '../../../types.js'; +import { getMigrationsDirectoryUrl } from '../../../utils.js'; import { getMigrationQueries } from '../../migration-queries.js'; import { MIGRATIONS_CREATED, @@ -10,9 +13,6 @@ import { getMigrationStatus, initializeMigrationsDirectory, } from '../../migrations.js'; -import { getMigrationsDirectoryUrl } from '../../../utils.js'; -import type { DBConfig } from '../../../types.js'; -import { relative } from 'node:path'; export async function cmd({ astroConfig, diff --git a/packages/db/src/core/cli/commands/push/index.ts b/packages/db/src/core/cli/commands/push/index.ts index b6bd773e2f03..81d232cb2904 100644 --- a/packages/db/src/core/cli/commands/push/index.ts +++ b/packages/db/src/core/cli/commands/push/index.ts @@ -2,22 +2,22 @@ import type { AstroConfig } from 'astro'; import { red } from 'kleur/colors'; import prompts from 'prompts'; import type { Arguments } from 'yargs-parser'; +import { MISSING_SESSION_ID_ERROR } from '../../../errors.js'; import { getManagedAppTokenOrExit } from '../../../tokens.js'; import { type DBConfig, type DBSnapshot } from '../../../types.js'; import { getMigrationsDirectoryUrl, getRemoteDatabaseUrl } from '../../../utils.js'; import { getMigrationQueries } from '../../migration-queries.js'; import { + INITIAL_SNAPSHOT, + MIGRATIONS_NOT_INITIALIZED, + MIGRATIONS_UP_TO_DATE, + MIGRATION_NEEDED, createEmptySnapshot, - getMigrations, getMigrationStatus, - INITIAL_SNAPSHOT, + getMigrations, loadInitialSnapshot, loadMigration, - MIGRATION_NEEDED, - MIGRATIONS_NOT_INITIALIZED, - MIGRATIONS_UP_TO_DATE, } from '../../migrations.js'; -import { MISSING_SESSION_ID_ERROR } from '../../../errors.js'; export async function cmd({ astroConfig, diff --git a/packages/db/src/core/cli/commands/shell/index.ts b/packages/db/src/core/cli/commands/shell/index.ts index 7d8f13437bbd..ef54b6b70db2 100644 --- a/packages/db/src/core/cli/commands/shell/index.ts +++ b/packages/db/src/core/cli/commands/shell/index.ts @@ -3,8 +3,8 @@ import { sql } from 'drizzle-orm'; import type { Arguments } from 'yargs-parser'; import { createRemoteDatabaseClient } from '../../../../runtime/db-client.js'; import { getManagedAppTokenOrExit } from '../../../tokens.js'; -import { getRemoteDatabaseUrl } from '../../../utils.js'; import type { DBConfigInput } from '../../../types.js'; +import { getRemoteDatabaseUrl } from '../../../utils.js'; export async function cmd({ flags, diff --git a/packages/db/src/core/cli/commands/verify/index.ts b/packages/db/src/core/cli/commands/verify/index.ts index ff182fc53c86..3b95835f7a64 100644 --- a/packages/db/src/core/cli/commands/verify/index.ts +++ b/packages/db/src/core/cli/commands/verify/index.ts @@ -1,5 +1,6 @@ import type { AstroConfig } from 'astro'; import type { Arguments } from 'yargs-parser'; +import type { DBConfig } from '../../../types.js'; import { getMigrationQueries } from '../../migration-queries.js'; import { MIGRATIONS_NOT_INITIALIZED, @@ -7,7 +8,6 @@ import { MIGRATION_NEEDED, getMigrationStatus, } from '../../migrations.js'; -import type { DBConfig } from '../../../types.js'; export async function cmd({ astroConfig, diff --git a/packages/db/src/core/cli/migration-queries.ts b/packages/db/src/core/cli/migration-queries.ts index aa9d7e316805..4265f36e424a 100644 --- a/packages/db/src/core/cli/migration-queries.ts +++ b/packages/db/src/core/cli/migration-queries.ts @@ -4,7 +4,6 @@ import * as color from 'kleur/colors'; import { customAlphabet } from 'nanoid'; import prompts from 'prompts'; import { hasPrimaryKey } from '../../runtime/index.js'; -import { isSerializedSQL } from '../../runtime/types.js'; import { getCreateIndexQueries, getCreateTableQuery, @@ -13,6 +12,7 @@ import { hasDefault, schemaTypeToSqlType, } from '../../runtime/queries.js'; +import { isSerializedSQL } from '../../runtime/types.js'; import { type BooleanColumn, type ColumnType, diff --git a/packages/db/src/core/cli/migrations.ts b/packages/db/src/core/cli/migrations.ts index 455ebc984fc7..514d4e798a11 100644 --- a/packages/db/src/core/cli/migrations.ts +++ b/packages/db/src/core/cli/migrations.ts @@ -1,7 +1,7 @@ import deepDiff from 'deep-diff'; import { mkdir, readFile, readdir, writeFile } from 'fs/promises'; -import { type DBSnapshot, type DBConfig } from '../types.js'; import { cyan, green, yellow } from 'kleur/colors'; +import { type DBConfig, type DBSnapshot } from '../types.js'; import { getMigrationsDirectoryUrl } from '../utils.js'; const { applyChange, diff: generateDiff } = deepDiff; diff --git a/packages/db/src/core/integration/index.ts b/packages/db/src/core/integration/index.ts index 2b476a4d3f95..4361ddfe7f67 100644 --- a/packages/db/src/core/integration/index.ts +++ b/packages/db/src/core/integration/index.ts @@ -1,18 +1,18 @@ import { existsSync } from 'fs'; -import { CONFIG_FILE_NAMES, DB_PATH } from '../consts.js'; -import { dbConfigSchema, type DBConfig } from '../types.js'; -import { getDbDirectoryUrl, type VitePlugin } from '../utils.js'; -import { errorMap } from './error-map.js'; import { dirname } from 'path'; import { fileURLToPath } from 'url'; import type { AstroIntegration } from 'astro'; import { mkdir, rm, writeFile } from 'fs/promises'; import { blue, yellow } from 'kleur/colors'; -import { fileURLIntegration } from './file-url.js'; -import { getManagedAppTokenOrExit, type ManagedAppToken } from '../tokens.js'; +import { CONFIG_FILE_NAMES, DB_PATH } from '../consts.js'; import { loadDbConfigFile } from '../load-file.js'; -import { vitePluginDb, type LateTables } from './vite-plugin-db.js'; +import { type ManagedAppToken, getManagedAppTokenOrExit } from '../tokens.js'; +import { type DBConfig, dbConfigSchema } from '../types.js'; +import { type VitePlugin, getDbDirectoryUrl } from '../utils.js'; +import { errorMap } from './error-map.js'; +import { fileURLIntegration } from './file-url.js'; import { typegen } from './typegen.js'; +import { type LateTables, vitePluginDb } from './vite-plugin-db.js'; import { vitePluginInjectEnvTs } from './vite-plugin-inject-env-ts.js'; function astroDBIntegration(): AstroIntegration { diff --git a/packages/db/src/core/integration/vite-plugin-db.ts b/packages/db/src/core/integration/vite-plugin-db.ts index ff97d2b21bf1..65a9fa6a03b6 100644 --- a/packages/db/src/core/integration/vite-plugin-db.ts +++ b/packages/db/src/core/integration/vite-plugin-db.ts @@ -1,4 +1,5 @@ import { fileURLToPath } from 'node:url'; +import { normalizePath } from 'vite'; import { SEED_DEV_FILE_NAME } from '../../runtime/queries.js'; import { DB_PATH, @@ -8,8 +9,7 @@ import { VIRTUAL_MODULE_ID, } from '../consts.js'; import type { DBTables } from '../types.js'; -import { getDbDirectoryUrl, getRemoteDatabaseUrl, type VitePlugin } from '../utils.js'; -import { normalizePath } from 'vite'; +import { type VitePlugin, getDbDirectoryUrl, getRemoteDatabaseUrl } from '../utils.js'; const LOCAL_DB_VIRTUAL_MODULE_ID = 'astro:local'; diff --git a/packages/db/src/core/load-file.ts b/packages/db/src/core/load-file.ts index 591ffa3906d3..cc1b23c014b2 100644 --- a/packages/db/src/core/load-file.ts +++ b/packages/db/src/core/load-file.ts @@ -1,9 +1,9 @@ +import { existsSync } from 'node:fs'; +import { unlink, writeFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; import { build as esbuild } from 'esbuild'; import { CONFIG_FILE_NAMES, VIRTUAL_MODULE_ID } from './consts.js'; -import { fileURLToPath } from 'node:url'; import { getConfigVirtualModContents } from './integration/vite-plugin-db.js'; -import { writeFile, unlink } from 'node:fs/promises'; -import { existsSync } from 'node:fs'; import { getDbDirectoryUrl } from './utils.js'; export async function loadDbConfigFile( diff --git a/packages/db/src/runtime/index.ts b/packages/db/src/runtime/index.ts index d68521f43511..501ae7a22233 100644 --- a/packages/db/src/runtime/index.ts +++ b/packages/db/src/runtime/index.ts @@ -1,5 +1,5 @@ -import type { LibSQLDatabase } from 'drizzle-orm/libsql'; import { type ColumnBuilderBaseConfig, type ColumnDataType, sql } from 'drizzle-orm'; +import type { LibSQLDatabase } from 'drizzle-orm/libsql'; import { type IndexBuilder, type SQLiteColumnBuilderBase, diff --git a/packages/db/src/runtime/queries.ts b/packages/db/src/runtime/queries.ts index b81b06edbe7f..4341fd1237ab 100644 --- a/packages/db/src/runtime/queries.ts +++ b/packages/db/src/runtime/queries.ts @@ -1,27 +1,27 @@ +import { LibsqlError } from '@libsql/client'; +import { type SQL, sql } from 'drizzle-orm'; +import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; +import { bold } from 'kleur/colors'; +import { + FOREIGN_KEY_DNE_ERROR, + FOREIGN_KEY_REFERENCES_EMPTY_ERROR, + FOREIGN_KEY_REFERENCES_LENGTH_ERROR, + REFERENCE_DNE_ERROR, + SEED_ERROR, +} from '../core/errors.js'; import type { BooleanColumn, + ColumnType, + DBColumn, DBTable, DBTables, - DBColumn, DateColumn, - ColumnType, JsonColumn, NumberColumn, TextColumn, } from '../core/types.js'; -import { bold } from 'kleur/colors'; -import { type SQL, sql } from 'drizzle-orm'; -import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; -import { hasPrimaryKey, type SqliteDB } from './index.js'; +import { type SqliteDB, hasPrimaryKey } from './index.js'; import { isSerializedSQL } from './types.js'; -import { - FOREIGN_KEY_REFERENCES_LENGTH_ERROR, - FOREIGN_KEY_REFERENCES_EMPTY_ERROR, - REFERENCE_DNE_ERROR, - FOREIGN_KEY_DNE_ERROR, - SEED_ERROR, -} from '../core/errors.js'; -import { LibsqlError } from '@libsql/client'; const sqlite = new SQLiteAsyncDialect(); diff --git a/packages/db/test/fixtures/basics/db/config.ts b/packages/db/test/fixtures/basics/db/config.ts index b2ed2066f1e1..f216caab6e45 100644 --- a/packages/db/test/fixtures/basics/db/config.ts +++ b/packages/db/test/fixtures/basics/db/config.ts @@ -1,5 +1,5 @@ -import { defineDB, defineTable, column } from 'astro:db'; import { Themes } from './theme'; +import { column, defineDB, defineTable } from 'astro:db'; const Author = defineTable({ columns: { diff --git a/packages/db/test/fixtures/basics/db/seed.ts b/packages/db/test/fixtures/basics/db/seed.ts index c1b61e0996db..33d82d523782 100644 --- a/packages/db/test/fixtures/basics/db/seed.ts +++ b/packages/db/test/fixtures/basics/db/seed.ts @@ -1,6 +1,6 @@ -import { db, Author } from 'astro:db'; -import { Themes as ThemesConfig } from './theme'; import { asDrizzleTable } from '@astrojs/db/utils'; +import { Themes as ThemesConfig } from './theme'; +import { Author, db } from 'astro:db'; const Themes = asDrizzleTable('Themes', ThemesConfig); diff --git a/packages/db/test/fixtures/basics/db/theme.ts b/packages/db/test/fixtures/basics/db/theme.ts index d3b89b68c4b8..aecc67f7d5ae 100644 --- a/packages/db/test/fixtures/basics/db/theme.ts +++ b/packages/db/test/fixtures/basics/db/theme.ts @@ -1,4 +1,4 @@ -import { defineTable, column, NOW, sql } from 'astro:db'; +import { NOW, column, defineTable, sql } from 'astro:db'; export const Themes = defineTable({ columns: { diff --git a/packages/db/test/fixtures/recipes/db/config.ts b/packages/db/test/fixtures/recipes/db/config.ts index 1cbcaa96e1a6..6334ba8edcfa 100644 --- a/packages/db/test/fixtures/recipes/db/config.ts +++ b/packages/db/test/fixtures/recipes/db/config.ts @@ -1,4 +1,4 @@ -import { defineTable, defineDB, column } from 'astro:db'; +import { column, defineDB, defineTable } from 'astro:db'; const Recipe = defineTable({ columns: { diff --git a/packages/db/test/fixtures/recipes/db/seed.ts b/packages/db/test/fixtures/recipes/db/seed.ts index b30c708fa43e..7a4892376fde 100644 --- a/packages/db/test/fixtures/recipes/db/seed.ts +++ b/packages/db/test/fixtures/recipes/db/seed.ts @@ -1,4 +1,4 @@ -import { db, Recipe, Ingredient } from 'astro:db'; +import { Ingredient, Recipe, db } from 'astro:db'; const pancakes = await db .insert(Recipe) diff --git a/packages/db/test/fixtures/ticketing-example/db/config.ts b/packages/db/test/fixtures/ticketing-example/db/config.ts index 28a50b769674..f8148eaed305 100644 --- a/packages/db/test/fixtures/ticketing-example/db/config.ts +++ b/packages/db/test/fixtures/ticketing-example/db/config.ts @@ -1,4 +1,4 @@ -import { defineDB, defineTable, column } from 'astro:db'; +import { column, defineDB, defineTable } from 'astro:db'; const Event = defineTable({ columns: { diff --git a/packages/db/test/unit/column-queries.test.js b/packages/db/test/unit/column-queries.test.js index 96c9c687a140..c4d60d5c6f37 100644 --- a/packages/db/test/unit/column-queries.test.js +++ b/packages/db/test/unit/column-queries.test.js @@ -4,10 +4,10 @@ import { getCollectionChangeQueries, getMigrationQueries, } from '../../dist/core/cli/migration-queries.js'; -import { getCreateTableQuery } from '../../dist/runtime/queries.js'; -import { column, defineTable } from '../../dist/runtime/config.js'; import { tableSchema } from '../../dist/core/types.js'; +import { column, defineTable } from '../../dist/runtime/config.js'; import { NOW } from '../../dist/runtime/index.js'; +import { getCreateTableQuery } from '../../dist/runtime/queries.js'; const TABLE_NAME = 'Users'; diff --git a/packages/db/test/unit/index-queries.test.js b/packages/db/test/unit/index-queries.test.js index 5e9b2130d960..ad588959d173 100644 --- a/packages/db/test/unit/index-queries.test.js +++ b/packages/db/test/unit/index-queries.test.js @@ -1,8 +1,8 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; import { getCollectionChangeQueries } from '../../dist/core/cli/migration-queries.js'; -import { column } from '../../dist/runtime/config.js'; import { tableSchema } from '../../dist/core/types.js'; +import { column } from '../../dist/runtime/config.js'; const userInitial = tableSchema.parse({ columns: { diff --git a/packages/db/test/unit/reference-queries.test.js b/packages/db/test/unit/reference-queries.test.js index 561879f438e4..a4b0bdd2d923 100644 --- a/packages/db/test/unit/reference-queries.test.js +++ b/packages/db/test/unit/reference-queries.test.js @@ -1,8 +1,8 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; import { getCollectionChangeQueries } from '../../dist/core/cli/migration-queries.js'; -import { column, defineTable } from '../../dist/runtime/config.js'; import { tablesSchema } from '../../dist/core/types.js'; +import { column, defineTable } from '../../dist/runtime/config.js'; const BaseUser = defineTable({ columns: { From 2db9031a9f7707683e40d6a66cbd41360a746f4b Mon Sep 17 00:00:00 2001 From: "Houston (Bot)" <108291165+astrobot-houston@users.noreply.github.com> Date: Sat, 2 Mar 2024 00:42:07 -0800 Subject: [PATCH 11/13] [ci] release (#10292) Co-authored-by: github-actions[bot] --- .changeset/giant-spies-type.md | 6 -- .changeset/rich-turtles-live.md | 6 -- examples/basics/package.json | 2 +- examples/blog/package.json | 2 +- examples/component/package.json | 2 +- examples/framework-alpine/package.json | 2 +- examples/framework-lit/package.json | 2 +- examples/framework-multiple/package.json | 2 +- examples/framework-preact/package.json | 2 +- examples/framework-react/package.json | 2 +- examples/framework-solid/package.json | 2 +- examples/framework-svelte/package.json | 2 +- examples/framework-vue/package.json | 2 +- examples/hackernews/package.json | 2 +- examples/integration/package.json | 2 +- examples/middleware/package.json | 2 +- examples/minimal/package.json | 2 +- examples/non-html-pages/package.json | 2 +- examples/portfolio/package.json | 2 +- examples/ssr/package.json | 2 +- examples/starlog/package.json | 2 +- examples/view-transitions/package.json | 2 +- examples/with-markdoc/package.json | 4 +- examples/with-markdown-plugins/package.json | 2 +- examples/with-markdown-shiki/package.json | 2 +- examples/with-mdx/package.json | 2 +- examples/with-nanostores/package.json | 2 +- examples/with-tailwindcss/package.json | 2 +- examples/with-vitest/package.json | 2 +- packages/astro/CHANGELOG.md | 8 +++ packages/astro/package.json | 2 +- packages/db/CHANGELOG.md | 6 ++ packages/db/package.json | 2 +- packages/integrations/markdoc/CHANGELOG.md | 6 ++ packages/integrations/markdoc/package.json | 2 +- pnpm-lock.yaml | 74 ++++++++------------- 36 files changed, 79 insertions(+), 89 deletions(-) delete mode 100644 .changeset/giant-spies-type.md delete mode 100644 .changeset/rich-turtles-live.md diff --git a/.changeset/giant-spies-type.md b/.changeset/giant-spies-type.md deleted file mode 100644 index 97018591a63f..000000000000 --- a/.changeset/giant-spies-type.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@astrojs/markdoc": patch -"astro": patch ---- - -Fixes original images sometimes being kept / deleted when they shouldn't in both MDX and Markdoc diff --git a/.changeset/rich-turtles-live.md b/.changeset/rich-turtles-live.md deleted file mode 100644 index 95802f8842f4..000000000000 --- a/.changeset/rich-turtles-live.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"astro": patch -"@astrojs/db": minor ---- - -Finalize db API to a shared db/ directory. diff --git a/examples/basics/package.json b/examples/basics/package.json index 7a0b6444b171..e4946c0b9d90 100644 --- a/examples/basics/package.json +++ b/examples/basics/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.4.8" + "astro": "^4.4.9" } } diff --git a/examples/blog/package.json b/examples/blog/package.json index 08ff9cbfe5e7..95f9cf22756d 100644 --- a/examples/blog/package.json +++ b/examples/blog/package.json @@ -14,6 +14,6 @@ "@astrojs/mdx": "^2.1.1", "@astrojs/rss": "^4.0.5", "@astrojs/sitemap": "^3.1.1", - "astro": "^4.4.8" + "astro": "^4.4.9" } } diff --git a/examples/component/package.json b/examples/component/package.json index 031ca9733acf..f29368a01892 100644 --- a/examples/component/package.json +++ b/examples/component/package.json @@ -15,7 +15,7 @@ ], "scripts": {}, "devDependencies": { - "astro": "^4.4.8" + "astro": "^4.4.9" }, "peerDependencies": { "astro": "^4.0.0" diff --git a/examples/framework-alpine/package.json b/examples/framework-alpine/package.json index e397fa085b24..0c49a23bec6e 100644 --- a/examples/framework-alpine/package.json +++ b/examples/framework-alpine/package.json @@ -14,6 +14,6 @@ "@astrojs/alpinejs": "^0.4.0", "@types/alpinejs": "^3.13.5", "alpinejs": "^3.13.3", - "astro": "^4.4.8" + "astro": "^4.4.9" } } diff --git a/examples/framework-lit/package.json b/examples/framework-lit/package.json index 594a709e24f0..e5206af4b2e9 100644 --- a/examples/framework-lit/package.json +++ b/examples/framework-lit/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/lit": "^4.0.1", "@webcomponents/template-shadowroot": "^0.2.1", - "astro": "^4.4.8", + "astro": "^4.4.9", "lit": "^3.1.2" } } diff --git a/examples/framework-multiple/package.json b/examples/framework-multiple/package.json index 12d57d8410cc..7d5cef5ba949 100644 --- a/examples/framework-multiple/package.json +++ b/examples/framework-multiple/package.json @@ -16,7 +16,7 @@ "@astrojs/solid-js": "^4.0.1", "@astrojs/svelte": "^5.2.0", "@astrojs/vue": "^4.0.8", - "astro": "^4.4.8", + "astro": "^4.4.9", "preact": "^10.19.2", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/framework-preact/package.json b/examples/framework-preact/package.json index a7aaa6022935..efafb58ce98c 100644 --- a/examples/framework-preact/package.json +++ b/examples/framework-preact/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/preact": "^3.1.1", "@preact/signals": "^1.2.1", - "astro": "^4.4.8", + "astro": "^4.4.9", "preact": "^10.19.2" } } diff --git a/examples/framework-react/package.json b/examples/framework-react/package.json index f8b6117e4507..fba2fb54fba2 100644 --- a/examples/framework-react/package.json +++ b/examples/framework-react/package.json @@ -14,7 +14,7 @@ "@astrojs/react": "^3.0.10", "@types/react": "^18.2.37", "@types/react-dom": "^18.2.15", - "astro": "^4.4.8", + "astro": "^4.4.9", "react": "^18.2.0", "react-dom": "^18.2.0" } diff --git a/examples/framework-solid/package.json b/examples/framework-solid/package.json index e337a679342f..e4e5023f29a9 100644 --- a/examples/framework-solid/package.json +++ b/examples/framework-solid/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/solid-js": "^4.0.1", - "astro": "^4.4.8", + "astro": "^4.4.9", "solid-js": "^1.8.5" } } diff --git a/examples/framework-svelte/package.json b/examples/framework-svelte/package.json index 986e936e9025..b50d0c2e362b 100644 --- a/examples/framework-svelte/package.json +++ b/examples/framework-svelte/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/svelte": "^5.2.0", - "astro": "^4.4.8", + "astro": "^4.4.9", "svelte": "^4.2.5" } } diff --git a/examples/framework-vue/package.json b/examples/framework-vue/package.json index 51dfa0ce769d..453833c1f469 100644 --- a/examples/framework-vue/package.json +++ b/examples/framework-vue/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/vue": "^4.0.8", - "astro": "^4.4.8", + "astro": "^4.4.9", "vue": "^3.3.8" } } diff --git a/examples/hackernews/package.json b/examples/hackernews/package.json index 4f6b7213fb6f..1022917fbd3d 100644 --- a/examples/hackernews/package.json +++ b/examples/hackernews/package.json @@ -12,6 +12,6 @@ }, "dependencies": { "@astrojs/node": "^8.2.3", - "astro": "^4.4.8" + "astro": "^4.4.9" } } diff --git a/examples/integration/package.json b/examples/integration/package.json index aad853c1ef81..aa8f861c9111 100644 --- a/examples/integration/package.json +++ b/examples/integration/package.json @@ -15,7 +15,7 @@ ], "scripts": {}, "devDependencies": { - "astro": "^4.4.8" + "astro": "^4.4.9" }, "peerDependencies": { "astro": "^4.0.0" diff --git a/examples/middleware/package.json b/examples/middleware/package.json index 849cdf143add..6931f2506e78 100644 --- a/examples/middleware/package.json +++ b/examples/middleware/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@astrojs/node": "^8.2.3", - "astro": "^4.4.8", + "astro": "^4.4.9", "html-minifier": "^4.0.0" }, "devDependencies": { diff --git a/examples/minimal/package.json b/examples/minimal/package.json index b7fa4d9c4817..daaec679ddcc 100644 --- a/examples/minimal/package.json +++ b/examples/minimal/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.4.8" + "astro": "^4.4.9" } } diff --git a/examples/non-html-pages/package.json b/examples/non-html-pages/package.json index 86ca1e0ab70e..9d81c702904f 100644 --- a/examples/non-html-pages/package.json +++ b/examples/non-html-pages/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.4.8" + "astro": "^4.4.9" } } diff --git a/examples/portfolio/package.json b/examples/portfolio/package.json index b82aa53b52cd..63c6a7c1a47a 100644 --- a/examples/portfolio/package.json +++ b/examples/portfolio/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.4.8" + "astro": "^4.4.9" } } diff --git a/examples/ssr/package.json b/examples/ssr/package.json index 6e530d7115f5..f30f14b86189 100644 --- a/examples/ssr/package.json +++ b/examples/ssr/package.json @@ -14,7 +14,7 @@ "dependencies": { "@astrojs/node": "^8.2.3", "@astrojs/svelte": "^5.2.0", - "astro": "^4.4.8", + "astro": "^4.4.9", "svelte": "^4.2.5" } } diff --git a/examples/starlog/package.json b/examples/starlog/package.json index 883712151f33..646a712aea10 100644 --- a/examples/starlog/package.json +++ b/examples/starlog/package.json @@ -10,7 +10,7 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.4.8", + "astro": "^4.4.9", "sass": "^1.69.5", "sharp": "^0.32.6" } diff --git a/examples/view-transitions/package.json b/examples/view-transitions/package.json index d74f5ec9eaf7..9b14f2f865dc 100644 --- a/examples/view-transitions/package.json +++ b/examples/view-transitions/package.json @@ -12,6 +12,6 @@ "devDependencies": { "@astrojs/tailwind": "^5.1.0", "@astrojs/node": "^8.2.3", - "astro": "^4.4.8" + "astro": "^4.4.9" } } diff --git a/examples/with-markdoc/package.json b/examples/with-markdoc/package.json index 6e1c487ccbd5..d2f4f5b927f1 100644 --- a/examples/with-markdoc/package.json +++ b/examples/with-markdoc/package.json @@ -11,7 +11,7 @@ "astro": "astro" }, "dependencies": { - "@astrojs/markdoc": "^0.9.0", - "astro": "^4.4.8" + "@astrojs/markdoc": "^0.9.1", + "astro": "^4.4.9" } } diff --git a/examples/with-markdown-plugins/package.json b/examples/with-markdown-plugins/package.json index 8074dfefac76..a0d3e4fedcc0 100644 --- a/examples/with-markdown-plugins/package.json +++ b/examples/with-markdown-plugins/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/markdown-remark": "^4.2.1", - "astro": "^4.4.8", + "astro": "^4.4.9", "hast-util-select": "^6.0.2", "rehype-autolink-headings": "^7.1.0", "rehype-slug": "^6.0.0", diff --git a/examples/with-markdown-shiki/package.json b/examples/with-markdown-shiki/package.json index f1519cc4d834..b5ac1ec1a71e 100644 --- a/examples/with-markdown-shiki/package.json +++ b/examples/with-markdown-shiki/package.json @@ -11,6 +11,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.4.8" + "astro": "^4.4.9" } } diff --git a/examples/with-mdx/package.json b/examples/with-mdx/package.json index cd3c57ee062a..a67bb505190e 100644 --- a/examples/with-mdx/package.json +++ b/examples/with-mdx/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/mdx": "^2.1.1", "@astrojs/preact": "^3.1.1", - "astro": "^4.4.8", + "astro": "^4.4.9", "preact": "^10.19.2" } } diff --git a/examples/with-nanostores/package.json b/examples/with-nanostores/package.json index e82990cb4afc..330464bb0f38 100644 --- a/examples/with-nanostores/package.json +++ b/examples/with-nanostores/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/preact": "^3.1.1", "@nanostores/preact": "^0.5.0", - "astro": "^4.4.8", + "astro": "^4.4.9", "nanostores": "^0.9.5", "preact": "^10.19.2" } diff --git a/examples/with-tailwindcss/package.json b/examples/with-tailwindcss/package.json index e9edc6b0ff89..39a205342d5c 100644 --- a/examples/with-tailwindcss/package.json +++ b/examples/with-tailwindcss/package.json @@ -14,7 +14,7 @@ "@astrojs/mdx": "^2.1.1", "@astrojs/tailwind": "^5.1.0", "@types/canvas-confetti": "^1.6.3", - "astro": "^4.4.8", + "astro": "^4.4.9", "autoprefixer": "^10.4.15", "canvas-confetti": "^1.9.1", "postcss": "^8.4.28", diff --git a/examples/with-vitest/package.json b/examples/with-vitest/package.json index 7e9e64d9b20c..05c636d421ad 100644 --- a/examples/with-vitest/package.json +++ b/examples/with-vitest/package.json @@ -12,7 +12,7 @@ "test": "vitest" }, "dependencies": { - "astro": "^4.4.8", + "astro": "^4.4.9", "vitest": "^1.3.1" } } diff --git a/packages/astro/CHANGELOG.md b/packages/astro/CHANGELOG.md index fcc699317d00..7df2f801fa3f 100644 --- a/packages/astro/CHANGELOG.md +++ b/packages/astro/CHANGELOG.md @@ -1,5 +1,13 @@ # astro +## 4.4.9 + +### Patch Changes + +- [#10278](https://github.com/withastro/astro/pull/10278) [`a548a3a99c2835c19662fc38636f92b2bda26614`](https://github.com/withastro/astro/commit/a548a3a99c2835c19662fc38636f92b2bda26614) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fixes original images sometimes being kept / deleted when they shouldn't in both MDX and Markdoc + +- [#10280](https://github.com/withastro/astro/pull/10280) [`3488be9b59d1cb65325b0e087c33bcd74aaa4926`](https://github.com/withastro/astro/commit/3488be9b59d1cb65325b0e087c33bcd74aaa4926) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Finalize db API to a shared db/ directory. + ## 4.4.8 ### Patch Changes diff --git a/packages/astro/package.json b/packages/astro/package.json index 36a0483cf54e..50ff30518012 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,6 +1,6 @@ { "name": "astro", - "version": "4.4.8", + "version": "4.4.9", "description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.", "type": "module", "author": "withastro", diff --git a/packages/db/CHANGELOG.md b/packages/db/CHANGELOG.md index 75d993f3de00..0f9a08f2b64a 100644 --- a/packages/db/CHANGELOG.md +++ b/packages/db/CHANGELOG.md @@ -1,5 +1,11 @@ # @astrojs/db +## 0.5.0 + +### Minor Changes + +- [#10280](https://github.com/withastro/astro/pull/10280) [`3488be9b59d1cb65325b0e087c33bcd74aaa4926`](https://github.com/withastro/astro/commit/3488be9b59d1cb65325b0e087c33bcd74aaa4926) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Finalize db API to a shared db/ directory. + ## 0.4.1 ### Patch Changes diff --git a/packages/db/package.json b/packages/db/package.json index 270376784f90..407f938973e5 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "@astrojs/db", - "version": "0.4.1", + "version": "0.5.0", "description": "", "license": "MIT", "type": "module", diff --git a/packages/integrations/markdoc/CHANGELOG.md b/packages/integrations/markdoc/CHANGELOG.md index 3acfb43cece9..fcfb941e8976 100644 --- a/packages/integrations/markdoc/CHANGELOG.md +++ b/packages/integrations/markdoc/CHANGELOG.md @@ -1,5 +1,11 @@ # @astrojs/markdoc +## 0.9.1 + +### Patch Changes + +- [#10278](https://github.com/withastro/astro/pull/10278) [`a548a3a99c2835c19662fc38636f92b2bda26614`](https://github.com/withastro/astro/commit/a548a3a99c2835c19662fc38636f92b2bda26614) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fixes original images sometimes being kept / deleted when they shouldn't in both MDX and Markdoc + ## 0.9.0 ### Minor Changes diff --git a/packages/integrations/markdoc/package.json b/packages/integrations/markdoc/package.json index 9d82609720e4..0d0917b58631 100644 --- a/packages/integrations/markdoc/package.json +++ b/packages/integrations/markdoc/package.json @@ -1,7 +1,7 @@ { "name": "@astrojs/markdoc", "description": "Add support for Markdoc in your Astro site", - "version": "0.9.0", + "version": "0.9.1", "type": "module", "types": "./dist/index.d.ts", "author": "withastro", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62588e2fc44c..e9caf5b9b10c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -134,7 +134,7 @@ importers: examples/basics: dependencies: astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro examples/blog: @@ -149,13 +149,13 @@ importers: specifier: ^3.1.1 version: link:../../packages/integrations/sitemap astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro examples/component: devDependencies: astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro examples/framework-alpine: @@ -170,7 +170,7 @@ importers: specifier: ^3.13.3 version: 3.13.3 astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro examples/framework-lit: @@ -182,7 +182,7 @@ importers: specifier: ^0.2.1 version: 0.2.1 astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro lit: specifier: ^3.1.2 @@ -206,7 +206,7 @@ importers: specifier: ^4.0.8 version: link:../../packages/integrations/vue astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro preact: specifier: ^10.19.2 @@ -236,7 +236,7 @@ importers: specifier: ^1.2.1 version: 1.2.1(preact@10.19.3) astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro preact: specifier: ^10.19.2 @@ -254,7 +254,7 @@ importers: specifier: ^18.2.15 version: 18.2.18 astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro react: specifier: ^18.2.0 @@ -269,7 +269,7 @@ importers: specifier: ^4.0.1 version: link:../../packages/integrations/solid astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro solid-js: specifier: ^1.8.5 @@ -281,7 +281,7 @@ importers: specifier: ^5.2.0 version: link:../../packages/integrations/svelte astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro svelte: specifier: ^4.2.5 @@ -293,7 +293,7 @@ importers: specifier: ^4.0.8 version: link:../../packages/integrations/vue astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro vue: specifier: ^3.3.8 @@ -305,13 +305,13 @@ importers: specifier: ^8.2.3 version: link:../../packages/integrations/node astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro examples/integration: devDependencies: astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro examples/middleware: @@ -320,7 +320,7 @@ importers: specifier: ^8.2.3 version: link:../../packages/integrations/node astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro html-minifier: specifier: ^4.0.0 @@ -333,19 +333,19 @@ importers: examples/minimal: dependencies: astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro examples/non-html-pages: dependencies: astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro examples/portfolio: dependencies: astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro examples/ssr: @@ -357,7 +357,7 @@ importers: specifier: ^5.2.0 version: link:../../packages/integrations/svelte astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro svelte: specifier: ^4.2.5 @@ -366,7 +366,7 @@ importers: examples/starlog: dependencies: astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro sass: specifier: ^1.69.5 @@ -384,16 +384,16 @@ importers: specifier: ^5.1.0 version: link:../../packages/integrations/tailwind astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro examples/with-markdoc: dependencies: '@astrojs/markdoc': - specifier: ^0.9.0 + specifier: ^0.9.1 version: link:../../packages/integrations/markdoc astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro examples/with-markdown-plugins: @@ -402,7 +402,7 @@ importers: specifier: ^4.2.1 version: link:../../packages/markdown/remark astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro hast-util-select: specifier: ^6.0.2 @@ -423,7 +423,7 @@ importers: examples/with-markdown-shiki: dependencies: astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro examples/with-mdx: @@ -435,7 +435,7 @@ importers: specifier: ^3.1.1 version: link:../../packages/integrations/preact astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro preact: specifier: ^10.19.2 @@ -450,7 +450,7 @@ importers: specifier: ^0.5.0 version: 0.5.0(nanostores@0.9.5)(preact@10.19.3) astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro nanostores: specifier: ^0.9.5 @@ -471,7 +471,7 @@ importers: specifier: ^1.6.3 version: 1.6.4 astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro autoprefixer: specifier: ^10.4.15 @@ -489,7 +489,7 @@ importers: examples/with-vitest: dependencies: astro: - specifier: ^4.4.8 + specifier: ^4.4.9 version: link:../../packages/astro vitest: specifier: ^1.3.1 @@ -3891,24 +3891,6 @@ importers: specifier: workspace:* version: link:../../../../astro - packages/db/test/fixtures/glob: - dependencies: - '@astrojs/db': - specifier: workspace:* - version: link:../../.. - astro: - specifier: workspace:* - version: link:../../../../astro - chokidar: - specifier: ^3.5.3 - version: 3.5.3 - drizzle-orm: - specifier: ^0.28.6 - version: 0.28.6(@libsql/client@0.4.3) - fast-glob: - specifier: ^3.3.2 - version: 3.3.2 - packages/db/test/fixtures/recipes: dependencies: '@astrojs/db': From 4bc360cd5f25496aca3232f6efb3710424a14a34 Mon Sep 17 00:00:00 2001 From: Sandeep Dilip <34922961+sanman1k98@users.noreply.github.com> Date: Sun, 3 Mar 2024 10:59:19 -0500 Subject: [PATCH 12/13] fix(#8625): smooth scrolling in SPA mode on iOS (#10235) * fix(#8625): smooth scrolling in SPA mode on iOS * perf(router): run cb every 200ms only when scolling * refactor(router): suggested changes and fixes Suggested changes: - change interval time from 200 to 50ms - initialize `last*` vars together with the call to `setInterval()` - clear interval when scroll positions stop changing, independent of history state Additional changes: - remove unused `throttle()` function - move guarded block to inside `onScrollEnd()` since using history navigation will trigger our "popstate" callback and fire additional "scroll" and "scrollend" events, causing redundant expensive calls to `replaceState()` * adds changeset --------- Co-authored-by: Martin Trapp <94928215+martrapp@users.noreply.github.com> --- .changeset/purple-tips-camp.md | 5 ++ packages/astro/src/transitions/router.ts | 73 +++++++++++++++--------- 2 files changed, 50 insertions(+), 28 deletions(-) create mode 100644 .changeset/purple-tips-camp.md diff --git a/.changeset/purple-tips-camp.md b/.changeset/purple-tips-camp.md new file mode 100644 index 000000000000..e2f1279b3ef3 --- /dev/null +++ b/.changeset/purple-tips-camp.md @@ -0,0 +1,5 @@ +--- +"astro": patch +--- + +Fixes jerky scrolling on IOS when using view transitions. diff --git a/packages/astro/src/transitions/router.ts b/packages/astro/src/transitions/router.ts index 6ca3f666a871..480ce7da2bc6 100644 --- a/packages/astro/src/transitions/router.ts +++ b/packages/astro/src/transitions/router.ts @@ -90,28 +90,6 @@ if (inBrowser) { } } -const throttle = (cb: (...args: any[]) => any, delay: number) => { - let wait = false; - // During the waiting time additional events are lost. - // So repeat the callback at the end if we have swallowed events. - let onceMore = false; - return (...args: any[]) => { - if (wait) { - onceMore = true; - return; - } - cb(...args); - wait = true; - setTimeout(() => { - if (onceMore) { - onceMore = false; - cb(...args); - } - wait = false; - }, delay); - }; -}; - // returns the contents of the page or null if the router can't deal with it. async function fetchHTML( href: string, @@ -625,10 +603,15 @@ function onPopState(ev: PopStateEvent) { transition(direction, originalLocation, new URL(location.href), {}, state); } -// There's not a good way to record scroll position before a back button. -// So the way we do it is by listening to scrollend if supported, and if not continuously record the scroll position. -const onScroll = () => { - updateScrollPosition({ scrollX, scrollY }); +const onScrollEnd = () => { + // NOTE: our "popstate" event handler may call `pushState()` or + // `replaceState()` and then `scrollTo()`, which will fire "scroll" and + // "scrollend" events. To avoid redundant work and expensive calls to + // `replaceState()`, we simply check that the values are different before + // updating. + if (scrollX !== history.state.scrollX || scrollY !== history.state.scrollY) { + updateScrollPosition({ scrollX, scrollY }); + } }; // initialization @@ -637,8 +620,42 @@ if (inBrowser) { originalLocation = new URL(location.href); addEventListener('popstate', onPopState); addEventListener('load', onPageLoad); - if ('onscrollend' in window) addEventListener('scrollend', onScroll); - else addEventListener('scroll', throttle(onScroll, 350), { passive: true }); + // There's not a good way to record scroll position before a history back + // navigation, so we will record it when the user has stopped scrolling. + if ('onscrollend' in window) addEventListener('scrollend', onScrollEnd); + else { + // Keep track of state between intervals + let intervalId: number | undefined, lastY: number, lastX: number, lastIndex: State["index"]; + const scrollInterval = () => { + // Check the index to see if a popstate event was fired + if (lastIndex !== history.state?.index) { + clearInterval(intervalId); + intervalId = undefined; + return; + } + // Check if the user stopped scrolling + if (lastY === scrollY && lastX === scrollX) { + // Cancel the interval and update scroll positions + clearInterval(intervalId); + intervalId = undefined; + onScrollEnd(); + return; + } else { + // Update vars with current positions + lastY = scrollY, lastX = scrollX; + } + } + // We can't know when or how often scroll events fire, so we'll just use them to start intervals + addEventListener( + "scroll", + () => { + if (intervalId !== undefined) return; + lastIndex = history.state.index, lastY = scrollY, lastX = scrollX; + intervalId = window.setInterval(scrollInterval, 50); + }, + { passive: true } + ); + }; } for (const script of document.scripts) { script.dataset.astroExec = ''; From 718eed704ad1864cdedf0704d5020af1439671cd Mon Sep 17 00:00:00 2001 From: Sandeep Dilip Date: Sun, 3 Mar 2024 16:00:18 +0000 Subject: [PATCH 13/13] [ci] format --- packages/astro/src/transitions/router.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/astro/src/transitions/router.ts b/packages/astro/src/transitions/router.ts index 480ce7da2bc6..02e2d69dfd20 100644 --- a/packages/astro/src/transitions/router.ts +++ b/packages/astro/src/transitions/router.ts @@ -625,7 +625,7 @@ if (inBrowser) { if ('onscrollend' in window) addEventListener('scrollend', onScrollEnd); else { // Keep track of state between intervals - let intervalId: number | undefined, lastY: number, lastX: number, lastIndex: State["index"]; + let intervalId: number | undefined, lastY: number, lastX: number, lastIndex: State['index']; const scrollInterval = () => { // Check the index to see if a popstate event was fired if (lastIndex !== history.state?.index) { @@ -642,20 +642,20 @@ if (inBrowser) { return; } else { // Update vars with current positions - lastY = scrollY, lastX = scrollX; + (lastY = scrollY), (lastX = scrollX); } - } + }; // We can't know when or how often scroll events fire, so we'll just use them to start intervals addEventListener( - "scroll", + 'scroll', () => { if (intervalId !== undefined) return; - lastIndex = history.state.index, lastY = scrollY, lastX = scrollX; + (lastIndex = history.state.index), (lastY = scrollY), (lastX = scrollX); intervalId = window.setInterval(scrollInterval, 50); }, { passive: true } ); - }; + } } for (const script of document.scripts) { script.dataset.astroExec = '';