diff --git a/.changeset/four-planets-smoke.md b/.changeset/four-planets-smoke.md new file mode 100644 index 000000000000..7c667aa5734e --- /dev/null +++ b/.changeset/four-planets-smoke.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Prevent astro:content from depending on Node builtins diff --git a/packages/astro/package.json b/packages/astro/package.json index 23410941c7d2..274694d8e1e5 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -53,6 +53,8 @@ "./assets/services/sharp": "./dist/assets/services/sharp.js", "./assets/services/squoosh": "./dist/assets/services/squoosh.js", "./content/internal": "./dist/content/internal.js", + "./content/runtime": "./dist/content/runtime.js", + "./content/runtime-assets": "./dist/content/runtime-assets.js", "./debug": "./components/Debug.astro", "./internal/*": "./dist/runtime/server/*", "./package.json": "./package.json", diff --git a/packages/astro/src/content/runtime-assets.ts b/packages/astro/src/content/runtime-assets.ts new file mode 100644 index 000000000000..f6448b10c69e --- /dev/null +++ b/packages/astro/src/content/runtime-assets.ts @@ -0,0 +1,28 @@ +import { z } from 'zod'; +import { imageMetadata, type Metadata } from '../assets/utils/metadata.js'; + +export function createImage(options: { assetsDir: string; relAssetsDir: string }) { + return () => { + if (options.assetsDir === 'undefined') { + throw new Error('Enable `experimental.assets` in your Astro config to use image()'); + } + + return z.string().transform(async (imagePath) => { + const fullPath = new URL(imagePath, options.assetsDir); + return await getImageMetadata(fullPath); + }); + }; +} + +async function getImageMetadata( + imagePath: URL +): Promise<(Metadata & { __astro_asset: true }) | undefined> { + const meta = await imageMetadata(imagePath); + + if (!meta) { + return undefined; + } + + delete meta.orientation; + return { ...meta, __astro_asset: true }; +} diff --git a/packages/astro/src/content/internal.ts b/packages/astro/src/content/runtime.ts similarity index 87% rename from packages/astro/src/content/internal.ts rename to packages/astro/src/content/runtime.ts index 951a52f6d235..d58e4be62639 100644 --- a/packages/astro/src/content/internal.ts +++ b/packages/astro/src/content/runtime.ts @@ -1,5 +1,3 @@ -import { z } from 'zod'; -import { imageMetadata, type Metadata } from '../assets/utils/metadata.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js'; import { prependForwardSlash } from '../core/path.js'; @@ -199,29 +197,3 @@ async function render({ remarkPluginFrontmatter: mod.frontmatter ?? {}, }; } - -export function createImage(options: { assetsDir: string; relAssetsDir: string }) { - return () => { - if (options.assetsDir === 'undefined') { - throw new Error('Enable `experimental.assets` in your Astro config to use image()'); - } - - return z.string().transform(async (imagePath) => { - const fullPath = new URL(imagePath, options.assetsDir); - return await getImageMetadata(fullPath); - }); - }; -} - -async function getImageMetadata( - imagePath: URL -): Promise<(Metadata & { __astro_asset: true }) | undefined> { - const meta = await imageMetadata(imagePath); - - if (!meta) { - return undefined; - } - - delete meta.orientation; - return { ...meta, __astro_asset: true }; -} diff --git a/packages/astro/src/content/template/virtual-mod-assets.mjs b/packages/astro/src/content/template/virtual-mod-assets.mjs new file mode 100644 index 000000000000..32b41dd97b1e --- /dev/null +++ b/packages/astro/src/content/template/virtual-mod-assets.mjs @@ -0,0 +1,9 @@ +import { + createImage +} from 'astro/content/runtime-assets'; + +const assetsDir = '@@ASSETS_DIR@@'; + +export const image = createImage({ + assetsDir, +}); diff --git a/packages/astro/src/content/template/virtual-mod.mjs b/packages/astro/src/content/template/virtual-mod.mjs index 5ce29dcf8fcc..c5dc1b4f32ad 100644 --- a/packages/astro/src/content/template/virtual-mod.mjs +++ b/packages/astro/src/content/template/virtual-mod.mjs @@ -3,8 +3,7 @@ import { createCollectionToGlobResultMap, createGetCollection, createGetEntryBySlug, - createImage, -} from 'astro/content/internal'; +} from 'astro/content/runtime'; export { z } from 'astro/zod'; @@ -13,7 +12,6 @@ export function defineCollection(config) { } const contentDir = '@@CONTENT_DIR@@'; -const assetsDir = '@@ASSETS_DIR@@'; const entryGlob = import.meta.glob('@@ENTRY_GLOB_PATH@@', { query: { astroContent: true }, @@ -40,7 +38,3 @@ export const getEntryBySlug = createGetEntryBySlug({ getCollection, collectionToRenderEntryMap, }); - -export const image = createImage({ - assetsDir, -}); diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts index 00d0f61e4b00..4de7e39d6916 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -347,6 +347,7 @@ export type ContentPaths = { cacheDir: URL; typesTemplate: URL; virtualModTemplate: URL; + virtualAssetsModTemplate: URL; config: { exists: boolean; url: URL; @@ -365,6 +366,7 @@ export function getContentPaths( assetsDir: new URL('./assets/', srcDir), typesTemplate: new URL('types.d.ts', templateDir), virtualModTemplate: new URL('virtual-mod.mjs', templateDir), + virtualAssetsModTemplate: new URL('virtual-mod-assets.mjs', templateDir), config: configStats, }; } diff --git a/packages/astro/src/content/vite-plugin-content-virtual-mod.ts b/packages/astro/src/content/vite-plugin-content-virtual-mod.ts index 99b6e3f3c036..6691326e9d1f 100644 --- a/packages/astro/src/content/vite-plugin-content-virtual-mod.ts +++ b/packages/astro/src/content/vite-plugin-content-virtual-mod.ts @@ -36,11 +36,16 @@ export function astroContentVirtualModPlugin({ const virtualModContents = fsMod .readFileSync(contentPaths.virtualModTemplate, 'utf-8') .replace('@@CONTENT_DIR@@', relContentDir) - .replace('@@ASSETS_DIR@@', assetsDir) .replace('@@ENTRY_GLOB_PATH@@', entryGlob) .replace('@@RENDER_ENTRY_GLOB_PATH@@', entryGlob); + const virtualAssetsModContents = fsMod + .readFileSync(contentPaths.virtualAssetsModTemplate, 'utf-8') + .replace('@@ASSETS_DIR@@', assetsDir); const astroContentVirtualModuleId = '\0' + VIRTUAL_MODULE_ID; + const allContents = settings.config.experimental.assets ? + (virtualModContents + virtualAssetsModContents) : + virtualModContents; return { name: 'astro-content-virtual-mod-plugin', @@ -53,7 +58,7 @@ export function astroContentVirtualModPlugin({ load(id) { if (id === astroContentVirtualModuleId) { return { - code: virtualModContents, + code: allContents, }; } }, diff --git a/packages/astro/test/content-collections.test.js b/packages/astro/test/content-collections.test.js index 67ef08235fb3..fbf820c30105 100644 --- a/packages/astro/test/content-collections.test.js +++ b/packages/astro/test/content-collections.test.js @@ -3,6 +3,7 @@ import * as cheerio from 'cheerio'; import { expect } from 'chai'; import { loadFixture } from './test-utils.js'; import testAdapter from './test-adapter.js'; +import { preventNodeBuiltinDependencyPlugin } from './test-plugins.js'; describe('Content Collections', () => { describe('Query', () => { @@ -222,6 +223,11 @@ describe('Content Collections', () => { root: './fixtures/content-ssr-integration/', output: 'server', adapter: testAdapter(), + vite: { + plugins: [ + preventNodeBuiltinDependencyPlugin() + ] + } }); await fixture.build(); app = await fixture.loadTestAdapterApp(); diff --git a/packages/astro/test/test-plugins.js b/packages/astro/test/test-plugins.js new file mode 100644 index 000000000000..75f944789415 --- /dev/null +++ b/packages/astro/test/test-plugins.js @@ -0,0 +1,17 @@ + +export function preventNodeBuiltinDependencyPlugin() { + // Verifies that `astro:content` does not have a hard dependency on Node builtins. + // This is to verify it will run on Cloudflare and Deno + return { + name: 'verify-no-node-stuff', + generateBundle() { + const nodeModules = ['node:fs', 'node:url', 'node:worker_threads', 'node:path']; + nodeModules.forEach(name => { + const mod = this.getModuleInfo(name); + if(mod) { + throw new Error(`Node builtins snuck in: ${name}`) + } + }); + } + }; +}